Dockerで1Passwordを使用する


## まえがき

筆者はよくスクレイピングをしているのですが、アクセスしているサイトの一つに「一定期間毎にパスワード変更を要求される」ものが存在します。
手動でパスワードを変更するだけでも結構面倒なのですが、それと同時にスクレイピングしているPCの.envファイルを変更するのがあまりにもやりたくなかったので、1Passwordで全てを管理しようと試みてみました。

そこで今回は、Dockerで1Passwordを使用する方法です。個人プランでも使用可能だと思われます。(筆者は家族プランで契約しています。)

## 前提

1Passwordの構造は少しややこしいのですが以下のようになっています。

- vaultにitemが保存されている
- serverはvaultに対してのアクセス権限を持っている
- server毎にtokenを発行する

簡単に図示すると以下の通りです。
```
server - vault - item
   |
 token
```
serverは各vaultに対してそれぞれread/writeの権限を管理でき、tokenはserverの権限を超えない範囲で各vaultに対するread/writeの権限を保持できます。
serverは1Passwordのデフォルトに存在する**Private vaultに対するアクセス権限を所持できない**ので、開発用の新たなvaultを作成する必要があります。（後ほど作成方法も紹介します。）

## 1Password Cliのインストール

tokenの発行はGUIでもできますが、CLIでも可能です。CLIで発行する場合、1Password Cliのインストールが必要ですので、その方法を説明いたします。
brewを導入済みの方は、

```bash
brew install --cask 1password-cli
```

でインストール可能です。
筆者の環境はversion2.16.0です。1系と2系でコマンドの形態が大きく異るため、誤って1系をインストールした場合はやり直しを推奨します。

```bash
$ op --version
> 2.16.0
```

それ以降の初期設定については[こちら](https://developer.1password.com/docs/cli/get-started/)などを参考にするかとよろしいかと思います。

## tokenの発行

CLIで発行する場合、以下の手順になります。

```bash
$ op vault create ${vault}
ID:                   ${ID}
Name:                 ${vault}
Type:                 USER_CREATED
Attribute version:    1
Content version:      1
Items:                0
Created:              now
Updated:              now

$ op connect server create ${server}
Set up a Connect server.
UUID: ${UUID}
Credentials file: /current/directory/1password-credentials.json

$ op connect vault grant --server ${server} --vault ${vault}
Connect server ${server} ${UUID} has been successfully granted access to vault ${vault} ${ID}.

$ op connect token create ${token} --server ${server} --vault ${vault}
(ここにtokenが出力されるので控える)
```

この際出力されたtoken及び`1password-credentials.json`はこのあと使いますので控えてください。

`${vault}`, `${server}`, `${token}`にはお好みの名前を入力してください。

## Dockerで1Passwordを利用する

基本的には[こちら](https://dev.classmethod.jp/articles/1password-secrets-automation)の記事を参考にするとよろしいかと思いますが、1PasswordにアクセスするPythonファイルもDocker化したかったので多少手を加えております。

```docker-compose.yml
version: "3.4"

services:
  py:
    container_name: py
    build: py
    volumes:
      - ./py:/root/app
    environment:
      TZ: $TZ
      OP_CONNECT_TOKEN: $OP_CONNECT_TOKEN
      VAULT_ID: $VAULT_ID

  op-connect-api:
    container_name: op-connect-api
    image: 1password/connect-api:latest
    volumes:
      - "./1password-credentials.json:/home/opuser/.op/1password-credentials.json"
      - "./data:/home/opuser/.op/data"

  op-connect-sync:
    image: 1password/connect-sync:latest
    volumes:
      - "./1password-credentials.json:/home/opuser/.op/1password-credentials.json"
      - "./data:/home/opuser/.op/data"
```
同一ネットワークに属するDockerコンテナからアクセスする予定なので、ポートの開放等は必要ありません。

先程保存した`1password-credentials.json`はDockerコンテナ内で使用するため、`docker-compose.yml`と同一のディレクトリに保存してください。

```py/main.py
from onepasswordconnectsdk.client import (
    new_client_from_environment
)
from onepasswordconnectsdk.models import (
    Item, ItemVault, Field
)
import string
import secrets
import os
from time import sleep

def pass_gen(size=64):
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
    # if you want to add special characters
    # chars += '!@^*-_=+?/.>,<"\\:;|]}[{\'`~%&$#()'
    return ''.join(secrets.choice(chars) for x in range(size))

# creating client using OP_CONNECT_TOKEN environment variable
client = new_client_from_environment("http://op-connect-api:8080")


# creating Item
item = Item(vault=ItemVault(id=os.environ["VAULT_ID"]),
            title=pass_gen(5),
            category="LOGIN",
            tags=["1password-connect"],
            fields=[Field(
                value="new_user",
                purpose="USERNAME"
            ),
                Field(
                value=pass_gen(),
                purpose="PASSWORD"
            )],
        )
created_item = client.create_item(os.environ["VAULT_ID"], item)
print('created', created_item)
item_id = created_item.id
sleep(2)

# get Item
get_item = client.get_item(item_id, os.environ["VAULT_ID"])
print('got', get_item)
sleep(2)

# update item
update_item = get_item
update_item.fields[1].value = pass_gen()

updated_item = client.update_item(item_id, os.environ["VAULT_ID"], update_item)
print('updated', updated_item)
sleep(2)

# delete item
client.delete_item(item_id, os.environ["VAULT_ID"])

vaults = client.get_vaults()
for vault in vaults:
    print(vault)
```
::: note
補足
[こちら](https://github.com/1Password/connect-sdk-python/blob/main/src/onepasswordconnectsdk/client.py#L102-L124)を見るとわかりますが、`VAULT_ID`は`VAULT_NAME`でも構いません。

:::


プログラムから作成した直後にアイテムにアクセスできなかったりしたので、適当にsleepを入れています。

GitHubリポジトリは[こちら](https://github.com/ogadra/onepassword-docker-access)

こうしてPythonから1Passwordにアクセスできました :tada: 

## おわりに

スクレイピングを始めとする自動化はパスワード管理がネックになりがちですが、1Passwordならそれを解決することができます。みなさまも1Passwordの導入を検討してみてはいかがでしょうか。（1Passwordの広告案件ではありません。念の為。）