├── .github └── workflows │ └── stale-checker.yml ├── .gitignore ├── LICENSE ├── README.md ├── account-management ├── README.md ├── create_vaults_from_input.py ├── remove-permissions-groups-and-vault.py ├── requirements.txt └── vault-details.sh ├── connect └── reverse-sharing │ ├── Dockerfile │ ├── README.md │ ├── app.py │ ├── demo │ └── reverse-sharing-demo.mp4 │ ├── docker-compose.yml │ ├── requirements.txt │ └── templates │ ├── index.html │ └── login.html ├── dev-onboarding-guide ├── ssh-checker.sh └── ssh-guide.md ├── device-trust └── reporting-db │ └── app_report.sql ├── item-management ├── README.md ├── bulk-update-by-csv │ ├── README.md │ ├── csv_update.py │ └── sample.txt ├── bulk-update-url-field-by-vault.ps1 ├── bulk-update-url-field-by-vault.py ├── create-vault-based-on-item.sh ├── modify-field-type-by-vault.sh ├── recording-item-share.sh └── requirements.txt ├── migration ├── README.md ├── cleanup-scripts │ ├── README.md │ ├── add_vault_prefix.py │ ├── move_items_to_tracked_vault.py │ ├── recreate_metadata_entry.py │ ├── remove_imported_prefix.py │ └── vault_dedupe_helper.py └── lastpass │ ├── README.md │ ├── folder_migrate.py │ ├── lastpass-migrate.zip │ ├── lpass.py │ ├── main.py │ ├── template_generator.py │ ├── template_generator_test.py │ ├── utils.py │ ├── utils_test.py │ └── vault_item_import.py ├── onepassword_sdks ├── README.md ├── demo-inventory-tracker-webapp │ ├── .env.template │ ├── Dockerfile │ ├── README.md │ ├── app.js │ ├── docker-compose.yml │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── styles.css │ └── views │ │ ├── includes │ │ └── header.ejs │ │ ├── index.ejs │ │ ├── item.ejs │ │ └── items.ejs ├── demo-reverse-sharing │ ├── .env.template │ ├── Dockerfile │ ├── README.md │ ├── app.py │ ├── docker-compose.yml │ ├── requirements.txt │ └── templates │ │ └── index.html ├── demo-share-okta-user-script │ ├── .env.template │ ├── README.md │ ├── create-okta-user.py │ └── requirements.txt ├── demo-share-script │ ├── README.md │ ├── files │ │ ├── README.md │ │ └── op-dev.png │ ├── requirements.txt │ └── sdk_share_script.py ├── demo-vault-backup-webapp │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ ├── package.json │ ├── public │ │ └── .gitkeep │ ├── uploads │ │ └── .gitkeep │ ├── views │ │ ├── backup.ejs │ │ ├── restore.ejs │ │ └── welcome.ejs │ └── webapp.js └── demo-vault-migration-webapp │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ ├── package.json │ ├── public │ └── styles.css │ ├── views │ ├── migration.ejs │ └── welcome.ejs │ └── webapp.js ├── reporting ├── README.md ├── beta │ └── golang-lambda-deployment │ │ ├── README.md │ │ ├── bootstrap │ │ ├── bootstrap.zip │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go ├── user-access-list.py ├── user-and-item-list.py ├── vault-user-access-report.py └── vault-user-group-access-report.py ├── scripted-provisioning ├── README.md ├── people.csv ├── provisioning.ps1 └── provisioning.py └── user-management ├── README.md ├── identify-absentees.ps1 └── identify-absentees.py /.github/workflows/stale-checker.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: "0 4 * * *" 11 | workflow_dispatch: 12 | 13 | jobs: 14 | stale: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | days-before-stale: 14 25 | stale-issue-label: "stale:needs-attention" 26 | stale-issue-message: > 27 | This issue has been automatically marked as stale because it has not had recent activity. 28 | It will be closed in two weeks if no further activity occurs. 29 | 1Password employees have been nudged. 30 | stale-pr-label: "stale:needs-attention" 31 | stale-pr-message: > 32 | This PR has been automatically marked as stale because it has not had recent activity. 33 | It will be closed in two weeks if no further activity occurs. 34 | 1Password employees have been nudged. 35 | days-before-close: 14 36 | close-issue-message: > 37 | This issue has been automatically closed due to inactivity. 38 | Please re-open if this still requires attention. 39 | close-pr-message: > 40 | This pull request has been automatically closed due to inactivity. 41 | Please re-open if it is still required. 42 | exempt-issue-labels: "keep-alive" 43 | exempt-pr-labels: "keep-alive" 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | .csv 6 | 7 | # Icon must end with two \r 8 | Icon 9 | 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | *cursor 30 | *.log 31 | logs 32 | 33 | 34 | 35 | # Node stuff 36 | # Logs 37 | logs 38 | *.log 39 | npm-debug.log* 40 | yarn-debug.log* 41 | yarn-error.log* 42 | lerna-debug.log* 43 | .pnpm-debug.log* 44 | 45 | # Diagnostic reports (https://nodejs.org/api/report.html) 46 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 47 | 48 | # Runtime data 49 | pids 50 | *.pid 51 | *.seed 52 | *.pid.lock 53 | 54 | # Directory for instrumented libs generated by jscoverage/JSCover 55 | lib-cov 56 | 57 | # Coverage directory used by tools like istanbul 58 | coverage 59 | *.lcov 60 | 61 | # nyc test coverage 62 | .nyc_output 63 | 64 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 65 | .grunt 66 | 67 | # Bower dependency directory (https://bower.io/) 68 | bower_components 69 | 70 | # node-waf configuration 71 | .lock-wscript 72 | 73 | # Compiled binary addons (https://nodejs.org/api/addons.html) 74 | build/Release 75 | 76 | # Dependency directories 77 | node_modules 78 | jspm_packages/ 79 | 80 | # Snowpack dependency directory (https://snowpack.dev/) 81 | web_modules/ 82 | 83 | # TypeScript cache 84 | *.tsbuildinfo 85 | 86 | # Optional npm cache directory 87 | .npm 88 | 89 | # Optional eslint cache 90 | .eslintcache 91 | 92 | # Optional stylelint cache 93 | .stylelintcache 94 | 95 | # Microbundle cache 96 | .rpt2_cache/ 97 | .rts2_cache_cjs/ 98 | .rts2_cache_es/ 99 | .rts2_cache_umd/ 100 | 101 | # Optional REPL history 102 | .node_repl_history 103 | 104 | # Output of 'npm pack' 105 | *.tgz 106 | 107 | # Yarn Integrity file 108 | .yarn-integrity 109 | 110 | # dotenv environment variable files 111 | .env 112 | .env.development.local 113 | .env.test.local 114 | .env.production.local 115 | .env.local 116 | 117 | # parcel-bundler cache (https://parceljs.org/) 118 | .cache 119 | .parcel-cache 120 | 121 | # Next.js build output 122 | .next 123 | out 124 | 125 | # Nuxt.js build / generate output 126 | .nuxt 127 | dist 128 | 129 | # Gatsby files 130 | .cache/ 131 | # Comment in the public line in if your project uses Gatsby and not Next.js 132 | # https://nextjs.org/blog/next-9-1#public-directory-support 133 | # public 134 | 135 | # vuepress build output 136 | .vuepress/dist 137 | 138 | # vuepress v2.x temp and cache directory 139 | .temp 140 | .cache 141 | 142 | # Docusaurus cache and generated files 143 | .docusaurus 144 | 145 | # Serverless directories 146 | .serverless/ 147 | 148 | # FuseBox cache 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | .dynamodb/ 153 | 154 | # TernJS port file 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | .vscode-test 159 | .vscode/ 160 | # yarn v2 161 | .yarn/cache 162 | .yarn/unplugged 163 | .yarn/build-state.yml 164 | .yarn/install-state.gz 165 | .pnp.* 166 | events-api/events-lambda-js-test/.aws-sam/build/template.yaml 167 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/buildspec.yml 168 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/package-lock.json 169 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/package.json 170 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/README.md 171 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/samconfig.toml 172 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/template.yaml 173 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/__tests__/unit/handlers/scheduled-event-logger.test.mjs 174 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/events/event-cloudwatch-event.json 175 | events-api/events-lambda-js-test/.aws-sam/build/EventsAPI/src/handlers/main.mjs 176 | c-scripts/abk/create_abk_vaults.py 177 | c-scripts/abk/others.txt 178 | 179 | # Task 180 | .task/ 181 | 182 | .venv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 1Password 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Examples, templates, and other goodies from the 1Password Solutions team 2 | 3 |

✨ News!

