Skip to content

Commit

Permalink
use field ids, better error mesages, update 1pass plugin docs
Browse files Browse the repository at this point in the history
  • Loading branch information
theoephraim committed Aug 16, 2024
1 parent d8e6e4c commit 9c18dd3
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 46 deletions.
2 changes: 1 addition & 1 deletion example-repo/.dmno/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default defineDmnoService({
value: OnePassSecretsDev.itemById("ut2dftalm3ugmxc6klavms6tfq", "bphvvrqjegfmd5yoz4buw2aequ", "username"),
},
OP_ITEM_BY_LINK: {
value: OnePassSecretsDev.itemByLink("https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=bphvvrqjegfmd5yoz4buw2aequ&h=dmnoinc.1password.com", "sectiontest/section-child"),
value: OnePassSecretsDev.itemByLink("https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=bphvvrqjegfmd5yoz4buw2aequ&h=dmnoinc.1password.com", ""),
},
OP_ITEM_BY_REFERENCE: {
value: OnePassSecretsDev.itemByReference("op://dev test/example/username"),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 78 additions & 36 deletions packages/docs-site/src/content/docs/docs/plugins/1password.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,41 @@ description: DMNO's 1Password plugin allows you to securely access your stored s
import { Steps, Icon } from '@astrojs/starlight/components';
import TabbedCode from '@/components/TabbedCode.astro';

DMNO's 1Password plugin allows you to securely access your stored secrets in 1Password. This plugin uses the 1Password CLI and can communicate by means of a [Service Account](https://developer.1password.com/docs/service-accounts) or during local development by [integrating with the 1Password desktop app](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration). It is compatible with any account type. Note that rate limits vary by account type, you can read more about that in the [1Password Developer documentation](https://developer.1password.com/docs/service-accounts/rate-limits/).
DMNO's [1Password](https://1password.com/) plugin allows you to securely access your secrets stored in 1Password. This plugin uses their [JavaScript SDK](https://github.com/1Password/onepassword-sdk-js/) to communicate with 1Password using a [Service Account](https://developer.1password.com/docs/service-accounts). Additionally, for local development, you can opt-in to use your system-installed [1Password CLI](https://developer.1password.com/docs/cli/get-started/) and its [integration with the 1Password desktop app](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration), which uses your personal access and enables the biometric unlocking of the desktop app.

It is compatible with any account type. Note that rate limits vary by account type, you can read more about that in the [1Password Developer documentation](https://developer.1password.com/docs/service-accounts/rate-limits/).

## Installation

Install the package in your service(s) that will use config from 1password.
Install the package in your service(s) that will use config from 1Password.

<TabbedCode packageName="@dmno/1password-plugin" />

-----

After installation, you'll need to initialize the plugin in your dmno config and wire it up to the config path that will hold your 1password service account token. It's ok if you have not created this service account yet - we'll do that in the next section.
After installation, you'll need to initialize the plugin in your dmno config and wire it up to the config path that will hold your 1Password service account token. It's ok if you have not created this service account yet - we'll do that in the next section.

```typescript title='.dmno/config.mts'
import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
```diff lang="ts" title='.dmno/config.mts'
+import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';

const OnePassBackend = new OnePasswordDmnoPlugin('1pass/prod', {
token: configPath('OP_TOKEN'),
});
+const OnePassBackend = new OnePasswordDmnoPlugin('1pass', {
+ token: configPath('OP_TOKEN'),
+});

export default defineDmnoService({
schema: {
OP_TOKEN: {
extends: OnePasswordTypes.serviceAccountToken,
// NOTE - the type itself is already marked as sensitive 🔐
},
+ OP_TOKEN: {
+ extends: OnePasswordTypes.serviceAccountToken,
+ // NOTE - the type itself is already marked as sensitive 🔐
+ },
},
});
```

:::tip
You must give each plugin instance a unique id so we can refer to it in other services and the CLI. In this case we used `1pass/prod`, as you can imagine using multiple 1password vaults and having another plugin instance of `1pass/non-prod`.
You must give each plugin instance a unique id so we can refer to it in other services and the `dmno` CLI.

In this case we used `1pass`, but you can imagine splitting vaults and access, and having multiple plugin instances - for example `1pass/prod` and `1pass/non-prod`.
:::

### Injecting the plugin in monorepo services
Expand All @@ -46,7 +50,7 @@ In a monorepo, if you are managing config for multiple services in a single vaul
import { OnePasswordDmnoPlugin } from '@dmno/1password-plugin';

// inject the already initialized plugin instead of re-initializing it
const OnePassBackend = OnePasswordDmnoPlugin.injectInstance('1pass/prod');
const OnePassBackend = OnePasswordDmnoPlugin.injectInstance('1pass');
```


Expand All @@ -63,14 +67,15 @@ For more on 1Password security, see their best practices for [CLI](https://devel

1. **Create a vault** in your 1Password account. This is where you'll store your secrets. You can create multiple vaults for different environments or services. [link](https://support.1password.com/create-share-vaults/#create-a-vault)

2. **Create a service account** in your 1Password account. This is a separate account that has access to the vault(s) you created. You can create multiple service accounts for different environments or services. [link](https://developer.1password.com/docs/service-accounts/get-started/)
2. **Create a new service account with access to the vault(s)**. This is a separate machine account meant to be used for machine-to-machine communication. Service accounts cannot be modified after creation, so you will need to create a new one with access granted to the correct vaults. This is done in the 1Password web interface.. [link](https://developer.1password.com/docs/service-accounts/get-started/)

3. **Grant vault access to the service account**. This is done in the 1Password web interface. You can add multiple service accounts to a single vault. [link](https://developer.1password.com/docs/service-accounts/manage-service-accounts/#manage-access)
3. **Grant vault access to users/teams (optional)**. Your developers may need access to at least some of your vaults, especially if using the `op` cli based auth mentioned below. [link](https://support.1password.com/create-share-vaults-teams/#share-a-vault)

4. **Ensure vault service account access is enabled (optional)**. Each vault has a toggle to disable service account access _in general_. It is on by default, so you will likely not need to do anything. [link](https://developer.1password.com/docs/service-accounts/manage-service-accounts/#manage-access)

4. **Grant vault access to users/teams (optional)**. During local development, if you'd rather not useon using the op cli's integration with the Desktop app
</Steps>

This service account token will now serve as your "secret-zero" - which grants access to the rest of your sensitive config stored in 1password. It must be set locally and in deployed environments, and as it is sensitive, we must pass in the value as an _override_ rather than storing it within the config. Locally this usually means storing it in your `.env.local` and on a deployed environment you'll usually set it within some kind of UI, wherever you would normally pass in secrets.
This service account token will now serve as your "secret-zero" - which grants access to the rest of your sensitive config stored in 1Password. It must be set locally (unless relying on cli-based auth) and in deployed environments. It is sensitive and so we must pass in the value as an _override_ rather than storing it within the config. Locally this usually means storing it in your `.env.local` and on a deployed environment you'll usually set it within some kind of UI, wherever you would normally pass in secrets.

```diff title=".dmno/.env.local"
+OP_TOKEN=ops_abc123...
Expand All @@ -82,27 +87,21 @@ Note that the config path of `OP_TOKEN` is arbitrary and you can see how it was
Consider how you want to organize your vaults and service accounts. You might have a single vault for all your secrets per environment, or you might have separate vaults for each service and environment. At a minimum, DMNO recommends having separate vaults for production and non-production environments.
:::


### Desktop app integration

During local development, you may find it convenient to use the onepoas


------

## Add your items

DMNO supports a few different ways to reference items in 1Password.

### Using a env blob (recommended)
### Using a env blob

Managing lots of individual 1password items and connecting them to your config can be a bit tedious, so we recommend storing multiple items together in a `.env` style text blob. Using this method, we'll have a single 1password item that can have one text entry per service containing the `.env` blob. This would be similar to applying a `.env.local` file as overrides, except they are secured and shared via 1password. This also makes it incredibly easy to migrate from using local `.env` files.
Managing lots of individual 1Password items and connecting them to your config can be a bit tedious, so when getting started, we recommend storing multiple items together in a `.env` style text blob. Using this method, we'll have a single 1Password item that can have one text entry per service containing the `.env` blob. This would be similar to applying a `.env.local` file as overrides, except they are secured and shared via 1Password. This also makes it incredibly easy to migrate from using local `.env` files.

To use this method, we need to tell the plugin which 1password item will store our `.env` blob(s). As this value is static and not sensitive, we can use a static value as our plugin input.
To use this method, we need to tell the plugin which 1Password item will store our `.env` blob(s). As this link is not sensitive, we can use a static value as our plugin input.

```diff lang="ts" title=".dmno/config.mts"
import { OnePasswordDmnoPlugin, OnePasswordTypes } from '@dmno/1password-plugin';
Then use the `.item()` value resolver as the value for any config values that will be stored in the linked 1Password item.

```diff lang="ts" title=".dmno/config.mts"
const OnePassBackend = new OnePasswordDmnoPlugin('1pass/prod', {
token: configPath('OP_TOKEN'),
+ envItemLink: 'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',
Expand All @@ -121,11 +120,22 @@ export default defineDmnoService({
});
```

:::tip[Where to find an item private link]
You can find the private link by clicking the 3 dots on the item in the 1Password interface and selecting `Copy Private Link`.
:::

Values are looked up within the linked 1Password item using a simple convention. We expect to find a _text field_ within the item with a label set to the current service name. The contents of that item are parsed as a [`.env` file](https://dotenvx.com/docs/env-file), and we look up items using the config item key. If no match is found, we will also look in an additional field with the label `_default`.

![1Password blob item example](../../../../assets/docs-images/plugins/1password/blob-item-example.png)

For example, in the item above, an item with the key `ONE_MORE` would fallback to the value in the `_default` field in any service that wasn't named `root`.


{/* TODO: add screenshot of 1pass item showing the entry? */}

### Using specific 1password items
### Using specific 1Password items

If you already have lots of indivdual items in 1password, or you just don't want to use the blob method, you can wire up invididual config items to specific 1password items. We provide several methods to do so. Note that while a 1password reference (e.g., `op://vaultname/itemname/path`) points all the way to a specific value, the other methods only get us to an item which usually contains multiple entries (account id, secret key, etc). In these cases you must also pass in an additional path to the specific entry. These paths use the entry labels.
If you already have lots of indivdual items in 1Password, or you just don't want to use the blob method, you can wire up invididual config items to specific 1Password items. We provide several methods to do so. Note that while a 1Password reference (e.g., `op://vaultname/itemname/path`) points all the way to a specific value, these reference urls are based on field labels and are not stable, and so the other methods are preferred.

```ts
export default defineDmnoService({
Expand All @@ -134,14 +144,14 @@ export default defineDmnoService({
ITEM_WITH_LINK: {
value: OnePassBackend.itemByLink(
'https://start.1password.com/open/i?a=I3GUA2KU6BD3FBHA47QNBIVEV4&v=ut2dftalm3ugmxc6klavms6tfq&i=n4wmgfq77mydg5lebtroa3ykvm&h=dmnoinc.1password.com',
'somepath',
'somefieldid',
),
},
// using vault + item UUIDs
// using UUIDs
ITEM_WITH_IDS: {
value: OnePassBackend.itemById('vaultUuid', 'itemUuid', 'somepath'),
value: OnePassBackend.itemById('vaultUuid', 'itemUuid', 'somefieldid'),
},
// using item reference
// using item reference url
ITEM_WITH_REFERENCE: {
value: OnePassBackend.itemByReference('op://vaultname/itemname/path'),
},
Expand All @@ -153,8 +163,40 @@ export default defineDmnoService({
You can find the private link by clicking the 3 dots on the item in the 1Password interface and selecting `Copy Private Link`.
:::

:::tip[Where to find field IDs]
Field IDs are not easy to get from the 1Password UI. Luckily when the supplied field ID is not found, our error message includes a list of all the possible IDs in the item. Simply start with an empty string or a `"?"` and use the DMNO error message to find the right field ID.
:::

:::tip[Where to find an item reference]
The secret reference for invidivual items can be found by clicking on the down arrow icon on the item and selecting `Copy Secret Reference`.
:::


### CLI / Desktop app integration

During local development, you may find it convenient to skip the service account tokens and instead rely on your system's `op` CLI and its [integration with the 1Password desktop app](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration). This means you will be connecting to 1Password as if you were using your local 1Password desktop application, including using it's biometric unlocking features.

If you don't see the menu option, you may need to enable it in the 1Password Developer settings and you may need to install the 1Password CLI as well.
<Steps>
1. **Opt-in while initializing the plugin**
```diff lang="ts" title='.dmno/config.mts'
const OnePassBackend = new OnePasswordDmnoPlugin('1pass', {
token: configPath('OP_TOKEN'),
+ fallbackToCliBasedAuth: true,
});
```

Of course you could point to a `configPath` in your schema and toggle the opt-in based on some other logic if you'd like.

2. **Install the `op` CLI**. See 1Password CLI [getting started guide](https://developer.1password.com/docs/cli/get-started/)

3. **Enable the 1Password desktop app CLI integration**. See [docs](https://developer.1password.com/docs/cli/get-started/#step-2-turn-on-the-1password-desktop-app-integration)

4. **Run `op signin` to log in on the CLI**. Ensure you are logged in to the correct account. You can run `op whoami` to see which account is currently connected to the CLI.
</Steps>

With this option enabled, if the resolved service account token is empty, we will call out to the `op` cli installed on your machine (it must be in your `$PATH`) and use the auth it provides. With the desktop app integration enabled, it will call out and may trigger biometric verification to unlock. It is slick and very convenient!

:::caution[Connecting as yourself]
Keep in mind that this method is connecting as _YOU_ who may have more access than a tightly scoped service account. Consider only enabling this method for a plugin instance that will be handling non-production secrets.
:::

Loading

0 comments on commit 9c18dd3

Please sign in to comment.