Skip to content

Commit 43bcba0

Browse files
Add a ResourceLoader to fetch 1Password secrets using the
1Password CLI, includes a FakeResourceProvider implementation GitOrigin-RevId: d64ef1c42ed84adcc9200f50049127a1636a0d08
1 parent e803555 commit 43bcba0

File tree

9 files changed

+258
-5
lines changed

9 files changed

+258
-5
lines changed

Diff for: misk-config/api/misk-config.api

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ public final class misk/resources/ResourceLoader$Companion {
9595

9696
public final class misk/resources/ResourceLoaderModule : misk/inject/KAbstractModule {
9797
public fun <init> ()V
98+
public fun <init> (Z)V
99+
public synthetic fun <init> (ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
98100
}
99101

100102
public final class misk/resources/TestingResourceLoaderModule : misk/inject/KAbstractModule {

Diff for: misk-config/src/main/kotlin/misk/resources/ResourceLoaderModule.kt

+14-1
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ import wisp.resources.EnvironmentResourceLoaderBackend
1010
import wisp.resources.FakeFilesystemLoaderBackend
1111
import wisp.resources.FilesystemLoaderBackend
1212
import wisp.resources.MemoryResourceLoaderBackend
13+
import wisp.resources.FakeResourceLoaderBackend
14+
import wisp.resources.OnePasswordResourceLoaderBackend
1315
import wisp.resources.ResourceLoader as WispResourceLoader
1416

15-
class ResourceLoaderModule : KAbstractModule() {
17+
class ResourceLoaderModule @JvmOverloads constructor(private val isReal: Boolean = true) : KAbstractModule() {
1618
override fun configure() {
1719
val mapBinder = newMapBinder<String, WispResourceLoader.Backend>()
1820

1921
mapBinder.addBinding(ClasspathResourceLoaderBackend.SCHEME).toInstance(ClasspathResourceLoaderBackend)
2022
mapBinder.addBinding(FilesystemLoaderBackend.SCHEME).toInstance(FilesystemLoaderBackend)
2123
mapBinder.addBinding(MemoryResourceLoaderBackend.SCHEME).toInstance(MemoryResourceLoaderBackend())
2224
mapBinder.addBinding(EnvironmentResourceLoaderBackend.SCHEME).toInstance(EnvironmentResourceLoaderBackend)
25+
if (!isReal) {
26+
mapBinder.addBinding(OnePasswordResourceLoaderBackend.SCHEME).toInstance(OnePasswordResourceLoaderBackend)
27+
}
2328
}
2429
}
2530

@@ -45,6 +50,14 @@ class TestingResourceLoaderModule : KAbstractModule() {
4550
internal fun fakeFilesystemLoaderBackend(
4651
@ForFakeFiles fakeFiles: Map<String, String>
4752
): WispResourceLoader.Backend = FakeFilesystemLoaderBackend(fakeFiles)
53+
54+
@ProvidesIntoMap
55+
@StringMapKey(OnePasswordResourceLoaderBackend.SCHEME)
56+
@Singleton
57+
@Suppress("unused")
58+
internal fun fakeOnePasswordResourceLoaderBackend(
59+
@ForFakeFiles fakeFiles: Map<String, String>
60+
): WispResourceLoader.Backend = FakeResourceLoaderBackend(fakeFiles)
4861
}
4962

5063
@Qualifier

Diff for: misk/src/main/kotlin/misk/MiskServiceModule.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class MiskRealServiceModule @JvmOverloads constructor(
2727
private val serviceManagerConfig: ServiceManagerConfig = ServiceManagerConfig(),
2828
) : KAbstractModule() {
2929
override fun configure() {
30-
install(ResourceLoaderModule())
30+
install(ResourceLoaderModule(isReal = true))
3131
install(RealEnvVarModule())
3232
install(ClockModule())
3333
install(SleeperModule())

Diff for: settings.gradle.kts

-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ pluginManagement {
88
}
99

1010
plugins {
11-
// When updating the cash plugin versions, update .buildkite/scripts/copy.bara.sky too
12-
id("com.squareup.cash.develocity") version "1.220.1"
13-
id("com.squareup.cash.remotecache") version "1.220.1"
1411
id("com.gradle.develocity") version "3.18.2"
1512
}
1613

Diff for: wisp/wisp-resource-loader-testing/api/wisp-resource-loader-testing.api

+8
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ public final class wisp/resources/FakeFilesystemLoaderBackend : wisp/resources/R
44
public fun open (Ljava/lang/String;)Lokio/BufferedSource;
55
}
66

7+
public final class wisp/resources/FakeResourceLoaderBackend : wisp/resources/ResourceLoader$Backend {
8+
public fun <init> (Ljava/util/Map;)V
9+
public fun checkPath (Ljava/lang/String;)V
10+
public fun exists (Ljava/lang/String;)Z
11+
public fun list (Ljava/lang/String;)Ljava/util/List;
12+
public fun open (Ljava/lang/String;)Lokio/BufferedSource;
13+
}
14+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package wisp.resources
2+
3+
import okio.Buffer
4+
import okio.BufferedSource
5+
6+
/**
7+
* A fake [ResourceLoader.Backend] loads resources from an in-memory map. This does not have the
8+
* same well-formed filepath guarantees that [FakeFilesystemLoaderBackend] provides, which assumes
9+
* resource paths are file-like and will throw exceptions for malformed resource paths
10+
*/
11+
class FakeResourceLoaderBackend(private val fakeResources: Map<String, String>) : ResourceLoader.Backend() {
12+
override fun checkPath(path: String) {
13+
require(fakeResources.containsKey(path))
14+
}
15+
16+
override fun list(path: String): List<String> {
17+
return if (fakeResources.containsKey(path)) listOf(path) else emptyList()
18+
}
19+
20+
override fun open(path: String): BufferedSource? {
21+
return fakeResources[path]?.let { Buffer().writeUtf8(it) }
22+
}
23+
24+
override fun exists(path: String): Boolean {
25+
return fakeResources.containsKey(path)
26+
}
27+
}

Diff for: wisp/wisp-resource-loader/api/wisp-resource-loader.api

+24
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,30 @@ public final class wisp/resources/MemoryResourceLoaderBackend : wisp/resources/R
4141
public final class wisp/resources/MemoryResourceLoaderBackend$Companion {
4242
}
4343

44+
public final class wisp/resources/OnePasswordResourceLoaderBackend : wisp/resources/ResourceLoader$Backend {
45+
public static final field INSTANCE Lwisp/resources/OnePasswordResourceLoaderBackend;
46+
public static final field SCHEME Ljava/lang/String;
47+
public fun checkPath (Ljava/lang/String;)V
48+
public fun exists (Ljava/lang/String;)Z
49+
public fun list (Ljava/lang/String;)Ljava/util/List;
50+
public fun open (Ljava/lang/String;)Lokio/BufferedSource;
51+
}
52+
53+
public final class wisp/resources/OnePasswordResourcePath {
54+
public static final field Companion Lwisp/resources/OnePasswordResourcePath$Companion;
55+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
56+
public final fun asCliArgs ()Ljava/util/List;
57+
public final fun asCliArgs (Ljava/lang/String;)Ljava/util/List;
58+
public static synthetic fun asCliArgs$default (Lwisp/resources/OnePasswordResourcePath;Ljava/lang/String;ILjava/lang/Object;)Ljava/util/List;
59+
public final fun getAccount ()Ljava/lang/String;
60+
public final fun getSecretReference ()Ljava/lang/String;
61+
public fun toString ()Ljava/lang/String;
62+
}
63+
64+
public final class wisp/resources/OnePasswordResourcePath$Companion {
65+
public final fun fromPath (Ljava/lang/String;)Lwisp/resources/OnePasswordResourcePath;
66+
}
67+
4468
public class wisp/resources/ResourceLoader {
4569
public static final field Companion Lwisp/resources/ResourceLoader$Companion;
4670
public fun <init> (Ljava/util/Map;)V
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package wisp.resources
2+
3+
import okio.Buffer
4+
import okio.BufferedSource
5+
import okio.source
6+
import java.io.IOException
7+
8+
/**
9+
* Read-only resources that are fetched from the 1password-cli (`op`) tool.
10+
*
11+
* To use, please:
12+
* 1. ensure the 1password-cli is installed with `hermit install op` or `brew install 1password-cli`.
13+
* 2. make sure to enable the cli integration in the 1password app: https://developer.1password.com/docs/cli/app-integration/#step-1-turn-on-the-app-integration.
14+
* 3. check that the secret being accessed is available. You can get the secret-reference by following: https://developer.1password.com/docs/cli/secret-references/#step-1-get-secret-references
15+
*
16+
* This uses the scheme `1password:`. Secret-references from 1password can therefore be used after
17+
* copy-pasting and replacing "op" like so: "1password://secretRef/goes/here". To use a specific
18+
* account, add the accountId like so "1password:accountId@//secretRef/goes/here".
19+
*/
20+
object OnePasswordResourceLoaderBackend : ResourceLoader.Backend() {
21+
const val SCHEME = "1password:"
22+
23+
override fun checkPath(path: String) {
24+
OnePasswordResourcePath.fromPath(path)
25+
}
26+
27+
override fun list(path: String): List<String> {
28+
require(path.isNotEmpty())
29+
30+
return listOf(OnePasswordResourcePath.fromPath(path).toString())
31+
}
32+
33+
override fun exists(path: String): Boolean {
34+
return try {
35+
fetch(path, "type").size > 0
36+
} catch (e: Exception) {
37+
false
38+
}
39+
}
40+
41+
override fun open(path: String): BufferedSource? {
42+
return fetch(path)
43+
}
44+
45+
private fun fetch(path: String, attribute: String? = null): Buffer {
46+
val resource = OnePasswordResourcePath.fromPath(path)
47+
val command = listOf("op", "read") + resource.asCliArgs(attribute)
48+
try {
49+
val process = ProcessBuilder().command(command).start()
50+
51+
val exitCode = process.waitFor()
52+
if (exitCode == 0) {
53+
return Buffer().apply { writeAll(process.inputStream.source()) }
54+
}
55+
56+
throw NoSuchElementException(
57+
"1Password secret $resource could not be found! Please make sure it is available via: `${command.joinToString(" ")}`"
58+
)
59+
} catch (e: IOException) {
60+
throw UnsupportedOperationException(
61+
"Error calling the 1password-cli. Please ensure it is installed with `hermit install op` or `brew install 1password-cli`",
62+
e,
63+
)
64+
}
65+
}
66+
}
67+
68+
/**
69+
* Represents a 1password secret-reference, with an optional extra account identifier to differentiate
70+
* if there are multiple 1password accounts. The secret-reference schema is documented at
71+
* https://developer.1password.com/docs/cli/secret-reference-syntax/. The only change for this use
72+
* case is the "op:" prefix is not present as the ResourceLoader implementation strips the schema.
73+
*/
74+
class OnePasswordResourcePath private constructor(val account: String?, val secretReference: String) {
75+
override fun toString(): String {
76+
return account?.let { "$it@$secretReference" } ?: secretReference
77+
}
78+
79+
@JvmOverloads
80+
fun asCliArgs(attribute: String? = null): List<String> {
81+
// For checking a specific attribute of the secret rather than the value (default)
82+
val attributeField = attribute?.let { "?attribute=$it" } ?: ""
83+
// Add `op:` prefix for the 1password-cli
84+
val secretRef = "op:$secretReference$attributeField"
85+
// Include account args, if specified
86+
if (account != null) {
87+
return listOf("--no-newline", "--account", account, secretRef)
88+
}
89+
return listOf("--no-newline", secretRef)
90+
}
91+
92+
companion object {
93+
/** Expects a path in the form "accountId@//secretRef/goes/here" or "//secretRef/goes/here" */
94+
fun fromPath(path: String): OnePasswordResourcePath {
95+
if (path.contains("@")) {
96+
val (account, secretReference) = path.split("@", limit = 2)
97+
require(secretReference.startsWith("//")) { "1Password secret reference must start with //" }
98+
return OnePasswordResourcePath(account, secretReference)
99+
}
100+
101+
require(path.startsWith("//")) { "1Password secret reference must start with //" }
102+
return OnePasswordResourcePath(null, path)
103+
}
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package wisp.resources
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.jupiter.api.Disabled
5+
import org.junit.jupiter.api.Test
6+
import kotlin.test.assertEquals
7+
import kotlin.test.assertFailsWith
8+
9+
class OnePasswordResourceLoaderBackendTest {
10+
11+
private val resourceLoader: ResourceLoader = ResourceLoader(
12+
mapOf(
13+
OnePasswordResourceLoaderBackend.SCHEME to OnePasswordResourceLoaderBackend,
14+
)
15+
)
16+
17+
private val nonExistentOnePasswordResource = "1password://Employee/Test login for unit tests/does-not-exist"
18+
private val existingOnePasswordResource = "1password://Employee/Test login for unit tests/password"
19+
private val existingOnePasswordResourceWithAccount = "1password:squareup.1password.com@//Employee/Test login for unit tests/password"
20+
21+
22+
@Test
23+
fun onePasswordResourcePath() {
24+
val resourcePath = OnePasswordResourcePath.fromPath("//My Vault Name/My Secret Name/field_name_here")
25+
assertThat(resourcePath.account).isNull()
26+
assertEquals(resourcePath.secretReference, "//My Vault Name/My Secret Name/field_name_here")
27+
assertEquals(resourcePath.asCliArgs(), listOf("--no-newline", "op://My Vault Name/My Secret Name/field_name_here"))
28+
assertEquals(resourcePath.asCliArgs(attribute = "type"), listOf("--no-newline", "op://My Vault Name/My Secret Name/field_name_here?attribute=type"))
29+
}
30+
31+
@Test
32+
fun onePasswordResourcePathWithAccountId() {
33+
val resourcePath = OnePasswordResourcePath.fromPath("myAccount.1password.com@//My Vault Name/My Secret Name/field_name_here")
34+
assertEquals(resourcePath.account, "myAccount.1password.com")
35+
assertEquals(resourcePath.secretReference, "//My Vault Name/My Secret Name/field_name_here")
36+
assertEquals(resourcePath.asCliArgs(), listOf("--no-newline", "--account", "myAccount.1password.com", "op://My Vault Name/My Secret Name/field_name_here"))
37+
assertEquals(resourcePath.asCliArgs(attribute = "type"), listOf("--no-newline", "--account", "myAccount.1password.com", "op://My Vault Name/My Secret Name/field_name_here?attribute=type"))
38+
}
39+
40+
@Test
41+
fun onePasswordResourcePathInvalidFormat() {
42+
assertFailsWith<IllegalArgumentException> {
43+
OnePasswordResourcePath.fromPath("Malformed Name/field_name_here")
44+
}
45+
46+
assertFailsWith<IllegalArgumentException> {
47+
OnePasswordResourcePath.fromPath("myAccount.1password.com@Malformed Name/field_name_here")
48+
}
49+
}
50+
51+
@Disabled("Requires no `op` binary on path")
52+
@Test
53+
fun onePasswordMissingBinary() {
54+
assertFailsWith<UnsupportedOperationException> {
55+
resourceLoader.utf8(existingOnePasswordResource)
56+
}
57+
}
58+
59+
@Disabled("Requires `op` binary on path")
60+
@Test
61+
fun onePasswordMissingSecret() {
62+
assertThat(resourceLoader.exists(nonExistentOnePasswordResource)).isFalse()
63+
assertFailsWith<NoSuchElementException> {
64+
resourceLoader.utf8(nonExistentOnePasswordResource)
65+
}
66+
}
67+
68+
@Disabled("Requires `op` binary on path and a test secret")
69+
@Test
70+
fun onePasswordReadsSecret() {
71+
assertThat(resourceLoader.exists(existingOnePasswordResource)).isTrue()
72+
assertThat(resourceLoader.exists(existingOnePasswordResourceWithAccount)).isTrue()
73+
74+
assertEquals(resourceLoader.utf8(existingOnePasswordResource), "test-value-here")
75+
assertEquals(resourceLoader.utf8(existingOnePasswordResourceWithAccount), "test-value-here")
76+
}
77+
}

0 commit comments

Comments
 (0)