4 | Introducing new item management with 1Password SDKs! 5 | 6 | * [Read all about the 1Password SDKs](https://developer.1password.com/docs/sdks/) 7 | * [Check out a demo app or script](/onepassword_sdks/) 8 | 9 | ## Introduction 10 | 11 | The 1Password [Command Line Interface](https://developer.1password.com/docs/cli/) (called `op` from this point forward) allows you to manage some aspects of a 1Password account, use secure secrets references to avoid storing secrets as plaintext environment variables, and perform CRUD actions on items you store in 1Password. 12 | 13 | This repository contains example and demo scripts for `op` with the intent of providing people with inspiration or a starting point for their own scripts. 14 | 15 | The scripts here assume you have the `op` [installed and configured](https://developer.1password.com/docs/cli/get-started). 16 | 17 | ## Complete 1Password command line tool documentation 18 | 19 | For full documentation of the 1Password command line interface, please visit [developer.1password.com/docs/cli/](https://developer.1password.com/docs/cli/) 20 | 21 | ## Handy tools 22 | 23 | `jq`, a command line tool with robust JSON support, is an essential tool when using `op`. Many of the provided examples use `jq` and you will need it installed before using any of the examples here. Download `jq` from [the developer](https://stedolan.github.io/jq/). 24 | 25 | ## Note 26 | 27 | Unless otherwise stated, these scripts are not intended to be run in an automated or unattended environment. 28 | 29 | Scripts provided here are not intended to be run as-is. They are intended as examples of how to perform certain tasks. You will need to modify the scripts to fit your exact needs and suite your specific environment. 30 | 31 | ## Contents 32 | 33 | * ✨ **NEW!** [Use 1Password SDKs to perform bulk actions](/onepassword_sdks/) 34 | * [Migrate from another password solution](migration/) 35 | * [Provision new users from a CSV](scripted-provisioning/) 36 | * [Scripts for auditing or managing existing users](user-management/) 37 | * [Scripts for managing your vaults and groups](account-management/) 38 | * [Scripts for creating or updating items in bulk, as well as item-sharing](item-management) 39 | -------------------------------------------------------------------------------- /account-management/README.md: -------------------------------------------------------------------------------- 1 | # Account management scripts 2 | 3 | ## Introduction 4 | 5 | These examples are intended to demonstrate how the 1Password command line tool can help you gain visibility and insight into your 1Password account. 6 | 7 | ## Script Descriptions 8 | 9 | ### [`create_vaults_from_input.py`](./create_vaults_from_input.py) 10 | This script will create a vault for each vault name in a file passed to the script with the `--file=` flag. Optionally remove your own access to the vault after creating it by running the script with the `--remove-me` flag. 11 | 12 | #### Usage 13 | 1. Install 1Password's CLI tool folowing the directions for your operating system here: https://developer.1password.com/docs/cli/get-started 14 | 2. In the 1Password desktop application enable the integration with the CLI, as documented here: https://developer.1password.com/docs/cli/get-started#step-2-turn-on-the-1password-desktop-app-integration 15 | 3. Open a terminal window and confirm: 16 | 4. the 1Password CLI (called `op`) is installed and working, and the desktop application integration is enabled, by trying `op signin` and signing into the desired 1Password account. 17 | 5. Ensure that you have a version of python installed by running `python --version` or `python3 --version` (ideally you have at least python 3.7). 18 | 6. Download this script to a folder on your computer (e.g., `~/Downloads`). 19 | 7. In your terminal, move to that folder (e.g., `cd ~/Downloads`) 20 | 8. Run the script by executing `python3 create_vaults_from_input.py --file=path/to/input/list` 21 | 22 | 23 | #### Options 24 | By default, when a person creates a vault, they are the "vault creator" and have full permissions for each vault they create. More than likely this will be desirable in your situation. 25 | 26 | However, if you'd like to have your access revoked so that you are not directly assigned to the vault after it's created, run the script using the `--remove-me` flag. 27 | * Note that even if you remove yourself from the vaults, members of the Owners and Administrators groups will still have "Manage Vault" permissions on these vaults, and will be able to manage their own or others' access to them. 28 | * To work around some current rate limiting issues related to permissions commands in the CLI, using the `--remove-me` flag will result in the script taking considerably longer to run (the script has an 11 second delay for each vault when modifying permissions to avoid rate limiting). 29 | 30 | ### [`remove-permissions-groups-and-vaults.py`](./remove-permissions-groups-and-vault.py) 31 | 32 | #### Description 33 | When run by a member of the Owners group, this script will remove permissions for every vault that any group you select has access to. 34 | 35 | When run by a non-Owner, this script will remove your selected permissions on vaults that the person running the script also has the `manage vault` permissions for. 36 | 37 | #### Requirements 38 | The Python script requires the `tqdm` package for progress bar functionality. Install it by running: 39 | ``` 40 | pip install -r requirements.txt 41 | ``` 42 | 43 | 44 | ### [`vault-details.sh`](vault-details.sh) 45 | 46 | When run by a member of the Owners group, this script provides the vault name, the number of items in the vault, the last time the vault contents were updated, and list which users and groups have access to that vault along with their permissions. 47 | 48 | When run by a non-Owner, it will provide these details for all vaults the user running the script has access to. -------------------------------------------------------------------------------- /account-management/create_vaults_from_input.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import argparse 3 | import csv 4 | import json 5 | import subprocess 6 | import sys 7 | import time 8 | 9 | parser = argparse.ArgumentParser( 10 | "Create a 1Password vault for each name in a list of vault names provided as a plaintext file using the --file flag.", 11 | "By default, you will have full permissions on each vault created by this script." 12 | "User the --remove-me flag if you'd like to revoke your own access to the vault after it is created. Members of the Owners and Administrators groups will still be able to manage this vault.", 13 | ) 14 | parser.add_argument( 15 | "--file", 16 | action="store", 17 | dest="filepath", 18 | help="Specify the path to a CSV file containing the required input data.", 19 | required=True, 20 | ) 21 | 22 | parser.add_argument( 23 | "--remove-me", 24 | action="store_true", 25 | dest="removeMe", 26 | help="Remove yourself from the vaults created by this script. To avoid rate limiting, this aggressively throttles the script and each vault will take 11 seconds to process.", 27 | ) 28 | 29 | args = parser.parse_args() 30 | 31 | 32 | # Get the User UUID of the person running the script. This is required for other parts of the script. 33 | def getMyUUID() -> str: 34 | print("Ensuring you're signed into 1Password and obtaining your User ID.\n") 35 | r = subprocess.run(["op", "whoami", "--format=json"], capture_output=True) 36 | 37 | # Catch error and kill process 38 | if r.returncode != 0: 39 | sys.exit( 40 | f"🔴 Unable to get your user UUID. Make sure you are are signed into the 1Password CLI. Error: {r.stderr.decode('utf-8')}" 41 | ) 42 | print(f"🟢 Obtained your User ID: {json.loads(r.stdout)['user_uuid']} \n") 43 | return json.loads(r.stdout)["user_uuid"] 44 | 45 | 46 | # Grants the group access to the corrosponding vault with defined permissions. 47 | def createVaultWithName(vault: str): 48 | retries = 0 49 | maxRetries = 3 50 | print(f"\t⌛ Attempting to create vault called {vault}.") 51 | while retries < maxRetries: 52 | r = subprocess.run( 53 | [ 54 | "op", 55 | "vault", 56 | "create", 57 | vault, 58 | ], 59 | capture_output=True, 60 | ) 61 | # time.sleep(11) 62 | # Handle rate limit error 63 | if "rate-limited" in r.stderr.decode("utf-8"): 64 | # Retry after waiting for 60 seconds 65 | print(r.stderr.decode("utf-8")) 66 | print("💤 Sleeping for 10 minutes, go grab a coffee.") 67 | time.sleep(600) 68 | retries += 1 69 | # Catch error but continue 70 | elif r.returncode != 0 and "rate-limited" not in r.stderr.decode("utf-8"): 71 | print( 72 | f"\t🔴 Unable to create vault named '{vault}'. Error: ", 73 | r.stderr.decode("utf-8"), 74 | ) 75 | break 76 | else: 77 | print(f"\t🟢 Successfully created '{vault}'") 78 | break 79 | 80 | 81 | # Revokes vault access for the person running the script. 82 | def removeCreatorPermissionsFor(vault: str, userID: str): 83 | retries = 0 84 | maxRetries = 3 85 | print(f"\t⌛ Attempting to remove your access to the newly created vault {vault}.") 86 | while retries < maxRetries: 87 | r = subprocess.run( 88 | ["op", "vault", "user", "revoke", f"--user={userID}", f"--vault={vault}"], 89 | capture_output=True, 90 | ) 91 | time.sleep(11) 92 | # Handle rate limit error 93 | if "rate-limited" in r.stderr.decode("utf-8"): 94 | # Retry after waiting for 60 seconds 95 | print(r.stderr.decode("utf-8")) 96 | print("💤 Sleeping for 10 minutes, go grab a coffee.") 97 | time.sleep(600) 98 | retries += 1 99 | # Catch error but continue 100 | elif r.returncode != 0 and "rate-limited" not in r.stderr.decode("utf-8"): 101 | print( 102 | f"\t🔴 There was an issue removing your access to the vault {vault}. Error: ", 103 | r.stderr.decode("utf-8"), 104 | ) 105 | return 106 | print(f"\t🟢 Succeeded in removing your access to vault {vault}.\n\n") 107 | return 108 | 109 | 110 | def main(): 111 | myUUID: str = getMyUUID() 112 | # Open the csv passed via the --file flag 113 | with open(args.filepath, "r", newline="", encoding="utf-8") as inputFile: 114 | csvReader = csv.reader(inputFile, skipinitialspace=True) 115 | for row in csvReader: 116 | vault: str = row[0].strip() 117 | createVaultWithName(vault) 118 | 119 | # If --remove-me flag was used, remove the script-runner's permission 120 | if args.removeMe: 121 | removeCreatorPermissionsFor(vault, myUUID) 122 | 123 | 124 | main() 125 | -------------------------------------------------------------------------------- /account-management/requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm -------------------------------------------------------------------------------- /account-management/vault-details.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script will loop through all vaults the currently-logged-in user has access to. 3 | # For each vault, it will provide the vault name, the number of items in the vault 4 | # the last time the vault contents were updated, and list which users and groups have access 5 | # to the vault along with their permissions. 6 | op signin 7 | for vault in $(op vault list --format=json | jq --raw-output '.[] .id') 8 | do 9 | echo "" 10 | echo "Vault Details" 11 | op vault get $vault --format=json | jq -r '.|{name, items, updated_at}' 12 | sleep 1 13 | echo "" 14 | echo "Users" 15 | op vault user list $vault 16 | sleep 1 17 | echo "" 18 | echo "Groups" 19 | op vault group list $vault 20 | sleep 1 21 | echo "" 22 | echo "End of Vault Details" 23 | sleep 2 24 | clear 25 | echo "" 26 | echo "" 27 | done -------------------------------------------------------------------------------- /connect/reverse-sharing/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.13-alpine 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the dependencies file to the working directory 8 | COPY requirements.txt . 9 | 10 | # Install any needed packages specified in requirements.txt 11 | # --no-cache-dir reduces image size 12 | # --trusted-host pypi.python.org can sometimes help bypass network issues in restricted environments 13 | RUN pip install --no-cache-dir --trusted-host pypi.python.org -r requirements.txt 14 | 15 | # Copy the current directory contents into the container at /app 16 | COPY . . 17 | 18 | # Make port 5000 available to the world outside this container 19 | # Note: Gunicorn will bind to this port. Flask dev server also uses it by default. 20 | EXPOSE 5000 21 | 22 | # Define environment variables (placeholders, should be set during 'docker run') 23 | # These are just examples, DO NOT HARDCODE secrets here. 24 | # ENV OP_VAULT_UUID=your_vault_id 25 | # ENV FLASK_SECRET_KEY=set_a_strong_secret_key_here 26 | 27 | # Run app.py using gunicorn when the container launches 28 | # Gunicorn is a production-ready WSGI server. 29 | # --bind 0.0.0.0:5000 makes the app accessible from outside the container. 30 | # app:app refers to the Flask application instance 'app' found in the file 'app.py'. 31 | CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] -------------------------------------------------------------------------------- /connect/reverse-sharing/README.md: -------------------------------------------------------------------------------- 1 | # Basic 1Password Connect Item Creator Webapp 2 | 3 | A very simple web application built with Flask and deployable via Docker. It provides a basic web form to create new Login items in a specified 1Password vault using the 1Password Connect REST API. **Note:** This version is a proof of concept that can be deployed locally. For production use, look at our [connect documentation](https://developer.1password.com/docs/connect/get-started/). 4 | 5 | ## Features 6 | 7 | - **Create 1Password Items:** Simple web form to input Title, Username, Password, and Notes for new Login items. 8 | - **1Password Connect Integration:** Interacts directly with your 1Password Connect server API. 9 | - **Web Interface:** Minimal interface built with Flask. 10 | - **Dockerized:** Easy deployment using Docker. 11 | - **Configuration via Environment:** Essential 1Password Connect settings are managed through environment variables. 12 | 13 | ## 1Password Setup 14 | 15 | 1. Create a vault in 1Password exclusively for this application and note the UUID. 16 | 2. In 1Password admin console, go to Developer -> Connect servers and create a new connect server with `Read, Write` permissions to that vault. Save the 1password-credentials.json file and place in this directory. 17 | 3. Create an access token with only `write` permissions to that vault. 18 | 4. Copy the token and save it securely. You will need it to create items in the vault. 19 | 20 | ## Prerequisites 21 | 22 | Before you begin, ensure you have the following installed and configured: 23 | 24 | 1. **Docker Engine:** [Install Docker](https://docs.docker.com/engine/install/) 25 | 2. **Environment Variables:** Set up the following environment variables in a `.env` file in this directory: 26 | - `OP_VAULT_UUID`: The UUID of the target 1Password vault. 27 | - `FLASK_SECRET_KEY`: (Recommended) A secret key for Flask, needed for features like flashing messages. While not strictly required for _this specific basic version's core function_, it's good practice to include it. Generate a random string (e.g., using `openssl rand -hex 32`). 28 | 29 | ## Project Structure 30 | 31 | Ensure you have the following files in your project directory: 32 | 33 | ``` 34 | ├── app.py # Flask application logic 35 | ├── Dockerfile # Docker build instructions 36 | ├── docker-compose.yml # Docker Compose file for easier management 37 | ├── requirements.txt # Python dependencies 38 | ├── README.md # This file 39 | ├── 1password-credentials.json # Credentials file for 1Password Connect 40 | ├── .env file # Environment variables for Docker Compose 41 | └── templates/ 42 | └── index.html # HTML form template for creating items 43 | └── login.html # HTML form template for entering bearer token 44 | ``` 45 | 46 | ## Running the Application 47 | 48 | 1. **Navigate:** Open a terminal or command prompt and navigate to the project directory containing the `Dockerfile` and application files. 49 | 2. **Build and run the Docker Image, tailing the logs:** 50 | 51 | ```bash 52 | docker compose up -d --build && docker compose logs -f webapp 53 | ``` 54 | 55 | 3. **Access the Application:** Open your web browser and navigate to `http://localhost:5001`. Enter the bearer token with write access to the target vault (the API token for your 1Password Connect server) in the form. 56 | 4. **Create Item:** Fill in the Title (required), Username, Password, and Notes fields for the new 1Password Login item. 57 | 5. **Submit:** Click "Create Item". You will see a success or error message displayed on the page. 58 | 6. **Check 1Password:** Log in to your 1Password account and check the specified vault to see the newly created item. 59 | -------------------------------------------------------------------------------- /connect/reverse-sharing/demo/reverse-sharing-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Password/solutions/51d749b42523f5c4d8213696302d0183369a9ea7/connect/reverse-sharing/demo/reverse-sharing-demo.mp4 -------------------------------------------------------------------------------- /connect/reverse-sharing/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # Define the web application service 3 | webapp: 4 | # Build the Docker image using the Dockerfile in the current directory (.) 5 | build: . 6 | networks: [op-connect] 7 | depends_on: [api, sync] 8 | # Name the container for easier identification 9 | container_name: op_connect_webapp_service 10 | # Map port 5001 on the host machine to port 5000 inside the container 11 | # This makes the web app accessible via http://localhost:5001 12 | ports: 13 | - "5001:5000" 14 | # Define environment variables needed by the Flask application (app.py) 15 | # It's highly recommended to use a .env file for secrets 16 | environment: 17 | # These variables will be read from a .env file in the same directory 18 | # or from your host machine's environment variables if the .env file is missing/incomplete 19 | - OP_CONNECT_HOST=http://api:8080 # The host and port of the Connect API 20 | - OP_VAULT_UUID=${OP_VAULT_UUID} 21 | - FLASK_SECRET_KEY=${FLASK_SECRET_KEY} # Important for Flask sessions/flashing 22 | # Optional: Set Flask environment to production (disables debug mode) 23 | # - FLASK_ENV=production 24 | # Define the restart policy for the container 25 | # 'unless-stopped': restarts the container unless it was explicitly stopped 26 | restart: unless-stopped 27 | # Optional: Mount local code for development (uncomment if needed) 28 | # This allows code changes to be reflected without rebuilding the image 29 | # volumes: 30 | # - .:/app 31 | api: 32 | image: 1password/connect-api 33 | networks: [op-connect] 34 | deploy: 35 | replicas: 1 36 | restart_policy: 37 | condition: any 38 | secrets: 39 | - source: credentials 40 | target: /home/opuser/.op/1password-credentials.json 41 | uid: "999" 42 | gid: "999" 43 | mode: 0400 44 | volumes: 45 | - data:/home/opuser/.op/data 46 | user: "999:999" 47 | 48 | sync: 49 | image: 1password/connect-sync 50 | networks: [op-connect] 51 | deploy: 52 | replicas: 1 53 | restart_policy: 54 | condition: any 55 | secrets: 56 | - source: credentials 57 | target: /home/opuser/.op/1password-credentials.json 58 | uid: "999" 59 | gid: "999" 60 | mode: 0400 61 | volumes: 62 | - data:/home/opuser/.op/data 63 | user: "999:999" 64 | 65 | volumes: 66 | data: 67 | 68 | secrets: 69 | credentials: 70 | file: 1password-credentials.json 71 | name: credentials 72 | 73 | networks: 74 | op-connect: 75 | # Note: No external networks or volumes are defined as this is a simple single-service setup 76 | # relying on environment variables for configuration. 77 | -------------------------------------------------------------------------------- /connect/reverse-sharing/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=2.0 2 | requests>=2.20 3 | gunicorn>=20.0 # Recommended for running Flask in Docker -------------------------------------------------------------------------------- /connect/reverse-sharing/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Create 1Password Item 7 | 88 | 89 | 90 |
91 | 94 |

Create New 1Password Login Item

95 | 96 | {% if message %} 97 |
98 | {{ message }} 99 |
100 | {% endif %} 101 | 102 |
103 |
104 | 105 | 106 |
107 |
108 | 109 | 110 |
111 |
112 | 113 | 114 |
115 |
116 | 117 | 118 |
119 |
120 | 121 |
122 |
123 |
124 | 125 | 126 | -------------------------------------------------------------------------------- /connect/reverse-sharing/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login - 1Password Item Creator 7 | 73 | 74 | 75 |
76 |

Enter API Token

77 | 78 | {% with messages = get_flashed_messages(with_categories=true) %} {% if 79 | messages %} {% for category, message in messages %} 80 |
{{ message }}
81 | {% endfor %} {% endif %} {% endwith %} 82 | 83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 |
91 |
92 |
93 | 94 | 95 | -------------------------------------------------------------------------------- /dev-onboarding-guide/ssh-checker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "1Password SSH Setup Checker" 4 | echo "------------------------------------" 5 | 6 | # Color definitions (optional) 7 | GREEN='\033[0;32m' 8 | RED='\033[0;31m' 9 | YELLOW='\033[1;33m' 10 | NC='\033[0m' # No Color 11 | 12 | # Helper function: display check status 13 | print_status() { 14 | if [ "$2" = "OK" ]; then 15 | echo -e "[${GREEN}OK${NC}] $1" 16 | elif [ "$2" = "FAIL" ]; then 17 | echo -e "[${RED}FAIL${NC}] $1" 18 | elif [ "$2" = "WARN" ]; then 19 | echo -e "[${YELLOW}WARN${NC}] $1" 20 | else 21 | echo -e "[INFO] $1" 22 | fi 23 | } 24 | 25 | # 1. Check SSH_AUTH_SOCK environment variable 26 | echo 27 | print_status "Checking SSH_AUTH_SOCK environment variable..." 28 | if [ -n "$SSH_AUTH_SOCK" ]; then 29 | print_status "SSH_AUTH_SOCK is set: $SSH_AUTH_SOCK" "OK" 30 | if [[ "$SSH_AUTH_SOCK" == *1Password* || "$SSH_AUTH_SOCK" == *1password* ]]; then 31 | print_status "SSH_AUTH_SOCK appears to point to a 1Password agent." "OK" 32 | else 33 | print_status "SSH_AUTH_SOCK may not point to a 1Password agent." "WARN" 34 | echo " Current value: $SSH_AUTH_SOCK" 35 | echo " Please ensure the SSH agent is enabled in 1Password desktop settings," 36 | echo " restart your terminal, or check your shell configuration file (~/.zshrc, ~/.bash_profile, etc.)." 37 | fi 38 | else 39 | print_status "SSH_AUTH_SOCK is not set." "FAIL" 40 | echo " Please ensure the SSH agent is enabled in 1Password desktop settings," 41 | echo " restart your terminal, or check your shell configuration file." 42 | fi 43 | 44 | # 2. Check if keys are loaded in the SSH agent (ssh-add -L) 45 | echo 46 | print_status "Checking for keys loaded in the SSH agent (ssh-add -L)..." 47 | # ssh-add -L can return an error code if the agent isn't running, so capture output 48 | agent_keys_output=$(ssh-add -L 2>&1) 49 | agent_keys_exit_code=$? 50 | 51 | if [ $agent_keys_exit_code -eq 0 ]; then 52 | if [[ "$agent_keys_output" == *"1Password"* || "$agent_keys_output" == *"1password"* || "$agent_keys_output" == *"//"* ]]; then # Common 1P key comment format 53 | print_status "1Password managed keys were listed from the SSH agent." "OK" 54 | echo " Listed keys:" 55 | echo -e " ${agent_keys_output}" 56 | elif [[ "$agent_keys_output" == "The agent has no identities." ]]; then 57 | print_status "No keys are loaded in the SSH agent." "WARN" 58 | echo " Ensure you have generated/imported SSH keys in 1Password and they are configured for use with the agent." 59 | echo " Alternatively, if your 1Password app is locked, try unlocking it." 60 | else 61 | print_status "Keys were listed from the SSH agent, but it's unclear if they are from 1Password." "WARN" 62 | echo " Listed keys:" 63 | echo -e " ${agent_keys_output}" 64 | echo " Please verify these are the keys you manage with 1Password." 65 | fi 66 | elif [ $agent_keys_exit_code -eq 2 ]; then # Error code for "Could not open a connection to your authentication agent." 67 | print_status "Could not connect to SSH agent. Is it running? (ssh-add -L returned error)" "FAIL" 68 | echo " Check your SSH_AUTH_SOCK setting and the status of the 1Password SSH agent." 69 | else # Other errors 70 | print_status "Error running ssh-add -L (Code: $agent_keys_exit_code)." "FAIL" 71 | echo " Error output: $agent_keys_output" 72 | fi 73 | 74 | # 3. Test SSH connection to GitHub (optional) 75 | echo 76 | print_status "Testing SSH connection to GitHub (ssh -T git@github.com)..." 77 | echo " This test will attempt to authenticate to GitHub." 78 | echo " Observe if 1Password prompts for biometrics or your system password." 79 | echo " A successful message often looks like 'Hi username! You've successfully authenticated...'" 80 | echo " (Do you want to run this test? [y/N])" 81 | read -r run_github_test 82 | 83 | if [[ "$run_github_test" =~ ^[Yy]$ ]]; then 84 | echo " Running: ssh -T git@github.com ..." 85 | ssh -T git@github.com 86 | echo " Test complete. Check the output above and any 1Password prompts." 87 | else 88 | print_status "GitHub connection test was skipped." 89 | fi 90 | 91 | echo 92 | echo "------------------------------------" 93 | echo "Check complete. If you encounter any issues, please refer to the onboarding guide" 94 | echo "or consult your team lead." 95 | -------------------------------------------------------------------------------- /device-trust/reporting-db/app_report.sql: -------------------------------------------------------------------------------- 1 | -- "Mac Apps Report" 2 | -- Internal-1P only https://app.kolide.com/4918/reporting/queries/2080 3 | 4 | -- Reporting DB query to retrieve all mac_apps installed across the fleet, 5 | -- filtering out a list of "approved apps" such as 1Password and anything 6 | -- built by either Apple or Google using their bundle_identifier. 7 | 8 | -- The final report contains an ordered list of "unapproved" apps with a 9 | -- JSON formatted device table containing the device name, serial and admin URL. 10 | 11 | WITH device_info AS ( 12 | SELECT 13 | id as device_id, 14 | name, 15 | serial, 16 | k2_url, 17 | id || ' (' || name || ')' as device_name 18 | FROM 19 | devices 20 | ), 21 | 22 | apps AS ( 23 | SELECT 24 | * 25 | FROM 26 | mac_apps 27 | WHERE 28 | 1=1 29 | AND path LIKE '/Applications%' 30 | AND name NOT LIKE '1Password%.app' 31 | AND bundle_identifier NOT LIKE 'com.apple.%' 32 | AND bundle_identifier NOT LIKE 'com.google.%' 33 | ) 34 | 35 | SELECT 36 | a.name, 37 | a.bundle_identifier, 38 | COUNT(*) as count, 39 | JSON_AGG( 40 | JSON_BUILD_OBJECT( 41 | 'device_name', d.device_name, 42 | 'device_serial', d.serial, 43 | 'url', d.k2_url 44 | ) ORDER BY d.device_name 45 | ) as device_table 46 | FROM apps as a 47 | JOIN device_info as d on d.device_id = a.device_id 48 | GROUP BY 1, 2 49 | ORDER BY count DESC -------------------------------------------------------------------------------- /item-management/README.md: -------------------------------------------------------------------------------- 1 | # Item management scripts 2 | 3 | ## Introduction 4 | 5 | These scripts are examples of how to perform bulk creation, read, update, or delete actions on items in 1Password. As well as an example script to create a temporary 1Password item to use for item sharing. 6 | 7 | For more information about managing items with the 1Password command line tool, see the complete documentation for [op item subcommands](https://developer.1password.com/docs/cli/reference/management-commands/item). 8 | 9 | ## Use-cases 10 | 11 | 12 | ### Bulk selectors 13 | 14 | * You can select items based on several criteria, including the the vault they are stored in, one or more tags, or the value of a field, such as website or username. 15 | * Examples of performing bulk actions on every item in a vault: 16 | * [bulk-update-url-field-by-vault.py](bulk-update-url-field-by-vault.py) or [bulk-update-url-field-by-vault.ps1](bulk-update-url-field-by-vault.ps1) 17 | * [modify-field-type-by-vault.sh](modify-field-type-by-vault.sh) 18 | * Examples of performing bulk actions on all items with a specific tag: 19 | 20 | For more details on how to select items, see [op item list](https://developer.1password.com/docs/cli/reference/management-commands/item#item-list). 21 | 22 | ### Modify the website value of multiple items 23 | 24 | **Requirements**: The Python script requires the `tqdm` package for progress bar functionality. Install it by running: 25 | ``` 26 | pip install -r requirements.txt 27 | ``` 28 | 29 | If you need to make the same change to the website field of many items, scripts that provide this functionality in both Python and Powershell are available: 30 | 31 | * Python: [bulk-update-url-field-by-vault.py](bulk-update-url-field-by-vault.py) 32 | * Powershell [bulk-update-url-field-by-vault.ps1](bulk-update-url-field-by-vault.ps1). 33 | 34 | ### Change field type while retaining value 35 | 36 | If you have multiple items with incorrect field types but the correct value (e.g., the field is of type `text` but the value is a password, PIN, or other secret), which may happen when importing customized items from outside 1Password, [modify-field-type-by-vault.sh](modify-field-type-by-vault.sh) provides an example of how to convert multiple fields of type `text` to type `password` without changing the value of those fields. 37 | 38 | ### Share Zoom Recording Link using 1Password Item Share 39 | 40 | If you have a Zoom Recording Link that you are looking to share, this script will create a placeholder 1Password Login Item, based off some of the information promoted for. It takes in a standard Zoom Recording link and seperates the two lines out into the URL and Password, to add to the 1Password item accordingly. Lastly it creates a [item-share link](https://developer.1password.com/docs/cli/reference/management-commands/item#item-share) to send out to the provided email addresses. [recording-item-share.sh](recording-item-share.sh) provides an example of how you can utilize the 1Password Item Share to distribute temporary credentials to contacts outside your 1Password account in a secure fashion. 41 | 42 | ### Create Vault based on item content 43 | 44 | Through this script, [create-vault-based-on-item.sh](create-vault-based-on-item.sh), items in a certain vault are checked, and a new vault is created based of a field in the item. From there the original item is recreated in the new vault and removed from the "landing" vault. The concent behind this, is that perhaps 1Password Connect is in place that is tied to a "landing" vault where new items are being created through a web form and Secrets Automation (this part is not part of the script). 45 | -------------------------------------------------------------------------------- /item-management/bulk-update-by-csv/README.md: -------------------------------------------------------------------------------- 1 | # 1Password Item Updater Script 2 | 3 | This Python script automates the process of updating passwords for items stored in your 1Password vaults using the 1Password Command Line Interface (op). It reads item details and new passwords from a CSV file and applies the changes. 4 | 5 | ## Features 6 | 7 | - Updates passwords for specified 1Password items 8 | - Reads input data from a CSV file 9 | - Identifies the correct password field to update (prioritizes primary password fields, then generic concealed fields) 10 | - Uses the official 1Password CLI (op) for secure interaction with your vaults 11 | - Provides console output for progress and error reporting 12 | 13 | ## Prerequisites 14 | 15 | - **1Password Account**: You need an active 1Password account 16 | - **1Password CLI (op)**: The 1Password Command Line Interface must be installed and configured on your system 17 | - **Signed into op CLI**: You must be signed into your 1Password account via the op CLI. You can do this by running `op signin` and following the prompts. If you use biometric unlock, ensure it's enabled for the CLI 18 | - **Python 3**: Python 3.6 or newer is required to run the script 19 | - **CSV File**: A CSV file containing the items to update, formatted as described below 20 | 21 | ## CSV File Format 22 | 23 | The script expects a CSV file with the following exact column headers: 24 | 25 | - `vault`: The name of the 1Password vault containing the item 26 | - `item title`: The exact title of the item in 1Password 27 | - `new password`: The new password to set for the item 28 | 29 | ### Example CSV (passwords_to_update.csv) 30 | 31 | ```bash 32 | vault,item title,new password 33 | Personal,My Website Login,aVeryStrongNewPassword123! 34 | Work,Server SSH Key,anotherSecureP@ssw0rd 35 | Shared Vault,Database Credentials,"complex_p@$$wOrd_with_commas,and_quotes" 36 | ``` 37 | 38 | ### Notes on CSV format 39 | 40 | - The header row is mandatory and must match the names above 41 | - If a new password (or any other field) contains a comma, enclose the entire field value in double quotes (`"`) 42 | 43 | ## Installation & Setup 44 | 45 | 1. Download the Script: Save the Python script (e.g., `update_op_passwords.py`) to your local machine 46 | 2. Make it Executable (Optional but Recommended for Linux/macOS): 47 | 48 | ```bash 49 | chmod +x update_op_passwords.py 50 | ``` 51 | 52 | 3. Prepare your CSV file as described in the "CSV File Format" section 53 | 54 | ## Usage 55 | 56 | Run the script from your terminal, providing the path to your CSV file as a command-line argument: 57 | 58 | Using python3: 59 | 60 | ```bash 61 | python3 update_op_passwords.py /path/to/your/passwords_to_update.csv 62 | ``` 63 | 64 | If you made it executable (Linux/macOS): 65 | 66 | ```bash 67 | ./update_op_passwords.py /path/to/your/passwords_to_update.csv 68 | ``` 69 | 70 | The script will then: 71 | 72 | 1. Verify it can connect to the op CLI 73 | 2. Read each row from your CSV file 74 | 3. For each item: 75 | - Fetch the item details from the specified vault 76 | - Identify the appropriate concealed field (password field) 77 | - Update the field with the new password from the CSV 78 | 4. Print progress messages or any errors encountered to the console 79 | 80 | ## How it Identifies the Password Field 81 | 82 | The script attempts to find the correct field to update using the following logic: 83 | 84 | 1. It looks for a field explicitly marked with `purpose: "PASSWORD"` in the item's JSON structure (this is common for Login items) 85 | 2. If no such field is found, it looks for the first field with `type: "CONCEALED"` 86 | 3. It constructs the necessary field designator for the `op item edit` command, including the section name if the field is part of a section (e.g., `Section Name.Field Label` or just `Field Label`) 87 | 88 | If you need to update a specific custom concealed field when multiple exist and none are marked as the primary password, this script might need modification. 89 | 90 | ## Important Considerations & Warnings 91 | 92 | - **BACKUP YOUR VAULT DATA**: Before running any script that modifies your 1Password data, ensure you have a reliable backup or are comfortable with the changes. Use this script at your own risk. 93 | - **Test Carefully**: It's highly recommended to test the script with a single, non-critical item first to ensure it behaves as expected in your environment. 94 | - **Security of CSV File**: The new password column in your CSV file contains sensitive information in plaintext. 95 | - Store this file securely 96 | - Restrict access to it 97 | - Consider deleting the file or at least the new password column after the update process is successfully completed and verified 98 | - **Error Handling**: The script includes basic error handling. Pay close attention to any error messages printed in the console, as they can help diagnose issues (e.g., item not found, incorrect vault name, op CLI problems). 99 | - **Rate Limiting**: While generally not an issue for typical CLI usage, updating an extremely large number of items very rapidly could potentially encounter rate limits from the 1Password service. 100 | - **Exact Item Titles**: The script relies on exact matches for "item title". Ensure these are correct in your CSV. 101 | 102 | ## Troubleshooting 103 | 104 | - **'op' command not found**: Ensure the 1Password CLI is installed and its location is in your system's PATH environment variable. 105 | - **Error: 1Password CLI 'op account list' command failed**: You are likely not signed into the op CLI. Run `op signin` and complete the authentication process. 106 | - **Item not found / Vault not found**: Double-check the vault and item title entries in your CSV file for typos or case sensitivity issues. Vault and item names must be exact. 107 | - **Could not find a suitable concealed field**: The item specified might not have any fields of type "CONCEALED", or the script could not identify one. Check the item structure in 1Password. 108 | 109 | ## Disclaimer 110 | 111 | This script is provided as-is, without any warranty. Always exercise caution when automating actions on sensitive data. 112 | -------------------------------------------------------------------------------- /item-management/bulk-update-by-csv/sample.txt: -------------------------------------------------------------------------------- 1 | vault,item title,new password 2 | Personal,My Website Login,aVeryStrongNewPassword123! 3 | Work,Server SSH Key,anotherSecureP@ssw0rd 4 | Shared Vault,Database Credentials,"complex_p@$$wOrd_with_commas,and_quotes" -------------------------------------------------------------------------------- /item-management/create-vault-based-on-item.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script will loop through vault specied in the vaultUUID variable. 3 | # For each item in this vault, it will create (without administrator group management permissions) 4 | # a new vault based off the name in the fieldvalue variable. 5 | # This could be a custom field such as "Company" in all the items in the vault being looped through. 6 | # The original item, is then created in the new vault and removed from the original vault. 7 | 8 | # This script was inspired by a use case where 1Password Connect is tied to a "landing" vault where new items 9 | # are being created through a web form for new clients/companies in this landing vault, where the new items 10 | # are only stored until they are moved to a specific vault for the client/company. 11 | 12 | #Change this value to the vault that you are moving items from, your landing vault. 13 | vaultUUID= 14 | 15 | #Change the value of this to match the corresponding field you are looking in the item in this landing vault. 16 | fieldvalue=Company 17 | 18 | # creates new vault based of item detail from field from variable `fieldvalue` specified above. 19 | # The field must be valid for the specified vault. 20 | for item in $(op item list --vault $vaultUUID --format=json | jq --raw-output '.[].id') 21 | do 22 | vaultname=`op item get $item --fields $fieldvalue` 23 | op vault create $vaultname --allow-admins-to-manage false 24 | op item get $item --format json | op item create --vault $vaultname 25 | op item delete $item --archive --vault $vaultUUID 26 | done -------------------------------------------------------------------------------- /item-management/modify-field-type-by-vault.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ######################################################### 3 | # MODIFY FIELD TYPE WHILE RETAINING VALUE 4 | ######################################################### 5 | # This example shows how to change the type of a field (e.g., from 6 | # "text" to "password") without changing the value of that field. 7 | # This script assumes that you are modifying multiple items that have the same 8 | # structure. In this case, this script assumes: 9 | # - You are changing "text" field types to "password" field types 10 | # - You are adjusting two top-level field called "Test field 1" 11 | # - You are adjusting four fields in a section called "Example" 12 | # - You can use the following example to generate 10 items with this 13 | # exact structure so you have sample data to experiment with. 14 | # ============================================================================== 15 | # GENERATE SAMPLE DATA 16 | # ============================================================================== 17 | # 18 | # count=0 19 | # while [ $count -le 10 ] 20 | # do 21 | # op item create --category=login --title="my example item $count" --vault= \ 22 | # --url=https://www.1password.com --generate-password=20,letters,digits \ 23 | # username=scott@acme.com \ 24 | # 'Test Field 1[text]=my test secret' \ 25 | # 'Additional Password 1[password]=mypassword1' \ 26 | # 'Example.Additional Password 2[text]=mypassword2' \ 27 | # 'Example.Additional Password 3[text]=mypassword3' \ 28 | # 'Example.Additional Password 4[text]=mypassword4' \ 29 | # 'Example.Additional Password 5[text]=mypassword5' 30 | # ((count++)) 31 | # done 32 | # 33 | # ============================================================================== 34 | # CHANGE FIELD TYPE IN SAMPLE DATA 35 | # ============================================================================== 36 | # This script will change all custom text fields to password fields in the sample data created above. 37 | # The value stored in the field will remain unchanged. This example will change all items in a vault 38 | # however you could alternatively select items by tag, name, or UUID instead. 39 | 40 | vaultUUID= 41 | for item in $(op item list --vault $vaultUUID --format=json | jq --raw-output '.[].id') 42 | do 43 | op item edit $item 'Test Field 1[password]' \ 44 | 'Example.Additional Password 2[password]' \ 45 | 'Example.Additional Password 3[password]' \ 46 | 'Example.Additional Password 4[password]' \ 47 | 'Example.Additional Password 5[password]' 48 | done -------------------------------------------------------------------------------- /item-management/recording-item-share.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #This script help you automate the creation of a 1Password Item to use for an item-sharing link for the emails provided in the prompts 4 | #The script cleans up the 1Password item after generating the share-item link. 5 | 6 | #set the Role for the Item Title, e.g. 1Password Solutions Architect 7 | role="1Password Solutions Architect" 8 | 9 | op signin 10 | 11 | echo "Enter the customer organization name:" 12 | read company 13 | 14 | #creates Item Title based off company name 15 | itemTitle="${company} + ${role} - Zoom Recording" 16 | 17 | #takes multi-line input of typical zoom link copy 18 | echo -e "\033[0;31mEnter the copied share link from Zoom\033[0m" 19 | echo -e "\033[0;32mUse the Enter key twice to exit the input\033[0m" 20 | zoomPaste=$(sed '/^$/q') 21 | stringarray=($zoomPaste) 22 | zoomURL=${stringarray[0]} 23 | zoomPC=${stringarray[2]} 24 | 25 | #creates new item for sharing 26 | op item create --vault private --category=login --title="${itemTitle}" --url="${zoomURL}" password="${zoomPC}" notes="This recording will only be accessible for 7 days." 27 | 28 | #Create item share link to send out to the customer 29 | echo -e "\033[0;31mEnter the customer email address(es), comma seperated:\033[0m" 30 | read emailadd 31 | echo "" 32 | echo "Generating a Share Item link for" "$emailadd" "to access:" "$itemTitle" 33 | op item share "$itemTitle" --vault Private --emails $emailadd --expiry 168h 34 | 35 | echo -e "\033[0;32mCopy the link above and share this link with ${emailsadd}\033[0m" 36 | 37 | #deletes created item to clean up vault. (remove the archive flag if you want to fully delete the item) 38 | op item delete "$itemTitle" --vault Private --archive 39 | -------------------------------------------------------------------------------- /item-management/requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm -------------------------------------------------------------------------------- /migration/README.md: -------------------------------------------------------------------------------- 1 | # Migration tools 2 | 3 | This section contains scripts to assist with migrating or importing data into 1Password. 4 | 5 | If you are migrating from another password management system, please visit to learn more about your options. 6 | 7 | If you are migrating from LastPass, read more about our LastPass importer here: 8 | 9 | ## Contents 10 | 11 | ### [/cleanup-scripts](./cleanup-scripts/) 12 | This directory contains several scripts to help people identify or clean up certain things after performing a migration from another password manager. Currently the scripts target certain tasks that LastPass migrators may encounter. 13 | 14 | * `vault_dedupe_helper.py` generates a csv-like report containing a list of all shared vaults you have access to, along with metadata about each vault. If, after migrating, you ended up with some duplicate vaults, this report might help you identify which duplicates to delete, and which one to retain 15 | * `add_vault_prefix.py` acts on a list of vault UUIDs. By default, it will prefix each vault name with `!` to make it easier to sort and further assess potential duplicates. This script can optionally be run in Delete Mode, which will delete the vault. 16 | 17 | ### [/lastpass](./lastpass/) 18 | This directory contains a script that offers a variety of ways to import data from LastPass to 1Password. 19 | 20 | We strongly encourage you to use the LastPass import tool included with 1Password for macOS, Windows, and Linux. If you are migrating from LastPass, read more about our LastPass importer here: 21 | 22 | However, this script may be helpful in certain scenarios, or as a demonstration of the capabilities of the [1Password CLI](https://developer.1password.com/docs/cli). -------------------------------------------------------------------------------- /migration/cleanup-scripts/add_vault_prefix.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import argparse 3 | import json 4 | import sys 5 | 6 | parser = argparse.ArgumentParser("Vault Prefixer", "Provide a path to a list of vault UUIDs.", "Prefixes all vaults passed to the script from the specified file with '!'.") 7 | parser.add_argument("--file", action="store", dest="filepath", help="Required. Specify the path to a file containing a linebreak-delimited list of vault UUIDs you'd like to process.", required=True) 8 | parser.add_argument("--delete", action="store_true", dest="deletemode", help="Use this option to delete vaults in the provided list, rather than append `!` to the vault name. USE WITH CAUTION.") 9 | args = parser.parse_args() 10 | 11 | # Prefix each vault in the input list with an exclamation mark to make them easy to locate and assess duplicate status. {} 12 | def prefixVault(): 13 | print("Prefixing all listed vaults with '!' for easy sorting and identification.\n\n") 14 | with open(args.filepath, "r", encoding="utf-8") as vaultList: 15 | for vault in vaultList: 16 | vaultID = vault.rstrip() 17 | try: 18 | vaultDetails = json.loads(subprocess.run(f"op vault get {vaultID} --format=json", shell=True, check=True, capture_output=True).stdout) 19 | vaultName = vaultDetails['name'] 20 | except (Exception) as err: 21 | print(f"Encountered an error getting the details for vault with ID {vaultID}: ", err) 22 | continue 23 | try: 24 | subprocess.run(["op", "vault", "edit", vaultID, f"--name=!{vaultName}"], check=True, capture_output=True) 25 | print(f"\tChanged \"{vaultName}\" => \"!{vaultName}\"") 26 | except (Exception) as err: 27 | print(f"Encountered an error renaming \"{vaultName}\": ", err) 28 | continue 29 | 30 | # Deletes each vault passed into this script 31 | def deleteVault(): 32 | print("Deleting vaults provided to the script.\n\n") 33 | # TRIPLE CHECK that the script runner understands the implications of their decision 34 | understands = False 35 | if understands == False: 36 | print("\n\tDELETING VAULTS IS PERMANENT.\n\tBe very sure that you want to delete the vaults you are passing to this script.\n\n") 37 | response = input("I know what I am doing and UNDERSTAND THAT I CANNOT RESTORE DELETED VAULTS (yes/no): ") 38 | if response == "yes": 39 | understands = True 40 | else: 41 | sys.exit("If you'd like to proceed with deleting vaults, please run the script again and type 'yes' when prompted.") 42 | 43 | with open(args.filepath, "r", encoding="utf-8") as vaultList: 44 | for vault in vaultList: 45 | vaultID = vault.rstrip() 46 | try: 47 | subprocess.run(f"op vault delete {vaultID}", shell=True, check=True, capture_output=True) 48 | print(f"\tDeleted vault {vaultID}") 49 | except (Exception) as err: 50 | print(f"Encountered an error deleting vault with ID {vaultID}: ", err) 51 | continue 52 | 53 | 54 | if args.deletemode is True: 55 | deleteVault() 56 | else: 57 | prefixVault() -------------------------------------------------------------------------------- /migration/cleanup-scripts/recreate_metadata_entry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import subprocess 5 | import csv 6 | import json 7 | from collections import defaultdict 8 | from datetime import datetime 9 | 10 | scriptPath = os.path.dirname(__file__) 11 | outputPath = scriptPath # Optionally choose an alternative output path here. 12 | 13 | class Vault: 14 | def __init__(self, csvRow): 15 | self.name = csvRow[0] 16 | self.uuid = csvRow[1] 17 | self.itemCount = csvRow[2] 18 | self.created = csvRow[3] 19 | self.updated = csvRow[4] 20 | self.userCount = csvRow[5] 21 | 22 | 23 | def findMetadataVaultUuid(vaults: dict): 24 | for k, v in vaults.items(): 25 | if v == "❗️ LastPass Imported Shared Folders Metadata": 26 | return k 27 | return None 28 | 29 | 30 | def getVaultItems(vaultID): 31 | try: 32 | return subprocess.run(f"op item list --vault {vaultID} --format=json", shell=True, check=True, capture_output=True).stdout 33 | except (Exception) as err: 34 | print(f"Encountered an error getting items for vault {vaultID}: ", err) 35 | return 36 | 37 | 38 | def getItemDetails(vaultUuid, itemUuid): 39 | try: 40 | return subprocess.run(f"op item get {itemUuid} --vault {vaultUuid} --format=json", shell=True, check=True, capture_output=True).stdout 41 | except (Exception) as err: 42 | print(f"Encountered an error getting item details for item {itemUuid} vault {vaultUuid}: ", err) 43 | return 44 | 45 | 46 | def createItem(vaultUuid, itemJson: str): 47 | from subprocess import Popen, PIPE, STDOUT 48 | try: 49 | p = Popen(["op", "item", "create", "--vault", vaultUuid], stdout=PIPE, stdin=PIPE, stderr=PIPE) 50 | return p.communicate(input=itemJson.encode("utf-8"))[0] 51 | except (Exception) as err: 52 | print(f"Encountered an error creating an item in vault {vaultUuid}: ", err) 53 | return 54 | 55 | 56 | def readDate(date): 57 | return datetime.strptime(date, '%Y-%m-%d %H:%M:%S').timestamp() 58 | 59 | 60 | def findOriginalVault(vaults): 61 | minimum = (0, readDate(vaults[0].created)) 62 | for i, vault in enumerate(vaults): 63 | date = readDate(vault.created) 64 | if date < minimum[1]: 65 | minimum = (i, date) 66 | return vaults[minimum[0]] 67 | 68 | 69 | def removeImportedPrefix(text): 70 | prefix = "Imported " 71 | if text.startswith(prefix): 72 | return text[len(prefix):] 73 | return text 74 | 75 | 76 | def main(): 77 | # all vaults ordered by UUID 78 | vaults = {} 79 | # vaults ordered by name 80 | duplicateVaults = defaultdict(list) 81 | with open(f"{outputPath}/vaultreport.csv", "r", newline="") as vaultFile: 82 | csvReader = csv.reader(vaultFile) 83 | for row in csvReader: 84 | # we remove the 'Imported ' prefix from all vaults before processing them in case any 85 | # of them got renamed 86 | row[0] = removeImportedPrefix(row[0]) 87 | duplicateVaults[row[0]].append(Vault(row)) 88 | vaults[row[1]] = row[0] 89 | 90 | # find the metadata vault 91 | metadataVaultUuid = findMetadataVaultUuid(vaults) 92 | # get its items 93 | metadataItems = json.loads(getVaultItems(metadataVaultUuid)) 94 | itemDetails = [json.loads(getItemDetails(metadataVaultUuid, item["id"])) for item in metadataItems] 95 | updatedFields = [] 96 | for item in itemDetails: 97 | # find the LastPass metadata fields and process them 98 | for field in item.get("fields", []): 99 | ids = field["id"].split(".LPID.") 100 | if len(ids) != 2: 101 | continue 102 | vaultUuid = ids[0] 103 | lpId = ids[1] 104 | name = vaults.get(vaultUuid, "") 105 | # We found a metadata entry that points to a vault that exists. 106 | # We will find the vault with the same name, but that got created first 107 | # and we will save the new metadata entry 108 | if len(name) > 0: 109 | dupVaults = duplicateVaults[name] 110 | origVault = findOriginalVault(dupVaults) 111 | updatedFields.append({ 112 | "id": f"{origVault.uuid}.LPID.{lpId}", 113 | "type": field["type"], 114 | "value": field["value"], 115 | }) 116 | 117 | # create a new metadata record with the updated entries 118 | newItemFields = [{ 119 | "id": "notesPlain", 120 | "type": "STRING", 121 | "purpose": "NOTES", 122 | "label": "notesPlain", 123 | "value": "" 124 | }] 125 | newItemFields.extend(updatedFields) 126 | newItem = { 127 | "title": "LastPass Metadata Recreated", 128 | "category": "SECURE_NOTE", 129 | "fields": newItemFields 130 | } 131 | createItem(metadataVaultUuid, json.dumps(newItem)) 132 | 133 | 134 | if __name__ == "__main__": 135 | main() 136 | -------------------------------------------------------------------------------- /migration/cleanup-scripts/remove_imported_prefix.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import json 3 | import sys 4 | 5 | 6 | # Check CLI version 7 | def checkCLIVersion(): 8 | r = subprocess.run(["op", "--version", "--format=json"], capture_output=True) 9 | major, minor = r.stdout.decode("utf-8").rstrip().split(".", 2)[:2] 10 | if not major == 2 and not int(minor) >= 25: 11 | sys.exit( 12 | "❌ You must be using version 2.25 or greater of the 1Password CLI. Please visit https://developer.1password.com/docs/cli/get-started to download the lastest version." 13 | ) 14 | 15 | 16 | def getVaults(): 17 | try: 18 | return subprocess.run( 19 | ["op", "vault", "list", "--permission=manage_vault", "--format=json"], 20 | check=True, 21 | capture_output=True, 22 | ).stdout 23 | except Exception as err: 24 | print( 25 | f"Encountered an error getting the list of vaults you have access to: ", err 26 | ) 27 | return 28 | 29 | 30 | def main(): 31 | checkCLIVersion() 32 | vaultList = json.loads(getVaults()) 33 | print( 34 | "Removing 'Imported' prefix from all imported vaults in your 1Password account.\n\n" 35 | ) 36 | for vault in vaultList: 37 | vaultID = vault["id"] 38 | vaultName = vault["name"] 39 | if vaultName.startswith("Imported "): 40 | trimmedName = vaultName.removeprefix("Imported ") 41 | try: 42 | subprocess.run( 43 | ["op", "vault", "edit", vaultID, f"--name={trimmedName}"], 44 | check=True, 45 | capture_output=True, 46 | ) 47 | print(f'\t Changed "{vaultName}" => "{trimmedName}"') 48 | except Exception as err: 49 | print(f"Encountered an error renaming {vaultName}: ", err) 50 | continue 51 | 52 | 53 | main() 54 | -------------------------------------------------------------------------------- /migration/cleanup-scripts/vault_dedupe_helper.py: -------------------------------------------------------------------------------- 1 | ### 2 | # A script that will produce a csv-like report to assist with de-duplication. 3 | # Outputs vaultName, vaultUUID, itemCount, vault creation date, vault updated, number of users 4 | ### 5 | import os 6 | import subprocess 7 | import csv 8 | import json 9 | from datetime import datetime 10 | import sys 11 | 12 | scriptPath = os.path.dirname(__file__) 13 | outputPath = scriptPath # Optionally choose an alternative output path here. 14 | 15 | 16 | # Check CLI version 17 | def checkCLIVersion(): 18 | r = subprocess.run(["op", "--version", "--format=json"], capture_output=True) 19 | major, minor = r.stdout.decode("utf-8").rstrip().split(".", 2)[:2] 20 | if not major == 2 and not int(minor) >= 25: 21 | sys.exit( 22 | "❌ You must be using version 2.25 or greater of the 1Password CLI. Please visit https://developer.1password.com/docs/cli/get-started to download the lastest version." 23 | ) 24 | 25 | 26 | # Get a list of vaults the logged-in user has access to 27 | def getVaults(): 28 | try: 29 | return subprocess.run( 30 | ["op", "vault", "list", "--permission=manage_vault", "--format=json"], 31 | check=True, 32 | capture_output=True, 33 | ).stdout 34 | except Exception as err: 35 | print( 36 | f"Encountered an error getting the list of vaults you have access to: ", err 37 | ) 38 | return 39 | 40 | 41 | # Get a list of users and their permissions for a vault 42 | def getVaultUserList(vaultID): 43 | try: 44 | return subprocess.run( 45 | f"op vault user list {vaultID} --format=json", 46 | shell=True, 47 | check=True, 48 | capture_output=True, 49 | ).stdout 50 | except Exception as err: 51 | print( 52 | f"Encountered an error getting the list of users for vault {vaultID}: ", err 53 | ) 54 | return 55 | 56 | 57 | # Get the details of a vault 58 | def getVaultDetails(vaultID): 59 | try: 60 | return subprocess.run( 61 | f"op vault get {vaultID} --format=json", 62 | shell=True, 63 | check=True, 64 | capture_output=True, 65 | ).stdout 66 | except Exception as err: 67 | print(f"Encountered an error getting details for vault {vaultID}: ", err) 68 | return 69 | 70 | 71 | # Make dates returned from the CLI a bit nicer 72 | def formatDate(date): 73 | isoDate = datetime.fromisoformat(date) 74 | return f"{isoDate.year}-{isoDate.month}-{isoDate.day} {isoDate.hour}:{isoDate.minute}:{isoDate.second}" 75 | 76 | 77 | def main(): 78 | checkCLIVersion() 79 | vaultList = json.loads(getVaults()) 80 | with open(f"{outputPath}/vaultreport.csv", "w", newline="") as outputFile: 81 | csvWriter = csv.writer(outputFile) 82 | fields = [ 83 | "vaultName", 84 | "vaultUUD", 85 | "itemCount", 86 | "vaultCreated", 87 | "vaultUpdated", 88 | "userCount", 89 | ] 90 | csvWriter.writerow(fields) 91 | 92 | for vault in vaultList: 93 | if vault["name"] == "Private": 94 | continue 95 | vaultUserList = json.loads(getVaultUserList(vault["id"])) 96 | vaultDetails = json.loads(getVaultDetails(vault["id"])) 97 | userCount = len(vaultUserList) 98 | dateCreated = formatDate(vaultDetails["created_at"]) 99 | dateUpdated = formatDate(vaultDetails["updated_at"]) 100 | 101 | csvWriter.writerow( 102 | [ 103 | vault["name"], 104 | vault["id"], 105 | vaultDetails["items"], 106 | dateCreated, 107 | dateUpdated, 108 | userCount, 109 | ] 110 | ) 111 | 112 | print( 113 | vault["name"], 114 | vault["id"], 115 | vaultDetails["items"], 116 | dateCreated, 117 | dateUpdated, 118 | userCount, 119 | ) 120 | 121 | 122 | main() 123 | -------------------------------------------------------------------------------- /migration/lastpass/folder_migrate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This script will create 1Password vaults from each unique value 4 | # in the `grouping` column of a LastPass export generated through their 5 | # web-based exported or the lpass CLI (this has not 6 | # been tested on exports from their browser extension or other methods). 7 | # Shared/Nested folders in 1Password will have separate, non-nested 8 | # vaults created. 9 | # 10 | # This script does NOT create items in 1Password. 11 | import csv 12 | import json 13 | import subprocess 14 | 15 | from utils import normalize_vault_name 16 | 17 | 18 | def migrate_folders(csv_data, options): 19 | created_vault_list = {} 20 | lp_folder_list = set() 21 | is_csv_from_web_exporter = False 22 | stats = { 23 | 'total': 0, 24 | 'migrated': 0, 25 | 'skipped': 0, 26 | } 27 | 28 | linereader = csv.reader(csv_data, delimiter=',', quotechar='"') 29 | heading = next(linereader) 30 | 31 | if 'totp' in heading: 32 | is_csv_from_web_exporter = True 33 | 34 | for row in linereader: 35 | vault_name = row[6] if is_csv_from_web_exporter else row[5] 36 | if vault_name.startswith("Shared") and options['ignore-shared']: 37 | print(f"\tLastPass folder \"{vault_name}\" => skipped (ignore shared folders)") 38 | stats["skipped"] += 1 39 | continue 40 | 41 | if vault_name: 42 | lp_folder_list.add(normalize_vault_name(vault_name)) 43 | 44 | stats['total'] = len(lp_folder_list) 45 | for folder in lp_folder_list: 46 | if not options['dry-run']: 47 | try: 48 | vault_create_command_output = subprocess.run([ 49 | "op", "vault", "create", 50 | folder, 51 | "--format=json" 52 | ], check=True, capture_output=True) 53 | except: 54 | print(f"\tLastPass folder \"{folder}\" => skipped (cannot be created)") 55 | stats["skipped"] += 1 56 | 57 | continue 58 | new_vault_uuid = json.loads(vault_create_command_output.stdout)["id"] 59 | created_vault_list[folder] = new_vault_uuid 60 | print(f"\t\"{folder}\" => created") 61 | stats["migrated"] += 1 62 | else: # if dry run 63 | created_vault_list[folder] = folder 64 | print(f"\t\"{folder}\" => created; skipped (dry run)") 65 | stats["migrated"] += 1 66 | 67 | if not options['dry-run']: 68 | print(f"\nFolders migration complete!\nTotal {stats['total']} folders.\nCreated {stats['migrated']} vaults.\nSkipped {stats['skipped']}.") 69 | else: 70 | print(f"\nFolders migration dry run complete!\nTotal {stats['total']} folders.\nCreated {stats['migrated']} vaults.\nSkipped {stats['skipped']} folders.") -------------------------------------------------------------------------------- /migration/lastpass/lastpass-migrate.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Password/solutions/51d749b42523f5c4d8213696302d0183369a9ea7/migration/lastpass/lastpass-migrate.zip -------------------------------------------------------------------------------- /migration/lastpass/lpass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import subprocess 3 | import sys 4 | 5 | 6 | def get_lp_data(): 7 | # Sign user into LastPass 8 | lp_username = input("You may be prompted once for your LastPass username and twice for your Master Password.\nThe first prompt signs you into the LastPass CLI, the second prompt approves the export.\nWe don't store these credentials and will only be used for this session.\n\nPlease enter your LastPass username.\n>") 9 | try: 10 | subprocess.run(["lpass", "login", lp_username]) 11 | except: 12 | sys.exit("Unable to sign into LastPass") 13 | 14 | # Issue export command to lpass 15 | try: 16 | lp_export = subprocess.run(["lpass", "export"], text=True, capture_output=True).stdout 17 | except: 18 | sys.exit("Unable to retrieve LastPass data") 19 | 20 | if len(lp_export) == 0: 21 | sys.exit("No items were exported from LastPass") 22 | 23 | return lp_export 24 | -------------------------------------------------------------------------------- /migration/lastpass/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import getopt 3 | import sys 4 | import io 5 | import lpass 6 | from folder_migrate import migrate_folders 7 | from vault_item_import import migrate_items 8 | 9 | 10 | def main(argv): 11 | options = { 12 | 'ignore-shared': False, 13 | 'dry-run': False, 14 | } 15 | csvfile = None 16 | is_migrating_folders = False 17 | is_migrating_items = False 18 | opts, args = getopt.getopt(argv, "fi", ["file=", "folders", "items", "ignore-shared", "dry-run"]) 19 | for opt, arg in opts: 20 | if opt == "--file": 21 | print(f'Export secrets from csv file {arg}') 22 | csvfile = open(arg, newline='', encoding="utf8") 23 | continue 24 | 25 | if opt in ("-f", "--folders"): 26 | is_migrating_folders = True 27 | continue 28 | 29 | if opt in ("-i", "--items"): 30 | is_migrating_items = True 31 | continue 32 | 33 | if opt == "--ignore-shared": 34 | options["ignore-shared"] = True 35 | continue 36 | 37 | if opt == "--dry-run": 38 | options["dry-run"] = True 39 | continue 40 | 41 | if not is_migrating_items and not is_migrating_folders: 42 | sys.exit("Please specify the flag to run migration -i for items and folders, -f for folders only") 43 | 44 | if is_migrating_items and is_migrating_folders: 45 | sys.exit("Please specify single flag to run migration -i for items and folders, -f for folders only") 46 | 47 | if not csvfile: 48 | print('Export secrets using lpass cli.\n') 49 | csvfile = io.StringIO(lpass.get_lp_data()) 50 | 51 | if is_migrating_folders: 52 | print('Migrating folders:') 53 | migrate_folders(csvfile, options) 54 | elif is_migrating_items: 55 | print('Migrating items:') 56 | migrate_items(csvfile, options) 57 | 58 | if csvfile: 59 | csvfile.close() 60 | 61 | 62 | if __name__ == "__main__": 63 | main(sys.argv[1:]) 64 | -------------------------------------------------------------------------------- /migration/lastpass/template_generator_test.py: -------------------------------------------------------------------------------- 1 | from template_generator import LPassData, TemplateGenerator 2 | 3 | 4 | def test_template_generator_credit_card(): 5 | lpass_data = LPassData( 6 | url="http://sn", 7 | username="", 8 | password="", 9 | otp_secret="", 10 | notes="NoteType:Credit Card\nLanguage:en-GB\nName on Card:Test User\nType:card type\nNumber:4141414141414141\nSecurity Code:123\nStart Date:December,2020\nExpiration Date:October,2025\nNotes:Fake credit card", 11 | title="Fake card", 12 | vault="test", 13 | ) 14 | template = TemplateGenerator(lpass_data).generate() 15 | assert template['title'] == 'Fake card' 16 | assert template['category'] == 'CREDIT_CARD' 17 | 18 | for field in template['fields']: 19 | if field['id'] == 'validFrom': 20 | assert field['value'] == '202012' 21 | elif field['id'] == 'expiry': 22 | assert field['value'] == '202510' 23 | elif field['id'] == 'cvv': 24 | assert field['value'] == '123' 25 | elif field['id'] == 'ccnum': 26 | assert field['value'] == '4141414141414141' 27 | elif field['id'] == 'type': 28 | assert field['value'] == 'card type' 29 | elif field['id'] == 'cardholder': 30 | assert field['value'] == 'Test User' 31 | elif field['id'] == 'notesPlain': 32 | assert field['value'] == 'Fake credit card' 33 | 34 | 35 | def test_template_generator_bank_account(): 36 | lpass_data = LPassData( 37 | url="http://sn", 38 | username="", 39 | password="", 40 | otp_secret="", 41 | notes="NoteType:Bank Account\nLanguage:en-GB\nBank Name:bank name\nAccount Type:account type\nRouting Number:routing number\nAccount Number:account number\nSWIFT Code:swift code\nIBAN Number:iban number\nPin:pin\nBranch Address:branch address\nBranch Phone:branch phone\nNotes:note", 42 | title="Fake bank account", 43 | vault="test", 44 | ) 45 | template = TemplateGenerator(lpass_data).generate() 46 | assert template['title'] == 'Fake bank account' 47 | assert template['category'] == 'BANK_ACCOUNT' 48 | 49 | for field in template['fields']: 50 | if field['id'] == 'notesPlain': 51 | assert field['value'] == 'note' 52 | elif field['id'] == 'bankName': 53 | assert field['value'] == 'bank name' 54 | elif field['id'] == 'accountType': 55 | assert field['value'] == 'account type' 56 | elif field['id'] == 'routingNo': 57 | assert field['value'] == 'routing number' 58 | elif field['id'] == 'accountNo': 59 | assert field['value'] == 'account number' 60 | elif field['id'] == 'swift': 61 | assert field['value'] == 'swift code' 62 | elif field['id'] == 'iban': 63 | assert field['value'] == 'iban number' 64 | elif field['id'] == 'telephonePin': 65 | assert field['value'] == 'pin' 66 | elif field['id'] == 'branchPhone': 67 | assert field['value'] == 'branch phone' 68 | elif field['id'] == 'branchAddress': 69 | assert field['value'] == 'branch address' 70 | 71 | 72 | def test_template_generator_secure_note(): 73 | lpass_data = LPassData( 74 | url="http://sn", 75 | username="", 76 | password="", 77 | otp_secret="", 78 | notes="Some very important text", 79 | title="Secret message", 80 | vault="test", 81 | ) 82 | template = TemplateGenerator(lpass_data).generate() 83 | assert template['title'] == 'Secret message' 84 | assert template['category'] == 'SECURE_NOTE' 85 | assert template['fields'][0]['value'] == 'Some very important text' 86 | -------------------------------------------------------------------------------- /migration/lastpass/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import calendar 3 | 4 | 5 | def normalize_vault_name(vault_name: str) -> str: 6 | return re.sub(r'[^a-zA-Z0-9\s-]', "_", vault_name) 7 | 8 | 9 | def lpass_date_to_1password_format(lpass_date: str) -> str: 10 | if not lpass_date: 11 | return "" 12 | [month, year] = lpass_date.split(",") 13 | monthNumber = list(calendar.month_abbr).index(month[:3]) 14 | if monthNumber < 10: 15 | return year + "0" + str(monthNumber) 16 | 17 | return year + str(monthNumber) 18 | -------------------------------------------------------------------------------- /migration/lastpass/utils_test.py: -------------------------------------------------------------------------------- 1 | # A debug script people simply migrating from LastPass to 1Password 2 | # can disregard as it is not used in the process migrating. 3 | import utils 4 | 5 | 6 | def test_normalize_vault_name(): 7 | assert utils.normalize_vault_name("dev\sub_dev\\nested folder") == "dev_sub_dev_nested folder" 8 | assert utils.normalize_vault_name("dev/sub_dev") == "dev_sub_dev" 9 | assert utils.normalize_vault_name("test-folder") == "test-folder" 10 | 11 | 12 | def test_lpass_date_to_1password(): 13 | date = utils.lpass_date_to_1password_format("October,2025") 14 | assert date == "202510" 15 | 16 | date2 = utils.lpass_date_to_1password_format("December,2023") 17 | assert date2 == "202312" 18 | 19 | date3 = utils.lpass_date_to_1password_format("January,2020") 20 | assert date3 == "202001" 21 | -------------------------------------------------------------------------------- /migration/lastpass/vault_item_import.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This script will create vaults and login items from a LastPass export 4 | # generated through their web-based exported or the lpass CLI (this has not 5 | # been tested on exports from their browser extension or other methods). 6 | # Shared/Nested folders in 1Password will have separate, non-nested 7 | # vaults created. Items not belonging to any shared folder will be created 8 | # in the user's Private vault. 9 | # 10 | # Credit to @jbsoliman for the original script and @svens-uk for many significant enhancements 11 | 12 | import csv 13 | import json 14 | import subprocess 15 | import sys 16 | 17 | from utils import normalize_vault_name 18 | from template_generator import LPassData, TemplateGenerator 19 | 20 | 21 | def fetch_personal_vault(): 22 | # Get the Private or Personal vault 23 | # The one listed first should be the correct one 24 | try: 25 | vault_list_command_output = subprocess.run([ 26 | "op", "vault", "list", 27 | "--format=json" 28 | ], check=True, capture_output=True) 29 | except: 30 | sys.exit("An error occurred when attempting to fetch your 1Password vaults.") 31 | 32 | vault_list = json.loads(vault_list_command_output.stdout) 33 | personal_vault = next(filter(lambda x: (x["name"] == "Private" or x["name"] == "Personal"), vault_list), None) 34 | 35 | if personal_vault is None: 36 | sys.exit("Couldn't find your Personal or Private vault.") 37 | 38 | return personal_vault 39 | 40 | 41 | def create_item(vault: str, template): 42 | # Create item 43 | subprocess.run([ 44 | "op", "item", "create", "-", f"--vault={vault}" 45 | ], input=template, text=True, stdout=subprocess.DEVNULL) 46 | 47 | 48 | def migrate_items(csv_data, options): 49 | created_vault_list = {} 50 | is_csv_from_web_exporter = False 51 | personal_vault = fetch_personal_vault() 52 | stats = { 53 | 'total': 0, 54 | 'migrated': 0, 55 | 'skipped': 0, 56 | 'vaults': 0, 57 | } 58 | 59 | linereader = csv.reader(csv_data, delimiter=',', quotechar='"') 60 | heading = next(linereader) 61 | if 'totp' in heading: 62 | is_csv_from_web_exporter = True 63 | 64 | for row in linereader: 65 | if len(row) == 0: 66 | continue 67 | 68 | stats['total'] += 1 69 | if is_csv_from_web_exporter: 70 | url = row[0] 71 | username = row[1] 72 | password = row[2] 73 | otp_secret = row[3] 74 | notes = row[4] 75 | title = row[5] 76 | vault = row[6] 77 | else: 78 | url = row[0] 79 | username = row[1] 80 | password = row[2] 81 | notes = row[3] 82 | title = row[4] 83 | vault = row[5] 84 | otp_secret = None 85 | 86 | if vault.startswith("Shared") and options['ignore-shared']: 87 | print(f"\t\"{title}\" => skipped (ignore shared credentials)") 88 | stats['skipped'] += 1 89 | continue 90 | 91 | # Check if vault is defined 92 | vault_defined = vault and vault != "" 93 | # Create vault, if needed 94 | if vault_defined and vault not in created_vault_list: 95 | if not options['dry-run']: 96 | try: 97 | normalized_vault_name = normalize_vault_name(vault) 98 | vault_create_command_output = subprocess.run([ 99 | "op", "vault", "create", 100 | normalized_vault_name, 101 | "--format=json" 102 | ], check=True, capture_output=True) 103 | except: 104 | print(f"\t\"{vault}\" => failed to create new vault \"{normalized_vault_name}\"") 105 | continue 106 | new_vault_uuid = json.loads(vault_create_command_output.stdout)["id"] 107 | created_vault_list[vault] = new_vault_uuid 108 | stats["vaults"] += 1 109 | print(f"\tFrom LastPass folder \"{vault}\" => created new vault \"{normalized_vault_name}\"") 110 | else: 111 | normalized_vault_name = normalize_vault_name(vault) 112 | created_vault_list[vault] = vault 113 | print(f"\tFrom LastPass folder \"{vault}\" => created new vault \"{normalized_vault_name}\"; skipped (dry run)") 114 | 115 | template = TemplateGenerator(LPassData( 116 | url=url, 117 | username=username, 118 | password=password, 119 | otp_secret=otp_secret, 120 | notes=notes, 121 | title=title, 122 | vault=vault, 123 | )).generate() 124 | 125 | if not template: 126 | print(f"\t\"{title}\" => skipped (incompatible item)") 127 | stats["skipped"] += 1 128 | continue 129 | 130 | json_template = json.dumps(template) 131 | if not options['dry-run']: 132 | vault_to_use = created_vault_list[vault] if vault_defined else personal_vault['id'] 133 | 134 | if options['dry-run']: 135 | print(f"\t\"{title}\" => migrated; skipped (dry run)") 136 | continue 137 | 138 | create_item(vault_to_use, json_template) 139 | stats["migrated"] += 1 140 | print(f"\t\"{title}\" => migrated") 141 | 142 | print(f"\nMigration complete!\nTotal {stats['total']} credentials.\nMigrated {stats['migrated']} credentials.\nCreated {stats['vaults']} vaults.\nSkipped {stats['skipped']} credentials.") 143 | -------------------------------------------------------------------------------- /onepassword_sdks/README.md: -------------------------------------------------------------------------------- 1 | # 1Password SDK Examples 2 | 3 | Securely automate your workflows with SDKs from 1Password. 4 | 5 | # Introduction 6 | This repository contains example scripts and apps that leverage 1Password SDKs to address a variety of use cases. 7 | 8 | These examples are not intended to be used for production purposes. They are intended to demonstrate the capabilities of the SDKs, highlight specific use cases, and provide basic guidance for how to accomplish certain tasks with the SDKs. 9 | 10 | # Get started 11 | * [Learn more about 1Password SDKs.](https://developer.1password.com/docs/sdks) 12 | * [Follow the getting started tutorial.](https://developer.1password.com/docs/sdks/setup-tutorial) 13 | 14 | # Security Note 15 | * Most of these demo applications and scripts require the use of Service Account tokens and other secrets. Store all secrets securely, like in 1Password. 16 | * These example will make changes to data in 1Password. Do not use these examples with production or other real world data. 17 | 18 | # Examples 19 | 20 | ## [Use 1Password as a backend for a web app](./demo-inventory-tracker-webapp/) 21 | By: Scott Lougheed 22 | #### Description 23 | This web application allows you to create and manage "IT Resources", such as computers. 24 | * Create new "Devices", including their name, model, serial number, and Admin user credentials. 25 | * Each device is represented by an item in 1Password. 26 | * Display a list of all current devices. 27 | * Edit or delete existing devices. 28 | 29 | #### Featuring 30 | * 1Password Javascript SDK 31 | * 1Password Service Accounts 32 | * 1Password Secret References 33 | * 1Password CLI (`op run`) 34 | 35 | #### Highlights 36 | * Collect sensitive information from anyone using a web form and store that information securely in 1Password. 37 | * Display information from 1Password items on a webpage. 38 | 39 | #### Potential real world use cases 40 | * If you're a client services organization, collect sensitive information from your clients using a web portal and store it in 1Password, even if your clients aren't 1Password users. 41 | * Credentials for client-owned services you manage. 42 | * Credit card information for clients. 43 | * Provide limited access to information stored in 1Password, such as to employees in an internal app. 44 | 45 | ## [Migrate data between 1Password tenants](./demo-vault-migration-webapp/) 46 | By: Ron Doucette 47 | 48 | #### Description 49 | This web application facilitates the movement of vaults and their contents between two separate 1Password accounts. 50 | 51 | #### Featuring 52 | * 1Password Javascript SDK 53 | * 1Password CLI 54 | * 1Password Service Accounts 55 | 56 | #### Highlights 57 | * Securely move data between two 1Password accounts without writing any data to disk. 58 | * Use 1Password SDKs to back a web front-end. 59 | 60 | #### Potential real world use cases 61 | * Consolidating 1Password information across multiple accounts during mergers, acquisitions, or other organizational transitions. 62 | 63 | 64 | ## [Share files and markdown securely with Secure Item Sharing Links](./demo-share-script/) 65 | By: Amanda Crawley 66 | #### Description 67 | This Python script creates a 1Password item from files in a directory of your choice for the purposes of securely sharing source code and a README. 68 | 69 | * A `README.md` file present in the directory becomes the markdown-formatted contents of the Notes section in a 1Password item. 70 | * Other files in the directory are attached to the 1Password item. 71 | 72 | Finally, the script produces a Secure Item Sharing Link so you can securely share the item with the intended recipient. 73 | 74 | #### Featuring 75 | * 1Password Python SDK 76 | * 1Password Service Accounts 77 | * File attachments to 1Password items 78 | * Item sharing links 79 | 80 | #### Highlights 81 | * Use the SDK to build bespoke command-line utilities. 82 | * Use the SDK to attach files to 1Password items. 83 | 84 | #### Potential real world use cases 85 | * Programmatically write or read cert or key files stored as attachments in 1Password for machine-to-machine authentication. 86 | * Share text and files securely with anyone, whether they use 1Password or not. 87 | * Create custom sharing utilities. 88 | 89 | ## [Secure and streamline employee onboarding with Okta](./demo-share-okta-user-script/) 90 | By: Amanda Crawley 91 | #### Description 92 | This Python script is an example of how you can streamline and secure employee onboarding. The script allows you to: 93 | 94 | * Create a new Okta user and generate a strong password for their Okta account. 95 | * Get a Secure Item Sharing Link for the new user's Okta credentials in 1Password. This link can be shared with the new employee, like by sending it to their personal email address. 96 | 97 | #### Featuring 98 | * 1Password Python SDK 99 | * Secure Item Sharing Links 100 | * Okta API 101 | 102 | #### Highlights 103 | * Use 1Password SDKs to interact with additional third party services. 104 | * Use 1Password SDKs to generate secure passwords for external services and store the information in 1Password. 105 | * Share 1Password items securely with anyone, whether they use 1Password or not. 106 | 107 | 108 | #### Potential real world use cases 109 | * Onboarding new employees, particularly when Okta is required to access email inboxes or where inboxes may not be provisioned quickly enough. -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/.env.template: -------------------------------------------------------------------------------- 1 | # Learn about 1Password Secret References https://developer.1password.com/docs/cli/secret-reference-syntax/ 2 | OP_SERVICE_ACCOUNT_TOKEN = "Secret Reference to Service Account Token in 1Password" 3 | OP_VAULT_ID = "SOME VAULT UUID" -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG NODE_VERSION=22.0.0 4 | 5 | FROM node:${NODE_VERSION}-alpine 6 | 7 | ENV NODE_ENV=production 8 | 9 | WORKDIR /usr/src/app 10 | 11 | RUN --mount=type=bind,source=package.json,target=package.json \ 12 | --mount=type=bind,source=package-lock.json,target=package-lock.json \ 13 | --mount=type=cache,target=/root/.npm \ 14 | npm ci --omit=dev 15 | 16 | USER node 17 | 18 | COPY . . 19 | 20 | EXPOSE 3000 21 | 22 | CMD ["npm", "run", "prod"] -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/README.md: -------------------------------------------------------------------------------- 1 | # JS SDK Example: Computer Inventory Tracker 2 | 3 | ## Prerequisites 4 | * [Docker Engine](https://docs.docker.com/get-started/get-docker/) 5 | * [1Password CLI](https://developer.1password.com/docs/cli/get-started) 6 | * A [1Password Service Account](https://developer.1password.com/docs/service-accounts) with read and write permissions in at least one vault. 7 | * Learn about [secret references](https://developer.1password.com/docs/cli/secret-reference-syntax/) 8 | 9 | ## Deploy the Web App 10 | 1. Create a [1Password Service Account](https://start.1password.com/developer-tools/infrastructure-secrets/serviceaccount/). Make sure to save the service account token in your 1Password account. 11 | 2. Add the [UUID](https://developer.1password.com/docs/sdks/concepts#unique-identifiers) of a vault the service account has read and write permissions in to the `.env` file. This is where 1Password items managed by the app will be stored. 12 | 3. Add the secret reference to the `.env.template` file and remove the `.template` suffix. 13 | 4. Add the UUID of a vault the Service Account has read and write permissions in to the .env file. This is where 1Password items managed by the app will be stored. 14 | 5. Build the image and deploy the container: 15 | ``` 16 | op run --env-file=.env -- docker compose up --build 17 | ``` 18 | 6. Visit the webapp in your browser on port 3000 (for example, `localhost:3000`) -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/app.js: -------------------------------------------------------------------------------- 1 | import sdk from "@1password/sdk"; 2 | import express from "express"; 3 | import bodyParser from "body-parser"; 4 | 5 | const app = express(); 6 | app.set("view engine", "ejs"); 7 | app.use(bodyParser.urlencoded({ extended: true })); 8 | app.use(bodyParser.json()); 9 | app.use(express.static("public")); 10 | 11 | const vaultId = process.env.OP_VAULT_ID; 12 | const port = 3000; 13 | 14 | const opClientConfig = { 15 | auth: process.env.OP_SERVICE_ACCOUNT_TOKEN, 16 | integrationName: "Inventory Tracker", 17 | integrationVersion: "v1.0.0", 18 | }; 19 | 20 | const client = await sdk.createClient(opClientConfig); 21 | 22 | const createItem = async (vaultID, data) => { 23 | console.log("Creating item with"); 24 | 25 | const item = await client.items.create({ 26 | title: data.deviceName, 27 | category: sdk.ItemCategory.Login, 28 | vaultId: vaultID, 29 | fields: [ 30 | { 31 | id: "username", 32 | title: "username", 33 | fieldType: sdk.ItemFieldType.Text, 34 | value: data.adminUsername, 35 | }, 36 | { 37 | id: "password", 38 | title: "password", 39 | fieldType: sdk.ItemFieldType.Concealed, 40 | value: data.password, 41 | }, 42 | { 43 | id: "deviceName", 44 | title: "Device Name", 45 | sectionId: "deviceDetails", 46 | fieldType: sdk.ItemFieldType.Text, 47 | value: data.deviceName, 48 | }, 49 | { 50 | id: "deviceModel", 51 | title: "Model Number", 52 | sectionId: "deviceDetails", 53 | fieldType: sdk.ItemFieldType.Text, 54 | value: data.deviceModel, 55 | }, 56 | { 57 | id: "deviceSerial", 58 | title: "Serial Number", 59 | sectionId: "deviceDetails", 60 | fieldType: sdk.ItemFieldType.Text, 61 | value: data.deviceSerial, 62 | }, 63 | { 64 | id: "adminUsername", 65 | title: "Device Admin Username", 66 | sectionId: "deviceDetails", 67 | fieldType: sdk.ItemFieldType.Text, 68 | value: data.adminUsername, 69 | }, 70 | ], 71 | sections: [ 72 | { 73 | id: "deviceDetails", 74 | title: "Device Details", 75 | }, 76 | ], 77 | }); 78 | return item; 79 | }; 80 | 81 | const generatePassword = async () => { 82 | return sdk.Secrets.generatePassword({ 83 | type: "Random", 84 | parameters: { 85 | includeDigits: true, 86 | includeSymbols: true, 87 | length: 32, 88 | }, 89 | }); 90 | }; 91 | 92 | const updateItem = async (updatedItemData, itemID) => { 93 | console.log(`Updating item with ID: ${itemID}`); 94 | const oldItem = await getOneItem(itemID); 95 | 96 | const newItem = { 97 | ...oldItem, 98 | title: updatedItemData.deviceName, 99 | fields: oldItem.fields.map((field) => { 100 | if (field.id === "password") { 101 | if (updatedItemData.password !== "") { 102 | return { ...field, value: updatedItemData.password }; 103 | } 104 | return field; 105 | } else if (field.id === "adminUsername" || field.id === "username") { 106 | return { 107 | ...field, 108 | value: updatedItemData.adminUsername, 109 | }; 110 | } else if (updatedItemData[field.id]) { 111 | return { ...field, value: updatedItemData[field.id] }; 112 | } 113 | return field; 114 | }), 115 | }; 116 | const updatedItem = await client.items.put(newItem); 117 | return updatedItem; 118 | }; 119 | 120 | const getAllItems = async () => { 121 | const itemList = await client.items.listAll(vaultId); 122 | 123 | const itemPromises = itemList.elements.map(async (item) => { 124 | return await client.items.get(vaultId, item.id); 125 | }); 126 | 127 | const items = await Promise.all(itemPromises); 128 | return items; 129 | }; 130 | 131 | const getOneItem = async (itemId) => { 132 | const foundItem = await client.items.get(vaultId, itemId); 133 | return foundItem; 134 | }; 135 | 136 | const deleteItem = async (itemId) => { 137 | console.log(`Attempting to delete item with ID: ${itemId}`); 138 | await client.items.delete(vaultId, itemId); 139 | }; 140 | 141 | app.get("/", (req, res) => { 142 | res.render("index", { 143 | title: "Knox IT Resource Manager", 144 | currentPage: "home", 145 | item: null, 146 | }); 147 | }); 148 | 149 | app.get("/generate-password", async (req, res) => { 150 | try { 151 | const generatedPassword = await generatePassword(); 152 | res.json({ generatedPassword: generatedPassword.password }); 153 | } catch (error) {} 154 | }); 155 | 156 | app.get("/items", async (req, res) => { 157 | try { 158 | const items = await getAllItems(); 159 | res.render("items", { 160 | items, 161 | currentPage: "item", 162 | title: "Inventory List", 163 | }); 164 | } catch (error) { 165 | console.error("Error fetching items:", error); 166 | res.status(500).send("Oops, something went wrong getting items"); 167 | } 168 | }); 169 | 170 | app.get("/items/:item", async (req, res) => { 171 | try { 172 | const item = await client.items.get(vaultId, req.params.item); 173 | res.render("item", { item, currentPage: "item", title: "Item Details" }); 174 | } catch (error) { 175 | console.error("Error fetching item:", error); 176 | res.status(404).send(`Unable to find item with UUID: ${req.params.item}`); 177 | } 178 | }); 179 | 180 | app.get("/edit/:item", async (req, res) => { 181 | try { 182 | const itemId = req.params.item; 183 | const item = await getOneItem(itemId); 184 | 185 | res.render("index", { item, currentPage: "item", title: "Edit Item" }); 186 | } catch (error) { 187 | console.error("Error fetching item:", error); 188 | res.status(500).send("There was an error"); 189 | } 190 | }); 191 | 192 | app.post("/submit", async (req, res) => { 193 | const { 194 | itemId, 195 | deviceName, 196 | deviceModel, 197 | deviceSerial, 198 | adminUsername, 199 | password, 200 | } = req.body; 201 | const updatedItemData = { 202 | deviceName, 203 | deviceModel, 204 | deviceSerial, 205 | adminUsername, 206 | password, 207 | }; 208 | try { 209 | if (itemId) { 210 | const updatedItem = await updateItem(updatedItemData, itemId); 211 | res.redirect(`items/${updatedItem.id}`); 212 | } else { 213 | const item = await createItem(vaultId, updatedItemData); 214 | res.redirect(`items/${item.id}`); 215 | } 216 | } catch (error) { 217 | console.error("Error submitting item:", error); 218 | res.status(500).send("There was an error"); 219 | } 220 | }); 221 | 222 | app.post("/delete", async (req, res) => { 223 | try { 224 | const { itemId } = req.body; 225 | await deleteItem(itemId); 226 | res.redirect("/items"); 227 | } catch (error) { 228 | console.error("Error deleting item:", error); 229 | res.status(500).send("There was an error"); 230 | } 231 | }); 232 | 233 | app.listen(port, () => { 234 | console.log(`Listening on port ${port}`); 235 | }); 236 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | it-tracker: 3 | build: 4 | context: . 5 | environment: 6 | NODE_ENV: production 7 | OP_SERVICE_ACCOUNT_TOKEN: 8 | OP_VAULT_ID: 9 | ports: 10 | - 3000:3000 11 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "it-tracker", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "scripts": { 6 | "dev": "op run --env-file=.env -- nodemon app.js", 7 | "prod": "node app.js" 8 | }, 9 | "keywords": [], 10 | "author": "1Password", 11 | "license": "ISC", 12 | "description": "Tool to track device inventory and admin credentials in 1Password.", 13 | "type": "module", 14 | "dependencies": { 15 | "@1password/sdk": "^0.2.1", 16 | "body-parser": "^1.20.2", 17 | "ejs": "^3.1.10", 18 | "express": "^4.19.2", 19 | "nodemon": "^3.1.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/public/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #007bff; 3 | --primary-color-dark: #0056b3; 4 | --text-color: #333; 5 | --label-color: #555; 6 | --background-color: #f4f4f4; 7 | } 8 | body { 9 | font-family: Arial, sans-serif; 10 | margin: 20px; 11 | background-color: var(--background-color); 12 | } 13 | 14 | .container { 15 | background: white; 16 | border-radius: 8px; 17 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 18 | padding: 1em; 19 | max-width: 500px; 20 | margin: auto; 21 | } 22 | 23 | .container.item { 24 | margin-bottom: 0.5em; 25 | } 26 | 27 | h1 { 28 | text-align: center; 29 | color: var(--text-color); 30 | } 31 | h2 { 32 | text-align: center; 33 | } 34 | 35 | .field { 36 | margin-bottom: 15px; 37 | } 38 | 39 | .field-title { 40 | font-weight: bold; 41 | color: var(--label-color); 42 | } 43 | 44 | .field-value { 45 | margin-left: 10px; 46 | color: var(--text-color); 47 | display: block; 48 | } 49 | 50 | .section-title { 51 | font-size: 1em; 52 | font-weight: 800; 53 | margin-top: 20px; 54 | color: var(--primary-color); 55 | } 56 | 57 | label { 58 | font-weight: bold; 59 | color: var(--label-color); 60 | display: block; 61 | margin-bottom: 5px; 62 | } 63 | 64 | input[type="text"], 65 | input[type="password"] { 66 | width: 100%; 67 | padding: 10px; 68 | margin-bottom: 15px; 69 | border: 1px solid #ccc; 70 | border-radius: 4px; 71 | box-sizing: border-box; 72 | } 73 | 74 | input[type="submit"] { 75 | background-color: #007bff; 76 | color: white; 77 | border: none; 78 | border-radius: 4px; 79 | padding: 10px 15px; 80 | cursor: pointer; 81 | font-size: 16px; 82 | width: 100%; 83 | } 84 | 85 | input[type="submit"]:hover { 86 | background-color: var(--primary-color-dark); /* Darker shade on hover */ 87 | } 88 | 89 | .nav-menu { 90 | display: flex; 91 | max-width: 25%; 92 | margin-inline: auto; 93 | justify-content: center; 94 | margin-bottom: 0.5em; 95 | color: white; 96 | font-size: medium; 97 | } 98 | 99 | .nav-menu > nav > ul { 100 | list-style: none; 101 | padding: 0; 102 | margin: 0; 103 | display: flex; 104 | } 105 | 106 | .nav-menu > nav > ul > li { 107 | background-color: var(--primary-color); 108 | padding: 0.4em; 109 | margin: 0 15px; 110 | border-radius: 0.4em; 111 | } 112 | 113 | .nav-menu > nav > ul > li.active, 114 | .nav-menu > nav > ul > li:hover { 115 | background-color: var(--primary-color-dark); 116 | border-radius: 0.25em; 117 | } 118 | 119 | .nav-menu > nav > ul > li > a { 120 | text-decoration: none; 121 | color: inherit; 122 | } 123 | 124 | .item-manage-button { 125 | margin-bottom: 0.3em; 126 | } 127 | 128 | .button-container { 129 | display: flex; 130 | justify-content: space-between; 131 | gap: 1em; 132 | } 133 | 134 | .admin-password-container { 135 | display: flex; 136 | justify-content: space-between; 137 | gap: 1em; 138 | align-items: baseline; 139 | max-height: 100%; 140 | } 141 | 142 | .admin-password-container input[type="password"] { 143 | flex: 1; 144 | } 145 | 146 | .admin-password-container button { 147 | display: flex; 148 | background-color: var(--primary-color); 149 | color: white; 150 | border: none; 151 | border-radius: 4px; 152 | height: 2.25rem; 153 | align-items: center; 154 | cursor: pointer; 155 | } 156 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/views/includes/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | 11 | 12 |
13 | 21 |
22 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/views/index.ejs: -------------------------------------------------------------------------------- 1 | <%- include("includes/header") %> 2 |
3 |

Device Information Form

4 |
5 | 6 | " required /> 7 | 8 | 9 | " required /> 10 | 11 | 12 | " required /> 13 | 14 | 15 | " required data-1p-ignore/> 16 | 17 |
18 | 23 | data-1p-ignore 24 | /> 25 | 26 |
27 | 28 | " /> 29 |
30 |
31 | 32 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/views/item.ejs: -------------------------------------------------------------------------------- 1 | <%- include("includes/header"); %> 2 |
3 |

Device Details

4 |

<%= item.title %>

5 | 6 |
Device Details
7 | <% item.fields.forEach(field=> { %> 8 |
9 | <%= field.title %>: 10 | <%= field.value %> 11 |
12 | <% }); %> 13 | 14 |
1Password Item Details
15 | <% ["Item Title", "1Password Item UUID" , "1Password Vault ID" 16 | ].forEach((title, index)=> { %> 17 |
18 | <%= title %>: 19 | 20 | <%= index===0 ? item.title : index===1 ? item.id : item.vaultId %> 21 | 22 |
23 | <% }); %> 24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-inventory-tracker-webapp/views/items.ejs: -------------------------------------------------------------------------------- 1 | <%- include("includes/header") %> 2 | <% if (items && items.length > 0) { %> <% items.forEach((item, index) => { 3 | %> 4 |
5 |

Device Summary

6 |

<%= item.title %>

7 |
8 | Device Name: 9 | <%= item.fields.find(field => field.id === "deviceName").value %> 10 | Model: 11 | <%= item.fields.find(field => field.id === "deviceModel").value %> 12 | Serial Number: 13 | <%= item.fields.find(field => field.id === "deviceSerial").value %> 14 |
15 |
16 | <% }); %> <% } else { %> 17 |
18 |

No Devices Found

19 |
20 | <% } %> 21 | 22 | 23 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-reverse-sharing/.env.template: -------------------------------------------------------------------------------- 1 | OP_SERVICE_ACCOUNT_TOKEN="your_1password_service_account_token" 2 | OP_VAULT_UUID="your_target_1password_vault_uuid" 3 | FLASK_SECRET_KEY="a_very_strong_random_secret_key_for_flask_sessions" 4 | FLASK_DEBUG=true # Set to false for production 5 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-reverse-sharing/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.13-slim-bookworm 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the dependencies file to the working directory 8 | COPY requirements.txt . 9 | 10 | # Install any needed packages specified in requirements.txt 11 | # Using --no-cache-dir to reduce image size 12 | RUN pip install --no-cache-dir -r requirements.txt 13 | 14 | # Copy the current directory contents into the container at /app 15 | COPY . . 16 | 17 | # Make port 5000 available to the world outside this container 18 | EXPOSE 5000 19 | 20 | # Run app.py using gunicorn when the container launches 21 | # Gunicorn is a production-ready WSGI server. 22 | CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] 23 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-reverse-sharing/README.md: -------------------------------------------------------------------------------- 1 | # 1Password SDK Demo: Reverse Sharing - Item Creator 2 | 3 | This project is a simple Flask web application that demonstrates how to use the 1Password Python SDK to create new "Login" items in a specified 1Password vault. It serves as an example of "reverse sharing," where an application programmatically adds credentials or other information to a 1Password vault. 4 | 5 | ## Features 6 | 7 | - Web interface to input details for a new 1Password Login item (title, username, password, notes). 8 | - Uses the 1Password Python SDK for secure interaction with your 1Password account. 9 | - Configuration via environment variables for easy setup. 10 | - Includes Docker and Docker Compose configurations for containerized deployment. 11 | 12 | ## Prerequisites 13 | 14 | Before you begin, ensure you have the following: 15 | 16 | - **Python 3.7+** (Python 3.9 is used in the Dockerfile) 17 | - **Pip** (Python package installer) 18 | - **1Password Account** with a Service Account configured. 19 | - You'll need the [**Service Account Token**](https://developer.1password.com/docs/service-accounts/get-started/#create-a-service-account). 20 | - You'll need the **UUID of the vault** where you want to create items. 21 | - **Docker** and **Docker Compose** (Optional, for containerized deployment) 22 | 23 | ## Setup 24 | 25 | 1. **Clone the repository (if you haven't already):** 26 | 27 | ```bash 28 | git clone https://github.com/1Password/solutions.git 29 | cd solutions/onepassword_sdks/demo-reverse-sharing 30 | ``` 31 | 32 | 2. **Create and configure the `.env` file:** 33 | Copy the example `.env.example` (if you have one) or create a new `.env` file in the project root: 34 | 35 | ```dotenv 36 | # .env file 37 | OP_SERVICE_ACCOUNT_TOKEN="your_1password_service_account_token" 38 | OP_VAULT_UUID="your_target_1password_vault_uuid" 39 | FLASK_SECRET_KEY="a_very_strong_random_secret_key_for_flask_sessions" 40 | FLASK_DEBUG=true # Set to false for production 41 | ``` 42 | 43 | **Important:** 44 | 45 | - Replace placeholders with your actual 1Password Service Account Token and Vault UUID. 46 | - Generate a strong, unique `FLASK_SECRET_KEY`. You can use `python -c 'import secrets; print(secrets.token_hex(24))'` to generate one. 47 | - The `FLASK_SECRET_KEY` can also be a 1Password secret reference like `op://connect-secrets/flask-secret-key/credential` if you are using 1Password Connect for secret injection, but for local development, a direct value is simpler. The provided `.env` file shows this pattern. 48 | 49 | 3. **Install Python dependencies:** 50 | It's recommended to use a virtual environment: 51 | 52 | ```bash 53 | python3 -m venv venv 54 | source venv/bin/activate # On Windows use `venv\Scripts\activate` 55 | pip install -r requirements.txt 56 | ``` 57 | 58 | ## Running the Application 59 | 60 | You have a few options to run the application: 61 | 62 | ### 1. Directly with Python 63 | 64 | Ensure your `.env` file is configured and you have activated your virtual environment. 65 | 66 | ```bash 67 | python app.py 68 | ``` 69 | 70 | The application will be available at `http://127.0.0.1:5000` (or `http://0.0.0.0:5000`). 71 | 72 | ### 2. Using Docker 73 | 74 | This method builds a Docker image and runs the application in a container. 75 | 76 | 1. **Build the Docker image:** 77 | 78 | ```bash 79 | docker build -t op-sdk-item-creator . 80 | ``` 81 | 82 | 2. **Run the Docker container:** 83 | You'll need to pass the environment variables from your `.env` file (or directly). 84 | 85 | ```bash 86 | docker run -p 5001:5000 \ 87 | --env-file .env \ 88 | op-sdk-item-creator 89 | ``` 90 | 91 | The application will be available at `http://localhost:5001`. 92 | 93 | ### 3. Using Docker Compose 94 | 95 | This is often the easiest way for local development with containers. 96 | 97 | 1. **Ensure your `.env` file is present and correctly configured in the same directory as `docker-compose.yml`.** Docker Compose will automatically pick it up. 98 | 99 | 2. **Start the application:** 100 | 101 | ```bash 102 | docker-compose up 103 | ``` 104 | 105 | To run in detached mode (in the background): 106 | 107 | ```bash 108 | docker-compose up -d 109 | ``` 110 | 111 | The application will be available at `http://localhost:5001`. 112 | 113 | 3. **To stop the application:** 114 | 115 | ```bash 116 | docker-compose down 117 | ``` 118 | 119 | ## Usage 120 | 121 | 1. Open your web browser and navigate to the application (e.g., `http://localhost:5001` if using Docker Compose or the Docker run command above, or `http://localhost:5000` if running directly with Python). 122 | 2. You will see a form to create a new 1Password Login item. 123 | 3. Fill in the "Title", "Username", "Password", and "Notes" fields. The "Title" field is required. 124 | 4. Click "Create Item". 125 | 5. The application will attempt to create the item in your configured 1Password vault using the SDK. 126 | 6. You will see a success or error message displayed on the page. 127 | 128 | ## Security Notes 129 | 130 | - **`FLASK_SECRET_KEY`**: It is crucial to set a strong, unique `FLASK_SECRET_KEY` for your application, especially in production. Do not use the default or a weak key. 131 | - **`OP_SERVICE_ACCOUNT_TOKEN`**: Treat your 1Password Service Account Token like a password. Keep it confidential and secure. Do not commit it directly into your version control system if the repository is public. The `.env` file approach helps keep it out of he codebase. 132 | - **Error Handling**: The application includes basic error handling and will display messages if item creation fails or if critical configurations are missing. Check the application logs for more detailed error information. 133 | 134 | ## Project Structure 135 | 136 | ```bash 137 | . 138 | ├── .env # Local environment variables (ignored by git) 139 | ├── app.py # Main Flask application logic 140 | ├── Dockerfile # Instructions to build the Docker image 141 | ├── docker-compose.yml # Docker Compose configuration 142 | ├── requirements.txt # Python dependencies 143 | └── templates/ 144 | └── index.html # HTML template for the web interface 145 | ``` 146 | 147 | --- 148 | 149 | This demo provides a basic framework. You can extend it further, for example, by adding support for different item categories, custom fields, or more robust error handling and user feedback. 150 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-reverse-sharing/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | from flask import Flask, request, render_template, flash, redirect, url_for, session 4 | from dotenv import load_dotenv 5 | 6 | # Import 1Password SDK components 7 | from onepassword import * 8 | 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv(".env") 12 | 13 | app = Flask(__name__) 14 | 15 | # --- Configuration from Environment Variables --- 16 | app.config["SECRET_KEY"] = os.environ.get("FLASK_SECRET_KEY") 17 | if not app.config["SECRET_KEY"]: 18 | app.logger.warning( 19 | "FLASK_SECRET_KEY is not set! Flash messages and session security may be compromised." 20 | ) 21 | app.config["SECRET_KEY"] = "dev-fallback-secret-key-for-flask" 22 | 23 | OP_SERVICE_ACCOUNT_TOKEN = os.environ.get("OP_SERVICE_ACCOUNT_TOKEN") 24 | OP_VAULT_UUID = os.environ.get("OP_VAULT_UUID") 25 | 26 | 27 | async def create_op_item_sdk(title, username_val, password_val, notes_content): 28 | """ 29 | Uses the 1Password Python SDK to create a new Login item, 30 | using ItemCreateParams and ItemFieldParams. 31 | """ 32 | app.logger.info("Attempting to initialize 1Password SDK client...") 33 | op_client = await Client.authenticate( 34 | auth=OP_SERVICE_ACCOUNT_TOKEN, 35 | integration_name="FlaskWebApp-1PItemCreator", 36 | integration_version="1.1.0", # Example version 37 | ) 38 | 39 | if not op_client: 40 | app.logger.error(f"SDK not available for item creation") 41 | return False, "1Password SDK client is not initialized." 42 | 43 | if not OP_VAULT_UUID: 44 | app.logger.error("OP_VAULT_UUID is not set. Cannot create item.") 45 | return False, "Target vault (OP_VAULT_UUID) is not configured on the server." 46 | 47 | try: 48 | app.logger.info( 49 | f"Constructing item '{title}' for vault '{OP_VAULT_UUID}' using ItemCreateParams." 50 | ) 51 | 52 | fields_to_add = [] 53 | if username_val: 54 | fields_to_add.append( 55 | ItemField( 56 | id="username", 57 | title="Username", 58 | value=username_val, 59 | fieldType=ItemFieldType.TEXT, 60 | ) 61 | ) 62 | if password_val: 63 | fields_to_add.append( 64 | ItemField( 65 | id="password", 66 | title="Password", 67 | value=password_val, 68 | fieldType=ItemFieldType.CONCEALED, 69 | ) 70 | ) 71 | 72 | item_payload = ItemCreateParams( 73 | vault_id=OP_VAULT_UUID, 74 | category=ItemCategory.LOGIN, 75 | title=title, 76 | fields=( 77 | fields_to_add if fields_to_add else None 78 | ), # Pass None if no fields, or SDK might expect empty list 79 | notes=notes_content if notes_content else None, # Pass notes if provided 80 | ) 81 | 82 | app.logger.info( 83 | f"Attempting to create item '{title}' via SDK with direct params." 84 | ) 85 | # The client.items.create method expects the ItemCreateParams object directly 86 | created_item = await op_client.items.create(item_payload) 87 | 88 | app.logger.info( 89 | f"Successfully created item '{created_item.title}' (ID: {created_item.id}) via SDK." 90 | ) 91 | return ( 92 | True, 93 | f"Successfully created item '{created_item.title}' (ID: {created_item.id}).", 94 | ) 95 | except Exception as e: 96 | error_msg = f"An unexpected error occurred during SDK item creation: {e}" 97 | app.logger.error(error_msg) 98 | return False, error_msg 99 | 100 | 101 | @app.route("/", methods=["GET", "POST"]) 102 | async def index(): 103 | """Main page for creating 1Password items using the SDK.""" 104 | 105 | if not OP_VAULT_UUID: 106 | flash( 107 | "Application Configuration Error: Target vault (OP_VAULT_UUID) is not set. Item creation is disabled.", 108 | "error", 109 | ) 110 | return render_template( 111 | "index.html", 112 | sdk_error="OP_VAULT_UUID not set.", 113 | title_val="", 114 | username_val="", 115 | password_val="", 116 | notes_val="", 117 | ) 118 | 119 | message = None 120 | success = False 121 | title_val = request.form.get("title", "") 122 | username_val = request.form.get("username", "") 123 | # Password is not re-populated in the form for security upon re-render 124 | notes_val = request.form.get("notes", "") 125 | 126 | if request.method == "POST": 127 | # Get password directly from form for POST, as it's not in title_val etc. 128 | password_from_form = request.form.get("password", "") 129 | 130 | if not title_val: 131 | flash("Title field is required.", "error") 132 | else: 133 | success, message = await create_op_item_sdk( 134 | title_val, username_val, password_from_form, notes_val 135 | ) 136 | if success: 137 | flash(message, "success") 138 | return redirect(url_for("index")) # Clear form on success 139 | else: 140 | flash(message, "error") 141 | 142 | # Re-render with current form values (except password) if there was an error 143 | return render_template( 144 | "index.html", 145 | message=message, 146 | success=success, 147 | title_val=title_val, 148 | username_val=username_val, 149 | password_val="", 150 | notes_val=notes_val, 151 | sdk_error=None, 152 | ) 153 | 154 | return render_template( 155 | "index.html", 156 | title_val="", 157 | username_val="", 158 | password_val="", 159 | notes_val="", 160 | sdk_error=None, 161 | ) 162 | 163 | 164 | if __name__ == "__main__": 165 | if ( 166 | not app.config["SECRET_KEY"] 167 | or app.config["SECRET_KEY"] == "dev-fallback-secret-key-for-flask" 168 | ): 169 | app.logger.warning( 170 | "CRITICAL: FLASK_SECRET_KEY is not set or using fallback. Set a strong secret key in environment." 171 | ) 172 | 173 | if not OP_SERVICE_ACCOUNT_TOKEN: 174 | app.logger.critical( 175 | "CRITICAL: OP_SERVICE_ACCOUNT_TOKEN is not set. The application will not be able to interact with 1Password." 176 | ) 177 | 178 | if not OP_VAULT_UUID: 179 | app.logger.warning( 180 | "WARNING: OP_VAULT_UUID is not set. Item creation will fail." 181 | ) 182 | app.run( 183 | debug=os.environ.get("FLASK_DEBUG", "false").lower() == "true", 184 | host="0.0.0.0", 185 | port=5000, 186 | ) 187 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-reverse-sharing/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | webapp: 3 | build: . # Build the Docker image using the Dockerfile in the current directory 4 | container_name: op_sdk_webapp_service 5 | ports: 6 | - "5001:5000" # Map port 5001 on the host to port 5000 in the container 7 | environment: 8 | # These variables will be sourced from a .env file in the same directory 9 | # or from your host machine's environment if the .env file is missing/incomplete. 10 | - OP_SERVICE_ACCOUNT_TOKEN=${OP_SERVICE_ACCOUNT_TOKEN} 11 | - OP_VAULT_UUID=${OP_VAULT_UUID} 12 | - FLASK_SECRET_KEY=${FLASK_SECRET_KEY} 13 | - FLASK_DEBUG=${FLASK_DEBUG:-false} # Default to false (production) if not set 14 | restart: unless-stopped # Restart the container unless it was explicitly stopped 15 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-reverse-sharing/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask[async]>=2.0 2 | requests>=2.20 3 | gunicorn>=20.0 4 | onepassword-sdk==0.3.0 5 | python-dotenv==1.1.0 6 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-reverse-sharing/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Create 1Password Item (SDK) 7 | 96 | 97 | 98 |

Create New 1Password Login Item (SDK)

99 | 100 |
101 | {% with messages = get_flashed_messages(with_categories=true) %} {% if 102 | messages %} {% for category, message in messages %} 103 |
{{ message }}
104 | {% endfor %} {% endif %} {% endwith %} 105 |
106 | 107 | {% if sdk_error %} 108 |
109 | Application Initialization Error: {{ sdk_error }}
110 | Item creation is disabled. Please contact the administrator. 111 |
112 | {% else %} 113 |
114 |
115 | 116 | 124 |
125 |
126 | 127 | 134 |
135 |
136 | 137 | 144 | {# Do not re-populate password #} 145 |
146 |
147 | 148 | 149 |
150 |
151 | 152 |
153 |
154 | {% endif %} 155 | 156 | 157 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-okta-user-script/.env.template: -------------------------------------------------------------------------------- 1 | OKTA_ORG_URL=https://your-domain.okta.com 2 | OKTA_API_TOKEN=your-okta-api-token 3 | # Learn about 1Password Secret References https://developer.1password.com/docs/cli/secret-reference-syntax/ 4 | OP_SERVICE_ACCOUNT_TOKEN = "Secret Reference to Service Account Token in 1Password" 5 | OP_VAULT_ID = "SOME VAULT UUID" -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-okta-user-script/README.md: -------------------------------------------------------------------------------- 1 | # Okta User Creation Utility 2 | 3 | A Python utility that creates a new user in Okta and stores their Okta credentials in 1Password, then creates a Secure Item Sharing Link for the credentials that can be sent to the new employee's personal email address. 4 | 5 | ## Overview 6 | 7 | This script streamlines the user onboarding process by: 8 | 9 | 1. Collecting user information through an interactive prompt. 10 | 2. Creating a new user account in Okta with a secure random password. 11 | 3. Storing the new user's Okta credentials in 1Password. 12 | 4. Creating a share link that allows you to securely share the Okta credentials with the new employee by sending the link to their personal email address. 13 | 14 | ## Requirements 15 | 16 | - Python 3.9+ 17 | - [1Password Service Account](https://developer.1password.com/docs/service-accounts/get-started/) with read, write, and share permissions in the vault where you want to store the new employee's Okta credentials. 18 | - Okta API token with user management permissions 19 | - Python packages: 20 | - `okta-sdk-python` 21 | - `onepassword-sdk` 22 | - `pyperclip` 23 | - `python-dotenv` 24 | 25 | ## Installation 26 | 27 | 1. Clone or download this repository. 28 | 2. Install the required dependencies: 29 | 30 | ```bash 31 | pip install okta-sdk-python onepassword-sdk pyperclip python-dotenv 32 | ``` 33 | 34 | 3. Create a `.env` file in the same directory as the script with the following variables: 35 | 36 | ``` 37 | OKTA_ORG_URL=https://your-domain.okta.com 38 | OKTA_API_TOKEN=your-okta-api-token 39 | OP_SERVICE_ACCOUNT_TOKEN=your-1password-service-account-token 40 | OP_VAULT_ID=your-1password-vault-id 41 | ``` 42 | 43 | ## Usage 44 | 45 | Run the script using Python: 46 | 47 | ```bash 48 | op run --env-file=.env -- python create-okta-user.py 49 | ``` 50 | 51 | The script will: 52 | 53 | 1. Prompt you for the user's information: 54 | - Work email address (will be used as username) 55 | - First name 56 | - Last name 57 | - Personal email address (for credential sharing) 58 | 2. Create the user in Okta with a secure 40-character random password. 59 | 3. Store the credentials in 1Password. 60 | 4. Generate a share link and copy it to your clipboard. 61 | 5. The share link will be valid for 14 days and can be sent to the user's personal email. 62 | 63 | ## Security Features 64 | 65 | - Generates strong 40-character random passwords. 66 | - Validates email formats before processing. 67 | - Uses one-time share links with 14-day expiration. 68 | - Securely stores credentials in 1Password. 69 | - Prevents password exposure by sharing directly from 1Password. 70 | 71 | ## Troubleshooting 72 | 73 | If you encounter errors: 74 | 75 | - Make sure all environment variables are correctly set in the `.env` file. 76 | - Verify your Okta API token has sufficient permissions. 77 | - Check that your 1Password service account token is valid and has read, write, and share permissions in the specified vault. 78 | - Make sure the specified 1Password vault exists and is accessible. 79 | 80 | ## Limitations 81 | 82 | - The script currently only supports basic user creation. 83 | - The script does not implement Okta Group assignments nor does it create additional Okta profile fields. 84 | - The script pre-defines a password recipe when generating a password for a newly-created user using a fixed 40-character random password. 1Password SDKs also support [arbitrary password recipes](https://developer.1password.com/docs/sdks/manage-items#generate-a-password). 85 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-okta-user-script/create-okta-user.py: -------------------------------------------------------------------------------- 1 | import os 2 | from okta.client import Client as OktaClient 3 | from okta.models import User, UserProfile, UserCredentials 4 | import asyncio 5 | from onepassword import * 6 | import pyperclip 7 | from dotenv import load_dotenv 8 | 9 | load_dotenv() 10 | 11 | 12 | def validate_email(email): 13 | """ 14 | Simple email validation 15 | """ 16 | import re 17 | 18 | email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" 19 | return re.match(email_regex, email) is not None 20 | 21 | 22 | def get_user_input(): 23 | """ 24 | Interactively collect user details 25 | """ 26 | while True: 27 | email = input("Enter user email: ").strip() 28 | if validate_email(email): 29 | break 30 | print("Invalid email format. Please try again.") 31 | 32 | first_name = input("Enter first name: ").strip() 33 | last_name = input("Enter last name: ").strip() 34 | personal_email = input("Enter user's personal email: ").strip() 35 | if not validate_email(personal_email): 36 | print("Invalid personal email format. Please try again.") 37 | personal_email = input("Enter user's personal email: ").strip() 38 | 39 | return email, first_name, last_name, personal_email 40 | 41 | 42 | async def create_okta_user(email, first_name, last_name, client, personal_email): 43 | """ 44 | Create a new user in Okta with the specified details. 45 | """ 46 | # Okta configuration 47 | orgUrl = os.getenv("OKTA_ORG_URL") 48 | config = { 49 | "orgUrl": orgUrl, 50 | "token": os.getenv("OKTA_API_TOKEN"), 51 | } 52 | 53 | # Initialize Okta client 54 | okta_client = OktaClient(config) 55 | 56 | # Create user profile 57 | profile = UserProfile( 58 | { 59 | "firstName": first_name, 60 | "lastName": last_name, 61 | "email": email, 62 | "login": email, # Usually set to email for simplicity 63 | } 64 | ) 65 | 66 | password = await create_password(client) 67 | # Create user credentials with password 68 | credentials = UserCredentials({"password": {"value": password}}) 69 | 70 | # Create user object 71 | user = User({"profile": profile, "credentials": credentials}) 72 | 73 | try: 74 | # Create user in Okta 75 | created_user, resp, err = await okta_client.create_user(user) 76 | 77 | if err: 78 | print(f"Error creating user: {err}") 79 | return None 80 | 81 | print(f"Okta user created successfully: {created_user.id}") 82 | await save_item( 83 | client, 84 | username=email, 85 | password=password, 86 | personal_email=personal_email, 87 | org_url=orgUrl, 88 | ) 89 | return created_user 90 | 91 | except Exception as e: 92 | print(f"An error occurred: {e}") 93 | return None 94 | 95 | 96 | async def create_password(client): 97 | random_password = client.secrets.generate_password( 98 | PasswordRecipeRandom( 99 | parameters=PasswordRecipeRandomInner( 100 | length=40, 101 | includeDigits=True, 102 | includeSymbols=True, 103 | ) 104 | ), 105 | ) 106 | return random_password.password 107 | 108 | 109 | async def save_item(client, username, password, personal_email, org_url): 110 | """ 111 | Save the Okta credentials in 1Password 112 | """ 113 | vault_id = os.getenv("OP_VAULT_ID") 114 | item = ItemCreateParams( 115 | title="Okta User Credentials - " + username, 116 | vault_id=vault_id, 117 | category=ItemCategory.LOGIN, 118 | fields=[ 119 | ItemField( 120 | id="username", 121 | title="Username", 122 | value=username, 123 | fieldType=ItemFieldType.TEXT, 124 | ), 125 | ItemField( 126 | id="password", 127 | title="Password", 128 | value=password, 129 | fieldType=ItemFieldType.CONCEALED, 130 | ), 131 | ], 132 | websites=[ 133 | Website( 134 | label="Okta", 135 | url=org_url, 136 | autofillBehavior=AutofillBehavior.ANYWHEREONWEBSITE, 137 | ), 138 | ], 139 | ) 140 | 141 | try: 142 | createdItem = await client.items.create(item) 143 | await create_share_link(client, createdItem, personal_email) 144 | except Exception as e: 145 | print(f"Error saving credentials: {e}") 146 | 147 | 148 | async def create_share_link( 149 | client: Client, 150 | item: Item, 151 | email: str, 152 | ) -> str: 153 | """Create a share link for the note.""" 154 | policy = await client.items.shares.get_account_policy( 155 | vault_id=item.vault_id, item_id=item.id 156 | ) 157 | recipients = [ 158 | ValidRecipientEmail( 159 | parameters=ValidRecipientEmailInner(email=email), 160 | ) 161 | ] 162 | 163 | try: 164 | share_result = await client.items.shares.create( 165 | item=item, 166 | policy=policy, 167 | params=ItemShareParams( 168 | one_time_only=False, 169 | expire_after=ItemShareDuration.FOURTEENDAYS, 170 | recipients=recipients, 171 | ), 172 | ) 173 | pyperclip.copy(share_result) 174 | print(f"Share link created: {share_result} - it's on your clipboard!") 175 | return share_result 176 | 177 | except Exception as e: 178 | print(f"Error creating share link: {e}") 179 | sys.exit(1) 180 | 181 | 182 | async def main(): 183 | print("Okta User Creation Utility") 184 | print("-------------------------") 185 | 186 | # Gets your service account token from the OP_SERVICE_ACCOUNT_TOKEN environment variable. 187 | token = os.getenv("OP_SERVICE_ACCOUNT_TOKEN") 188 | 189 | # Connects to 1Password. Fill in your own integration name and version. 190 | client = await Client.authenticate( 191 | auth=token, 192 | integration_name="Create Okta User Script", 193 | integration_version="v1.0", 194 | ) 195 | 196 | # Get user input 197 | email, first_name, last_name, personal_email = get_user_input() 198 | 199 | # Create user in Okta 200 | await create_okta_user(email, first_name, last_name, client, personal_email) 201 | 202 | 203 | if __name__ == "__main__": 204 | asyncio.run(main()) 205 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-okta-user-script/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv==1.1.0 2 | okta==2.9.10 3 | pyperclip==1.9.0 4 | onepassword-sdk==0.2.1 -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-script/README.md: -------------------------------------------------------------------------------- 1 | # 1Password Note Sharing Script 2 | 3 | A Python utility for creating secure notes in 1Password with file attachments and share links. The contents of the note are derived from a `README.md` file located in a specified folder. This script is designed to facilitate secure sharing of documentation and files outside an organization. 4 | 5 | ## Overview 6 | 7 | This script automates the process of: 8 | 9 | 1. Reading a `README.md` file from a specified folder. 10 | 2. Creating a secure note in 1Password with the README content. 11 | 3. Attaching specified files from the same folder. 12 | 4. Creating a share link for the note with customizable settings. 13 | 14 | ## Requirements 15 | 16 | - Python 3.9+ 17 | - A [1Password Service Account](https://developer.1password.com/docs/service-accounts/get-started/) with read, write, and share permissions in the vault where you want to create the item. 18 | - Python packages: 19 | - `onepassword-sdk` 20 | - `pyperclip` 21 | 22 | ## Installation 23 | 24 | 1. Clone or download this repository. 25 | 2. Install the required dependencies: 26 | 27 | ```bash 28 | pip install onepassword-sdk 29 | ``` 30 | 31 | 3. Make sure you have a 1Password Service Account token set in your environment: 32 | 33 | ```bash 34 | export OP_SERVICE_ACCOUNT_TOKEN="your-token-here" 35 | ``` 36 | 37 | ## Usage 38 | 39 | ```bash 40 | python3 sdk_share_script.py --vault [options] 41 | ``` 42 | 43 | ### Required Arguments 44 | 45 | - `folder_path`: Path to the folder containing the README.md and files to attach 46 | - `--vault` or `-v`: Name of the 1Password vault to use 47 | 48 | ### Optional Arguments 49 | 50 | - `--title` or `-t`: Custom title for the note (default: "README" + current date) 51 | - `--view-once`: Create a view-once share link (link expires after one viewing) 52 | - `--expire-after` or `-e`: Days until share link expires (1, 7, 14, or 30; default: 30) 53 | - `--emails` or `-m`: Space-separated list of email addresses to share with 54 | 55 | ## Examples 56 | 57 | ### Basic Usage 58 | 59 | Create a note with the README content and attach all other files from the folder: 60 | 61 | ```bash 62 | python3 sdk_share_script.py ./my_project --vault "Company Vault" 63 | ``` 64 | 65 | ### Custom Title and Expiration 66 | 67 | ```bash 68 | python3 sdk_share_script.py ./deploy_scripts --vault "Dev Team" --title "Deployment Instructions" --expire-after 7 69 | ``` 70 | 71 | ### Share With Specific Recipients 72 | 73 | ```bash 74 | python3 sdk_share_script.py ./onboarding --vault "HR" --emails john@example.com sarah@example.com --view-once 75 | ``` 76 | 77 | ## How It Works 78 | 79 | 1. The script reads the `README.md` file from the specified folder. 80 | 2. It creates a secure note in the specified 1Password vault with the README content. 81 | 3. It attaches all other files from the folder to the note. 82 | 4. It generates a share link with the specified expiration and recipient settings. 83 | 5. It outputs a summary of the actions performed. 84 | 85 | ## Troubleshooting 86 | 87 | If you encounter errors: 88 | 89 | - Make sure your 1Password Service Account token is valid and has read, write, and share permissions in the vault. 90 | - Verify the specified vault exists and is accessible. 91 | - Check that the folder contains a `README.md` file. 92 | 93 | ## Security Considerations 94 | 95 | - Service Account tokens have powerful access; store them securely. 96 | - Use the shortest practical expiration time for share links. 97 | - Use view-once links for sensitive information. 98 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-script/files/README.md: -------------------------------------------------------------------------------- 1 | This is a test of the super secret file uploader -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-script/files/op-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Password/solutions/51d749b42523f5c4d8213696302d0183369a9ea7/onepassword_sdks/demo-share-script/files/op-dev.png -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-script/requirements.txt: -------------------------------------------------------------------------------- 1 | pyperclip==1.9.0 2 | onepassword-sdk==0.2.1 -------------------------------------------------------------------------------- /onepassword_sdks/demo-share-script/sdk_share_script.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | This script: 4 | 1. Reads a README.md file from a specified folder 5 | 2. Creates a note in 1Password with the README content 6 | 3. Adds a specified file from the same folder as an attachment 7 | 4. Creates a share link for the note 8 | 9 | Uses the official 1Password Python SDK (onepassword). 10 | """ 11 | 12 | import asyncio 13 | import os 14 | import sys 15 | import argparse 16 | import pyperclip 17 | from datetime import datetime 18 | from typing import Dict, Optional, List 19 | from onepassword import * 20 | 21 | 22 | def read_readme(folder_path: str) -> str: 23 | """Read the README.md file from the specified folder.""" 24 | readme_path = os.path.join(folder_path, "README.md") 25 | if not os.path.exists(readme_path): 26 | print(f"Error: README.md not found in {folder_path}") 27 | sys.exit(1) 28 | 29 | try: 30 | with open(readme_path, "r", encoding="utf-8") as file: 31 | content = file.read() 32 | return content 33 | except Exception as e: 34 | print(f"Error reading README.md: {e}") 35 | sys.exit(1) 36 | 37 | 38 | async def get_vault_id_by_name(client: Client, vault_name: str) -> Optional[str]: 39 | """Get the vault ID by name.""" 40 | try: 41 | vaults = await get_vault_details(client) 42 | for vault_id, vault_title in vaults.items(): 43 | if vault_title.lower() == vault_name.lower(): 44 | return vault_id 45 | 46 | print(f"Error: Vault '{vault_name}' not found") 47 | print("Available vaults:") 48 | for vault in vaults: 49 | print(f"- {vault.get('name')}") 50 | sys.exit(1) 51 | except Exception as e: 52 | print(f"Error listing vaults: {e}") 53 | sys.exit(1) 54 | 55 | 56 | async def create_note_with_readme( 57 | client: Client, vault_id: str, title: str, content: str, folder_path: str 58 | ) -> Item: 59 | """Create a secure note in 1Password with README content.""" 60 | try: 61 | # create an empty array of FileCreateParams 62 | filesToAttach = [] 63 | for filename in os.listdir(folder_path): 64 | file_path = os.path.join(folder_path, filename) 65 | if os.path.isfile(file_path) and filename != "README.md": 66 | try: 67 | with open(file_path, "rb") as f: 68 | file_content = f.read() 69 | # Attach the file to the note 70 | file_create_params = FileCreateParams( 71 | name=filename, 72 | content=file_content, 73 | sectionId="scriptsSection", 74 | fieldId=filename, 75 | ) 76 | filesToAttach.append(file_create_params) 77 | print(f"File {filename} attached successfully") 78 | except IOError as e: 79 | print(f"Error attaching file {filename}: {e}") 80 | continue 81 | 82 | item_create_params = ItemCreateParams( 83 | vault_id=vault_id, 84 | category=ItemCategory.SECURENOTE, 85 | title=title, 86 | notes=content, 87 | sections=[ItemSection(id="scriptsSection", title="Scripts")], 88 | files=filesToAttach, 89 | ) 90 | 91 | item = await client.items.create(item_create_params) 92 | 93 | print(f"Note created successfully with ID: {item.id}") 94 | return item 95 | 96 | except Exception as e: 97 | print(f"Error creating note in 1Password: {e}") 98 | sys.exit(1) 99 | 100 | 101 | async def create_share_link( 102 | client: Client, 103 | item: Item, 104 | view_once: bool = False, 105 | expire_after: int = 30, 106 | emails: Optional[List[str]] = None, 107 | ) -> str: 108 | """Create a share link for the note.""" 109 | expiry_string = get_expiry_string(expire_after) 110 | policy = await client.items.shares.get_account_policy( 111 | vault_id=item.vault_id, item_id=item.id 112 | ) 113 | recipients = None 114 | if emails: 115 | recipients = [ 116 | ValidRecipientEmail( 117 | type=ValidRecipientTypes.EMAIL, 118 | parameters=ValidRecipientEmailInner(email=email), 119 | ) 120 | for email in emails 121 | ] 122 | 123 | try: 124 | share_result = await client.items.shares.create( 125 | item=item, 126 | policy=policy, 127 | params=ItemShareParams( 128 | one_time_only=view_once, 129 | expire_after=expiry_string, 130 | recipients=recipients, 131 | ), 132 | ) 133 | return share_result 134 | 135 | except Exception as e: 136 | print(f"Error creating share link: {e}") 137 | sys.exit(1) 138 | 139 | 140 | async def get_vault_details(client: Client) -> Dict[str, str]: 141 | """Cache vault details to avoid repeated API calls.""" 142 | vaults = await client.vaults.list_all() 143 | return {vault.id: vault.title async for vault in vaults} 144 | 145 | 146 | def get_expiry_string(days: int) -> str: 147 | """Convert number of days to expiry string format.""" 148 | expiry_map = {1: "OneDay", 7: "SevenDays", 14: "FourteenDays", 30: "ThirtyDays"} 149 | return expiry_map.get(days, "ThirtyDays") 150 | 151 | 152 | async def main(): 153 | parser = argparse.ArgumentParser( 154 | description="Create a 1Password note from README.md and attach files" 155 | ) 156 | 157 | parser.add_argument( 158 | "folder", help="Path to folder containing README.md and the files to attach" 159 | ) 160 | parser.add_argument("--vault", "-v", required=True, help="1Password vault name") 161 | parser.add_argument( 162 | "--title", "-t", help="Title for the note (default: README + current date)" 163 | ) 164 | parser.add_argument( 165 | "--view-once", action="store_true", help="Create a view-once share link" 166 | ) 167 | parser.add_argument( 168 | "--expire-after", 169 | "-e", 170 | type=int, 171 | choices=[1, 7, 14, 30], 172 | default=30, 173 | help="Number of days the share link is valid (default: 30)", 174 | ) 175 | parser.add_argument( 176 | "--emails", 177 | "-m", 178 | nargs="+", 179 | help="List of emails to add as recipients for the share link", 180 | ) 181 | 182 | args = parser.parse_args() 183 | 184 | # Initiate the 1Password client 185 | token = os.getenv("OP_SERVICE_ACCOUNT_TOKEN") 186 | client = await Client.authenticate( 187 | auth=token, 188 | integration_name="1Password SDK Share Script", 189 | integration_version="1.0", 190 | ) 191 | 192 | # Get the vault ID from the name 193 | vault_id = await get_vault_id_by_name(client, args.vault) 194 | 195 | # Get the README content 196 | readme_content = read_readme(args.folder) 197 | 198 | # Set a default title if not provided 199 | if not args.title: 200 | current_date = datetime.now().strftime("%Y-%m-%d") 201 | args.title = f"README {current_date}" 202 | 203 | # Create the note with the README content 204 | item = await create_note_with_readme( 205 | client, vault_id, args.title, readme_content, args.folder 206 | ) 207 | 208 | # Create a share link 209 | share_link = await create_share_link( 210 | client, item, args.view_once, args.expire_after, args.emails 211 | ) 212 | 213 | pyperclip.copy(share_link) 214 | 215 | print("\nSummary:") 216 | print(f"- Note '{args.title}' created in vault '{args.vault}'") 217 | print(f"- Files attached to the note") 218 | print(f"- Share link created: {share_link} - it's on your clipboard!") 219 | print(f"- Link expires in: {args.expire_after} days") 220 | if args.view_once: 221 | print("- Link is set to view-once mode") 222 | if args.emails: 223 | print(f"- Recipients: {', '.join(args.emails)}") 224 | 225 | 226 | if __name__ == "__main__": 227 | asyncio.run(main()) 228 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-backup-webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js 18 as the base image for the container 2 | FROM node:lts-bookworm-slim 3 | 4 | # Set the working directory to /app inside the container 5 | WORKDIR /webapp 6 | 7 | # Install required tools and configure the 1Password CLI repository 8 | RUN apt-get update && \ 9 | apt-get install -y \ 10 | curl \ 11 | gnupg \ 12 | ca-certificates \ 13 | lsb-release && \ 14 | curl -sS https://downloads.1password.com/linux/keys/1password.asc | \ 15 | gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg && \ 16 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/$(dpkg --print-architecture) stable main" | \ 17 | tee /etc/apt/sources.list.d/1password.list && \ 18 | mkdir -p /etc/debsig/policies/AC2D62742012EA22/ && \ 19 | curl -sS https://downloads.1password.com/linux/debian/debsig/1password.pol | \ 20 | tee /etc/debsig/policies/AC2D62742012EA22/1password.pol && \ 21 | mkdir -p /usr/share/debsig/keyrings/AC2D62742012EA22 && \ 22 | curl -sS https://downloads.1password.com/linux/keys/1password.asc | \ 23 | gpg --dearmor --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg && \ 24 | apt-get update && \ 25 | apt-get install -y 1password-cli 26 | 27 | # Check the installed version of 1Password CLI to ensure it’s working 28 | RUN op --version 29 | 30 | # Copy package.json and package-lock.json (if present) to the working directory 31 | COPY package*.json ./ 32 | 33 | # Install Node.js dependencies defined in package.json 34 | RUN npm install 35 | 36 | # Copy all remaining application files to the container 37 | COPY . . 38 | 39 | # Set permissions: 755 for directories, 644 for files 40 | RUN chown -R node:node /webapp && \ 41 | find /webapp -type d -exec chmod 755 {} \; && \ 42 | find /webapp -type f -exec chmod 644 {} \; 43 | 44 | # Switch to non-root user 45 | USER node 46 | 47 | # Expose port 3002 for the application to listen on 48 | EXPOSE 3002 49 | 50 | # Define the command to start the Node.js application 51 | CMD ["npm", "start"] 52 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-backup-webapp/README.md: -------------------------------------------------------------------------------- 1 | # 1Password Encrypted Vault Backup App 2 | 3 | This web app enables secure backup and restoration of 1Password vaults, encrypting the backup with a user-provided passcode and storing sensitive keys in a dedicated 1Password vault. It uses the 1Password JS SDK and CLI for vault operations, with encryption handled via Argon2 and AES-256-CBC. 4 | 5 | ## Overview 6 | 7 | This app allows you to: 8 | - Connect to a 1Password account using a service account token. 9 | - List and select vaults for backup. 10 | - Encrypt and save vault data to a downloadable file. 11 | - Store encryption keys in a secure 1Password vault. 12 | - Restore vaults from an encrypted backup file to a destination account. 13 | 14 | ## Requirements 15 | 16 | - [Docker](https://docs.docker.com/get-started/get-docker/) 17 | - [1Password Service Account](https://developer.1password.com/docs/service-accounts/get-started) with: 18 | - Read access for listing vaults and items (backup). 19 | - Vault creation and item creation permissions (restore). 20 | 21 | ## Installation 22 | 23 | 1. [Install Docker](https://docs.docker.com/get-started/get-docker/). 24 | 2. Clone or download this project. 25 | 3. Navigate to the project folder and run: 26 | 27 | ``` 28 | docker compose up -d 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Backup 34 | 1. Open `https://localhost:3002` in your browser. 35 | 2. Click **Backup** in the sidebar. 36 | 3. Enter your 1Password service account token and click **Connect**. 37 | 4. Select vaults to back up and provide a passcode (minimum 8 characters). 38 | 5. Click **Backup Selected Vaults** or **Backup All Vaults**. 39 | 6. Save the generated encryption keys to a new 1Password vault or download them. 40 | 7. Download the encrypted `backup.1pbackup` file. 41 | 42 | ### Restore 43 | 1. Open `https://localhost:3002` and click **Restore**. 44 | 2. Upload the `backup.1pbackup` file and enter the service account token, passcode, and system key. 45 | 3. Select vaults to restore from the backup. 46 | 4. Click **Restore Selected Vaults**. 47 | 5. Verify the restored vaults in the destination account. 48 | 49 | ## Special Handling with CLI 50 | 51 | - **Vault Creation**: Uses 1Password CLI (`op vault create`) to create new vaults for restored data and key storage, as vault creation is not supported by the SDK. 52 | 53 | ## Security Features 54 | 55 | - Runs on HTTPS with a self-signed certificate (local testing). 56 | - Uses Argon2 for key derivation and AES-256-CBC for backup encryption. 57 | - Verifies backup integrity with HMAC-SHA256. 58 | - Saves encryption keys (passcode and system key) in a secure 1Password vault. 59 | - Uses `p-limit` to prevent overwhelming the 1Password API. 60 | - Implements retry logic for API rate limits or conflicts. 61 | 62 | ## Troubleshooting 63 | 64 | - Ensure Docker is running and the container is active (`docker logs `). 65 | - Verify service account token permissions (read for backup, create for restore/keys). 66 | - Check `https://localhost:3002` is accessible; accept the self-signed certificate if prompted. 67 | - Confirm passcode and system key match the backup file during restoration. 68 | - Ensure the backup file is not corrupted or tampered with (HMAC verification failure). 69 | 70 | ## Limitations 71 | 72 | - Passkeys cannot be backed up or restored (use 1Password desktop/mobile apps). 73 | - SDK does not support archived items for backup/restore. 74 | - Restored vault names are appended with "(Restored)". 75 | - Fixed concurrency limits (2 vaults, 1 item at a time) may need tuning for large backups. -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-backup-webapp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | image: demo-vault-backup-webapp:v1.0.0 7 | ports: 8 | - "3002:3002" 9 | environment: 10 | - NODE_ENV=development 11 | command: npm start 12 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-backup-webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-vault-backup-webapp", 3 | "version": "1.0.0", 4 | "main": "webapp.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "node webapp.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "@1password/sdk": "^0.3.0", 15 | "argon2": "^0.41.1", 16 | "body-parser": "^2.2.0", 17 | "ejs": "^3.1.10", 18 | "express": "^4.21.2", 19 | "express-session": "^1.18.1", 20 | "multer": "^1.4.5-lts.2", 21 | "p-limit": "^4.0.0", 22 | "selfsigned": "^2.4.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-backup-webapp/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Password/solutions/51d749b42523f5c4d8213696302d0183369a9ea7/onepassword_sdks/demo-vault-backup-webapp/public/.gitkeep -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-backup-webapp/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Password/solutions/51d749b42523f5c4d8213696302d0183369a9ea7/onepassword_sdks/demo-vault-backup-webapp/uploads/.gitkeep -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-backup-webapp/views/welcome.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1Password Backup & Restore Tool 6 | 7 | 8 | 33 | 34 | 35 |
36 | 44 |
45 |
46 |

Welcome to the 1Password Backup & Restore Tool

47 |

Backup and restore your 1Password vaults securely. Use the sidebar to get started.

48 |
49 |
50 |
51 | 52 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-migration-webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js 18 as the base image for the container 2 | FROM node:lts-bookworm-slim 3 | 4 | # Set the working directory to /app inside the container 5 | WORKDIR /webapp 6 | 7 | # Install required tools and configure the 1Password CLI repository 8 | RUN apt-get update && \ 9 | apt-get install -y \ 10 | curl \ 11 | gnupg \ 12 | ca-certificates \ 13 | lsb-release &&\ 14 | curl -sS https://downloads.1password.com/linux/keys/1password.asc | \ 15 | gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg && \ 16 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/$(dpkg --print-architecture) stable main" | \ 17 | tee /etc/apt/sources.list.d/1password.list && \ 18 | mkdir -p /etc/debsig/policies/AC2D62742012EA22/ && \ 19 | curl -sS https://downloads.1password.com/linux/debian/debsig/1password.pol | \ 20 | tee /etc/debsig/policies/AC2D62742012EA22/1password.pol && \ 21 | mkdir -p /usr/share/debsig/keyrings/AC2D62742012EA22 && \ 22 | curl -sS https://downloads.1password.com/linux/keys/1password.asc | \ 23 | gpg --dearmor --output /usr/share/debsig/keyrings/AC2D62742012EA22/debsig.gpg && \ 24 | apt-get update && \ 25 | apt-get install -y 1password-cli 26 | 27 | # Check the installed version of 1Password CLI to ensure it’s working 28 | RUN op --version 29 | 30 | # Copy package.json and package-lock.json (if present) to the working directory 31 | COPY package*.json ./ 32 | 33 | # Install Node.js dependencies defined in package.json 34 | RUN npm install 35 | 36 | # Copy all remaining application files to the container 37 | COPY . . 38 | 39 | # Set permissions: 755 for directories, 644 for files 40 | RUN chown -R node:node /webapp && \ 41 | find /webapp -type d -exec chmod 755 {} \; && \ 42 | find /webapp -type f -exec chmod 644 {} \; 43 | 44 | # Switch to non-root user 45 | USER node 46 | 47 | # Expose port 3001 for the application to listen on 48 | EXPOSE 3001 49 | 50 | # Define the command to start the Node.js application 51 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-migration-webapp/README.md: -------------------------------------------------------------------------------- 1 | # 1Password Vault Migration App 2 | 3 | This web app allows you to move vaults from one 1Password account to another. It uses the 1Password JS SDK, as well as 1Password CLI to handle actions not yet supported by the SDK at this time. 4 | 5 | ## Overview 6 | 7 | This app helps you to move vaults between 1Password accounts by: 8 | 9 | - Giving you a simple web page to connect to your source and destination 1Password accounts using service account tokens. 10 | - Showing you all the vaults from the source account so you can pick which ones to move. 11 | - Moving the vaults you select (or all of them) to the destination account. 12 | - Using a mix of the SDK and CLI to handle certain tasks. 13 | 14 | ## Requirements 15 | 16 | - [Docker](https://docs.docker.com/get-started/get-docker/) 17 | - [1Password CLI](https://developer.1password.com/docs/cli/get-started) 18 | - [1Password Service Account](https://developer.1password.com/docs/service-accounts/get-started#create-a-service-account) that can access your source and destination accounts: 19 | - The source token needs read access, so it can see vaults and items in the source account. 20 | - The destination token needs create vault permissions, so it can make new vaults and add items in the destination vault. 21 | 22 | ## Installation 23 | 24 | 1. [Install Docker on your computer](https://docs.docker.com/get-started/get-docker/) 25 | 2. Clone or download this project to your computer. 26 | 3. In your terminal, navigate to the project folder. To build and run the Docker image with the Dockerfile, run the following in your terminal: 27 | ``` 28 | docker compose up -d 29 | ``` 30 | 31 | ## Usage 32 | 33 | 1. Open your browser and go to `https://localhost:3001`. 34 | 2. On the welcome page, click **Vault Migration** in the sidebar to get to the migration tool. 35 | 3. Enter the 1Password service account tokens for your source and destination accounts in the Migration Setup form, then select **Connect**. 36 | 4. You’ll see a table with all the vaults from the source account. You can: 37 | - Check the boxes for the vaults you want to move and select **Migrate Selected Vaults**. 38 | - Select **Migrate All Vaults** to move everything. 39 | 5. Verify your information is in the destination account once the vault migration completes. 40 | 41 | ## Special Handling with CLI 42 | 43 | - **Vault Creation**: The app uses 1Password CLI (`op vault create`) to make new vaults in the destination account, since vault creation isn't yet supported by the SDK. 44 | 45 | ## Security Features 46 | 47 | - Runs on HTTPS with a self-signed certificate (good for local testing). 48 | - Keeps service account tokens in `sessionStorage` on the browser side, so they’re not stored on the server. 49 | - Has retry logic that waits longer each time if the API says "too many requests" or there’s a conflict. 50 | - Uses `p-limit` to make sure we don’t send too many requests at once and overwhelm the 1Password API. 51 | 52 | ## Troubleshooting 53 | 54 | If something goes wrong: 55 | 56 | - Make sure Docker is installed and running on your computer. 57 | - Double-check your 1Password service account tokens for both source and destination accounts—they need to be valid, with read access for the source token and create vaults for the destination token. 58 | - Make sure the Docker container is running and you can reach it at `https://localhost:3001`. You can see the logs with `docker logs ` to figure out what’s up. 59 | - If your browser complains about SSL, just accept the self-signed certificate for `localhost`. 60 | 61 | ## Limitations 62 | 63 | - Neither the 1Password SDK, CLI, nor vault data export can access or report on passkey fields. To transfer passkeys between 1Password accounts, use the 1Password desktop or mobile app. 64 | - The SDK does not natively support retrieving archived items for vault migration. 65 | - The application uses a self-signed certificate for HTTPS, suitable for local testing but requiring a valid certificate for production deployment. 66 | - Vault names cannot be modified during migration; the script appends "(Migrated)" to the destination vault names. 67 | - The application has fixed concurrency limits (2 vaults and 1 item processed simultaneously), which may need adjustment for large-scale migrations. -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-migration-webapp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | image: demo-vault-migration-webapp:v1.0.3 7 | ports: 8 | - "3001:3001" 9 | environment: 10 | - NODE_ENV=development 11 | command: npm start 12 | 13 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-migration-webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solutions-migrate-vaults", 3 | "version": "1.0.0", 4 | "main": "webapp.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "node webapp.js" 8 | }, 9 | "keywords": [], 10 | "author": "1Password", 11 | "license": "ISC", 12 | "description": "Tool to assist with migrating vaults from 1Password account to another.", 13 | "dependencies": { 14 | "@1password/sdk": "^0.3.0", 15 | "body-parser": "^2.2.0", 16 | "ejs": "^3.1.10", 17 | "express": "^4.21.2", 18 | "express-session": "^1.18.1", 19 | "p-limit": "^4.0.0", 20 | "selfsigned": "^2.4.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-migration-webapp/public/styles.css: -------------------------------------------------------------------------------- 1 | /* Set default styles for the body */ 2 | body { 3 | font-family: 'Arial', sans-serif; 4 | background-color: #2C3E50; /* Consistent dark blue background to align with migration page */ 5 | color: #FFFFFF; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | /* Style the vault table container */ 11 | .vault-table { 12 | background-color: #FFFFFF; 13 | color: #1F2A44; /* Dark text for contrast against white background */ 14 | border-radius: 10px; 15 | padding: 15px; 16 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ 17 | } 18 | 19 | /* Ensure the inner table takes full width and has no spacing between cells */ 20 | .vault-table table { 21 | width: 100%; 22 | border-collapse: collapse; 23 | } 24 | 25 | /* Define padding and alignment for table headers and cells */ 26 | .vault-table th, .vault-table td { 27 | padding: 10px; 28 | text-align: left; 29 | } 30 | 31 | /* Style table headers with a distinct background */ 32 | .vault-table th { 33 | background-color: #006CFF; /* Bright blue to highlight headers */ 34 | color: #FFFFFF; 35 | } 36 | 37 | /* Apply alternating row colors for better readability */ 38 | .vault-table tr:nth-child(even) { 39 | background-color: #F5F6F5; /* Light gray for even rows */ 40 | } 41 | 42 | /* Style error messages for visibility */ 43 | .error { 44 | color: #FF4D4F; /* Red color to indicate errors */ 45 | margin-top: 10px; 46 | } -------------------------------------------------------------------------------- /onepassword_sdks/demo-vault-migration-webapp/views/welcome.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vault Migration Tool 6 | 7 | 8 | 140 | 141 | 142 |
143 | 150 |
151 |
152 |

Welcome to the Vault Migration Tool

153 |

Migrate vaults between 1Password tenants with ease. Click "Vault Migration" in the sidebar to get started.

154 |
155 |
156 |
157 | 158 | -------------------------------------------------------------------------------- /reporting/README.md: -------------------------------------------------------------------------------- 1 | # Reporting Scripts 2 | 3 | This directory provides a series of scripts demonstrating how to use the CLI to generate summary reports of vault access, contents, and permissions. 4 | 5 | > 💡 **NOTE** 6 | > Many of these scripts require version 2.25 or newer of the 1Password CLI. Download the latest version [here](https://developer.1password.com/docs/cli/get-started). 7 | 8 | ## User vault access reports 9 | The following scripts provide information about people who are directly granted access to vaults. It does not include groups. 10 | 11 | ### [`user-access-list.py`](./user-access-list.py) 12 | - Creates a csv file for each user in your 1Password account, and includes the following columns: 13 | - userName, userEmail, userUUID, userState, userCreatedAt, userUpdatedAt, directlyAssignedVaults, groups 14 | - where `userState` is their status (e.g., ACTIVE, SUSPENDED, etc), `directlyAssignedVaults` refers to vaults the user has been granted access to directly (not by group membership) and `groups` is a list of groups the person is a member of. 15 | 16 | If you need to review who has access to vaults both by direct assignment and group membership, see [vault-user-group-access-report.py](#vault-user-group-access-reportpy). 17 | 18 | ### [`user-and-item-list.py`](./user-and-item-list.py) 19 | - Creates a csv file of each user and item that has access to a list of vaults. 20 | - use `--file path/to/vaultlist` to provide a list of UUIDs, or use no flag to get a report for all vaults the Owners group has access to. 21 | - Columns included: "vaultName", "vaultUUD", "userName", "userEmail", "userUUID", "userPermissions", "itemName", "itemUUID" 22 | - This does not include users who have access to vaults as a result of membership in a group granted access to a vault. 23 | 24 | ### [`vault-user-access-report.py`](./vault-user-access-report.py) 25 | 26 | - Creates a csv file listing vaults, every user that has been directly granted access to each vault, and their permissions. 27 | - Use `--file path/to/vaultlist` to provide a list of UUIDs, or use no flag to get a report for all vaults the Owners group has access to. 28 | - Columns included: "vaultName", "vaultUUID", "name", "email", "userUUID", "userPermissions" 29 | - This does NOT show users who have access to vaults as a result of their membership in an assigned group. 30 | 31 | 32 | ## User and group vault access reports 33 | The following scripts provide information about vault access for both groups and individual users. Some scripts will decompose groups into their individual members, some may only provide information about the group itself. 34 | 35 | ### [`vault-user-group-access-report.py`](./vault-user-group-access-report.py) 36 | 37 | - Creates a csv file listing vaults, every user that has been granted access to the vault directly or by virtue of their group membership, and their vault permissions. 38 | - Columns included: "vaultName", "vaultUUID", "userName", "userEmail", "userUUID", "userState", "assignment", "permissions" 39 | - "userState" indicates if the user is `ACTIVE`, `SUSPENDED`, `INVITED`, etc. a 40 | - "assignment" indicates whether they were directly assigned ("direct") or have access to a vault due to membership in an assigned group ("group(groupName)") 41 | - If users have access to a vault by multiple assignments (e.g., is directly assigned and a member of one or more group, or is a member of multiple groups all of which are assigned to the vault) they will appear on one row for every way they've been granted access. This can facilitate the identification of redundant assigments or unwanted permissions sprawl 42 | 43 | -------------------------------------------------------------------------------- /reporting/beta/golang-lambda-deployment/README.md: -------------------------------------------------------------------------------- 1 | # 1Password Events API Base Golang Lambda Script 2 | 3 | This folder contains the necessary components for getting starting piping events from the 1Password Event API into Cloudwatch Logs using a Lambda. If you like the region and the names of variables, you can use the AWS UI to create a few things and then upload bootstrap.zip as is. 4 | 5 | If you need to make changes to the region or other parts of the script, you will need to recompile the script. See "[Build From Code](#build-from-code)" for guidance on recompiling. 6 | 7 | ## Resources 8 | 9 | [Events API Documentation](https://developer.1password.com/docs/events-api/) 10 | 11 | ## Using as-is 12 | 13 | 1. Setup the Events Reporting integration in 1Password using [these instructions](https://support.1password.com/events-reporting/#step-1-set-up-an-events-reporting-integration) and selecting `Other`. The only event types in the current script are Sign-In events. 14 | 2. Log into the us-east-1 region in AWS and create the following: 15 | - In AWS Secrets Manager, create a secret called `op-events-api-token` 16 | - Secret Type: Other type of secret 17 | - Key/value pairs: Plaintext -> paste in the bearer token 18 | - Everything else can be left as default 19 | - In AWS Systems Manager -> Parameter Store, create a secret called `op-events-api-cursor` 20 | - Tier: Standard 21 | - Type: String 22 | - Data Type: text 23 | - Value: first_run 24 | - In AWS CloudWatch, create a log group called `op-events-api-signins`, leaving everything else as default. Add a log stream to this log group call `op-events-api-signins-stream` 25 | - In AWS Lambda, create a new function called `op-events-api-signins-lambda` 26 | - Runtime: Amazon Linux 2 27 | - Architecture: arm64 28 | - Create a new role with basic Lambda permissions 29 | 3. After the lambda is created, go to AWS IAM -> Roles and click on `op-events-api-signins-lambda-role-#####` (###### just represents some random characters) and edit the json under the policy `AWSLambdaBasicExecutionRole-#####` to be: 30 | 31 | ```json 32 | { 33 | "Version": "2012-10-17", 34 | "Statement": [ 35 | { 36 | "Effect": "Allow", 37 | "Action": [ 38 | "logs:CreateLogGroup", 39 | "secretsmanager:DescribeSecret", 40 | "secretsmanager:GetSecretValue", 41 | "ssm:GetParameter", 42 | "ssm:PutParameter" 43 | ], 44 | "Resource": "arn:aws:logs:us-east-1::*" 45 | }, 46 | { 47 | "Effect": "Allow", 48 | "Action": [ 49 | "logs:CreateLogStream", 50 | "logs:PutLogEvents", 51 | "secretsmanager:DescribeSecret", 52 | "secretsmanager:GetSecretValue", 53 | "ssm:GetParameter", 54 | "ssm:PutParameter" 55 | ], 56 | "Resource": [ 57 | "arn:aws:logs:us-east-1::log-group:/aws/lambda/op-events-api-signins-lambda:*" 58 | ] 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | and then Add permissions -> attach policies, then add the policies `AmazonSSMFullAccess` and `SecretsManagerReadWrite` 65 | 4. Return to the lambda function and on the right, select Upload from -> zip file and then navigate to and find bootstrap.zip from this folder. 66 | 5. Navigate to test and then click the orange `Test` button, and you should get a success, and when you check the cloudwatch group and stream, there should be a new entry. 67 | 6. Add a trigger for how frequently you would like this to run. For example, EventBridge, new rule with expression `rate(10 minutes)` 68 | 69 | ## Notes 70 | 71 | - This is very much still in beta and has not been tested extensively 72 | - This is not optimized yet - it's a first pass to get it working, there are definitely smarter ways to do this 73 | - This starts with the last 24 hours of data, and does not currently load any events before then 74 | 75 | 76 | ## Build from code 77 | If you need to make changes to the script to fit your environment or specifications, you will need to recompile the script. Once you've made your changes, use the following command to compile. Once compiled, you may follow the [deployment directions](#using-as-is), using the newly-compiled boostrap.zip. 78 | 79 | ```bash 80 | GOOS=linux GOARCH=arm64 go build -tags lambda.norpc -o bootstrap main.go && zip bootstrap.zip bootstrap 81 | ``` 82 | -------------------------------------------------------------------------------- /reporting/beta/golang-lambda-deployment/bootstrap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Password/solutions/51d749b42523f5c4d8213696302d0183369a9ea7/reporting/beta/golang-lambda-deployment/bootstrap -------------------------------------------------------------------------------- /reporting/beta/golang-lambda-deployment/bootstrap.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1Password/solutions/51d749b42523f5c4d8213696302d0183369a9ea7/reporting/beta/golang-lambda-deployment/bootstrap.zip -------------------------------------------------------------------------------- /reporting/beta/golang-lambda-deployment/go.mod: -------------------------------------------------------------------------------- 1 | module amanda-lambda 2 | 3 | go 1.21.6 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.46.0 7 | github.com/aws/aws-sdk-go-v2 v1.24.1 8 | github.com/aws/aws-sdk-go-v2/config v1.26.6 9 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 10 | ) 11 | 12 | require ( 13 | github.com/aws/aws-sdk-go v1.50.15 // indirect 14 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect 15 | github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect 16 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect 17 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect 20 | github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.32.0 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect 27 | github.com/aws/smithy-go v1.19.0 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/jmespath/go-jmespath v0.4.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /reporting/beta/golang-lambda-deployment/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-lambda-go v1.46.0 h1:UWVnvh2h2gecOlFhHQfIPQcD8pL/f7pVCutmFl+oXU8= 2 | github.com/aws/aws-lambda-go v1.46.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= 3 | github.com/aws/aws-sdk-go v1.50.15 h1:wEMnPfEQQFaoIJwuO18zq/vtG4Ft7NxQ3r9xlEi/8zg= 4 | github.com/aws/aws-sdk-go v1.50.15/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= 5 | github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= 6 | github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= 7 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= 8 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= 9 | github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o= 10 | github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4= 11 | github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= 12 | github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= 13 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= 14 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= 15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= 16 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= 17 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= 18 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= 19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls= 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= 21 | github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.32.0 h1:VdKYfVPIDzmfSQk5gOQ5uueKiuKMkJuB/KOXmQ9Ytag= 22 | github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.32.0/go.mod h1:jZNaJEtn9TLi3pfxycLz79HVkKxP8ZdYm92iaNFgBsA= 23 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= 24 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= 25 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= 26 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= 27 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 h1:A5sGOT/mukuU+4At1vkSIWAN8tPwPCoYZBp7aruR540= 28 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2/go.mod h1:qutL00aW8GSo2D0I6UEOqMvRS3ZyuBrOC1BLe5D2jPc= 29 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE= 30 | github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM= 31 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= 32 | github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= 33 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= 34 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= 35 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= 36 | github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= 37 | github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= 38 | github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= 39 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 41 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 43 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 44 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 45 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 46 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 47 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 51 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 54 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 55 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 56 | -------------------------------------------------------------------------------- /reporting/beta/golang-lambda-deployment/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "time" 11 | 12 | "context" 13 | 14 | "github.com/aws/aws-lambda-go/lambda" 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | "github.com/aws/aws-sdk-go-v2/config" 17 | "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" 18 | "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" 19 | "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 20 | "github.com/aws/aws-sdk-go-v2/service/ssm" 21 | ) 22 | 23 | type Signins struct { 24 | Cursor string `json:"cursor"` 25 | Has_more bool `json:"has_more"` 26 | Items []Item `json:"items"` 27 | } 28 | 29 | type TargetUser struct { 30 | UUID string `json:"uuid"` 31 | Name string `json:"name"` 32 | Email string `json:"email"` 33 | } 34 | 35 | type Client struct { 36 | AppName string `json:"app_name"` 37 | AppVersion string `json:"app_version"` 38 | PlatformName string `json:"platform_name"` 39 | PlatformVersion string `json:"platform_version"` 40 | OSName string `json:"os_name"` 41 | OSVersion string `json:"os_version"` 42 | IPAddress string `json:"ip_address"` 43 | } 44 | 45 | type Location struct { 46 | Country string `json:"country"` 47 | Region string `json:"region"` 48 | City string `json:"city"` 49 | Latitude float64 `json:"latitude"` 50 | Longitude float64 `json:"longitude"` 51 | } 52 | 53 | type Item struct { 54 | UUID string `json:"uuid"` 55 | SessionUUID string `json:"session_uuid"` 56 | Timestamp time.Time `json:"timestamp"` 57 | Country string `json:"country"` 58 | Category string `json:"category"` 59 | Type string `json:"type"` 60 | Details string `json:"details"` 61 | TargetUser TargetUser `json:"target_user"` 62 | Client Client `json:"client"` 63 | Location Location `json:"location"` 64 | } 65 | 66 | var ( 67 | eventAPIurl string = "https://events.1password.com" 68 | region string = "us-east-1" 69 | secretName string = "op-events-api-token" 70 | parameterName string = "op-events-api-cursor" 71 | cloudwatchLogGroup string = "op-events-api-signins" 72 | cloudwatchStream string = "op-events-api-signins-stream" 73 | ) 74 | 75 | func main() { 76 | lambda.Start(getSignInEvents) 77 | } 78 | 79 | // Retrieves data from the sign-in events endpoint. 80 | // Depending on your implementation you may want to create functions for each of the three endpoints, 81 | // or refactor this function to accept an endpoint and any other required properties, calling it once for each of the three endpoints. 82 | func getSignInEvents() { 83 | api_token := loadToken() 84 | start_time := time.Now().AddDate(0, 0, -1) 85 | 86 | currCursor := getCursor() 87 | 88 | var payload []byte 89 | if currCursor == "first_run" || currCursor == "" { 90 | payload = []byte(fmt.Sprintf(`{ 91 | "limit": 1000, 92 | "start_time": "%s" 93 | }`, start_time.Format(time.RFC3339))) 94 | } else { 95 | payload = []byte(fmt.Sprintf(`{ 96 | "cursor": "%s" 97 | }`, currCursor)) 98 | } 99 | 100 | client := &http.Client{} 101 | 102 | signinsRequest, _ := http.NewRequest("POST", fmt.Sprintf("%s/api/v1/signinattempts", eventAPIurl), bytes.NewBuffer(payload)) 103 | signinsRequest.Header.Set("Content-Type", "application/json") 104 | signinsRequest.Header.Set("Authorization", "Bearer "+api_token) 105 | signinsResponse, signinsError := client.Do(signinsRequest) 106 | if signinsError != nil { 107 | panic(signinsError) 108 | } 109 | defer signinsResponse.Body.Close() 110 | signinsBody, _ := io.ReadAll(signinsResponse.Body) 111 | 112 | var results Signins 113 | json.Unmarshal(signinsBody, &results) 114 | 115 | if len(results.Items) != 0 { 116 | writeLogs(results.Items) 117 | setCursor(results.Cursor) 118 | } 119 | 120 | if results.Has_more { 121 | getSignInEvents() 122 | } 123 | } 124 | 125 | func loadConfig() aws.Config { 126 | config, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | 131 | return config 132 | } 133 | 134 | func loadToken() string { 135 | // Create Secrets Manager client 136 | svc := secretsmanager.NewFromConfig(loadConfig()) 137 | 138 | input := &secretsmanager.GetSecretValueInput{ 139 | SecretId: aws.String(secretName), 140 | VersionStage: aws.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified 141 | } 142 | 143 | result, err := svc.GetSecretValue(context.TODO(), input) 144 | if err != nil { 145 | // For a list of exceptions thrown, see 146 | // https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html 147 | log.Fatal(err.Error()) 148 | } 149 | 150 | // Decrypts secret using the associated KMS key. 151 | return *result.SecretString 152 | 153 | } 154 | 155 | // Get API cursor from ParameterStore. Could be refactored to take an endpoint as 156 | // an argument and fetch the corresponding cursor. 157 | func getCursor() string { 158 | paramInput := ssm.GetParameterInput{ 159 | Name: aws.String(parameterName), 160 | } 161 | paramStore := ssm.NewFromConfig(loadConfig()) 162 | output, err := paramStore.GetParameter(context.TODO(), ¶mInput) 163 | if err != nil { 164 | log.Print(err) 165 | return "" 166 | } 167 | 168 | return *output.Parameter.Value 169 | } 170 | 171 | // Writes new cursor to parameter store. Could be refactored to take an endpoint as 172 | // an argument and fetch the corresponding cursor. 173 | func setCursor(cursor string) { 174 | paramInput := ssm.PutParameterInput{ 175 | Name: aws.String(parameterName), 176 | Value: &cursor, 177 | Overwrite: aws.Bool(true), 178 | } 179 | paramStore := ssm.NewFromConfig(loadConfig()) 180 | 181 | paramStore.PutParameter(context.TODO(), ¶mInput) 182 | } 183 | 184 | // Write logs to destination. In this case CloudWatch, but would require refactoring 185 | // for your specific implementation and destination. 186 | func writeLogs(logItems []Item) { 187 | // Create a CloudWatchLogs client with additional configuration 188 | svc := cloudwatchlogs.NewFromConfig(loadConfig()) 189 | 190 | // Each array of InputLogEvent's needs to be within a 24-hour window 191 | // In theory, this is only an issue on first call 192 | var events []types.InputLogEvent 193 | for _, item := range logItems { 194 | timestamp := item.Timestamp.UnixMilli() 195 | itemByte, _ := json.Marshal(item) 196 | itemString := string(itemByte) 197 | event := types.InputLogEvent{ 198 | Message: &itemString, 199 | Timestamp: ×tamp, 200 | } 201 | 202 | events = append(events, event) 203 | } 204 | 205 | logEvent := cloudwatchlogs.PutLogEventsInput{ 206 | LogEvents: events, 207 | LogGroupName: &cloudwatchLogGroup, 208 | LogStreamName: &cloudwatchStream, 209 | } 210 | 211 | _, err := svc.PutLogEvents(context.TODO(), &logEvent) 212 | 213 | // log.Println("writeLogs error:") 214 | log.Println(err) 215 | } 216 | -------------------------------------------------------------------------------- /reporting/user-access-list.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import csv 4 | import json 5 | 6 | scriptPath = os.path.dirname(__file__) 7 | outputPath = scriptPath 8 | 9 | 10 | class User: 11 | def __init__( 12 | self, name, email, uuid, state, type, createdAt, updatedAt, groups, vaults 13 | ): 14 | self.name = name 15 | self.email = email 16 | self.uuid = uuid 17 | self.state = state 18 | self.type = type 19 | self.createdAt = createdAt 20 | self.updatedAt = updatedAt 21 | self.groups = groups 22 | self.vaults = vaults 23 | 24 | 25 | def getAllUsers(): 26 | return json.loads( 27 | subprocess.run( 28 | ["op", "user", "list", "--format=json"], check=True, capture_output=True 29 | ).stdout 30 | ) 31 | 32 | 33 | def getUserInfo(userUUID): 34 | return json.loads( 35 | subprocess.run( 36 | ["op", "user", "get", userUUID, "--format=json"], 37 | check=True, 38 | capture_output=True, 39 | ).stdout 40 | ) 41 | 42 | 43 | def getUserVaults(userUUID): 44 | return json.loads( 45 | subprocess.run( 46 | ["op", "vault", "list", f"--user={userUUID}", "--format=json"], 47 | check=True, 48 | capture_output=True, 49 | ).stdout 50 | ) 51 | 52 | 53 | def getUserGroups(userUUID): 54 | return json.loads( 55 | subprocess.run( 56 | ["op", "group", "list", f"--user={userUUID}", "--format=json"], 57 | check=True, 58 | capture_output=True, 59 | ).stdout 60 | ) 61 | 62 | 63 | def writeReport(users): 64 | with open(f"{outputPath}/user_access_list.csv", "w", newline="") as outputFile: 65 | csvWriter = csv.writer(outputFile) 66 | fields = [ 67 | "userName", 68 | "userEmail", 69 | "userUUID", 70 | "userState", 71 | "userType", 72 | "userCreatedAt", 73 | "userUpdatedAt", 74 | "directlyAssignedVaults", 75 | "groups", 76 | ] 77 | csvWriter.writerow(fields) 78 | 79 | for user in users: 80 | csvWriter.writerow( 81 | [ 82 | user.name, 83 | user.email, 84 | user.uuid, 85 | user.state, 86 | user.type, 87 | user.createdAt, 88 | user.updatedAt, 89 | user.vaults, 90 | user.groups, 91 | ] 92 | ) 93 | 94 | 95 | def main(): 96 | rawUsers = getAllUsers() 97 | accountUsers = [] 98 | usercount = len(rawUsers) 99 | counter = 1 100 | for user in rawUsers: 101 | print(f"Processing user {counter}/{usercount}") 102 | userData = getUserInfo(user["id"]) 103 | userGroups = getUserGroups(user["id"]) 104 | userVaults = getUserVaults(user["id"]) 105 | groups = [(group["name"], group["id"]) for group in userGroups] 106 | vaults = [(vault["name"], vault["id"]) for vault in userVaults] 107 | accountUsers.append( 108 | User( 109 | name=user["name"], 110 | email=user["email"], 111 | uuid=user["id"], 112 | state=user["state"], 113 | type=user["type"], 114 | createdAt=userData["created_at"], 115 | updatedAt=userData["updated_at"], 116 | groups=str(groups).removeprefix("[").removesuffix("]"), 117 | vaults=str(vaults).removeprefix("[").removesuffix("]"), 118 | ) 119 | ) 120 | counter += 1 121 | 122 | writeReport(accountUsers) 123 | 124 | 125 | main() 126 | -------------------------------------------------------------------------------- /reporting/vault-user-access-report.py: -------------------------------------------------------------------------------- 1 | # A script that provides a list of users and their permissions for each vault 2 | import os 3 | import subprocess 4 | import csv 5 | import json 6 | import argparse 7 | import sys 8 | 9 | parser = argparse.ArgumentParser( 10 | "User and Permissions Report Generator", 11 | "Generates a csv-like report of each user and their permissions for each vault passed to this script.", 12 | ) 13 | parser.add_argument( 14 | "--file", 15 | action="store", 16 | dest="filepath", 17 | help="Specify a path to a file containing a line-deliminted list of vault UUIDs to include in the report.", 18 | ) 19 | args = parser.parse_args() 20 | 21 | scriptPath = os.path.dirname(__file__) 22 | inputFilePath = args.filepath 23 | outputPath = scriptPath # Optionally choose an alternative output path here. 24 | 25 | 26 | # Check CLI version 27 | def checkCLIVersion(): 28 | r = subprocess.run(["op", "--version", "--format=json"], capture_output=True) 29 | major, minor = r.stdout.decode("utf-8").rstrip().split(".", 2)[:2] 30 | if not major == 2 and not int(minor) >= 25: 31 | sys.exit( 32 | "❌ You must be using version 2.25 or greater of the 1Password CLI. Please visit https://developer.1password.com/docs/cli/get-started to download the lastest version." 33 | ) 34 | 35 | 36 | # get a list of vaults the logged-in user has access to 37 | def getAllOwnerVaults(): 38 | vaultList = subprocess.run( 39 | ["op", "vault", "list", "--permission=manage_vault", "--format=json"], 40 | check=True, 41 | capture_output=True, 42 | ).stdout 43 | return vaultList 44 | 45 | 46 | def getSpecifiedVaults(): 47 | with open(inputFilePath, "r", encoding="utf-8") as f: 48 | vaultList = [] 49 | for id in f: 50 | vaultList.append( 51 | json.loads( 52 | subprocess.run( 53 | ["op", "vault", "get", id.rstrip(), "--format=json"], 54 | check=True, 55 | capture_output=True, 56 | ).stdout 57 | ) 58 | ) 59 | return vaultList 60 | 61 | 62 | # get a list of users and their permissions for a vault 63 | def getVaultUserList(vaultID): 64 | vaultUserList = subprocess.run( 65 | ["op", "vault", "user", "list", vaultID, "--format=json"], 66 | check=True, 67 | capture_output=True, 68 | ).stdout 69 | return vaultUserList 70 | 71 | 72 | # get all groups in the account 73 | def getVaultGroupList(vaultID): 74 | vaultGroupList = subprocess.run( 75 | ["op", "vault", "group", "list", vaultID, "--format=json"], 76 | check=True, 77 | capture_output=True, 78 | ).stdout 79 | return vaultGroupList 80 | 81 | 82 | # Given a list of vaults, for each vault, list the users that have access along with their permissions. 83 | # Write the results to a csv file with columns: "vaultName", "vaultUUID", "name","email", "userUUID", "permissions" 84 | def main(): 85 | checkCLIVersion() 86 | if inputFilePath is None: 87 | try: 88 | vaultList = json.loads(getAllOwnerVaults()) 89 | except Exception as e: 90 | sys.exit( 91 | "Unable to get a list of vaults for the Owner group. Please sign into 1Password as a member of the Owners group or provide a list of vault UUIDs using the --file option." 92 | ) 93 | else: 94 | vaultList = getSpecifiedVaults() 95 | 96 | with open(f"{outputPath}/output.csv", "w", newline="") as outputFile: 97 | csvWriter = csv.writer(outputFile) 98 | fields = [ 99 | "vaultName", 100 | "vaultUUID", 101 | "userName", 102 | "groupName", 103 | "email", 104 | "userOrGroupUUID", 105 | "permissions", 106 | ] 107 | csvWriter.writerow(fields) 108 | for vault in vaultList: 109 | vaultUserList = json.loads(getVaultUserList(vault["id"])) 110 | vaultGroupList = json.loads(getVaultGroupList(vault["id"])) 111 | for user in vaultUserList: 112 | csvWriter.writerow( 113 | [ 114 | vault["name"], 115 | vault["id"], 116 | user["name"], 117 | None, 118 | user["email"], 119 | user["id"], 120 | user["permissions"], 121 | ] 122 | ) 123 | print( 124 | vault["name"], 125 | vault["id"], 126 | user["name"], 127 | user["email"], 128 | user["id"], 129 | user["permissions"], 130 | ) 131 | for group in vaultGroupList: 132 | csvWriter.writerow( 133 | [ 134 | vault["name"], 135 | vault["id"], 136 | None, 137 | group["name"], 138 | None, 139 | group["id"], 140 | group["permissions"], 141 | ] 142 | ) 143 | print( 144 | vault["name"], 145 | vault["id"], 146 | group["name"], 147 | group["id"], 148 | group["permissions"], 149 | ) 150 | 151 | 152 | main() 153 | -------------------------------------------------------------------------------- /reporting/vault-user-group-access-report.py: -------------------------------------------------------------------------------- 1 | # A script that provides a list of users who have access to each vault in 2 | # a 1Password account, and whether that assignment is a direct assignment or 3 | # granted by group membership, and their vault permissions 4 | # 5 | # This script must be run by a member of the Owners group of a 1Password Business account 6 | import os 7 | import subprocess 8 | import csv 9 | import json 10 | import sys 11 | from dataclasses import dataclass 12 | 13 | scriptPath = os.path.dirname(__file__) 14 | outputPath = scriptPath 15 | 16 | 17 | class User: 18 | users = [] 19 | 20 | def __init__(self, name, email, uuid, state, groups=None): 21 | self.name = name 22 | self.email = email 23 | self.uuid = uuid 24 | self.state = state 25 | self.groups = [] 26 | self.vaults = [] 27 | 28 | 29 | class Group: 30 | groups = [] 31 | 32 | def __init__(self, name, uuid): 33 | self.name = name 34 | self.uuid = uuid 35 | self.users = [] 36 | 37 | 38 | class Vault: 39 | vaults = [] 40 | 41 | def __init__(self, name, uuid, users=None, groups=None): 42 | self.name = name 43 | self.uuid = uuid 44 | self.users = [] 45 | self.groups = [] 46 | Vault.vaults.append(self) 47 | 48 | def addUser(self, user): 49 | self.users.append(user) 50 | 51 | @classmethod 52 | def getAll(cls): 53 | return [inst for inst in cls.vaults] 54 | 55 | @classmethod 56 | def getByID(cls, vaultID): 57 | for vault in cls.vaults: 58 | if vault.uuid == vaultID: 59 | return vault 60 | 61 | 62 | # Check CLI version 63 | def checkCLIVersion(): 64 | r = subprocess.run(["op", "--version", "--format=json"], capture_output=True) 65 | major, minor = r.stdout.decode("utf-8").rstrip().split(".", 2)[:2] 66 | if not major == 2 and not int(minor) >= 25: 67 | sys.exit( 68 | "❌ You must be using version 2.25 or greater of the 1Password CLI. Please visit https://developer.1password.com/docs/cli/get-started to download the lastest version." 69 | ) 70 | 71 | 72 | def getAllOwnerVaults(): 73 | vaultList = subprocess.run( 74 | ["op", "vault", "list", "--permission=manage_vault", "--format=json"], 75 | check=True, 76 | capture_output=True, 77 | ).stdout 78 | for vault in json.loads(vaultList): 79 | Vault( 80 | name=vault["name"], 81 | uuid=vault["id"], 82 | ) 83 | 84 | 85 | def getAllUsers(): 86 | accountUserList = subprocess.run( 87 | ["op", "user", "list", "--format=json"], check=True, capture_output=True 88 | ).stdout 89 | for user in json.loads(accountUserList): 90 | User( 91 | email=user["email"], name=user["name"], uuid=user["id"], state=user["state"] 92 | ) 93 | 94 | 95 | def getAllGroups(): 96 | accountGroupList = subprocess.run( 97 | ["op", "group", "list", "--format=json"], check=True, capture_output=True 98 | ).stdout 99 | for group in json.loads(accountGroupList): 100 | Group(name=group["name"], uuid=group["id"]) 101 | 102 | 103 | def getVaultUserList(vaultID): 104 | vaultUserList = subprocess.run( 105 | ["op", "vault", "user", "list", vaultID, "--format=json"], 106 | check=True, 107 | capture_output=True, 108 | ).stdout 109 | return vaultUserList 110 | 111 | 112 | def getVaultGroupList(vaultID): 113 | vaultGroupList = subprocess.run( 114 | ["op", "vault", "group", "list", vaultID, "--format=json"], 115 | check=True, 116 | capture_output=True, 117 | ).stdout 118 | return vaultGroupList 119 | 120 | 121 | def getGroupMembers(groupID): 122 | try: 123 | groupMembers = subprocess.run( 124 | ["op", "group", "user", "list", groupID, "--format=json"], 125 | check=True, 126 | capture_output=True, 127 | ).stdout 128 | # If the vault has no assigned groups, prevent the script from stopping when None is returned 129 | except Exception: 130 | groupMembers = [] 131 | print("group has no members") 132 | 133 | return groupMembers 134 | 135 | 136 | def writeReport(vaults: Vault): 137 | with open(f"{outputPath}/vaultAccessReport.csv", "w", newline="") as outputFile: 138 | csvWriter = csv.writer(outputFile) 139 | fields = [ 140 | "vaultName", 141 | "vaultUUID", 142 | "name", 143 | "email", 144 | "userUUID", 145 | "status", 146 | "assignment", 147 | "permissions", 148 | ] 149 | csvWriter.writerow(fields) 150 | for vault in vaults: 151 | vaultName = vault.name 152 | vaultUUID = vault.uuid 153 | # write each row 154 | for user in vault.users: 155 | csvWriter.writerow( 156 | [ 157 | vaultName, 158 | vaultUUID, 159 | user["name"], 160 | user["email"], 161 | user["uuid"], 162 | user["state"], 163 | user["assignment"], 164 | user["permissions"], 165 | ] 166 | ) 167 | 168 | 169 | def main(): 170 | checkCLIVersion() 171 | counter = 1 172 | # Populate initial data 173 | try: 174 | getAllOwnerVaults() 175 | except Exception as e: 176 | sys.exit( 177 | "Unable to get a list of vaults for the Owner group. Please sign into 1Password as a member of the Owners group. Error: ", 178 | e, 179 | ) 180 | 181 | # Get user assignments and group assignments 182 | vaults = Vault.getAll() 183 | vaultCount = len(vaults) 184 | for vault in vaults: 185 | print( 186 | f'\tPROCESSING vault {counter}/{vaultCount} "{vault.name}". This may take a moment...' 187 | ) 188 | try: 189 | users = json.loads(getVaultUserList(vault.uuid)) 190 | except Exception as e: 191 | sys.exit( 192 | f"Unable to get a list of users directly assigned to vault {vault.name}. Error: {e}" 193 | ) 194 | for user in users: 195 | vault.users.append( 196 | { 197 | "name": user["name"], 198 | "email": user["email"], 199 | "uuid": user["id"], 200 | "assignment": "Direct", 201 | "state": user["state"], 202 | "permissions": user["permissions"], 203 | } 204 | ) 205 | 206 | # For assigned groups, decompose into individual users 207 | try: 208 | groups = json.loads(getVaultGroupList(vault.uuid)) 209 | except Exception as e: 210 | sys.exit( 211 | f"Unable to get a list of groups assigned to vault {vault.name}. Error: {e}" 212 | ) 213 | for group in groups: 214 | vault.groups.append( 215 | { 216 | "name": group["name"], 217 | "groupUUID": group["id"], 218 | "permissions": group["permissions"], 219 | } 220 | ) 221 | try: 222 | groupUsers = json.loads(getGroupMembers(group["id"])) 223 | except Exception as e: 224 | sys.exit( 225 | f"Unable to get a list of group members in group {group['name']} assigned to vault {vault.name}. Error: {e}" 226 | ) 227 | if groupUsers is not None: 228 | for groupUser in groupUsers: 229 | vault.users.append( 230 | { 231 | "name": groupUser["name"], 232 | "email": groupUser["email"], 233 | "uuid": groupUser["id"], 234 | "assignment": f'Group ({group["name"]})', 235 | "state": groupUser["state"], 236 | "permissions": group["permissions"], 237 | } 238 | ) 239 | counter += 1 240 | try: 241 | writeReport(vaults) 242 | except Exception as e: 243 | sys.exit("Unable to write data to a file on disk.") 244 | 245 | 246 | main() 247 | -------------------------------------------------------------------------------- /scripted-provisioning/README.md: -------------------------------------------------------------------------------- 1 | # Scripted Provisioning with the 1Password CLI Tool 2 | 3 | ## Overview 4 | 5 | This script leverages the 1Password Command Line Interface (CLI) v2 to process a list of names and email addresses from a CSV file, allowing you to invite, suspend, reactivate, or delete users based on your selection. 6 | 7 | Additionally, you can opt to manually enter user details instead of using a CSV file or export a CSV list of existing users, which can then be used for the aforementioned actions. The script specifically looks for `Name` and `Email` headers in the CSV and ignores other columns. 8 | 9 | ## Account Configuration 10 | 11 | To enable provisioning through the CLI, configure your 1Password account by navigating to . This creates a "Provisioning Manager" group with the `Provision People` permission, allowing group members to invite users via the 1Password CLI. Add the dedicated provisioning account to this group, and include additional accounts as needed. 12 | 13 | ## Setup 14 | 15 | Your input file must be a CSV with the following requirements: 16 | - It must include a header row. 17 | 18 | Example CSV format: 19 | ``` 20 | Name,Email 21 | FirstName LastName,email@example.com 22 | OtherName MoreName,another_email@example.com 23 | ``` 24 | 25 | ### Additional Notes 26 | - Names can include multiple components (e.g., `Wendy van der Appleseed` is valid). 27 | - Non-Latin characters (e.g., Simplified Chinese) are supported but have not been thoroughly tested; results may vary. -------------------------------------------------------------------------------- /scripted-provisioning/people.csv: -------------------------------------------------------------------------------- 1 | Name,Email 2 | firstName lastName,email@email.email 3 | firstName2 lastName2,email2@email.email 4 | -------------------------------------------------------------------------------- /user-management/README.md: -------------------------------------------------------------------------------- 1 | ## Identify Absentees 2 | 3 | The [identify-absentees.ps1](identify-absentees.ps1) & [identify-absentees.py](identify-absentees.py) scripts will prompt you for a number of days (N) after which you consider a user to not be adequately engaged (or "absent") from 1Password. It will then create a list of users who have not authenticated into 1Password for N days or longer. You can use this list to reach out to disengaged users or as input for other scripts to perform bulk actions on those users. 4 | 5 | This script also provides some suggestions for modifying it's output depending on your needs. 6 | --------------------------------------------------------------------------------