diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2dbaa45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +terraform/resources/.DS_Store +terraform/.DS_Store +.DS_Store + +terraform/resources/ssh/*.tfvars +terraform/resources/ssh/*.tfstate +terraform/resources/ssh/*.tfstate.backup +terraform/resources/ssh/*.tfstate.lock.info +terraform/resources/ssh/.terraform/ +terraform/resources/ssh/.vagrant/ +terraform/resources/ssh/.sentinel + +*.tfstate +*.tfstate.backup +*.tfstate.lock.info + +*.log + +.terraform/ +.vagrant/ + +*.pem + +*.bak + +*gitignore*.tf + +.DS_Store + +.vscode/ + +operations/automation-script/apply.json +operations/automation-script/configversion.json +operations/automation-script/run.template.json +operations/automation-script/run.json +operations/automation-script/variable.template.json +operations/automation-script/variable.json +operations/automation-script/workspace.template.json +operations/automation-script/workspace.json +operations/sentinel-policies-scripts/create-policy.template.json +operations/sentinel-policies-scripts/create-policy.json +operations/variable-scripts/variable.template.json +operations/variable-scripts/variable.json + + +.sentinel + + +.sass-cache/ +.jekyll-metadata +*.DS_Store +package.json +.vscode/settings.json +*tfstate* +audit.csv diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fea2377..f580259 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to the project +# Contributing to strongDM Contrib -Thanks for considering contributing to our project! +Thanks for considering contributing to our project! We welcome contributions from customers, enthusiasts, or anyone interested in strongDM! You can contribute in any of the following ways: * Submitting bugs or feature requests @@ -25,7 +25,7 @@ In case you want to contribute with code (fixes, new functionalities) or documen 6. Push changes to your fork 7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. -Please consider the following rules when creating your PR (adapted from the [Auth0 Contributing Guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)): +Please consider the following rules when creating your PR: * Only fix/add the functionality in question OR address wide-spread whitespace/style issues, not both. * Add unit or integration tests for fixed or changed functionality (if a test suite already exists). * Address a single concern in the least number of changed lines as possible. diff --git a/README-sample.md b/README-sample.md deleted file mode 100644 index cc98e8e..0000000 --- a/README-sample.md +++ /dev/null @@ -1,22 +0,0 @@ -# Project Name - -Here you should include a description of the project and the problem it's solving using strongDM. - -## Table of Contents -* [Installation](#installation) -* [Getting Started](#getting-started) -* [Contributing](#contributing) -* [Support](#support) - -## Installation -Explain how to install the project/tool. Provide commands or animated GIFs if needed. - -## Getting Started -Explain how to get quickly started with the tool. Provide commands or animated GIFs if needed, and create as many subsections as needed. - -## Contributing -Refer to the [contributing](CONTRIBUTING.md) guidelines or dump part of the information here. - -## Support -Refer to the [support](SUPPORT.md) guidelines or dump part of the information here. - diff --git a/README.md b/README.md index 09ec0f2..83bacce 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,13 @@ -# Garden Template -The Garden Template contains sample files you could use for creating new [Code Garden](https://github.com/strongdm/garden) Repositories. It includes templates for: -* [README](README-sample.md) -* [License](LICENSE) -* [Contributing](CONTRIBUTING.md) -* [Support](SUPPORT.md) -* Report [bug](.github/ISSUE_TEMPLATE/bug_report.md) or [feature requests](.github/ISSUE_TEMPLATE/feature_request.md) -* [Pull Request](.github/PULL_REQUEST_TEMPLATE/pull_request_template.md) -* [Documentation](docs) +# strongDM Contrib -In order to use this repository, you could: -* Use it as a Template - Green button at the top of the repo -* Clone it and manually adjust it - Useful if you want to start a fresh project history +This repository provides sample code created by strongDM staff, customers, and others. This code may include shell scripts to use with the strongDM CLI, code snippets written for strongDM SDK's, Terraform templates, etc. Generally, this repo is organized by the task you are trying to accomplish (i.e. `authentication` for items related to SSO). So feel free to look around, we hope you find something helpful! -After cloning the repo, remember to: -1. Remove this README file -2. Rename the file README-sample.md to README.md and adjust the content -3. Adjust the Contributing and Support guidelines -4. Adjust the templates for bugs and feature requests under the .github folder +## Table of Contents +* [Contributing](#contributing) +* [Support](#support) -A template repo that can be used as a reference: [Auth0 Open Source Template](https://github.com/auth0/open-source-template) +## Contributing +We welcome contributions from customers, enthusiasts, or anyone interested in strongDM! Please refer to the [contributing](CONTRIBUTING.md) page. + +## Support +Code and scripts here are provided AS-IS and may or may not be updated in the future at our discretion. You should review and test any code thoroughly before deploying to production. For details on getting help, please see the [support](SUPPORT.md) guidelines. diff --git a/SUPPORT.md b/SUPPORT.md index 1de6cef..da670f4 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,8 +1,5 @@ -# Garden Support +# strongDM Contrib Support -Here's a list of options you could try: +Code and scripts here are provided AS-IS and may or may not be updated in the future at our discretion. You should review and test any code thoroughly before deploying to production. -* [Documentation](docs) -* [Discussions](../../discussions) - -On Discussions you could receive support from community members willing to point you in the right direction. +If you do run into bugs or issues with these scripts, please start by searching [Issues](../../issues) or [Discussions](../../discussions). You can start a new discussion to get help from the community, or file a new ticket under Issues as needed. We also encourage you to respond to discussions or issues, and submit pull requests for new code or fixes. diff --git a/ansible/aws_playbooks/README.md b/ansible/aws_playbooks/README.md new file mode 100644 index 0000000..2f0ceb0 --- /dev/null +++ b/ansible/aws_playbooks/README.md @@ -0,0 +1,55 @@ +# Self register AWS Ansible Playbooks + +## AWS SDM Gateway + +Within the playbook there is a vars section you'll need to update within the AWS task and down in the scripts task. Some of the information you'll need to pull from AWS. You can find all EC2 vars examples [here](https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_module.html) + +- [Self Registering SDM AWS Gateway Playbook](aws_self_register_playbooks/aws-self-register-gateway.yml) + +Inside the script you'll need to add your SDM Admin Token. + +- [Ansible SDM Gateway Self Register Script](aws_self_register_playbooks/scripts/sdm-gatewayadd.sh) + +## AWS SSH Server + +Within the playbook there is a vars section you'll need to update within the AWS task and down in the scripts task. Some of the information you'll need to pull from AWS. You can find all EC2 vars examples [here](https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_module.html) + +- [Self Registering SDM SSH Resource Playbook](aws_self_register_playbooks/aws-self-register-ssh.yml) + +Inside the script you'll need to add your SDM Admin Token. + +- [Ansible SDM SSH Self Register Script](aws_self_register_playbooks/scripts/sdm-sshadd.sh) + +# Single Ansible Playbooks + +## SDM Gateway Install + +This playbook will run on any host within the inventory file. I've built a full playbook without the need of a script. To target a specific group changes the `hosts:` parameter. It will auto register any AWS machine with a public address. + +- [Self Registering SDM Gateway Playbook](playbooks/sdm_gateway_install.yml) + +_Example: `ansible-playbook sdm_gateway_install.yml -i sdm-gateways --extra-vars 'SDM_ADMIN_TOKEN={{string for sdm token}}'`_ + +## SDM Relay Install + +This playbook will run on any host within the inventory file. To target a specific group changes the `hosts:` parameter. + +- [Self Registering SDM Relay Playbook](playbooks/sdm_relay_install.yml) + +_Example: `ansible-playbook sdm_relay_install.yml -i sdm-relays --extra-vars 'SDM_ADMIN_TOKEN={{string for sdm token}}'`_ + +## SDM SSH Public Cert Install + +This playbook will run on any host within the inventory file. To target a specific group changes the `hosts:` parameter. You'll need to pass `SDM_PUB_CA` using `--extra-vars` to append in the public CA. + +_Example: `ansible-playbook sdm_pub_cert_ssh_install.yml -i ssh-servers --extra-vars 'SDM_ADMIN_TOKEN={{string for sdm token}} SDM_PUB_CA={{string for sdm ca}}'`_ + +- [Self Registering SDM Public SSH Cert Playbook](playbooks/sdm_pub_cert_ssh_install.yml) + +## SDM SSH Install + +This playbook will run on any host within the inventory file. To target a specific group changes the `hosts:` parameter. + +- [Self Registering SDM SSH Playbook](playbooks/sdm_ssh_install.yml) + +_Example: `ansible-playbook sdm_ssh_install.yml -i ssh-servers --extra-vars 'SDM_ADMIN_TOKEN={{string for sdm token}}'`_ \ No newline at end of file diff --git a/ansible/aws_playbooks/aws_self_register_playbooks/aws-self-register-gateway.yml b/ansible/aws_playbooks/aws_self_register_playbooks/aws-self-register-gateway.yml new file mode 100644 index 0000000..70190b5 --- /dev/null +++ b/ansible/aws_playbooks/aws_self_register_playbooks/aws-self-register-gateway.yml @@ -0,0 +1,56 @@ +--- +# Basic provisioning example +- name: Ansible AWS Variables + vars: + aws_region: + aws_key_pair: + aws_instance_type: + aws_image_id: + aws_subnet_id: + aws_sec_group_name: + instance_name: + user_name: + hosts: localhost + tasks: + - name: launching AWS instance using Ansible + ec2: + #Set AWS Region + region: "{{ aws_region }}" + #Set key pair in AWS + key_name: "{{ aws_key_pair }}" + #Set instance size + instance_type: "{{ aws_instance_type }}" + #Update AWS Image ID (Region specific) + image: "{{ aws_image_id }}" + wait: yes + count: 1 + #Enter VPC Subnet ID + vpc_subnet_id: "{{ aws_subnet_id }}" + assign_public_ip: yes + #Enter AWS Security Group Name + group: "{{ aws_sec_group_name }}" + #Add Tags as needed + instance_tags: + Name: "{{ instance_name }}" + Creator: Ansible + register: ec2_sdm + - name: Add new instance to SDM's host group + add_host: + hostname: "{{ item.public_ip }}" + groupname: ec2sdm + with_items: "{{ ec2_sdm.instances }}" + - name: Let's wait for SSH to come up. Usually that takes ~10 seconds + local_action: wait_for + host={{ item.public_ip }} + port=22 + state=started + with_items: '{{ ec2_sdm.instances }}' +#Self Registered Example +- hosts: ec2sdm + name: configuration play + user: ubuntu + become: yes + gather_facts: true + tasks: + #Update Path to script + - script: scripts/sdm-gatewayadd.sh \ No newline at end of file diff --git a/ansible/aws_playbooks/aws_self_register_playbooks/aws-self-register-ssh.yml b/ansible/aws_playbooks/aws_self_register_playbooks/aws-self-register-ssh.yml new file mode 100644 index 0000000..2c1df29 --- /dev/null +++ b/ansible/aws_playbooks/aws_self_register_playbooks/aws-self-register-ssh.yml @@ -0,0 +1,58 @@ +--- +# Basic provisioning example +- name: Ansible test + vars: + aws_region: + aws_key_pair: + aws_instance_type: + aws_image_id: + aws_subnet_id: + aws_sec_group_name: + instance_name: + instance_count: + hosts: localhost + tasks: + - name: launching AWS instance using Ansible + ec2: + #Set AWS Region + region: '{{ aws_region }}' + #Set key pair in AWS + key_name: '{{ aws_key_pair }}' + #Set instance size + instance_type: '{{ aws_instance_type }}' + #Update AWS Image ID (Region specific) + image: '{{ aws_image_id }}' + wait: yes + count: '{{ instance_count }}' + #Enter VPC Subnet ID + vpc_subnet_id: '{{ aws_subnet_id }}' + assign_public_ip: yes + #Enter AWS Security Group Name + group: '{{ aws_sec_group_name }}' + #Add Tags as needed + instance_tags: + Name: '{{ instance_name }}' + Creator: Ansible + register: ec2_sdm + - name: Add new instance to SDM's host group + add_host: + hostname: '{{ item.public_ip }}' + groupname: launched-ec2-sdm + with_items: '{{ ec2_sdm.instances }}' + - name: Let's wait for SSH to come up. Usually that takes ~10 seconds + local_action: wait_for + host={{ item.public_ip }} + port=22 + state=started + with_items: '{{ ec2_sdm.instances }}' +#Self Registered Example +- hosts: launched-ec2-sdm + vars: + username: + name: configuration play + user: '{{ username }}' + become: yes + gather_facts: true + tasks: + #Update Path to script + - script: scripts/sdm-sshadd.sh \ No newline at end of file diff --git a/ansible/aws_playbooks/aws_self_register_playbooks/scripts/sdm-gatewayadd.sh b/ansible/aws_playbooks/aws_self_register_playbooks/scripts/sdm-gatewayadd.sh new file mode 100644 index 0000000..6a56ed9 --- /dev/null +++ b/ansible/aws_playbooks/aws_self_register_playbooks/scripts/sdm-gatewayadd.sh @@ -0,0 +1,10 @@ +#!/bin/bash +apt update && apt upgrade -y +apt install zip curl wget -y +curl -J -O -L https://app.strongdm.com/releases/cli/linux +unzip *.zip +export SDM_ADMIN_TOKEN={{ SDM_ADMIN_TOKEN }} +export INSTANCE_HOSTNAME=$(curl http://169.254.169.254/latest/meta-data/public-hostname) +export SDM_RELAY_TOKEN=`./sdm relay create-gateway $INSTANCE_HOSTNAME:5000 0.0.0.0:5000` +unset SDM_ADMIN_TOKEN +./sdm install --relay --token=$SDM_RELAY_TOKEN \ No newline at end of file diff --git a/ansible/aws_playbooks/aws_self_register_playbooks/scripts/sdm-sshadd.sh b/ansible/aws_playbooks/aws_self_register_playbooks/scripts/sdm-sshadd.sh new file mode 100644 index 0000000..7cdc338 --- /dev/null +++ b/ansible/aws_playbooks/aws_self_register_playbooks/scripts/sdm-sshadd.sh @@ -0,0 +1,12 @@ +#!/bin/bash + export SDM_ADMIN_TOKEN= {{ SDM_ADMIN_TOKEN }} + apt update + apt install -y unzip + curl -o sdm.zip -L https://app.strongdm.com/releases/cli/linux + unzip sdm.zip + ./sdm admin ssh add \ + -p `curl http://169.254.169.254/latest/meta-data/instance-id` \ + $USERNAME@`curl http://169.254.169.254/latest/meta-data/public-hostname` \ + | tee -a "/home/$USERNAME/.ssh/authorized_keys" + ./sdm admin roles grant `curl http://169.254.169.254/latest/meta-data/instance-id` {{ SDM_Role }} + rm sdm.zip \ No newline at end of file diff --git a/ansible/aws_playbooks/playbooks/sdm_gateway_install.yml b/ansible/aws_playbooks/playbooks/sdm_gateway_install.yml new file mode 100644 index 0000000..db70456 --- /dev/null +++ b/ansible/aws_playbooks/playbooks/sdm_gateway_install.yml @@ -0,0 +1,60 @@ +--- +- hosts: all + become: yes + name: Install SDM Gateway + tasks: + - name: Install system updates for CentOS systems + yum: + name: '*' + state: latest + update_cache: yes + #cache_valid_time: 3600 + when: + - ansible_os_family == "RedHat" + - ansible_distribution == "CentOS" + - name: Install system updates for Debian systems + apt: + name: '*' + state: latest + update_cache: yes + #cache_valid_time: 3600 + when: + - ansible_os_family == "Debian" + - ansible_distribution == "Ubuntu" + - name: Install dependencies + package: + name: + - unzip + - curl + - wget + state: present + - name: Download SDM Binary + command: + cmd: curl -J -O -L https://app.strongdm.com/releases/cli/linux + - name: Find SDM Zip + find: + paths: "./" + patterns: "sdmcli*.zip" + register: find_result + - name: Unpack SDM + command: + cmd: unzip "{{ item.path }}" + with_items: "{{ find_result.files }}" + - name: Login with SDM + shell: ./sdm login --admin-token='{{ SDM_ADMIN_TOKEN }}' + - name: Grab Hostname + shell: export HOSTNAME=$(curl http://169.254.169.254/latest/meta-data/public-hostname) + - name: Get SDM Gateway Token And Install + shell: './sdm install --relay --token=$(./sdm relay create-gateway $HOSTNAME:5000 0.0.0.0:5000)' + - name: Unset SDM_ADMIN_TOKEN + set_fact: + SDM_ADMIN_TOKEN: + - name: Remove SDM CLI + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + with_items: "{{ find_result.files }}" + - name: Remove SDM + ansible.builtin.file: + path: "./sdm" + state: absent \ No newline at end of file diff --git a/ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml b/ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml new file mode 100644 index 0000000..4af05f7 --- /dev/null +++ b/ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml @@ -0,0 +1,80 @@ +--- +- hosts: all + become: yes + name: SDM Remote SSH Self-Enroll + tasks: + - name: Copy contents of SDM Public CA + copy: + dest: /etc/ssh/sdm_ca.pub + content: | + {{ SDM_PUB_CA }} + - name: Change file permissions on pub key + ansible.builtin.file: + path: "/etc/ssh/sdm_ca.pub" + mode: '0600' + - name: Append to /etc/ssh/sshd_config + ansible.builtin.lineinfile: + insertafter: EOF + line: TrustedUserCAKeys /etc/ssh/sdm_ca.pub + path: /etc/ssh/sshd_config + - name: Restart ssh service + ansible.builtin.service: + state: restarted + name: ssh + - name: Install system updates for CentOS systems + yum: + name: '*' + state: latest + update_cache: yes + #cache_valid_time: 3600 + when: + - ansible_os_family == "RedHat" + - ansible_distribution == "CentOS" + - name: Install system updates for Debian systems + apt: + name: '*' + state: latest + update_cache: yes + #cache_valid_time: 3600 + when: + - ansible_os_family == "Debian" + - ansible_distribution == "Ubuntu" + - name: Install dependencies + package: + name: + - unzip + - curl + - wget + state: present + - name: Download SDM Binary + command: + cmd: curl -J -O -L https://app.strongdm.com/releases/cli/linux + - name: Find SDM Zip + find: + paths: "./" + patterns: "sdmcli*.zip" + register: find_result + - name: Unpack SDM + command: + cmd: unzip "{{ item.path }}" + with_items: "{{ find_result.files }}" + - name: Login with SDM + shell: ./sdm login --admin-token='{{ SDM_ADMIN_TOKEN }}' + args: + executable: /bin/bash + - name: Get SDM Gateway Token And Install + shell: ./sdm admin servers add sshCert --hostname `curl http://169.254.169.254/latest/meta-data/local-ipv4` --username $USER --port 22 + args: + executable: /bin/bash + - name: Unset SDM_ADMIN_TOKEN + set_fact: + SDM_ADMIN_TOKEN: + - name: Remove SDM CLI + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + with_items: "{{ find_result.files }}" + - name: Remove SDM + ansible.builtin.file: + path: "./sdm" + state: absent \ No newline at end of file diff --git a/ansible/aws_playbooks/playbooks/sdm_relay_install.yml b/ansible/aws_playbooks/playbooks/sdm_relay_install.yml new file mode 100644 index 0000000..0c203cd --- /dev/null +++ b/ansible/aws_playbooks/playbooks/sdm_relay_install.yml @@ -0,0 +1,62 @@ +--- +- hosts: all + become: yes + name: Install SDM Relay + tasks: + - name: Install system updates for CentOS systems + yum: + name: '*' + state: latest + update_cache: yes + #cache_valid_time: 3600 + when: + - ansible_os_family == "RedHat" + - ansible_distribution == "CentOS" + - name: Install system updates for Debian systems + apt: + name: '*' + state: latest + update_cache: yes + #cache_valid_time: 3600 + when: + - ansible_os_family == "Debian" + - ansible_distribution == "Ubuntu" + - name: Install dependencies + package: + name: + - unzip + - curl + - wget + state: present + - name: Download SDM Binary + command: + cmd: curl -J -O -L https://app.strongdm.com/releases/cli/linux + - name: Find SDM Zip + find: + paths: "./" + patterns: "sdmcli*.zip" + register: find_result + - name: Unpack SDM + command: + cmd: unzip "{{ item.path }}" + with_items: "{{ find_result.files }}" + - name: Login with SDM + shell: ./sdm login --admin-token='{{ SDM_ADMIN_TOKEN }}' + args: + executable: /bin/bash + - name: Get SDM Gateway Token And Install + shell: ./sdm install --relay --token=$(./sdm relay create) + args: + executable: /bin/bash + - name: Unset SDM_ADMIN_TOKEN + set_fact: + SDM_ADMIN_TOKEN: + - name: Remove SDM CLI + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + with_items: "{{ find_result.files }}" + - name: Remove SDM + ansible.builtin.file: + path: "./sdm" + state: absent \ No newline at end of file diff --git a/ansible/aws_playbooks/playbooks/sdm_ssh_install.yml b/ansible/aws_playbooks/playbooks/sdm_ssh_install.yml new file mode 100644 index 0000000..2f32cab --- /dev/null +++ b/ansible/aws_playbooks/playbooks/sdm_ssh_install.yml @@ -0,0 +1,66 @@ +--- +- hosts: all + become: yes + name: SDM Remote SSH Self-Enroll + tasks: + - name: Install system updates for CentOS systems + yum: + name: '*' + state: latest + update_cache: yes + #cache_valid_time: 3600 + when: + - ansible_os_family == "RedHat" + - ansible_distribution == "CentOS" + - name: Install system updates for Debian systems + apt: + name: '*' + state: latest + update_cache: yes + #cache_valid_time: 3600 + when: + - ansible_os_family == "Debian" + - ansible_distribution == "Ubuntu" + - name: Install dependencies + package: + name: + - unzip + - curl + - wget + state: present + - name: Download SDM Binary + command: + cmd: curl -J -O -L https://app.strongdm.com/releases/cli/linux + - name: Find SDM Zip + find: + paths: "./" + patterns: "sdmcli*.zip" + register: find_result + - name: Unpack SDM + command: + cmd: unzip "{{ item.path }}" + with_items: "{{ find_result.files }}" + - name: Login with SDM + shell: ./sdm login --admin-token='{{ SDM_ADMIN_TOKEN }}' + args: + executable: /bin/bash + - name: Get SDM Gateway Token And Install + shell: ./sdm admin ssh add -p `curl http://169.254.169.254/latest/meta-data/instance-id` $USER@`curl http://169.254.169.254/latest/meta-data/local-ipv4`| tee -a '/home/$USER/.ssh/authorized_keys' + args: + executable: /bin/bash + - name: Unset SDM_ADMIN_TOKEN + set_fact: + SDM_ADMIN_TOKEN: + - name: Remove SDM CLI + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + with_items: "{{ find_result.files }}" + - name: Remove SDM + ansible.builtin.file: + path: "./sdm" + state: absent + - name: Restart ssh service + ansible.builtin.service: + state: restarted + name: ssh \ No newline at end of file diff --git a/archive/README.md b/archive/README.md new file mode 100644 index 0000000..6b81d35 --- /dev/null +++ b/archive/README.md @@ -0,0 +1,5 @@ +# Archive + +This is a place where you can find all artifacts that ever existed in this repository, but we consider deprecated or just not necessary anymore. + +All changes are reflected in the Git history, we just want to make easier the finding process. diff --git a/archive/authentication/okta/README.md b/archive/authentication/okta/README.md new file mode 100644 index 0000000..85c339d --- /dev/null +++ b/archive/authentication/okta/README.md @@ -0,0 +1 @@ +This folder contains code for use with strongDM and Okta. \ No newline at end of file diff --git a/archive/authentication/okta/okta-sync-multi-group/.gitignore b/archive/authentication/okta/okta-sync-multi-group/.gitignore new file mode 100644 index 0000000..3a8ce9c --- /dev/null +++ b/archive/authentication/okta/okta-sync-multi-group/.gitignore @@ -0,0 +1 @@ +set-env.sh diff --git a/archive/authentication/okta/okta-sync-multi-group/README.md b/archive/authentication/okta/okta-sync-multi-group/README.md new file mode 100644 index 0000000..aa45f6a --- /dev/null +++ b/archive/authentication/okta/okta-sync-multi-group/README.md @@ -0,0 +1,31 @@ +# Synchronize Okta Users with Multiple Groups +Shim go script for synchronizing Okta users/groups with strongDM. + +Codebase guidelines from: https://github.com/strongdm/strongdm-sdk-go-examples/tree/master/contrib/okta-sync + +## How it works +This script reads a separate JSON file, [matchers.yml](matchers.yml), which maps Okta groups to resources in SDM by type or name. + +For each Group defined in the YML, an SDM Role will be created, and access to the defined resources will be granted to that Role, using the [filters spec](https://www.strongdm.com/docs/automation/getting-started/filters). + +Any user that matches the Okta search filter and has groups associated listed in [matchers.yml](matchers.yml) will be created in SDM, and assigned to the corresponding SDM Role. + +strongDM only supports 1:1 user-role mappings, when there are multiple groups assigned to a okta user, a composite role is created with multiple sub-roles assigned to it. + +The script won't remove any Roles or Users in SDM, unless you use the flags: `-delete-roles-not-in-okta` or `-delete-users-not-in-okta`. + +## How to use this script +1. Set the following environment variables: SDM_API_ACCESS_KEY, SDM_API_SECRET_KEY, OKTA_CLIENT_TOKEN, and OKTA_CLIENT_ORGURL. +2. Edit the [matchers.yml](matchers.yml) file to a) define which groups to sync from Okta to strongDM as [Roles](https://www.strongdm.com/docs/admin-ui-guide/user-management/roles), and b) which strongDM resources users in those groups will receive access to. + + > For example, the sample file in this folder would create strongDM Roles with access to all `mysql` and `postgres` resources in your organization. + +## Sample +``` +$ go run . -delete-roles-not-in-okta -delete-users-not-in-okta +5 Okta users, 3 strongDM users in okta, 3 strongDM roles in okta +``` + +Considerations: +* For better reporting add `-plan` to your command. +* When using `-delete-users-not-in-okta​` remember to add your SDM admin emails to Okta, otherwise you could remove the account administrators. diff --git a/archive/authentication/okta/okta-sync-multi-group/go.mod b/archive/authentication/okta/okta-sync-multi-group/go.mod new file mode 100644 index 0000000..c22d6db --- /dev/null +++ b/archive/authentication/okta/okta-sync-multi-group/go.mod @@ -0,0 +1,10 @@ +module strongdm.com/okta-sync-multi-role + +go 1.16 + +require ( + github.com/okta/okta-sdk-golang v1.1.0 + github.com/pkg/errors v0.9.1 + github.com/strongdm/strongdm-sdk-go v0.9.28 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/archive/authentication/okta/okta-sync-multi-group/go.sum b/archive/authentication/okta/okta-sync-multi-group/go.sum new file mode 100644 index 0000000..b2e5e17 --- /dev/null +++ b/archive/authentication/okta/okta-sync-multi-group/go.sum @@ -0,0 +1,121 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM= +github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk= +github.com/okta/okta-sdk-golang v1.1.0 h1:sr/KYSMRhs4F2NWEbqWXqN4y4cKKcfzrtOiBqR/J6mI= +github.com/okta/okta-sdk-golang v1.1.0/go.mod h1:KEjmr3Zo+wP3gVa3XhwIvENBfh7L/iRUeIl6ruQYOK0= +github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y= +github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/square/go-jose v2.4.1+incompatible h1:KFYc54wTtgnd3x4B/Y7Zr1s/QaEx2BNzRsB3Hae5LHo= +github.com/square/go-jose v2.4.1+incompatible/go.mod h1:7MxpAF/1WTVUu8Am+T5kNy+t0902CaLWM4Z745MkOa8= +github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc/go.mod h1:JbpHhNyeVc538vtj/ECJ3gPYm1VEitNjsLhm4eJQQbg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/strongdm/strongdm-sdk-go v0.9.28 h1:SvB2z8kMX89Tfmiw1zhjmdYqBlUCO1jxxTbwOScbh70= +github.com/strongdm/strongdm-sdk-go v0.9.28/go.mod h1:rXX9x9j6IgGYyjWjAzMjh2PTMRZsH0/eKCuzUi10xok= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= +gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/archive/authentication/okta/okta-sync-multi-group/main.go b/archive/authentication/okta/okta-sync-multi-group/main.go new file mode 100644 index 0000000..016cd42 --- /dev/null +++ b/archive/authentication/okta/okta-sync-multi-group/main.go @@ -0,0 +1,658 @@ +// Copyright 2020 StrongDM Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/okta/okta-sdk-golang/okta" + "github.com/okta/okta-sdk-golang/okta/query" + "github.com/pkg/errors" + sdm "github.com/strongdm/strongdm-sdk-go" + "gopkg.in/yaml.v2" +) + +const OKTA_USERS_LIMIT = 500 + +var oktaQueryString = "(status eq \"ACTIVE\")" + +var jsonFlag = flag.Bool("json", false, "dump a JSON report for debugging") +var planFlag = flag.Bool("plan", false, "do not apply changes just plan and output the result") + +// carefully use this flags +var deleteRolesNotInOktaFlag = flag.Bool("delete-roles-not-in-okta", false, "delete roles not present in okta") +var deleteUsersNotInOktaFlag = flag.Bool("delete-users-not-in-okta", false, "delete users not present in okta") + +func init() { + flag.Parse() +} + +type syncReport struct { + Start time.Time `json:"start"` + Complete time.Time `json:"complete"` + OktaUserCount int `json:"oktaUsersCount"` + OktaUsers oktaUserList `json:"oktaUsers"` + + SDMUsersInOktaCount int `json:"sdmUsersInOktaCount"` + SDMUsersInOkta userList `json:"sdmUsersInOkta"` + SDMUserNotInOktaCount int `json:"sdmUsersNotInOktaCount"` + SDMUsersNotInOkta userList `json:"sdmUsersNotInOkta"` + + SDMRoleInOktaCount int `json:"sdmRolesInOktaCount"` + SDMRolesInOkta roleList `json:"sdmRolesInOkta"` + SDMRoleNotInOktaCount int `json:"sdmRolesNotInOktaCount"` + SDMRolesNotInOkta roleList `json:"sdmRolesNotInOkta"` + + Matchers *MatcherConfig `json:"matchers"` +} + +func (rpt *syncReport) String() string { + if !*jsonFlag { + return rpt.short() + } + + out, err := json.MarshalIndent(rpt, "", "\t") + if err != nil { + return fmt.Sprintf("error building JSON report: %s\n\n%s", err, rpt.short()) + } + return string(out) +} + +func (rpt *syncReport) short() string { + return fmt.Sprintf("%d Okta users, %d strongDM users in okta, %d strongDM roles in okta\n", + rpt.OktaUserCount, rpt.SDMUsersInOktaCount, rpt.SDMRoleInOktaCount) +} + +func main() { + ctx := context.Background() + + if os.Getenv("SDM_API_ACCESS_KEY") == "" || + os.Getenv("SDM_API_SECRET_KEY") == "" || + os.Getenv("OKTA_CLIENT_TOKEN") == "" || + os.Getenv("OKTA_CLIENT_ORGURL") == "" { + fmt.Println("SDM_API_ACCESS_KEY, SDM_API_SECRET_KEY, OKTA_CLIENT_TOKEN, and OKTA_CLIENT_ORGURL must be set") + os.Exit(1) + return + } + + client, err := sdm.New(os.Getenv("SDM_API_ACCESS_KEY"), os.Getenv("SDM_API_SECRET_KEY")) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to initialize strongDM client: %v\n", err) + os.Exit(1) + return + } + + var rpt syncReport + rpt.Start = time.Now() + + matchers, err := loadMatchers() + if err != nil { + fmt.Fprintf(os.Stderr, "error loading Matchers users: %v\n", err) + os.Exit(1) + return + } + rpt.Matchers = matchers + + oktaUsers, err := loadOktaUsers(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading Okta users: %v\n", err) + os.Exit(1) + return + } + rpt.OktaUsers = oktaUsers + rpt.OktaUserCount = len(oktaUsers) + + initialRoles, err := loadRoles(ctx, client) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading roles: %v\n", err) + os.Exit(1) + return + } + + initialUsers, err := loadUsers(ctx, client) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading users: %v\n", err) + os.Exit(1) + return + } + + if !*planFlag { + matchingRoles, unmatchingRoles, err := syncRoles(ctx, client, initialRoles, matchers) + if err != nil { + fmt.Fprintf(os.Stderr, "error synchronizing roles: %v\n", err) + os.Exit(1) + return + } + rpt.SDMRolesInOkta = matchingRoles + rpt.SDMRoleInOktaCount = len(matchingRoles) + rpt.SDMRolesNotInOkta = unmatchingRoles + rpt.SDMRoleNotInOktaCount = len(unmatchingRoles) + + matchingUsers, unmatchingUsers, err := syncUsers(ctx, client, initialUsers, matchingRoles, oktaUsers, matchers) + if err != nil { + fmt.Fprintf(os.Stderr, "error synchronizing users: %v\n", err) + os.Exit(1) + return + } + rpt.SDMUsersInOkta = matchingUsers + rpt.SDMUsersInOktaCount = len(matchingUsers) + rpt.SDMUsersNotInOkta = unmatchingUsers + rpt.SDMUserNotInOktaCount = len(unmatchingUsers) + } + + rpt.Complete = time.Now() + fmt.Println(rpt.String()) +} + +type oktaUserList []oktaUser + +type oktaUser struct { + Login string `json:"login"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Groups []string `json:"groups"` +} + +type roleList []roleRow + +type roleRow struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type userList []userRow + +type userRow struct { + ID string `json:"id"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + Role string `json:"roleName"` +} + +type MatcherConfig struct { + Groups []struct { + Name string `yaml:"name"` + Resources []string `yaml:"resources"` + } `yaml:"groups"` +} + +type entitlementList []entitlement + +type entitlement struct { + ResourceID string `json:"id"` + Name string `json:"name"` +} + +func loadMatchers() (*MatcherConfig, error) { + body, err := ioutil.ReadFile("matchers.yml") + if err != nil { + return nil, errors.Wrap(err, "unable to read from matchers configuration file") + } + + var m MatcherConfig + err = yaml.UnmarshalStrict(body, &m) + if err != nil { + return nil, errors.Wrap(err, "error unmarshalling matcher configuration") + } + + return &m, err +} + +func loadOktaUsers(ctx context.Context) (oktaUserList, error) { + client, err := okta.NewClient(ctx) + if err != nil { + return nil, errors.Wrap(err, "invalid Okta configuration") + } + search := query.NewQueryParams(query.WithSearch(oktaQueryString), query.WithLimit(OKTA_USERS_LIMIT)) + + apiUsers, _, err := client.User.ListUsers(search) + if err != nil { + return nil, errors.Wrap(err, "unable to retrieve okta users") + } + + var users []oktaUser + for _, u := range apiUsers { + profile := (*u.Profile) + + groups, _, err := client.User.ListUserGroups(u.Id, nil) + if err != nil { + return nil, errors.Wrap(err, "unable to retrieve okta user groups") + } + + var groupNames []string + for _, g := range groups { + groupNames = append(groupNames, g.Profile.Name) + } + + var u oktaUser + u.Login = profile["login"].(string) + u.FirstName = profile["firstName"].(string) + u.LastName = profile["lastName"].(string) + u.Groups = groupNames + users = append(users, u) + } + return users, nil +} + +func loadRoles(ctx context.Context, client *sdm.Client) (roleList, error) { + roles, err := client.Roles().List(ctx, "") + if err != nil { + return nil, err + } + var result roleList + for roles.Next() { + role := roles.Value() + result = append(result, roleRow{ + ID: role.ID, + Name: role.Name, + }) + } + if roles.Err() != nil { + return nil, roles.Err() + } + return result, nil +} + +func loadUsers(ctx context.Context, client *sdm.Client) (userList, error) { + accountAttachments, err := client.AccountAttachments().List(ctx, "") + if err != nil { + return nil, err + } + roles := map[string]string{} + for accountAttachments.Next() { + attachment := accountAttachments.Value() + roles[attachment.AccountID] = attachment.RoleID + } + if accountAttachments.Err() != nil { + return nil, accountAttachments.Err() + } + + accounts, err := client.Accounts().List(ctx, "type:user") + if err != nil { + return nil, err + } + var result userList + for accounts.Next() { + user := accounts.Value().(*sdm.User) + result = append(result, userRow{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + Role: roles[user.ID], + }) + } + if accounts.Err() != nil { + return nil, accounts.Err() + } + return result, nil +} + +func syncRoles(ctx context.Context, client *sdm.Client, initialRoles roleList, matchers *MatcherConfig) (roleList, roleList, error) { + entitlementsByGroup, err := matchEntitlements(ctx, client, matchers) + if err != nil { + return nil, nil, err + } + matchingRoles, err := createMatchingRoles(ctx, client, entitlementsByGroup) + if err != nil { + return nil, nil, err + } + unmatchingRoles := calculateUnmatchingRoles(initialRoles, matchingRoles) + if *deleteRolesNotInOktaFlag { + err = deleteUnmatchingRoles(ctx, client, unmatchingRoles) + if err != nil { + return nil, nil, err + } + } + return matchingRoles, unmatchingRoles, nil +} + +// matchEntitlements creates lists of concrete datasources and servers by group name +func matchEntitlements(ctx context.Context, client *sdm.Client, matchers *MatcherConfig) (map[string]entitlementList, error) { + result := make(map[string]entitlementList) + for _, matcher := range matchers.Groups { + uniq := make(map[entitlement]bool) + for _, expression := range matcher.Resources { + resources, err := client.Resources().List(ctx, expression) + if err != nil { + return nil, err + } + for resources.Next() { + rs := resources.Value() + uniq[entitlement{ResourceID: rs.GetID()}] = true + } + if resources.Err() != nil { + return nil, err + } + } + result[matcher.Name] = make(entitlementList, 0) // for creating groups without available resources + for u := range uniq { + result[matcher.Name] = append(result[matcher.Name], u) + } + } + return result, nil +} + +func createMatchingRoles(ctx context.Context, client *sdm.Client, entitlementsByGroup map[string]entitlementList) (roleList, error) { + finalRoles := roleList{} + for groupName, entitlements := range entitlementsByGroup { + role, err := loadOrCreateRole(ctx, client, groupName, false) + if err != nil { + return nil, err + } + for _, e := range entitlements { + err := createRoleGrant(ctx, client, role.ID, e.ResourceID) + if err != nil { + return nil, err + } + } + finalRoles = append(finalRoles, roleRow{ + ID: role.ID, + Name: role.Name, + }) + } + return finalRoles, nil +} + +func loadOrCreateRole(ctx context.Context, client *sdm.Client, roleName string, isComposite bool) (*sdm.Role, error) { + roles, err := client.Roles().List(ctx, fmt.Sprintf("name:\"%s\"", roleName)) + if err != nil { + return nil, err + } + if roles.Next() { + return roles.Value(), nil + } + + resp, err := client.Roles().Create(ctx, &sdm.Role{ + Name: roleName, + Composite: isComposite, + }) + if err != nil { + return nil, err + } + return resp.Role, nil +} + +func createRoleGrant(ctx context.Context, client *sdm.Client, roleID string, resourceID string) error { + _, err := client.RoleGrants().Create(ctx, &sdm.RoleGrant{ + RoleID: roleID, + ResourceID: resourceID, + }) + var alreadyExistsErr *sdm.AlreadyExistsError + if err != nil && !errors.As(err, &alreadyExistsErr) { + return err + } + return nil +} + +func calculateUnmatchingRoles(initialRoles roleList, matchingRoles roleList) roleList { + unmatchingRoles := roleList{} + for _, irole := range initialRoles { + found := false + for _, mrole := range matchingRoles { + if irole.ID == mrole.ID { + found = true + break + } + } + if !found { + unmatchingRoles = append(unmatchingRoles, irole) + } + } + return unmatchingRoles +} + +func deleteUnmatchingRoles(ctx context.Context, client *sdm.Client, unmatchingRoles roleList) error { + for _, role := range unmatchingRoles { + _, err := client.Roles().Delete(ctx, role.ID) + if err != nil { + return err + } + } + return nil +} + +func syncUsers(ctx context.Context, client *sdm.Client, initialUsers userList, roles roleList, oktaUsers oktaUserList, matchers *MatcherConfig) (userList, userList, error) { + matchingUsers, err := createMatchingUsers(ctx, client, roles, oktaUsers, matchers) + if err != nil { + return nil, nil, err + } + unmatchingUsers := calculateUnmatchingUsers(initialUsers, matchingUsers) + if *deleteUsersNotInOktaFlag { + err = deleteUnmatchingUsers(ctx, client, unmatchingUsers) + if err != nil { + return nil, nil, err + } + } + return matchingUsers, unmatchingUsers, nil +} + +func createMatchingUsers(ctx context.Context, client *sdm.Client, roles roleList, oktaUsers oktaUserList, matchers *MatcherConfig) (userList, error) { + matchingUsers := userList{} + for _, oktaUser := range oktaUsers { + if !oktaUserHasMatchingGroup(oktaUser, matchers) { + fmt.Fprintf(os.Stderr, "ignoring user %s - no group in matchers assigned to it\n", oktaUser.Login) + continue + } + user, err := loadOrCreateUser(ctx, client, oktaUser) + var alreadyExistsErr *sdm.AlreadyExistsError + if errors.As(err, &alreadyExistsErr) { + fmt.Fprintf(os.Stderr, "ignoring user %s - might be assigned to a different org\n", oktaUser.Login) + continue + } + if err != nil { + return nil, err + } + err = removePreviousAccountAttachments(ctx, client, user.ID) + if err != nil { + return nil, err + } + oktaGroups := matchingGroups(oktaUser.Groups, matchers) + var roleName string + if len(oktaGroups) == 1 { + roleID, err := findRoleID(oktaGroups[0], roles) + if err != nil { + return nil, err + } + err = assignRole(ctx, client, user.ID, roleID) + if err != nil { + return nil, err + } + roleName = oktaGroups[0] + } else if len(oktaGroups) > 1 { + compositeRole, err := createCompositeRole(ctx, client, roles, oktaGroups) + if err != nil { + return nil, err + } + err = assignRole(ctx, client, user.ID, compositeRole.ID) + if err != nil { + return nil, err + } + roleName = compositeRole.Name + } + matchingUsers = append(matchingUsers, userRow{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + Role: roleName, + }) + } + return matchingUsers, nil +} + +func oktaUserHasMatchingGroup(oktaUser oktaUser, matchers *MatcherConfig) bool { + for _, oktaGroup := range oktaUser.Groups { + for _, matcherGroup := range matchers.Groups { + if oktaGroup == matcherGroup.Name { + return true + } + } + } + return false +} + +func loadOrCreateUser(ctx context.Context, client *sdm.Client, oktaUser oktaUser) (*sdm.User, error) { + users, err := client.Accounts().List(ctx, fmt.Sprintf("email:\"%s\"", oktaUser.Login)) + if err != nil { + return nil, err + } + if users.Next() { + return users.Value().(*sdm.User), nil + } + + resp, err := client.Accounts().Create(ctx, &sdm.User{ + Email: oktaUser.Login, + FirstName: oktaUser.FirstName, + LastName: oktaUser.LastName, + }) + if err != nil { + return nil, err + } + return resp.Account.(*sdm.User), nil +} + +func removePreviousAccountAttachments(ctx context.Context, client *sdm.Client, userID string) error { + attachments, err := client.AccountAttachments().List(ctx, fmt.Sprintf("accountId:\"%s\"", userID)) + if err != nil { + return err + } + for attachments.Next() { + attachmentID := attachments.Value().ID + _, err := client.AccountAttachments().Delete(ctx, attachmentID) + if err != nil { + return err + } + } + return nil +} + +func matchingGroups(oktaGroups []string, matchers *MatcherConfig) []string { + result := []string{} + for _, oktaGroup := range oktaGroups { + for _, matcherGroup := range matchers.Groups { + if oktaGroup == matcherGroup.Name { + result = append(result, oktaGroup) + } + } + } + return result +} + +func findRoleID(groupName string, roles roleList) (string, error) { + for _, r := range roles { + if r.Name == groupName { + return r.ID, nil + } + } + return "", fmt.Errorf("cannot find roleID for roleName = %s", groupName) +} + +func assignRole(ctx context.Context, client *sdm.Client, userID string, roleID string) error { + _, err := client.AccountAttachments().Create(ctx, &sdm.AccountAttachment{ + AccountID: userID, + RoleID: roleID, + }) + var alreadyExistsErr *sdm.AlreadyExistsError + if err != nil && !errors.As(err, &alreadyExistsErr) { + return err + } + return nil +} + +func createCompositeRole(ctx context.Context, client *sdm.Client, roles roleList, oktaGroups []string) (*sdm.Role, error) { + compositeRoleName := strings.Join(oktaGroups, "_") + compositeRole, err := loadOrCreateRole(ctx, client, compositeRoleName, true) + if err != nil { + return nil, err + } + err = removePreviousCompositeRoleAttachments(ctx, client, compositeRole.ID) + if err != nil { + return nil, err + } + err = assignNewCompositeRoleAttachments(ctx, client, compositeRole.ID, roles, oktaGroups) + if err != nil { + return nil, err + } + return compositeRole, nil +} + +func removePreviousCompositeRoleAttachments(ctx context.Context, client *sdm.Client, compositeRoleID string) error { + attachments, err := client.RoleAttachments().List(ctx, fmt.Sprintf("compositeRoleId:\"%s\"", compositeRoleID)) + if err != nil { + return err + } + for attachments.Next() { + attachmentID := attachments.Value().ID + _, err := client.RoleAttachments().Delete(ctx, attachmentID) + if err != nil { + return err + } + } + return nil +} + +func assignNewCompositeRoleAttachments(ctx context.Context, client *sdm.Client, compositeRoleID string, roles roleList, oktaGroups []string) error { + for _, group := range oktaGroups { + roleID, err := findRoleID(group, roles) + if err != nil { + return err + } + _, err = client.RoleAttachments().Create(ctx, &sdm.RoleAttachment{ + CompositeRoleID: compositeRoleID, + AttachedRoleID: roleID, + }) + var alreadyExistsErr *sdm.AlreadyExistsError + if err != nil && !errors.As(err, &alreadyExistsErr) { + return err + } + } + return nil +} + +func calculateUnmatchingUsers(initialUsers userList, matchingUsers userList) userList { + unmatchingUsers := userList{} + for _, iuser := range initialUsers { + found := false + for _, muser := range matchingUsers { + if iuser.ID == muser.ID { + found = true + break + } + } + if !found { + unmatchingUsers = append(unmatchingUsers, iuser) + } + } + return unmatchingUsers +} + +func deleteUnmatchingUsers(ctx context.Context, client *sdm.Client, unmatchingUsers userList) error { + for _, user := range unmatchingUsers { + _, err := client.Accounts().Delete(ctx, user.ID) + if err != nil { + return err + } + } + return nil +} diff --git a/archive/authentication/okta/okta-sync-multi-group/matchers.yml b/archive/authentication/okta/okta-sync-multi-group/matchers.yml new file mode 100644 index 0000000..3eab7b2 --- /dev/null +++ b/archive/authentication/okta/okta-sync-multi-group/matchers.yml @@ -0,0 +1,18 @@ +--- +groups: + - + name: Other + resources: + - type:mysql + - + name: rodo-group-support + resources: + - type:mysql + - + name: rodo-group-engineering + resources: + - type:postgres + - + name: rodo-group-data + resources: + - type:athena \ No newline at end of file diff --git a/archive/authentication/okta/okta-sync-update/README.md b/archive/authentication/okta/okta-sync-update/README.md new file mode 100644 index 0000000..5644363 --- /dev/null +++ b/archive/authentication/okta/okta-sync-update/README.md @@ -0,0 +1,41 @@ +## Overview +This script is an update to our standard example located here: + https://github.com/strongdm/strongdm-sdk-go-examples/tree/master/contrib/okta-sync + +It is written against Okta golang API version 1.x. + +It partially implements user/Group sync from Okta > SDM, and switches access grants from the user to the Role (Group) level. + +--- + +## How to use this script + +1. Set the following environment variables: SDM_API_ACCESS_KEY, SDM_API_SECRET_KEY, OKTA_CLIENT_TOKEN, and OKTA_CLIENT_ORGURL. +2. Edit the matchers.yml file to a) define which groups to sync from Okta to strongDM as [Roles](https://www.strongdm.com/docs/admin-ui-guide/user-management/roles), and b) which strongDM resources users in those groups will receive access to. + + > For example, the sample file in this folder would create strongDM Roles named Support and Engineering. Users would receive access to all `mysql` and `postgres` resources in your organization, respectively. + + + +3. Edit the variable `oktaQueryString` in the .GO file to specify which users to sync to strongDM. (The Okta SDK does not provide a method to retrieve _group members_, unfortunately.) + +--- + +## How it works + +This script reads a separate JSON file, matchers.yml, which maps Okta groups to resources in SDM by type or name. +For each Group defined in the YML, an SDM Role will be created, and access to the defined resources will be granted to that Role. +Any user that matches the Okta search filter will be created in SDM (see the "oktaQueryString" definition just below). +If they belong to an Okta Group that is defined in the YML, they will be assigned to the corresponding SDM Role. + +The script won't remove any Roles or Users in SDM. However, it will remove any grants for Groups/Roles that are not defined in the YML. It will also add/remove grants for Groups/Roles if you change the mapping in the YML. + +An important consideration is that Okta supports multiple group assignment, but strongDM does not. This means that a user with multiple Group memberships will be assigned to the first Group/Role provided by Okta. + +--- + +## How to set up Okta +We recommend that you consider creating SDM-specific Groups in Okta, e.g. sdm-qa, sdm-dev, and assign users in Okta accordingly. +Then define only these groups in the YML, with appropriate resource mapping. + +You may wish to modify the oktaQueryString to match only users who belong to the SDM-specific Groups. \ No newline at end of file diff --git a/archive/authentication/okta/okta-sync-update/matchers.yml b/archive/authentication/okta/okta-sync-update/matchers.yml new file mode 100644 index 0000000..e2a2adc --- /dev/null +++ b/archive/authentication/okta/okta-sync-update/matchers.yml @@ -0,0 +1,10 @@ +--- +groups: + - + name: Support + resources: + - type:mysql + - + name: Engineering + resources: + - type:postgres diff --git a/archive/authentication/okta/okta-sync-update/okta-sync-provisioning.go b/archive/authentication/okta/okta-sync-update/okta-sync-provisioning.go new file mode 100644 index 0000000..b5e6b59 --- /dev/null +++ b/archive/authentication/okta/okta-sync-update/okta-sync-provisioning.go @@ -0,0 +1,662 @@ +// Copyright 2020 StrongDM Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/* +PLEASE NOTE: this is sample code intended to demonstrate strongDM SDK functionality. +You should review and test thoroughly before deploying to production. +This code is provided AS-IS and may or may not be updated in the future at our discretion. +Our Support team will be happy to assist with general SDK questions or issues. +*/ + +/* This script is an update to our standard example located here: + https://github.com/strongdm/strongdm-sdk-go-examples/tree/master/contrib/okta-sync + +It is written against Okta golang API version 1.x. + +It partially implements user/Group sync from Okta > SDM, and switches access grants from the user to the Role (Group) level. + +NB: you must set the following environment variables: SDM_API_ACCESS_KEY, SDM_API_SECRET_KEY, OKTA_CLIENT_TOKEN, and OKTA_CLIENT_ORGURL. + +This script reads a separate JSON file, matchers.yml, which maps Okta groups to resources in SDM by type or name. +For each Group defined in the YML, an SDM Role will be created, and access to the defined resources will be granted to that Role. +Any user that matches the Okta search filter will be created in SDM (see the "oktaQueryString" definition just below). +If they belong to an Okta Group that is defined in the YML, they will be assigned to the corresponding SDM Role. + +The script won't remove any Roles or Users in SDM. +However, it will remove any grants for Groups/Roles that are not defined in the YML. +It will also add/remove grants for Groups/Roles if you change the mapping in the YML. + +An important consideration is that Okta supports multiple group assignment, but strongDM does not. +This means that a user with multiple Group memberships will be assigned to the first Group/Role provided by Okta. +We recommend that you consider creating SDM-specific Groups in Okta, e.g. sdm-qa, sdm-dev, and assign users in Okta accordingly. +Then define only these groups in the YML, with appropriate resource mapping. +You may wish to modify the oktaQueryString to match only users who belong to the SDM-specific Groups. +*/ + +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + "time" + + "github.com/okta/okta-sdk-golang/okta" + "github.com/okta/okta-sdk-golang/okta/query" + "github.com/pkg/errors" + sdm "github.com/strongdm/strongdm-sdk-go" + "gopkg.in/yaml.v2" +) + +// modify this Okta query filter with any valid Okta API parameters to control user creation in SDM +var oktaQueryString = "profile.login eq \"david+test@strongdm.com\" and (status eq \"ACTIVE\")" + +var verbose = flag.Bool("json", false, "dump a JSON report for debugging") +var plan = flag.Bool("plan", false, "do not apply changes just plan and output the result") + +// global Groups list, used to store Okta groups, and create missing Roles +var oktaGroups []string + +var debug = false + +func init() { + flag.Parse() +} + +type syncReport struct { + Start time.Time `json:"start"` + Complete time.Time `json:"complete"` + OktaUserCount int `json:"oktaUsersCount"` + OktaUsers []oktaUser `json:"oktaUsers"` + OktaGroupCount int `json:"oktaGroupCount"` + OktaGroups []string `json:"oktaGroups"` + + SDMUserCount int `json:"sdmUsersCount"` + SDMUsers []userRow `json:"sdmUsers"` + SDMRoleCount int `json:"sdmRoleCount"` + SDMRoles []string `json:"sdmRoles"` + + BothUserCount int `json:"bothUsersCount"` + + SDMResourcesCount int `json:"sdmResourcesCount"` + SDMResources []entitlement `json:"sdmResources"` + + PermissionsGranted int `json:"permissionsGranted"` + PermissionsRevoked int `json:"permissionsRevoked"` + Grants []entitlement `json:"grants"` + Revocations []rolePermissionsRow `json:"revocations"` + + Matchers *MatcherConfig `json:"matchers"` +} + +func (rpt *syncReport) String() string { + if !*verbose { + return rpt.short() + } + + out, err := json.MarshalIndent(rpt, "", "\t") + if err != nil { + return fmt.Sprintf("error building JSON report: %s\n\n%s", err, rpt.short()) + } + return string(out) +} + +func (rpt *syncReport) short() string { + return fmt.Sprintf("%d Okta users, %d strongDM users, %d matching users, %d grants, %d revocations, %d Groups, %d Roles\n", + rpt.OktaUserCount, rpt.SDMUserCount, rpt.BothUserCount, + rpt.PermissionsGranted, rpt.PermissionsRevoked, rpt.OktaGroupCount, rpt.SDMRoleCount) +} + +func main() { + ctx := context.Background() + + if os.Getenv("SDM_API_ACCESS_KEY") == "" || + os.Getenv("SDM_API_SECRET_KEY") == "" || + os.Getenv("OKTA_CLIENT_TOKEN") == "" || + os.Getenv("OKTA_CLIENT_ORGURL") == "" { + fmt.Println("SDM_API_ACCESS_KEY, SDM_API_SECRET_KEY, OKTA_CLIENT_TOKEN, and OKTA_CLIENT_ORGURL must be set") + os.Exit(1) + return + } + + client, err := sdm.New(os.Getenv("SDM_API_ACCESS_KEY"), os.Getenv("SDM_API_SECRET_KEY")) + if err != nil { + fmt.Println("failed to initialize strongDM client: ", err) + os.Exit(1) + return + } + + var rpt syncReport + rpt.Start = time.Now() + + matchers, err := loadMatchers() + if err != nil { + fmt.Printf("error loading Matchers users: %v\n", err) + os.Exit(1) + return + } + rpt.Matchers = matchers + + // get all Okta users that match filter defined in loadOktaUsers() + // this also populates oktaGroups + oktaUsers, err := loadOktaUsers(ctx) + if err != nil { + fmt.Printf("error loading Okta users: %v\n", err) + os.Exit(1) + return + } + rpt.OktaUsers = oktaUsers + rpt.OktaUserCount = len(oktaUsers) + rpt.OktaGroupCount = len(oktaGroups) + + // determine set of datasources and servers they should have access to by group + entitlements, err := matchEntitlements(ctx, client, matchers) + if err != nil { + fmt.Printf("error matching entitlements: %v\n", err) + os.Exit(1) + return + } + + // for each defined entitlement, use the Okta Group name + // to find/create a corresponding Role in SDM + for a := range entitlements { + if debug { + println("range entitlements: ", a) + } + rpt.SDMRoleCount++ + myRole := &sdm.Role{Name: a} + _, err = client.Roles().Create(ctx, myRole) + if err != nil && !(strings.Contains(err.Error(), "item already exists")) { + fmt.Printf("error creating Role: %v\n", err) + } + } + + rolePermissions, err := loadRoleGrants(ctx, client) + if err != nil { + fmt.Printf("error loading permissions: %v\n", err) + os.Exit(1) + return + } + + users, err := loadAccounts(ctx, client) + if err != nil { + fmt.Printf("error loading users: %v\n", err) + os.Exit(1) + return + } + rpt.SDMUsers = users + rpt.SDMUserCount = len(users) + + resources, err := loadResources(ctx, client) + if err != nil { + fmt.Printf("error loading datasources: %v\n", err) + os.Exit(1) + return + } + rpt.SDMResources = resources + rpt.SDMResourcesCount = len(resources) + + // use list of Okta users & compare to SDM, create missing users in SDM + // NB: Okta allows blank First/Last names, SDM does not + // If desired, this could be modified to only create SDM users if their Okta Group matches one defined in matchers.yml + for _, oktaUser := range oktaUsers { + if findUser(users, oktaUser) { + break + } else { + println("Okta user not found in SDM. Attempting user creation ...") + _, err := client.Accounts().Create(ctx, &sdm.User{ + FirstName: oktaUser.FirstName, + LastName: oktaUser.LastName, + Email: oktaUser.Login, + }) + if err != nil { + log.Fatal("Error while creating user: ", err) + } else { + fmt.Println("User creation successful!!!") + // reload list of SDM Accounts, for group assignment later + users, _ = loadAccounts(ctx, client) + } + } + } + + bothCount := make(map[string]bool) + for _, oktaUser := range oktaUsers { + if debug { + fmt.Println("oktauser: ", oktaUser) + } + for _, sdmUser := range users { + if debug { + fmt.Println("sdmUser: ", sdmUser) + } + if strings.ToLower(sdmUser.Email) == strings.ToLower(oktaUser.Login) { + if debug { + println("User matches!") + } + bothCount[sdmUser.Email] = true + for _, g := range oktaUser.Groups { + if debug { + fmt.Println("group: ", g) + } + + resp, err := client.Roles().List(ctx, "name:\""+g+"\"") + if err != nil { + fmt.Println("error finding user Role: ", err) + } + for resp.Next() { + role := resp.Value() + if debug { + fmt.Println("found role: ", role) + } + attachment := &sdm.AccountAttachment{ + AccountID: sdmUser.ID, + RoleID: role.ID, + } + attachmentResponse, err := client.AccountAttachments().Create(ctx, attachment) + if err == nil { + attachmentID := attachmentResponse.AccountAttachment.ID + log.Printf("Successfully created account attachment: ID: %v\n", attachmentID) + } else if !strings.Contains(err.Error(), "item already exists") { + fmt.Println("error finding user Role: ", err) + } + } + } + } + } + } + rpt.BothUserCount = len(bothCount) + + matchingByRole := make(map[roleRow]entitlementList) + if debug { + fmt.Println(matchingByRole) + } + + for _, group := range oktaGroups { + uniq := make(map[entitlement]bool) + for _, e := range entitlements[group] { + uniq[e] = true + } + + for e := range uniq { + if debug { + fmt.Println("DEBUG: name:" + group) + } + resp, err := client.Roles().List(ctx, "name:\""+group+"\"") + if err != nil { + fmt.Println("error finding Role", err) + } + for resp.Next() { + role := resp.Value() + newRoleRow := roleRow{ + ID: role.ID, + Name: role.Name, + } + + matchingByRole[newRoleRow] = append(matchingByRole[newRoleRow], e) + if debug { + fmt.Println(matchingByRole) + } + break + } + } + } + + toGrant := []rolePermissionsRow{} + toRevoke := []rolePermissionsRow{} + for r, entitlements := range matchingByRole { + // are there any entitlements not permitted? grant. + for _, e := range entitlements { + found := false + for _, p := range rolePermissions { + if p.RoleID == r.ID && p.DatasourceID == e.DatasourceID { + found = true + } + } + if !found { + if !*plan { + toGrant = append(toGrant, rolePermissionsRow{RoleID: r.ID, DatasourceID: e.DatasourceID}) + rpt.PermissionsGranted++ + } else { + fmt.Printf("Plan: grant %v to user %v\n", e.DatasourceID, r.ID) + } + } + } + } + + rpt.Grants = []entitlement{} + for _, g := range toGrant { + rpt.Grants = append(rpt.Grants, entitlement{DatasourceID: g.DatasourceID}) + } + + // are there any permissions not entitled? revoke. + for _, p := range rolePermissions { + found := false + for r, entitlements := range matchingByRole { + if p.RoleID == r.ID { + for _, e := range entitlements { + if p.RoleID == r.ID && e.DatasourceID == p.DatasourceID { + found = true + } + } + } + } + if !found { + if !*plan { + toRevoke = append(toRevoke, p) + rpt.PermissionsRevoked++ + } else { + fmt.Printf("Plan: revoke %s from user %s\n", p.DatasourceID, p.RoleID) + } + } + } + + rpt.Revocations = toRevoke + + if !*plan { + for _, grant := range toGrant { + _, err := client.RoleGrants().Create(ctx, &sdm.RoleGrant{ + RoleID: grant.RoleID, + ResourceID: grant.DatasourceID, + }) + var alreadyExistsErr *sdm.AlreadyExistsError + if err != nil && !errors.As(err, &alreadyExistsErr) { + fmt.Println("error granting: ", err) + } + } + for _, grant := range toRevoke { + _, err := client.RoleGrants().Delete(ctx, grant.ID) + var notFoundError *sdm.NotFoundError + if err != nil && !errors.As(err, ¬FoundError) { + fmt.Println("error revoking", err) + } + } + } + + rpt.Complete = time.Now() + fmt.Println(rpt.String()) +} + +type oktaUser struct { + Login string `json:"login"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Groups []string `json:"groups"` +} + +type userList []userRow + +type userRow struct { + ID string `json:"id"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + Role string `json:"roleName"` +} + +type roleList []roleRow + +type roleRow struct { + ID string `json:"id"` + Name string `json:"Name"` +} + +type permissionsList []permissionsRow +type rolePermissionsList []rolePermissionsRow + +type permissionsRow struct { + ID string `json:"-"` + UserID string `json:"userID"` + DatasourceID string `json:"datasourceID"` +} + +type rolePermissionsRow struct { + ID string `json:"-"` + RoleID string `json:"userID"` + DatasourceID string `json:"datasourceID"` +} + +type entitlementList []entitlement + +type entitlement struct { + DatasourceID string `json:"id"` + Name string `json:"name"` +} + +func loadOktaUsers(ctx context.Context) ([]oktaUser, error) { + client, err := okta.NewClient(ctx) + if err != nil { + return nil, errors.Wrap(err, "invalid Okta configuration") + } + search := query.NewQueryParams(query.WithSearch(oktaQueryString)) + + apiUsers, _, err := client.User.ListUsers(search) + if err != nil { + return nil, errors.Wrap(err, "unable to retrieve okta users") + } + + var users []oktaUser + for _, u := range apiUsers { + login := (*u.Profile)["login"].(string) + firstName := (*u.Profile)["firstName"].(string) + lastName := (*u.Profile)["lastName"].(string) + + groups, _, err := client.User.ListUserGroups(u.Id, search) + + if err != nil { + return nil, errors.Wrap(err, "unable to retrieve okta user groups") + } + + var groupNames []string + for _, g := range groups { + if debug { + println("loadOktausers: ", login, g.Profile.Name) + } + groupNames = append(groupNames, g.Profile.Name) + oktaGroups = AppendIfMissing(oktaGroups, g.Profile.Name) + } + + var u oktaUser + u.Login = login + u.FirstName = firstName + u.LastName = lastName + u.Groups = groupNames + users = append(users, u) + } + return users, nil +} + +func loadAccountGrants(ctx context.Context, client *sdm.Client) ([]permissionsRow, error) { + grants, err := client.AccountGrants().List(ctx, "") + if err != nil { + return nil, err + } + var result permissionsList + for grants.Next() { + grant := grants.Value() + result = append(result, permissionsRow{ + ID: grant.ID, + UserID: grant.AccountID, + DatasourceID: grant.ResourceID, + }) + } + if grants.Err() != nil { + return nil, grants.Err() + } + return result, nil +} + +func loadRoleGrants(ctx context.Context, client *sdm.Client) ([]rolePermissionsRow, error) { + roleGrants, err := client.RoleGrants().List(ctx, "") + if err != nil { + return nil, err + } + var result rolePermissionsList + for roleGrants.Next() { + grant := roleGrants.Value() + result = append(result, rolePermissionsRow{ + ID: grant.ID, + RoleID: grant.RoleID, + DatasourceID: grant.ResourceID, + }) + } + if roleGrants.Err() != nil { + return nil, roleGrants.Err() + } + return result, nil +} + +func loadAccounts(ctx context.Context, client *sdm.Client) ([]userRow, error) { + accountAttachments, err := client.AccountAttachments().List(ctx, "") + if err != nil { + return nil, err + } + roles := map[string]string{} + for accountAttachments.Next() { + attachment := accountAttachments.Value() + roles[attachment.AccountID] = attachment.RoleID + } + if accountAttachments.Err() != nil { + return nil, accountAttachments.Err() + } + + accounts, err := client.Accounts().List(ctx, "type:user") + if err != nil { + return nil, err + } + var result userList + for accounts.Next() { + user := accounts.Value().(*sdm.User) + result = append(result, userRow{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + Role: roles[user.ID], + }) + } + if accounts.Err() != nil { + return nil, accounts.Err() + } + return result, nil +} + +func loadResources(ctx context.Context, client *sdm.Client) ([]entitlement, error) { + // limit grant/revoke to datasources and servers only, allowing websites + // to be granted manually for the time being + var resources entitlementList + resp, err := client.Resources().List(ctx, "category:datasource") + if err != nil { + return nil, err + } + for resp.Next() { + resource := resp.Value() + resources = append(resources, entitlement{ + DatasourceID: resource.GetID(), + Name: resource.GetName(), + }) + } + if resp.Err() != nil { + return nil, resp.Err() + } + resp, err = client.Resources().List(ctx, "category:server") + if err != nil { + return nil, err + } + for resp.Next() { + resource := resp.Value() + resources = append(resources, entitlement{ + DatasourceID: resource.GetID(), + Name: resource.GetName(), + }) + } + if resp.Err() != nil { + return nil, resp.Err() + } + return resources, nil +} + +// MatcherConfig stores mapping data from matchers.yml +type MatcherConfig struct { + Groups []struct { + Name string `yaml:"name"` + Resources []string `yaml:"resources"` + } `yaml:"groups"` +} + +func loadMatchers() (*MatcherConfig, error) { + body, err := ioutil.ReadFile("matchers.yml") + if err != nil { + return nil, errors.Wrap(err, "unable to read from matchers configuration file") + } + + var m MatcherConfig + err = yaml.UnmarshalStrict(body, &m) + if err != nil { + return nil, errors.Wrap(err, "error unmarshalling matcher configuration") + } + + return &m, err +} + +// matchEntitlements creates lists of concrete datasources and servers by group name +func matchEntitlements(ctx context.Context, client *sdm.Client, matchers *MatcherConfig) (map[string]entitlementList, error) { + result := make(map[string]entitlementList) + for _, matcher := range matchers.Groups { + if debug { + println("inside matchEntitlements: ", matcher.Name) + } + uniq := make(map[entitlement]bool) + for _, expression := range matcher.Resources { + resources, err := client.Resources().List(ctx, expression) + if err != nil { + return nil, err + } + for resources.Next() { + rs := resources.Value() + uniq[entitlement{DatasourceID: rs.GetID()}] = true + } + if resources.Err() != nil { + return nil, err + } + } + for u := range uniq { + result[matcher.Name] = append(result[matcher.Name], u) + } + } + return result, nil +} + +func findUser(a []userRow, x oktaUser) bool { + for _, n := range a { + if strings.ToLower(n.Email) == strings.ToLower(x.Login) { + return true + } + } + return false +} + +// AppendIfMissing adds Group/Role name to existing list, if not already present +func AppendIfMissing(slice []string, i string) []string { + for _, ele := range slice { + if ele == i { + return slice + } + } + return append(slice, i) +} diff --git a/archive/logging/kubernetes/impersonation/README.md b/archive/logging/kubernetes/impersonation/README.md new file mode 100644 index 0000000..0b970c7 --- /dev/null +++ b/archive/logging/kubernetes/impersonation/README.md @@ -0,0 +1,26 @@ +# ARCHIVED CONTENT + +This solution was designed to work with roles and composite roles, and has been deprecated in favor of [this solution that works with the newer multi-role system](https://github.com/strongdm/contrib/tree/main/logging/kubernetes/impersonation). If your strongDM organization has not yet been transitioned to this system, you may continue to use this script. + +# Convert SDM roles for Kubernetes User Impersonation + +This folder contains a Python script that helps with automation of roles for Kubernetes user impersonation. + +## Requirements +* Python3 +* A [strongDM API key pair](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) with the following permissions: + * Roles: List, Create + * Grants: Write +* An existing Role(s) that you wish to convert and/or add to a Composite Role for k8s user impersonation. + + +## Usage +* Run `pip install -r requirements.txt` +* Run the script with at least one role name: `k8s_auto.py -r CurrentRole`. This will: + * Create a corresponding Composite Role with the same name, plus `_k8s`. + * Add the specified Role to that new Composite Role. +* You may optionally include a "mapping role" via the `-m` flag. This is a role which has already been mapped via a YML file to some RBAC group in Kubernetes. For example, `k8s_auto.py -r CurrentRoleName -m MapRoleName`. +* When users from the role you passed into the script access a k8s cluster via SDM, the client will pass the user's name and all three Role names to the cluster for authorization and auditing. + +## Considerations: +* If the Composite Role already exists, we assume this script has been run before. The script will warn and exit. diff --git a/archive/logging/kubernetes/impersonation/k8s_auto.py b/archive/logging/kubernetes/impersonation/k8s_auto.py new file mode 100644 index 0000000..9aee954 --- /dev/null +++ b/archive/logging/kubernetes/impersonation/k8s_auto.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +import strongdm, time, os, argparse, sys, logging + +# Set logging level +logging.basicConfig(level = logging.INFO) + +# Parse command line arguments +# The role/-r argument is required, the map/-m is optional +parser = argparse.ArgumentParser() +parser.add_argument("-r", "--role", help="SDM role name to convert", required=True) +parser.add_argument("-m", "--map", help="SDM mapping role to add") +args = parser.parse_args() + +# Get SDM API keys from environment +access_key = os.environ['SDM_API_ACCESS_KEY'] +secret_key = os.environ['SDM_API_SECRET_KEY'] + +client = strongdm.Client(access_key, secret_key) + +# Function to create the Composite Role +def createCompRole(roleName): + compRole = strongdm.Role( + name=roleName + "_k8s", + composite=True + ) + comp_response = client.roles.create(compRole, timeout=30) + return comp_response + +# Function to assign normal roles to Composite +def addRoleToComp(role, compRoleId): + compAttachment = strongdm.RoleAttachment( + composite_role_id=compRoleId, + attached_role_id=role.id, + ) + client.role_attachments.create(compAttachment, timeout=30) + +def main(): + if args.role: + try: + # Create the new Composite role + resp = createCompRole(args.role) + except strongdm.AlreadyExistsError as ex: + logging.error('The Composite Role already exists! Please check the Admin UI to confirm the specified role is assigned to it.') + exit(-1) + except Exception as ex: + logging.error('Failed to create Composite Role: '+ str(ex)) + exit(-1) + logging.info('Composite Role %s_k8s created successfully!' % args.role) + + # Get the role passed in via CLI + list = client.roles.list('name:"%s"' % args.role) + for u in list: + try: + # Add that role to the new Composite Role + addRoleToComp(u, resp.role.id) + except Exception as ex: + logging.error('Failed to add role %s to Composite Role: '+ str(ex) % args.role) + exit(-1) + logging.info('Role %s assigned successfully!' % args.role) + + # Perform the same operation for the 'map' role, if specified + if args.map: + list = client.roles.list('name:"%s"' % args.map) + for v in list: + try: + addRoleToComp(v, resp.role.id) + except Exception as ex: + logging.error('Failed to add role to Composite Role: '+ str(ex)) + exit(-1) + logging.info('Map role %s assigned successfully!' % args.map) + +if __name__ == "__main__": + main() + + + + + diff --git a/archive/logging/kubernetes/impersonation/requirements.txt b/archive/logging/kubernetes/impersonation/requirements.txt new file mode 100644 index 0000000..9e6ee6e --- /dev/null +++ b/archive/logging/kubernetes/impersonation/requirements.txt @@ -0,0 +1,2 @@ +strongdm +argparse \ No newline at end of file diff --git a/audit/account_logins_past_90_days/README b/audit/account_logins_past_90_days/README new file mode 100644 index 0000000..0294a86 --- /dev/null +++ b/audit/account_logins_past_90_days/README @@ -0,0 +1,10 @@ +# StrongDM Logins within past 90 days + +This script is intended to ouput a CSV that shows the last login times for active StrongDM accounts, both human and service accounts. + +For each account, either the last login time will be supplied, or a message that no logins during the last 90 days were detected. + +Prerequisites +* Python 3 +* StrongDM client installed +* Logged into the strongDM client \ No newline at end of file diff --git a/audit/account_logins_past_90_days/main.py b/audit/account_logins_past_90_days/main.py new file mode 100644 index 0000000..336c4eb --- /dev/null +++ b/audit/account_logins_past_90_days/main.py @@ -0,0 +1,141 @@ +import os +import datetime +import subprocess +import platform +import json +import csv + + +def get_accounts(): + ''' + Gather Accounts from the sdm CLI + * Requires being logged into the sdm client + ''' + sdm_command = ['sdm', + 'audit', + 'users', + 'list', + '-j'] + result = subprocess.run(sdm_command, stdout=subprocess.PIPE, check=False) + accounts = format_cli_results(result) + return accounts + + +def get_activities(lookback_period): + ''' + Gather Activities using the sdm cli + * Requires being logged into the sdm client + ''' + today = datetime.date.today() + from_date = today - datetime.timedelta(days=lookback_period) + from_date = from_date.strftime('%Y/%m/%d') + sdm_command = ['sdm', + 'audit', + 'activities', + '-e', + '-j', + '--from', + from_date] + result = subprocess.run(sdm_command, stdout=subprocess.PIPE, check=False) + activities = format_cli_results(result) + return activities + + +def format_cli_results(result): + ''' + Convert CLI results into Python Dict + ''' + result_string = result.stdout.decode('utf-8') + # Json results include newlines + # So splitting lines gets indivudal json objects + individual_record_strings = result_string.splitlines() + formatted_cli_results = [] + # Convert individual json string objects into python dicts + for record in individual_record_strings: + json_record = json.loads(record) + formatted_cli_results.append(json_record) + return formatted_cli_results + + +def prepare_stats_for_collection(accounts): + ''' + Create stats Dict + ''' + stats = {} + for account in accounts: + # Skip Suspended Accounts + if account['strongRole'] == 'suspended': + continue + # Differentiate between Service and Human account types + if account['firstName'] == 'Service Account': + stats[account['id']] = {"name": account['lastName'], "type": "service_account"} + else: + stats[account['id']] = {"name": account['email'], "type": "human_account"} + for account in stats.items(): + stats[account[0]].update({"last_login": None}) + return stats + + +def generate_stats(activities, accounts): + '''Stats Dict for data profiling''' + stats = prepare_stats_for_collection(accounts) + for activity in activities: + # Skip events: + # * That were not logins + # * That were from support users, etc + # * That were from deleted users + if activity['activity'].startswith('user logged into') is False or activity['actorUserID'] not in stats: + continue + if stats[activity['actorUserID']]['last_login'] is None: + stats[activity['actorUserID']]['last_login'] = activity['timestamp'] + elif stats[activity['actorUserID']]['last_login'] < activity['timestamp']: + stats[activity['actorUserID']]['last_login'] = activity['timestamp'] + for account in stats.items(): + if account[1]['last_login'] is None: + stats[account[0]].update({"last_login": "No logins for the last 90 days."}) + return stats + + +def format_stats_for_csv(stats): + ''' + Convert single stats Dict to List of Dicts + to simplify csv creation + ''' + formatted_stats = [] + for key in stats: + formatted_stats.append(stats[key]) + return formatted_stats + + +def create_csv(stats): + '''Write stats to CSV''' + formatted_stats = format_stats_for_csv(stats) + field_names = ['name', 'type', 'last_login'] + with open('StrongDM_Logins_Past_90_Days.csv', 'w', encoding='utf-8') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=field_names) + writer.writeheader() + writer.writerows(formatted_stats) + +def open_csv(): + ''' Open CSV on completion ''' + filepath='StrongDM_Logins_Past_90_Days.csv' + if platform.system() == 'Darwin': # Mac + subprocess.call(('open', filepath)) + elif platform.system() == 'Windows': # Windows + os.startfile(filepath) + else: # linux variants + subprocess.call(('xdg-open', filepath)) + + +def main(): + ''' Run it all!''' + accounts = get_accounts() + activities = get_activities(90) + stats = generate_stats(activities, accounts) + create_csv(stats) + open_csv() + exit() + + +if __name__ == "__main__": + main() diff --git a/audit/composite_role_audit/README.md b/audit/composite_role_audit/README.md new file mode 100644 index 0000000..9e170c5 --- /dev/null +++ b/audit/composite_role_audit/README.md @@ -0,0 +1,22 @@ +# List Composite Roles + +This folder contains a Python script that lists all composite roles, and each of their members, sub-roles, and related resources, if any. + +## Requirements +* Python3 +* A [strongDM API key pair](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) with the following permissions: + * Roles: List + * Datasources: List + * Grants: Read + * Accounts: Read + +## Usage +* Run `pip install strongdm==1.0.35` +* Run the script e.g. `python comp_users.py`. + +## Notes +* Composite roles are deprecated in Python SDK 2.0.0 and later. Please use version 1.0.35 or earlier. +* Composite roles no longer apply if you have migrated to the latest strongDM Admin UI. If you are not sure if this applies to you, please contact your Customer Success Manger, or email support@strongdm.com. + + + diff --git a/audit/composite_role_audit/comp_users.py b/audit/composite_role_audit/comp_users.py new file mode 100644 index 0000000..4640621 --- /dev/null +++ b/audit/composite_role_audit/comp_users.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +import strongdm, os + +api_access_key = "key" +api_secret_key = "secret" + +client = strongdm.Client(api_access_key, api_secret_key) + +def list_roles(): + comp_roles = list(client.roles.list("composite:true")) + for c in comp_roles: + print("Comp Role Name: **" + c.name + "** has these users:") + # are any users attached to the comp role? + comp_users = list(client.account_attachments.list('role_id:{}'.format(c.id))) + if (len(comp_users) > 0): + for u in comp_users: + user = client.accounts.get(u.account_id) + print(" " + user.account.email) + else: + print(" No direct members!") + print("and these sub-roles:") + role_attachments = list(client.role_attachments.list('composite_role_id:{}'.format(c.id))) + if (len(role_attachments) > 0): + for ra in role_attachments: + role = client.roles.get(ra.attached_role_id) + print(" Role: *" + role.role.name + "*, whose members are:") + # are any users attached to the sub-role? + role_users = list(client.account_attachments.list('role_id:{}'.format(ra.attached_role_id))) + if (len(role_users) > 0): + for u in role_users: + user = client.accounts.get(u.account_id) + print(" " + user.account.email) + else: + print(" No members!") + print(" and linked resources are:") + grants = list(client.role_grants.list('role_id:{}'.format(ra.attached_role_id))) + if (len(grants) > 0): + for g in grants: + res = client.resources.get(g.resource_id) + print(" Resource: ", res.resource.name) + else: + print(" No resources!") + + else: + print(" No sub-roles!") + print("=================") + +def main(): + list_roles() + +main() \ No newline at end of file diff --git a/audit/roles/README.md b/audit/roles/README.md new file mode 100644 index 0000000..d875c3b --- /dev/null +++ b/audit/roles/README.md @@ -0,0 +1,14 @@ +# List all Roles and associated Resources + +This folder contains a Python script that lists all Roles and the Resources linked to each. + +## Requirements +* Python3 +* A [strongDM API key pair](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) with the following permissions: + * Roles: List + * Datasources: List + * Grants: Read + +## Usage +* Run `pip install strongdm` +* Run the script e.g. `python role_resources.py`. diff --git a/audit/roles/role_resources.py b/audit/roles/role_resources.py new file mode 100644 index 0000000..ad5b5f0 --- /dev/null +++ b/audit/roles/role_resources.py @@ -0,0 +1,89 @@ +import json +import os +import strongdm + +access_key=os.getenv("SDM_API_ACCESS_KEY") +secret_key=os.getenv("SDM_API_SECRET_KEY") + +client = strongdm.Client(access_key, secret_key) + +def get_all_roles(): + """ + Return all roles + """ + try: + return list(client.roles.list('')) + except Exception as ex: + raise Exception("List roles failed: " + str(ex)) from ex + +def get_role_by_name(name): + """ + Return a SDM role by name + """ + try: + sdm_roles = list(client.roles.list('name:"{}"'.format(name))) + except Exception as ex: + raise Exception("List roles failed: " + str(ex)) from ex + if len(sdm_roles) == 0: + raise Exception("Sorry, cannot find that role!") + return sdm_roles[0] + +def get_all_resources_by_role(role_name, filter = ''): + """ + Return all resources by role name + """ + try: + sdm_role = get_role_by_name(role_name) + resources_filters = get_resources_filters_by_role(sdm_role) + if filter: + resources_filters = [f"{rf},{filter}" for rf in resources_filters] + return get_unique_resources(resources_filters) + except Exception as ex: + raise Exception("List resources by role failed: " + str(ex)) from ex + +def get_resources_filters_by_role(sdm_role): + if not hasattr(sdm_role, 'access_rules') or sdm_role.access_rules is None: + sdm_role_grants = list(client.role_grants.list(f"role_id:{sdm_role.id}")) + return [f"id:{rg.resource_id}" for rg in sdm_role_grants] + # then this org is using Access Overhaul + access_rules = json.loads(sdm_role.access_rules) if isinstance(sdm_role.access_rules, str) else sdm_role.access_rules + resources_filters = [] + for ar in access_rules: + filter = [] + if ar.get('ids'): + filter.append(",".join([f"id:{id}" for id in ar['ids']])) + if ar.get('type'): + filter.append(f"type:{ar['type']}") + if ar.get('tags'): + tags = [] + for key, value in ar['tags'].items(): + tags.append('tag:"{}"="{}"'.format(key, value)) + filter.append(",".join(tags)) + resources_filters.append(",".join(filter)) + return resources_filters + +def get_unique_resources(resources_filter): + resources_map = {} + for filter in resources_filter: + resources = remove_none_values(client.resources.list(filter)) + resources_map |= {r.id: r for r in resources if resources_map.get(r.id) is None} + return resources_map.values() + +def remove_none_values(elements): + return [e for e in elements if e is not None] + +def print_border(): + print("~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=") + +def main(): + roles = get_all_roles() + for role in roles: + print("Role name: \"" + role.name + "\" includes the following resources:") + resources = get_all_resources_by_role(role.name) + print(" ", [r.name for r in resources]) + print_border() + +if __name__ == "__main__": + main() + + diff --git a/audit/user_audit/README.md b/audit/user_audit/README.md new file mode 100644 index 0000000..8528cc8 --- /dev/null +++ b/audit/user_audit/README.md @@ -0,0 +1,21 @@ +# List all Users with Roles and Resources + +This folder contains a Python script that lists all users, with their Role(s) and all resources granted by those Roles. + +## Requirements +* Python3 +* A [strongDM API key pair](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) with the following permissions: + * Roles: List + * Datasources: List + * Users: List + * Grants: List + +## Usage +* Run `pip install strongdm` +* Set two environment variables, using the API key values from above: SDM_API_ACCESS_KEY, and SDM_API_SECRET_KEY. Alternately, you can code those values directly in the script. +* Run the script e.g. `python audit_sdm_users.py`. +* The script will write data to a local CSV file (path defined at top of script). +* It will attempt to open the file using the default application defined on your system. + +## Notes +* This script does not list permanent or temporary direct grants -- though it could be modified to do so! diff --git a/audit/user_audit/audit_sdm_users.py b/audit/user_audit/audit_sdm_users.py new file mode 100644 index 0000000..ced98c3 --- /dev/null +++ b/audit/user_audit/audit_sdm_users.py @@ -0,0 +1,118 @@ +#! python + +import json +import os +import strongdm, time, os, argparse, sys, logging, csv, subprocess, platform + +# This will hold user/role/resource data. +audit_dict = {} +# Output file +filepath = 'audit.csv' + +access_key=os.getenv("SDM_API_ACCESS_KEY") +secret_key=os.getenv("SDM_API_SECRET_KEY") + +client = strongdm.Client(access_key, secret_key) + +def get_role_by_id(id): + try: + sdm_roles = list(client.roles.list('id:"{}"'.format(id))) + except Exception as ex: + raise Exception("List roles failed: " + str(ex)) from ex + if len(sdm_roles) == 0: + raise Exception("Sorry, cannot find that role!") + return sdm_roles[0] + +def get_all_resources_by_role(role_id, filter = ''): + """ + Return all resources by role name + """ + try: + sdm_role = get_role_by_id(role_id) + resources_filters = get_resources_filters_by_role(sdm_role) + if filter: + resources_filters = [f"{rf},{filter}" for rf in resources_filters] + return get_unique_resources(resources_filters) + except Exception as ex: + raise Exception("List resources by role failed: " + str(ex)) from ex + +def get_resources_filters_by_role(sdm_role): + if not hasattr(sdm_role, 'access_rules') or sdm_role.access_rules is None: + sdm_role_grants = list(client.role_grants.list(f"role_id:{sdm_role.id}")) + return [f"id:{rg.resource_id}" for rg in sdm_role_grants] + # then this org is using Access Overhaul + access_rules = json.loads(sdm_role.access_rules) if isinstance(sdm_role.access_rules, str) else sdm_role.access_rules + resources_filters = [] + for ar in access_rules: + filter = [] + if ar.get('ids'): + filter.append(",".join([f"id:{id}" for id in ar['ids']])) + if ar.get('type'): + filter.append(f"type:{ar['type']}") + if ar.get('tags'): + tags = [] + for key, value in ar['tags'].items(): + tags.append('tag:"{}"="{}"'.format(key, value)) + filter.append(",".join(tags)) + resources_filters.append(",".join(filter)) + return resources_filters + +def get_unique_resources(resources_filter): + resources_map = {} + for filter in resources_filter: + resources = remove_none_values(client.resources.list(filter)) + resources_map |= {r.id: r for r in resources if resources_map.get(r.id) is None} + return resources_map.values() + +def remove_none_values(elements): + return [e for e in elements if e is not None] + +def get_all_users(): + users = list(client.accounts.list('type:user,suspended:false')) + return users + +def get_user_roles(users): + for user in users: + role_list = [] + resource_list = [] + attachments = list(client.account_attachments.list('account_id:{}'.format(user.id))) + for a in attachments: + role = client.roles.get(a.role_id) + resources = get_all_resources_by_role(a.role_id) + role_list.append(role.role.name) + role_list.append("/") + + for resource in resources: + # Append the name to resource_list + resource_list.append(resource.name) + resource_list.append("/") + + # Store the userID and email, for printing later + audit_dict[user.id] = [user.email, role_list, resource_list] + +def print_csv(): + with open(filepath, 'w', newline='') as csvfile: + spamwriter = csv.writer(csvfile, delimiter=',', + quotechar='|', quoting=csv.QUOTE_MINIMAL) + spamwriter.writerow(["AccountID", "Email", "Roles", "Resources"]) + for key, val in audit_dict.items(): + spamwriter.writerow([key,val[0], ' '.join(val[1]).strip(" / "), ' '.join(val[2]).strip(" / ") ] ) + +def open_csv(): + if platform.system() == 'Darwin': # Mac + subprocess.call(('open', filepath)) + elif platform.system() == 'Windows': # Windows + os.startfile(filepath) + else: # linux variants + subprocess.call(('xdg-open', filepath)) + +def main(): + users = get_all_users() + get_user_roles(users) + print_csv() + open_csv() + +if __name__ == "__main__": + main() + + diff --git a/authentication/README.md b/authentication/README.md new file mode 100644 index 0000000..d9de157 --- /dev/null +++ b/authentication/README.md @@ -0,0 +1,3 @@ +# Authentication + +Previous versions of the Okta integration scripts were moved to the [archive](../archive/) in favor of the new implementation that supports multiple Identity Providers. We strongly recommend you to use the current version, because we've included different fixes for issues identified by our customers. diff --git a/authentication/multi-idp/sync-update/.gitignore b/authentication/multi-idp/sync-update/.gitignore new file mode 100644 index 0000000..a7ede26 --- /dev/null +++ b/authentication/multi-idp/sync-update/.gitignore @@ -0,0 +1,5 @@ +set-env.sh +credentials.json +token.json +__debug_bin +.vscode/ diff --git a/authentication/multi-idp/sync-update/README.md b/authentication/multi-idp/sync-update/README.md new file mode 100644 index 0000000..ea972e8 --- /dev/null +++ b/authentication/multi-idp/sync-update/README.md @@ -0,0 +1,72 @@ +# Synchronize Users from different Identity Providers + +🚧 _This script is written for legacy strongDM organizations, and will not work properly with the new Access Overhaul features. If you are unsure whether your organization is enabled for Access Overhaul, please contact your strongDM Account Manager, or write to support@strongdm.com_ + +Shim go script for synchronizing users/groups from different Identity Providers (IdP) with strongDM. Current version supports Okta and Google Directory. + +Original version: https://github.com/strongdm/strongdm-sdk-go-examples/tree/master/contrib/okta-sync + +## How it works +This script reads a separate JSON file, [matchers.yml](matchers.yml), which maps IdP groups to resources in SDM by type or name. + +For each Group defined in the YML, an SDM Role will be created, and access to the defined resources will be granted to that Role, using the [filters spec](https://www.strongdm.com/docs/automation/getting-started/filters). + +Any active user present in the IdP (Okta, Google Directory) and with associated group(s) listed in [matchers.yml](matchers.yml) will be created in SDM, and assigned to the corresponding SDM Role. + +strongDM only supports 1:1 user-role mappings, when there are multiple groups assigned to a okta user, a composite role is created with multiple sub-roles assigned to it. + +The script won't remove any Roles or Users in SDM, unless you use the flags: `-delete-unmatching-roles` or `-delete-unmatching-users`. SDM admin users are ignored during deletion. + +## How to use this script +1. Set the environment variables: SDM_API_ACCESS_KEY and SDM_API_SECRET_KEY. + * For Okta set OKTA_CLIENT_TOKEN and OKTA_CLIENT_ORGURL. + * For Google set credentials.json +2. Edit the [matchers.yml](matchers.yml) file to a) define which groups to sync from the IdP to strongDM as [Roles](https://www.strongdm.com/docs/admin-ui-guide/user-management/roles), and b) which strongDM resources users in those groups will receive access to. + + > For example, the sample file in this folder would create strongDM Roles with access to all `mysql` and `postgres` resources in your organization. + +## Sample +Help: +``` +$ go run . -help + -delete-unmatching-roles + delete roles present in SDM but not in matchers.yml + -delete-unmatching-users + delete users present in SDM but not in the selected IdP or assigned to any role in matchers.yml + -google + use Google as IdP + -json + dump a JSON report for debugging + -log + include logging information + -okta + use Okta as IdP + -plan + do not apply changes just show initial queries +```` + +Okta: +``` +$ go run . -okta -delete-unmatching-roles -delete-unmatching-users +5 IdP users, 3 strongDM users in IdP, 3 strongDM roles in Idp +``` + +Google: +``` +$ go run . -google -delete-unmatching-roles -delete-unmatching-users +5 IdP users, 3 strongDM users in IdP, 3 strongDM roles in Idp +``` + +Considerations: +* For better reporting add `-plan` to your command. + +## Google +1. Enable OAuth Consent: https://console.cloud.google.com/apis/credentials/consent (Internal is OK) +2. Create credentials for a Desktop App: https://console.cloud.google.com/apis/credentials +3. Enable Admin SDK API: https://console.cloud.google.com/apis/api/admin.googleapis.com/overview +4. Administrate Users and OrgUnits: https://admin.google.com/u/2/ac/users + * A user can only be assigned to one OrgUnit at a time + +Considerations: +* Reference: https://developers.google.com/admin-sdk/directory/v1/quickstart/go +* Google uses paths for OrgUnits. For indicating `/` use the flag Root in `matchers.yml`. diff --git a/authentication/multi-idp/sync-update/go.mod b/authentication/multi-idp/sync-update/go.mod new file mode 100644 index 0000000..4994078 --- /dev/null +++ b/authentication/multi-idp/sync-update/go.mod @@ -0,0 +1,14 @@ +module strongdm.com/multi-idp-sync-update + +go 1.16 + +require ( + github.com/okta/okta-sdk-golang v1.1.0 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.8.1 + github.com/strongdm/strongdm-sdk-go v0.9.30 + github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 + golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1 + google.golang.org/api v0.48.0 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/authentication/multi-idp/sync-update/go.sum b/authentication/multi-idp/sync-update/go.sum new file mode 100644 index 0000000..b7f732b --- /dev/null +++ b/authentication/multi-idp/sync-update/go.sum @@ -0,0 +1,533 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0 h1:bAMqZidYkmIsUqe6PtkEPT7Q+vfizScn+jfNA6jwK9c= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM= +github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk= +github.com/okta/okta-sdk-golang v1.1.0 h1:sr/KYSMRhs4F2NWEbqWXqN4y4cKKcfzrtOiBqR/J6mI= +github.com/okta/okta-sdk-golang v1.1.0/go.mod h1:KEjmr3Zo+wP3gVa3XhwIvENBfh7L/iRUeIl6ruQYOK0= +github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627 h1:pSCLCl6joCFRnjpeojzOpEYs4q7Vditq8fySFG5ap3Y= +github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/square/go-jose v2.4.1+incompatible h1:KFYc54wTtgnd3x4B/Y7Zr1s/QaEx2BNzRsB3Hae5LHo= +github.com/square/go-jose v2.4.1+incompatible/go.mod h1:7MxpAF/1WTVUu8Am+T5kNy+t0902CaLWM4Z745MkOa8= +github.com/square/go-jose/v3 v3.0.0-20200225220504-708a9fe87ddc/go.mod h1:JbpHhNyeVc538vtj/ECJ3gPYm1VEitNjsLhm4eJQQbg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/strongdm/strongdm-sdk-go v0.9.30 h1:Rz1m14x66rEBDoLufsyh1gYtkaZJ3rAalLW2evfhJqU= +github.com/strongdm/strongdm-sdk-go v0.9.30/go.mod h1:rXX9x9j6IgGYyjWjAzMjh2PTMRZsH0/eKCuzUi10xok= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1 h1:x622Z2o4hgCr/4CiKWc51jHVKaWdtVpBNmEI8wI9Qns= +golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0 h1:RDAPWfNFY06dffEXfn7hZF5Fr1ZbnChzfQZAPyBd1+I= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08 h1:pc16UedxnxXXtGxHCSUhafAoVHQZ0yXl8ZelMH4EETc= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y= +gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/authentication/multi-idp/sync-update/google.go b/authentication/multi-idp/sync-update/google.go new file mode 100644 index 0000000..ce5063c --- /dev/null +++ b/authentication/multi-idp/sync-update/google.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + + "github.com/pkg/errors" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/option" +) + +// see https://developers.google.com/admin-sdk/directory/v1/limits +const USERS_LIMIT = 500 + +var googleHTTPClient *http.Client + +func ValidateGoogleEnv() error { + b, err := ioutil.ReadFile("credentials.json") + if err != nil { + return errors.Errorf("Unable to read client secret file: %v", err) + } + config, err := google.ConfigFromJSON(b, admin.AdminDirectoryUserReadonlyScope) + if err != nil { + return errors.Errorf("Unable to parse client secret file to config: %v", err) + } + googleHTTPClient = getClient(config) + return nil +} + +func LoadGoogleUsers(ctx context.Context, matchers *MatcherConfig) (idpUserList, error) { + srv, err := admin.NewService(ctx, option.WithHTTPClient(googleHTTPClient)) + if err != nil { + return nil, err + } + + r, err := srv.Users.List().Customer("my_customer").MaxResults(USERS_LIMIT).Do() + if err != nil { + return nil, err + } + + var users []idpUser + rootRoleName := getRootRoleName(matchers) + for _, googleUser := range r.Users { + users = append(users, idpUser{ + Login: googleUser.PrimaryEmail, + FirstName: googleUser.Name.GivenName, + LastName: googleUser.Name.FamilyName, + Groups: getGroups(googleUser.OrgUnitPath, rootRoleName, matchers), + }) + } + return users, nil +} + +// Retrieve a token, saves the token, then returns the generated client. +func getClient(config *oauth2.Config) *http.Client { + // The file token.json stores the user's access and refresh tokens, and is + // created automatically when the authorization flow completes for the first + // time. + tokFile := "token.json" + tok, err := tokenFromFile(tokFile) + if err != nil { + tok = getTokenFromWeb(config) + saveToken(tokFile, tok) + } + return config.Client(context.Background(), tok) +} + +// Request a token from the web, then returns the retrieved token. +func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { + authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + fmt.Printf("Go to the following link in your browser then type the "+ + "authorization code: \n%v\n", authURL) + + var authCode string + if _, err := fmt.Scan(&authCode); err != nil { + log.Fatalf("Unable to read authorization code: %v", err) + } + + tok, err := config.Exchange(context.TODO(), authCode) + if err != nil { + log.Fatalf("Unable to retrieve token from web: %v", err) + } + return tok +} + +// Retrieves a token from a local file. +func tokenFromFile(file string) (*oauth2.Token, error) { + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + tok := &oauth2.Token{} + err = json.NewDecoder(f).Decode(tok) + return tok, err +} + +// Saves a token to a file path. +func saveToken(path string, token *oauth2.Token) { + fmt.Printf("Saving credential file to: %s\n", path) + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + log.Fatalf("Unable to cache oauth token: %v", err) + } + defer f.Close() + json.NewEncoder(f).Encode(token) +} + +func getGroups(orgUnitPath string, rootRoleName string, matchers *MatcherConfig) []string { + if orgUnitPath == "/" { + if rootRoleName == "" { + return []string{} + } else { + return []string{rootRoleName} + } + } + orgUnits := strings.Split(orgUnitPath, "/") + return []string{orgUnits[len(orgUnits)-1]} +} + +func getRootRoleName(matchers *MatcherConfig) string { + for _, group := range matchers.Groups { + if group.Root { + return group.Name + } + } + return "" +} diff --git a/authentication/multi-idp/sync-update/main.go b/authentication/multi-idp/sync-update/main.go new file mode 100644 index 0000000..981e518 --- /dev/null +++ b/authentication/multi-idp/sync-update/main.go @@ -0,0 +1,689 @@ +// Copyright 2020 StrongDM Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/pkg/errors" + sdm "github.com/strongdm/strongdm-sdk-go" + "gopkg.in/yaml.v2" +) + +var jsonFlag = flag.Bool("json", false, "dump a JSON report for debugging") +var planFlag = flag.Bool("plan", false, "do not apply changes just show initial queries") +var logFlag = flag.Bool("log", false, "include logging information") + +// use carefully +var deleteUnmatchingRolesFlag = flag.Bool("delete-unmatching-roles", false, "delete roles present in SDM but not in matchers.yml") +var deleteUnmatchingUsersFlag = flag.Bool("delete-unmatching-users", false, "delete users present in SDM but not in the selected IdP or assigned to any role in matchers.yml") + +var oktaFlag = flag.Bool("okta", false, "use Okta as IdP") +var googleFlag = flag.Bool("google", false, "use Google as IdP") + +func init() { + flag.Parse() + if (!*oktaFlag && !*googleFlag) || (*oktaFlag && *googleFlag) { + fmt.Fprintf(os.Stderr, "You need to specify one Identity Provider (IdP): Okta or Google\n") + fmt.Fprintf(os.Stderr, "Use -okta or -google\n") + os.Exit(-1) + } + log.SetLevel(log.PanicLevel) + if *logFlag { + log.SetLevel(log.InfoLevel) + } +} + +type syncReport struct { + Start time.Time `json:"start"` + Complete time.Time `json:"complete"` + IdPUserCount int `json:"idpUsersCount"` + IdPUsers idpUserList `json:"idpUsers"` + + SDMUsersInIdPCount int `json:"sdmUsersInIdPCount"` + SDMUsersInIdP userList `json:"sdmUsersInIdP"` + SDMUserNotInIdPCount int `json:"sdmUsersNotInIdPCount"` + SDMUsersNotInIdP userList `json:"sdmUsersNotInIdP"` + + SDMRoleInIdPCount int `json:"sdmRolesInIdPCount"` + SDMRolesInIdP roleList `json:"sdmRolesInIdP"` + SDMRoleNotInIdPCount int `json:"sdmRolesNotInIdPCount"` + SDMRolesNotInIdP roleList `json:"sdmRolesNotInIdP"` + + Matchers *MatcherConfig `json:"matchers"` +} + +func (rpt *syncReport) String() string { + if !*jsonFlag { + return rpt.short() + } + + out, err := json.MarshalIndent(rpt, "", "\t") + if err != nil { + return fmt.Sprintf("error building JSON report: %s\n\n%s", err, rpt.short()) + } + return string(out) +} + +func (rpt *syncReport) short() string { + return fmt.Sprintf("%d IdP users, %d strongDM users in IdP, %d strongDM roles in IdP\n", + rpt.IdPUserCount, rpt.SDMUsersInIdPCount, rpt.SDMRoleInIdPCount) +} + +func main() { + ctx := context.Background() + + if os.Getenv("SDM_API_ACCESS_KEY") == "" || os.Getenv("SDM_API_SECRET_KEY") == "" { + fmt.Println("SDM_API_ACCESS_KEY and SDM_API_SECRET_KEY must be set") + os.Exit(1) + return + } + + if *oktaFlag { + err := ValidateOktaEnv() + if err != nil { + fmt.Fprintf(os.Stderr, "invalid Okta environment: %v\n", err) + os.Exit(1) + return + } + } + + if *googleFlag { + err := ValidateGoogleEnv() + if err != nil { + fmt.Fprintf(os.Stderr, "invalid Google environment: %v\n", err) + os.Exit(1) + return + } + } + + client, err := sdm.New(os.Getenv("SDM_API_ACCESS_KEY"), os.Getenv("SDM_API_SECRET_KEY")) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to initialize strongDM client: %v\n", err) + os.Exit(1) + return + } + + var rpt syncReport + rpt.Start = time.Now() + log.Info("Starting sync...") + + matchers, err := loadMatchers() + if err != nil { + fmt.Fprintf(os.Stderr, "error loading Matchers users: %v\n", err) + os.Exit(1) + return + } + rpt.Matchers = matchers + log.Infof("Loaded %d matchers", len(matchers.Groups)) + + idpUsers, err := loadIdpUsers(ctx, matchers) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading users from IdP: %v\n", err) + os.Exit(1) + return + } + rpt.IdPUsers = idpUsers + rpt.IdPUserCount = len(idpUsers) + log.Infof("Loaded %d users from IdP", rpt.IdPUserCount) + + initialRoles, err := loadRoles(ctx, client) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading roles: %v\n", err) + os.Exit(1) + return + } + log.Infof("Loaded %d initial roles from SDM", len(initialRoles)) + + initialUsers, err := loadUsers(ctx, client) + if err != nil { + fmt.Fprintf(os.Stderr, "error loading users: %v\n", err) + os.Exit(1) + return + } + log.Infof("Loaded %d initial users from SDM", len(initialRoles)) + + if !*planFlag { + log.Info("Synchronizing users and roles") + matchingRoles, unmatchingRoles, err := syncRoles(ctx, client, initialRoles, matchers) + if err != nil { + fmt.Fprintf(os.Stderr, "error synchronizing roles: %v\n", err) + os.Exit(1) + return + } + rpt.SDMRolesInIdP = matchingRoles + rpt.SDMRoleInIdPCount = len(matchingRoles) + rpt.SDMRolesNotInIdP = unmatchingRoles + rpt.SDMRoleNotInIdPCount = len(unmatchingRoles) + log.Infof("%d SDM roles in IdP", rpt.SDMRoleInIdPCount) + log.Infof("%d SDM roles not in IdP", rpt.SDMRoleNotInIdPCount) + + matchingUsers, unmatchingUsers, err := syncUsers(ctx, client, initialUsers, matchingRoles, idpUsers, matchers) + if err != nil { + fmt.Fprintf(os.Stderr, "error synchronizing users: %v\n", err) + os.Exit(1) + return + } + rpt.SDMUsersInIdP = matchingUsers + rpt.SDMUsersInIdPCount = len(matchingUsers) + rpt.SDMUsersNotInIdP = unmatchingUsers + rpt.SDMUserNotInIdPCount = len(unmatchingUsers) + log.Infof("%d SDM users in IdP", rpt.SDMRoleInIdPCount) + log.Infof("%d SDM users not in IdP", rpt.SDMRoleNotInIdPCount) + } + + rpt.Complete = time.Now() + fmt.Println(rpt.String()) +} + +type idpUserList []idpUser + +type idpUser struct { + Login string `json:"login"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Groups []string `json:"groups"` +} + +type roleList []roleRow + +type roleRow struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type userList []userRow + +type userRow struct { + ID string `json:"id"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + Role string `json:"roleName"` +} + +type MatcherConfig struct { + Groups []struct { + Name string `yaml:"name"` + Resources []string `yaml:"resources"` + Root bool `yaml:"root"` + } `yaml:"groups"` +} + +type entitlementList []entitlement + +type entitlement struct { + ResourceID string `json:"id"` + Name string `json:"name"` +} + +func loadMatchers() (*MatcherConfig, error) { + body, err := ioutil.ReadFile("matchers.yml") + if err != nil { + return nil, errors.Wrap(err, "unable to read from matchers configuration file") + } + + var m MatcherConfig + err = yaml.UnmarshalStrict(body, &m) + if err != nil { + return nil, errors.Wrap(err, "error unmarshalling matcher configuration") + } + err = validateMatchers(&m) + if err != nil { + return nil, err + } + + return &m, err +} + +func validateMatchers(matchers *MatcherConfig) error { + count := 0 + for _, group := range matchers.Groups { + if group.Root { + count++ + } + } + if count > 1 { + return errors.Errorf("cannot have more than one root group") + } + return nil +} + +func loadIdpUsers(ctx context.Context, matchers *MatcherConfig) (idpUserList, error) { + if *oktaFlag { + return LoadOktaUsers(ctx) + } else if *googleFlag { + return LoadGoogleUsers(ctx, matchers) + } else { + return nil, errors.Errorf("invalid IdP") + } +} + +func loadRoles(ctx context.Context, client *sdm.Client) (roleList, error) { + roles, err := client.Roles().List(ctx, "") + if err != nil { + return nil, err + } + var result roleList + for roles.Next() { + role := roles.Value() + result = append(result, roleRow{ + ID: role.ID, + Name: role.Name, + }) + } + if roles.Err() != nil { + return nil, roles.Err() + } + return result, nil +} + +func loadUsers(ctx context.Context, client *sdm.Client) (userList, error) { + accountAttachments, err := client.AccountAttachments().List(ctx, "") + if err != nil { + return nil, err + } + roles := map[string]string{} + for accountAttachments.Next() { + attachment := accountAttachments.Value() + roles[attachment.AccountID] = attachment.RoleID + } + if accountAttachments.Err() != nil { + return nil, accountAttachments.Err() + } + + accounts, err := client.Accounts().List(ctx, "type:user") + if err != nil { + return nil, err + } + var result userList + for accounts.Next() { + user := accounts.Value().(*sdm.User) + result = append(result, userRow{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + Role: roles[user.ID], + }) + } + if accounts.Err() != nil { + return nil, accounts.Err() + } + return result, nil +} + +func syncRoles(ctx context.Context, client *sdm.Client, initialRoles roleList, matchers *MatcherConfig) (roleList, roleList, error) { + entitlementsByGroup, err := matchEntitlements(ctx, client, matchers) + if err != nil { + return nil, nil, err + } + matchingRoles, err := createMatchingRoles(ctx, client, entitlementsByGroup) + if err != nil { + return nil, nil, err + } + unmatchingRoles := calculateUnmatchingRoles(initialRoles, matchingRoles) + if *deleteUnmatchingRolesFlag { + err = deleteUnmatchingRoles(ctx, client, unmatchingRoles) + if err != nil { + return nil, nil, err + } + } + return matchingRoles, unmatchingRoles, nil +} + +// matchEntitlements creates lists of concrete datasources and servers by group name +func matchEntitlements(ctx context.Context, client *sdm.Client, matchers *MatcherConfig) (map[string]entitlementList, error) { + result := make(map[string]entitlementList) + for _, matcher := range matchers.Groups { + uniq := make(map[entitlement]bool) + for _, expression := range matcher.Resources { + resources, err := client.Resources().List(ctx, expression) + if err != nil { + return nil, err + } + for resources.Next() { + rs := resources.Value() + if rs == nil { // check for beta resources not in the SDK + continue + } + uniq[entitlement{ResourceID: rs.GetID()}] = true + } + if resources.Err() != nil { + return nil, err + } + } + result[matcher.Name] = make(entitlementList, 0) // for creating groups without available resources + for u := range uniq { + result[matcher.Name] = append(result[matcher.Name], u) + } + } + return result, nil +} + +func createMatchingRoles(ctx context.Context, client *sdm.Client, entitlementsByGroup map[string]entitlementList) (roleList, error) { + finalRoles := roleList{} + for groupName, entitlements := range entitlementsByGroup { + role, err := loadOrCreateRole(ctx, client, groupName, false) + if err != nil { + return nil, err + } + for _, e := range entitlements { + err := createRoleGrant(ctx, client, role.ID, e.ResourceID) + if err != nil { + return nil, err + } + } + finalRoles = append(finalRoles, roleRow{ + ID: role.ID, + Name: role.Name, + }) + } + return finalRoles, nil +} + +func loadOrCreateRole(ctx context.Context, client *sdm.Client, roleName string, isComposite bool) (*sdm.Role, error) { + roles, err := client.Roles().List(ctx, fmt.Sprintf("name:\"%s\"", roleName)) + if err != nil { + return nil, err + } + if roles.Next() { + return roles.Value(), nil + } + + resp, err := client.Roles().Create(ctx, &sdm.Role{ + Name: roleName, + Composite: isComposite, + }) + if err != nil { + return nil, err + } + return resp.Role, nil +} + +func createRoleGrant(ctx context.Context, client *sdm.Client, roleID string, resourceID string) error { + _, err := client.RoleGrants().Create(ctx, &sdm.RoleGrant{ + RoleID: roleID, + ResourceID: resourceID, + }) + var alreadyExistsErr *sdm.AlreadyExistsError + if err != nil && !errors.As(err, &alreadyExistsErr) { + return err + } + return nil +} + +func calculateUnmatchingRoles(initialRoles roleList, matchingRoles roleList) roleList { + unmatchingRoles := roleList{} + for _, irole := range initialRoles { + found := false + for _, mrole := range matchingRoles { + if irole.ID == mrole.ID { + found = true + break + } + } + if !found { + unmatchingRoles = append(unmatchingRoles, irole) + } + } + return unmatchingRoles +} + +func deleteUnmatchingRoles(ctx context.Context, client *sdm.Client, unmatchingRoles roleList) error { + for _, role := range unmatchingRoles { + _, err := client.Roles().Delete(ctx, role.ID) + if err != nil { + return err + } + } + return nil +} + +func syncUsers(ctx context.Context, client *sdm.Client, initialUsers userList, roles roleList, idpUsers idpUserList, matchers *MatcherConfig) (userList, userList, error) { + matchingUsers, err := createMatchingUsers(ctx, client, roles, idpUsers, matchers) + if err != nil { + return nil, nil, err + } + unmatchingUsers := calculateUnmatchingUsers(initialUsers, matchingUsers) + if *deleteUnmatchingUsersFlag { + err = deleteUnmatchingUsers(ctx, client, unmatchingUsers) + if err != nil { + return nil, nil, err + } + } + return matchingUsers, unmatchingUsers, nil +} + +func createMatchingUsers(ctx context.Context, client *sdm.Client, roles roleList, idpUsers idpUserList, matchers *MatcherConfig) (userList, error) { + matchingUsers := userList{} + for _, idpUser := range idpUsers { + if !idpUserHasMatchingGroup(idpUser, matchers) { + log.Infof("ignoring user %s - no group in matchers assigned to it\n", idpUser.Login) + continue + } + user, err := loadOrCreateUser(ctx, client, idpUser) + var alreadyExistsErr *sdm.AlreadyExistsError + if errors.As(err, &alreadyExistsErr) { + log.Infof("ignoring user %s - might be assigned to a different org\n", idpUser.Login) + continue + } + if err != nil { + return nil, err + } + err = removePreviousAccountAttachments(ctx, client, user.ID) + if err != nil { + return nil, err + } + matchingGroups := calculateMatchingGroups(idpUser.Groups, matchers) + var roleName string + if len(matchingGroups) == 1 { + roleID, err := findRoleID(matchingGroups[0], roles) + if err != nil { + return nil, err + } + err = assignRole(ctx, client, user.ID, roleID) + if err != nil { + return nil, err + } + roleName = matchingGroups[0] + } else if len(matchingGroups) > 1 { + compositeRole, err := createCompositeRole(ctx, client, roles, matchingGroups) + if err != nil { + return nil, err + } + err = assignRole(ctx, client, user.ID, compositeRole.ID) + if err != nil { + return nil, err + } + roleName = compositeRole.Name + } + matchingUsers = append(matchingUsers, userRow{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + Role: roleName, + }) + } + return matchingUsers, nil +} + +func idpUserHasMatchingGroup(idpUser idpUser, matchers *MatcherConfig) bool { + for _, idpGroup := range idpUser.Groups { + for _, matcherGroup := range matchers.Groups { + if idpGroup == matcherGroup.Name { + return true + } + } + } + return false +} + +func loadOrCreateUser(ctx context.Context, client *sdm.Client, idpUser idpUser) (*sdm.User, error) { + users, err := client.Accounts().List(ctx, fmt.Sprintf("email:\"%s\"", idpUser.Login)) + if err != nil { + return nil, err + } + if users.Next() { + return users.Value().(*sdm.User), nil + } + + resp, err := client.Accounts().Create(ctx, &sdm.User{ + Email: idpUser.Login, + FirstName: idpUser.FirstName, + LastName: idpUser.LastName, + }) + if err != nil { + return nil, err + } + return resp.Account.(*sdm.User), nil +} + +func removePreviousAccountAttachments(ctx context.Context, client *sdm.Client, userID string) error { + attachments, err := client.AccountAttachments().List(ctx, fmt.Sprintf("accountId:\"%s\"", userID)) + if err != nil { + return err + } + for attachments.Next() { + attachmentID := attachments.Value().ID + _, err := client.AccountAttachments().Delete(ctx, attachmentID) + if err != nil { + return err + } + } + return nil +} + +func calculateMatchingGroups(idpGroups []string, matchers *MatcherConfig) []string { + result := []string{} + for _, idpGroup := range idpGroups { + for _, matcherGroup := range matchers.Groups { + if idpGroup == matcherGroup.Name { + result = append(result, idpGroup) + } + } + } + return result +} + +func findRoleID(groupName string, roles roleList) (string, error) { + for _, r := range roles { + if r.Name == groupName { + return r.ID, nil + } + } + return "", fmt.Errorf("cannot find roleID for roleName = %s", groupName) +} + +func assignRole(ctx context.Context, client *sdm.Client, userID string, roleID string) error { + _, err := client.AccountAttachments().Create(ctx, &sdm.AccountAttachment{ + AccountID: userID, + RoleID: roleID, + }) + var alreadyExistsErr *sdm.AlreadyExistsError + if err != nil && !errors.As(err, &alreadyExistsErr) { + return err + } + return nil +} + +func createCompositeRole(ctx context.Context, client *sdm.Client, roles roleList, idpGroups []string) (*sdm.Role, error) { + compositeRoleName := strings.Join(idpGroups, "_") + compositeRole, err := loadOrCreateRole(ctx, client, compositeRoleName, true) + if err != nil { + return nil, err + } + err = removePreviousCompositeRoleAttachments(ctx, client, compositeRole.ID) + if err != nil { + return nil, err + } + err = assignNewCompositeRoleAttachments(ctx, client, compositeRole.ID, roles, idpGroups) + if err != nil { + return nil, err + } + return compositeRole, nil +} + +func removePreviousCompositeRoleAttachments(ctx context.Context, client *sdm.Client, compositeRoleID string) error { + attachments, err := client.RoleAttachments().List(ctx, fmt.Sprintf("compositeRoleId:\"%s\"", compositeRoleID)) + if err != nil { + return err + } + for attachments.Next() { + attachmentID := attachments.Value().ID + _, err := client.RoleAttachments().Delete(ctx, attachmentID) + if err != nil { + return err + } + } + return nil +} + +func assignNewCompositeRoleAttachments(ctx context.Context, client *sdm.Client, compositeRoleID string, roles roleList, idpGroups []string) error { + for _, group := range idpGroups { + roleID, err := findRoleID(group, roles) + if err != nil { + return err + } + _, err = client.RoleAttachments().Create(ctx, &sdm.RoleAttachment{ + CompositeRoleID: compositeRoleID, + AttachedRoleID: roleID, + }) + var alreadyExistsErr *sdm.AlreadyExistsError + if err != nil && !errors.As(err, &alreadyExistsErr) { + return err + } + } + return nil +} + +func calculateUnmatchingUsers(initialUsers userList, matchingUsers userList) userList { + unmatchingUsers := userList{} + for _, iuser := range initialUsers { + found := false + for _, muser := range matchingUsers { + if iuser.ID == muser.ID { + found = true + break + } + } + if !found { + unmatchingUsers = append(unmatchingUsers, iuser) + } + } + return unmatchingUsers +} + +func deleteUnmatchingUsers(ctx context.Context, client *sdm.Client, unmatchingUsers userList) error { + for _, user := range unmatchingUsers { + _, err := client.Accounts().Delete(ctx, user.ID) + if err != nil && strings.Contains(err.Error(), "access denied") { + fmt.Fprintf(os.Stderr, "you don't have enough permissions or cannot remove user: %s %v\n", user.Email, err) + } else if err != nil { + return err + } + } + return nil +} diff --git a/authentication/multi-idp/sync-update/matchers.yml b/authentication/multi-idp/sync-update/matchers.yml new file mode 100644 index 0000000..d0c7763 --- /dev/null +++ b/authentication/multi-idp/sync-update/matchers.yml @@ -0,0 +1,22 @@ +--- +# Be mindful of proper YAML structure +groups: + - + name: General + # Only one element can be root + root: true + resources: + # Use valid SDM filters, see: https://www.strongdm.com/docs/automation/getting-started/filters + - type:mysql + - + name: rodo-group-support + resources: + - type:mysql + - + name: rodo-group-engineering + resources: + - type:postgres + - + name: rodo-group-data + resources: + - type:athena \ No newline at end of file diff --git a/authentication/multi-idp/sync-update/okta.go b/authentication/multi-idp/sync-update/okta.go new file mode 100644 index 0000000..1a6f3c6 --- /dev/null +++ b/authentication/multi-idp/sync-update/okta.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "net/url" + "os" + "strings" + + "github.com/okta/okta-sdk-golang/okta" + "github.com/okta/okta-sdk-golang/okta/query" + "github.com/pkg/errors" + "github.com/tomnomnom/linkheader" +) + +const QUERY_STRING = "(status eq \"ACTIVE\")" +const API_LIMIT = 200 + +func ValidateOktaEnv() error { + if os.Getenv("OKTA_CLIENT_TOKEN") == "" || os.Getenv("OKTA_CLIENT_ORGURL") == "" { + return errors.Errorf("OKTA_CLIENT_TOKEN and OKTA_CLIENT_ORGURL must be set when using Okta") + } + return nil +} + +func LoadOktaUsers(ctx context.Context) (idpUserList, error) { + client, err := okta.NewClient(ctx) + if err != nil { + return nil, errors.Wrap(err, "invalid Okta configuration") + } + + var users []idpUser + after := "" + for { + search := query.NewQueryParams(query.WithSearch(QUERY_STRING), query.WithLimit(API_LIMIT), query.WithAfter(after)) + apiUsers, resp, err := client.User.ListUsers(search) + if err != nil { + return nil, errors.Wrap(err, "unable to retrieve okta users") + } + + for _, u := range apiUsers { + groups, _, err := client.User.ListUserGroups(u.Id, nil) + if err != nil { + return nil, errors.Wrap(err, "unable to retrieve okta user groups") + } + + var groupNames []string + for _, g := range groups { + groupNames = append(groupNames, g.Profile.Name) + } + + profile := (*u.Profile) + users = append(users, idpUser{ + Login: profile["login"].(string), + FirstName: profile["firstName"].(string), + LastName: profile["lastName"].(string), + Groups: groupNames, + }) + } + + after, err = getQueryAfter(resp) + if err != nil { + return nil, errors.Wrap(err, "unable to parse after value from query") + } + if after == "" { + break + } + } + return users, nil +} + +func getQueryAfter(resp *okta.Response) (string, error) { + links := linkheader.Parse(strings.Join(resp.Header["Link"], ",")) + for _, link := range links { + if link.Rel == "next" { + u, err := url.Parse(link.URL) + if err != nil { + return "", err + } + m, _ := url.ParseQuery(u.RawQuery) + return m["after"][0], nil + } + } + return "", nil +} diff --git a/automations/AWS-autoscaling/README.md b/automations/AWS-autoscaling/README.md new file mode 100644 index 0000000..be0258f --- /dev/null +++ b/automations/AWS-autoscaling/README.md @@ -0,0 +1,25 @@ +# Overview + +These scripts use a combination of shell script (for AWS operations) and strongDM Python SDK (to register the machine as both Gateway and SSH Resource). It can be used standalone in AWS EC2, or as a user-data script in an EC2 Autoscaling Launch Template. + +## Details + +Here is how this solution works: + +- the shell script uses Bash to get an AWS machine-level token, and the token to get machine metadata +- it sets those values as environment variables for the Python script to use +- that script uses our SDK to: + - register the current machine as an SSH Public Key resource + - write the new public key to the user's `authorized_keys` file + - register the current machine as a Gateway + - write the new token to a local file +- the shell script reads that file, uses it to install the Gateway, then deletes the file + +## Requirements + +As follows: + +- AWS EC2 environment +- API key pair in the script. (Or refactor to read from an external source like Secrets Manager.) +- Key scope is: `Datasource:create`, and `Relays:create`. +- The AMI in your Launch Template should have Python installed (tested with Python 3 on Ubuntu), or you can edit the shell script to install it. \ No newline at end of file diff --git a/automations/AWS-autoscaling/gw.py b/automations/AWS-autoscaling/gw.py new file mode 100644 index 0000000..a5642ae --- /dev/null +++ b/automations/AWS-autoscaling/gw.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import strongdm, time, os + +access_key="key" +secret_key="secret" + +# Get AWS environment vars +instance_ip = os.getenv("INSTANCE_IP") +instance_id = os.getenv("INSTANCE_ID") +ssh_user = os.getenv("TARGET_USER") + +# Create SDM client +client = strongdm.Client(access_key, secret_key) + +# Create the SSH resource +server = strongdm.SSH(hostname=instance_ip,name=instance_id,username=ssh_user,port=22) +ssh = client.resources.create(server) +# Append the new public key to authorized_keys +with open("/home/{}/.ssh/authorized_keys".format(ssh_user),'a',) as f: + f.write(ssh.resource.public_key) +f.close() + +# Create gateway +gateway = strongdm.Gateway( + name="name-us-west-2------{}".format(instance_id), + listen_address="{}:5000".format(instance_ip), +) +resp = client.nodes.create(gateway) + +# Write gateway token to file for installation +# Shell script will delete after it's consumed +with open("token.txt",'a',) as f: + f.write(resp.token) +f.close() + diff --git a/automations/AWS-autoscaling/user-data.sh b/automations/AWS-autoscaling/user-data.sh new file mode 100644 index 0000000..b48de05 --- /dev/null +++ b/automations/AWS-autoscaling/user-data.sh @@ -0,0 +1,26 @@ +#!/bin/bash -xe + apt update + apt install -y unzip jq awscli python3-pip + curl -J -O -L https://app.strongdm.com/releases/cli/linux + unzip sdm* + cp sdm /usr/local/bin + pip install strongdm + + TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + INSTANCE_IDENTITY="$(curl --silent -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/dynamic/instance-identity/document)" + + export INSTANCE_ID="$(echo $INSTANCE_IDENTITY | jq -r .instanceId)" + export INSTANCE_IP="$(echo $INSTANCE_IDENTITY | jq -r .privateIp)" + export TARGET_USER=ubuntu + +# Call python file to create SSH, and write token to authorized_keys +python3 /home/ubuntu/gw.py + + sdm logout + rm -rf /root/.sdm/* + rm -rf ~ubuntu/.sdm/* + + unset SDM_VERBOSE + +sudo ./sdm install --relay --token=`cat /home/$TARGET_USER/token.txt` +rm -rf /home/$TARGET_USER/token.txt diff --git a/cli/sdm-ext/README.Md b/cli/sdm-ext/README.Md new file mode 100644 index 0000000..894ad71 --- /dev/null +++ b/cli/sdm-ext/README.Md @@ -0,0 +1,38 @@ +# SDM-EXT + +The SDM-EXT project aims to create a CLI (Command Line Interface) so that the user can execute all the commands provided by the SDM CLI plus some exclusive commands from the SDM-EXT CLI. The "ext" stands for "extension". + +# ****Installation and configuration**** + +You need to install SDM CLI and configure it on your machine. See [Installing the SDM CLI](https://www.strongdm.com/docs/user-guide/client-installation) for how to install. After that, make sure that the SDM CLI can be accessed using the sdm alias in your terminal. + +To run the SDM-EXT CLI, after downloading the project and going to its directory, run the command `go build sdm-ext.go` and export the generated executable file to be able to run the SDM-EXT CLI using only the `sdm-ext` command. + +# SDM-EXT flags + +Using `sdm-ext` is basically the same as using the `sdm` command, but with some additional flags. + +You can use the following sdm-ext flags: + +- `--file value`: used to pass a JSON file; +- `-f value`: is also used to pass a JSON file; +- `--stdin`: used to get user input in JSON format; +- `-i`: is also used to get user input in JSON format; + +# Examples + +## Exclusive SDM-EXT CLI Commands + +- `sdm-ext admin servers add --file ./json-examples/server-list.json` + - It registers a list of servers in the strongDM app from a JSON file. +- `sdm-ext admin servers add --f ./json-examples/server-list.json` + - It also registers a list of servers in the strongDM app from a JSON file. +- `sdm-ext admin servers add --stdin` + - It registers a list of servers in the strongDM app from a user input in JSON format. +- `sdm-ext admin servers add -i` + - It also registers a list of servers in the strongDM app from a user input in JSON format. + +## SDM CLI commands executed with the SDM-EXT CLI + +- `sdm-ext admin servers list` + - This uses the original SDM CLI to list all servers registered in the strongDM app. diff --git a/cli/sdm-ext/adapter/adapter.go b/cli/sdm-ext/adapter/adapter.go new file mode 100644 index 0000000..056380a --- /dev/null +++ b/cli/sdm-ext/adapter/adapter.go @@ -0,0 +1,17 @@ +package adapter + +import ( + "ext/service" + "fmt" +) + +func Servers(commandName string, mappedOptions map[string]string) error { + adminService := service.NewAdminService() + + switch commandName { + case "add": + return adminService.AdminServersAdd(mappedOptions) + default: + return fmt.Errorf("unknown command name: %s", commandName) + } +} diff --git a/cli/sdm-ext/adapter/adapter_test.go b/cli/sdm-ext/adapter/adapter_test.go new file mode 100644 index 0000000..b854ee3 --- /dev/null +++ b/cli/sdm-ext/adapter/adapter_test.go @@ -0,0 +1,38 @@ +package adapter + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestServers(t *testing.T) { + tests := serversTests{} + t.Run("Test Servers when command name is add", + tests.testWhenCommandNameIsAdd) + t.Run("Test Servers when the passed commad name is unknown", + tests.testWhenThePassedCommandNameIsUnknown) +} + +type serversTests struct{} + +func (tests serversTests) testWhenCommandNameIsAdd(t *testing.T) { + commandName := "add" + mappedOptions := map[string]string{} + + actualErr := Servers(commandName, mappedOptions) + + assert.Nil(t, actualErr) +} + +func (tests serversTests) testWhenThePassedCommandNameIsUnknown(t *testing.T) { + commandName := "adx" + mappedOptions := map[string]string{} + + actualErr := Servers(commandName, mappedOptions) + + expectedErr := errors.New("unknown command name: adx") + + assert.Equal(t, expectedErr, actualErr) +} diff --git a/cli/sdm-ext/cli/app.go b/cli/sdm-ext/cli/app.go new file mode 100644 index 0000000..85124ef --- /dev/null +++ b/cli/sdm-ext/cli/app.go @@ -0,0 +1,33 @@ +package cli + +import ( + "fmt" + + "github.com/urfave/cli" +) + +// Version is set by the build system. +var Version = "" + +func NewApp() *cli.App { + app := cli.NewApp() + app.Name = "sdm-ext" + app.Usage = "sdm-ext is an extension of sdm admin" + app.Version = Version + if app.Version == "" { + app.HideVersion = true + } + + app.Commands = []cli.Command{} + app.Commands = append(app.Commands, adminCommand) + app.CommandNotFound = commandNotFound + + return app +} + +func commandNotFound(ctx *cli.Context, command string) { + sdmImpl := NewSdm() + stdout, stderr := sdmImpl.execute() + fmt.Print(stdout.String()) + fmt.Print(stderr.String()) +} diff --git a/cli/sdm-ext/cli/app_test.go b/cli/sdm-ext/cli/app_test.go new file mode 100644 index 0000000..7de059f --- /dev/null +++ b/cli/sdm-ext/cli/app_test.go @@ -0,0 +1,73 @@ +package cli + +import ( + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExecuteWithSdm(t *testing.T) { + tests := executeWithSdmTests{} + t.Run("TestExecuteWithSdm when command is successful", + tests.testWhenSdmCommandIsSuccessful) + t.Run("TestExecuteWithSdm when command fails", + tests.testWhenSdmCommandFails) +} + +type executeWithSdmTests struct{} + +func (tests executeWithSdmTests) testWhenSdmCommandIsSuccessful(t *testing.T) { + sdmImpl := NewSdmMock(runSuccessfulCommandMock) + + actualStdout, actualStderr := sdmImpl.execute() + + expectedStdout := new(strings.Builder) + expectedStdout.Write(getFilledStdout()) + expectedStderr := new(strings.Builder) + expectedStderr.Write(getEmptyStderr()) + + assert.Equal(t, expectedStdout, actualStdout) + assert.Equal(t, expectedStderr, actualStderr) +} + +func (tests executeWithSdmTests) testWhenSdmCommandFails(t *testing.T) { + sdmImpl := NewSdmMock(runFailedCommandMock) + + actualStdout, actualStderr := sdmImpl.execute() + + expectedStdout := new(strings.Builder) + expectedStdout.Write(getEmptyStdout()) + expectedStderr := new(strings.Builder) + expectedStderr.Write(getFilledStderr()) + + assert.Equal(t, expectedStdout, actualStdout) + assert.Equal(t, expectedStderr, actualStderr) +} + +func runSuccessfulCommandMock(cmd *exec.Cmd) { + cmd.Stdout.Write(getFilledStdout()) + cmd.Stderr.Write(getEmptyStderr()) +} + +func runFailedCommandMock(cmd *exec.Cmd) { + cmd.Stdout.Write(getEmptyStdout()) + cmd.Stderr.Write(getFilledStderr()) +} + +func getFilledStdout() []byte { + return []byte("stdout") +} + +func getEmptyStdout() []byte { + return []byte("") +} + +func getFilledStderr() []byte { + return []byte("stderr") +} + +func getEmptyStderr() []byte { + return []byte("") +} diff --git a/cli/sdm-ext/cli/sdm.go b/cli/sdm-ext/cli/sdm.go new file mode 100644 index 0000000..05982ab --- /dev/null +++ b/cli/sdm-ext/cli/sdm.go @@ -0,0 +1,42 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +type sdm interface { + execute() (*strings.Builder, *strings.Builder) +} + +type sdmImpl struct { + runCommand func(cmd *exec.Cmd) +} + +func NewSdm() *sdmImpl { + return &sdmImpl{runCommand} +} + +func (i sdmImpl) execute() (*strings.Builder, *strings.Builder) { + stdout := new(strings.Builder) + stderr := new(strings.Builder) + + args := os.Args[1:] + cmd := exec.Command("sdm", args...) + + cmd.Stdout = stdout + cmd.Stderr = stderr + + i.runCommand(cmd) + + return stdout, stderr +} + +func runCommand(cmd *exec.Cmd) { + err := cmd.Run() + if err != nil { + fmt.Println(err) + } +} diff --git a/cli/sdm-ext/cli/sdm_ext.go b/cli/sdm-ext/cli/sdm_ext.go new file mode 100644 index 0000000..996504d --- /dev/null +++ b/cli/sdm-ext/cli/sdm_ext.go @@ -0,0 +1,74 @@ +package cli + +import ( + "ext/adapter" + "ext/util" + + "github.com/urfave/cli" +) + +type sdmExt interface { + adminServersAddAction(ctx *cli.Context) error +} + +type sdmExtImpl struct { + getArgs func(ctx *cli.Context) cli.Args + convertStrSliceToStr func(strList []string) string + checkRegexMatch func(regexList []string, arguments string) (bool, error) + getSdmCommand func(appName, commandName, arguments string) string + commandNotFound func(ctx *cli.Context, command string) + getAppName func(ctx *cli.Context) string + getCommandName func(ctx *cli.Context) string + mapCommandArguments func(arguments []string, flags []cli.Flag) map[string]string + servers func(commandName string, mappedOptions map[string]string) error +} + +func NewSdmExt() *sdmExtImpl { + return &sdmExtImpl{ + getArgs: getArgs, + convertStrSliceToStr: util.ConvertStrSliceToStr, + checkRegexMatch: util.CheckRegexMatch, + getSdmCommand: getSdmCommand, + commandNotFound: commandNotFound, + getAppName: getAppName, + getCommandName: getCommandName, + mapCommandArguments: util.MapCommandArguments, + servers: adapter.Servers, + } +} + +func (i *sdmExtImpl) patchGetArgs(getArgs func(ctx *cli.Context) cli.Args) { + i.getArgs = getArgs +} + +func (i *sdmExtImpl) patchConvertStrSliceToStr(convertStrSliceToStr func(strList []string) string) { + i.convertStrSliceToStr = convertStrSliceToStr +} + +func (i *sdmExtImpl) patchCheckRegexMatch(checkRegexMatch func(regexList []string, arguments string) (bool, error)) { + i.checkRegexMatch = checkRegexMatch +} + +func (i *sdmExtImpl) patchGetSdmCommand(getSdmCommand func(appName, commandName, arguments string) string) { + i.getSdmCommand = getSdmCommand +} + +func (i *sdmExtImpl) patchCommandNotFound(commandNotFound func(ctx *cli.Context, command string)) { + i.commandNotFound = commandNotFound +} + +func (i *sdmExtImpl) patchGetAppName(getAppName func(ctx *cli.Context) string) { + i.getAppName = getAppName +} + +func (i *sdmExtImpl) patchGetCommandName(getCommandName func(ctx *cli.Context) string) { + i.getCommandName = getCommandName +} + +func (i *sdmExtImpl) patchMapCommandArguments(mapCommandArguments func(arguments []string, flags []cli.Flag) map[string]string) { + i.mapCommandArguments = mapCommandArguments +} + +func (i *sdmExtImpl) patchServers(servers func(commandName string, mappedOptions map[string]string) error) { + i.servers = servers +} diff --git a/cli/sdm-ext/cli/servers.go b/cli/sdm-ext/cli/servers.go new file mode 100644 index 0000000..ee1ecf6 --- /dev/null +++ b/cli/sdm-ext/cli/servers.go @@ -0,0 +1,96 @@ +package cli + +import ( + "ext/util" + "fmt" + "strings" + + "github.com/urfave/cli" +) + +const ( + FILE_REGEX_PATTERN = `^--file [\.:\/\\\w,-]+\.[A-Za-z]+$` + F_REGEX_PATTERN = `^-f [\.:\/\\\w,-]+\.[A-Za-z]+$` + STDIN_REGEX_PATTERN = `^--stdin$` + I_REGEX_PATTERN = `^-i$` +) + +var adminCommand = cli.Command{ + Name: "admin", + Usage: "administrative commands", + Subcommands: cli.Commands{adminServersCommand}, +} + +var adminServersCommand = cli.Command{ + Name: "servers", + Usage: "manage servers", + Subcommands: cli.Commands{adminServersAddCommand}, +} + +var adminServersAddCommand = cli.Command{ + Name: "add", + Aliases: []string{"create"}, + Usage: "add one or more server", + Flags: adminServersAddFlags, + Action: NewSdmExt().adminServersAddAction, + SkipFlagParsing: true, +} + +var adminServersAddFlags = []cli.Flag{ + util.GetAdminServersAddFileFlag(), + util.GetAdminServersAddStdinFlag(), +} + +func (i sdmExtImpl) adminServersAddAction(ctx *cli.Context) error { + argumentList := i.getArgs(ctx) + arguments := i.convertStrSliceToStr(argumentList) + matched, err := i.checkRegexMatch(getRegexList(), arguments) + if err != nil { + return err + } + + if !matched { + sdmCommand := i.getSdmCommand(i.getAppName(ctx), i.getCommandName(ctx), arguments) + i.commandNotFound(ctx, sdmCommand) + + return nil + } + + mappedArguments := i.mapCommandArguments(argumentList, adminServersAddFlags) + err = i.servers(ctx.Command.Name, mappedArguments) + if err != nil { + return err + } + + return nil +} + +func getArgs(ctx *cli.Context) cli.Args { + return ctx.Args() +} + +func getSdmCommand(appName, commandName, arguments string) string { + newAppName := removeSdmExt(appName) + return fmt.Sprintf("%s %s %s", newAppName, commandName, arguments) +} + +func getAppName(ctx *cli.Context) string { + return ctx.App.Name +} + +func getCommandName(ctx *cli.Context) string { + return ctx.Command.Name +} + +func removeSdmExt(appName string) string { + return strings.Replace(appName, "sdm-ext ", "", 1) +} + +func getRegexList() []string { + return []string{ + FILE_REGEX_PATTERN, + F_REGEX_PATTERN, + STDIN_REGEX_PATTERN, + I_REGEX_PATTERN, + } +} diff --git a/cli/sdm-ext/cli/servers_test.go b/cli/sdm-ext/cli/servers_test.go new file mode 100644 index 0000000..1c737b5 --- /dev/null +++ b/cli/sdm-ext/cli/servers_test.go @@ -0,0 +1,239 @@ +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestAdminServersAddAction(t *testing.T) { + tests := adminServersAddActionTests{} + t.Run("Test adminServersAddAction when the passed command is valid", + tests.testWhenThePassedCommandIsValid) + t.Run("Test adminServersAddAction when there is no arguments", + tests.testWhenThereIsNoArguments) + t.Run("Test adminServersAddAction when the passed flag does not exist in sdm-ext cli", + tests.testWhenThePassedFlagDoesNotExistInSdmExtCli) + t.Run("Test adminServersAddAction when a subcommand is passed between add command and flag", + tests.testWhenASubcommandIsPassedBetweenAddCommandAndFlag) + t.Run("Test adminServersAddAction when a subcommand is passed after flag value", + tests.testWhenTheSubcommandIsPassedAfterFlagValue) + t.Run("Test adminServersAddAction when a subcommand is passed after flag", + tests.testWhenTheSubcommandIsPassedAfterFlag) +} + +type adminServersAddActionTests struct{} + +func (tests adminServersAddActionTests) testWhenThePassedCommandIsValid(t *testing.T) { + sdmExtImpl := NewSdmExt() + + sdmExtImpl.patchGetArgs(getArgsMock) + sdmExtImpl.patchConvertStrSliceToStr(convertStrSliceToStrMock) + sdmExtImpl.patchCheckRegexMatch(checkRegexMatchWhenMatchesMock) + sdmExtImpl.patchMapCommandArguments(mapCommandArgumentsMock) + sdmExtImpl.patchServers(serversMock) + + actualErr := sdmExtImpl.adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenThereIsNoArguments(t *testing.T) { + sdmExtImpl := NewSdmExt() + + sdmExtImpl.patchGetArgs(getEmptyArgsMock) + sdmExtImpl.patchConvertStrSliceToStr(convertStrSliceToStrMock) + sdmExtImpl.patchCheckRegexMatch(checkRegexMatchWhenMatchesMock) + sdmExtImpl.patchMapCommandArguments(mapCommandArgumentsMock) + sdmExtImpl.patchServers(serversMock) + + actualErr := sdmExtImpl.adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenThePassedFlagDoesNotExistInSdmExtCli(t *testing.T) { + sdmExtImpl := NewSdmExt() + + sdmExtImpl.patchGetArgs(getArgsWithWrongFlagMock) + sdmExtImpl.patchConvertStrSliceToStr(convertStrSliceToStrWithWrongFlagMock) + sdmExtImpl.patchCheckRegexMatch(checkRegexMatchWhenDoesNotMatchesMock) + sdmExtImpl.patchGetSdmCommand(getSdmCommandMock) + sdmExtImpl.patchGetAppName(getAppNameMock) + sdmExtImpl.patchGetCommandName(getCommandNameMock) + sdmExtImpl.patchCommandNotFound(commandNotFoundMock) + + actualErr := sdmExtImpl.adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenASubcommandIsPassedBetweenAddCommandAndFlag(t *testing.T) { + sdmExtImpl := NewSdmExt() + + sdmExtImpl.patchGetArgs(getArgsWithSubcommandBetweenCommandAndFlagMock) + sdmExtImpl.patchConvertStrSliceToStr(convertStrSliceToStrWithSubcommandBetweenCommandAndFlagMock) + sdmExtImpl.patchCheckRegexMatch(checkRegexMatchWhenDoesNotMatchesMock) + sdmExtImpl.patchGetSdmCommand(getSdmCommandMock) + sdmExtImpl.patchGetAppName(getAppNameMock) + sdmExtImpl.patchGetCommandName(getCommandNameMock) + sdmExtImpl.patchCommandNotFound(commandNotFoundMock) + + actualErr := sdmExtImpl.adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenTheSubcommandIsPassedAfterFlagValue(t *testing.T) { + sdmExtImpl := NewSdmExt() + + sdmExtImpl.patchGetArgs(getArgsWithSubcommandAfterFlagValueMock) + sdmExtImpl.patchConvertStrSliceToStr(convertStrSliceToStrWithSubcommandAfterFlagValueMock) + sdmExtImpl.patchCheckRegexMatch(checkRegexMatchWhenDoesNotMatchesMock) + sdmExtImpl.patchGetSdmCommand(getSdmCommandMock) + sdmExtImpl.patchGetAppName(getAppNameMock) + sdmExtImpl.patchGetCommandName(getCommandNameMock) + sdmExtImpl.patchCommandNotFound(commandNotFoundMock) + + actualErr := sdmExtImpl.adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenTheSubcommandIsPassedAfterFlag(t *testing.T) { + sdmExtImpl := NewSdmExt() + + sdmExtImpl.patchGetArgs(getArgsWithSubcommandAfterFlagMock) + sdmExtImpl.patchConvertStrSliceToStr(convertStrSliceToStrWithSubcommandAfterFlagMock) + sdmExtImpl.patchCheckRegexMatch(checkRegexMatchWhenDoesNotMatchesMock) + sdmExtImpl.patchGetSdmCommand(getSdmCommandMock) + sdmExtImpl.patchGetAppName(getAppNameMock) + sdmExtImpl.patchGetCommandName(getCommandNameMock) + sdmExtImpl.patchCommandNotFound(commandNotFoundMock) + + actualErr := sdmExtImpl.adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func TestGetSdmCommand(t *testing.T) { + tests := getSdmCommandTests{} + t.Run("Test getSdmCommand when it is successful", + tests.testWhenItIsSucessful) +} + +type getSdmCommandTests struct{} + +func (tests getSdmCommandTests) testWhenItIsSucessful(t *testing.T) { + appName := "sdm-ext admin servers" + commandName := "add" + arguments := "--files file.json" + actualSdmCommand := getSdmCommand(appName, commandName, arguments) + + expectedSdmCommand := "admin servers add --files file.json" + + assert.Equal(t, actualSdmCommand, expectedSdmCommand) +} + +func TestRemoveSdmExt(t *testing.T) { + tests := removeSdmExtTests{} + t.Run("Test removeSdmExt when it is successful", + tests.testWhenItIsSucessful) + t.Run("Test removeSdmExt when it does not contain sdm-ext", + tests.testWhenItDoesNotContainSdmExt) +} + +type removeSdmExtTests struct{} + +func (tests removeSdmExtTests) testWhenItIsSucessful(t *testing.T) { + appName := "sdm-ext admin servers" + actualNewAppName := removeSdmExt(appName) + + expectedNewAppName := "admin servers" + + assert.Equal(t, actualNewAppName, expectedNewAppName) +} + +func (tests removeSdmExtTests) testWhenItDoesNotContainSdmExt(t *testing.T) { + appName := "sdm admin servers" + actualNewAppName := removeSdmExt(appName) + + expectedNewAppName := "sdm admin servers" + + assert.Equal(t, expectedNewAppName, actualNewAppName) +} + +func getArgsMock(ctx *cli.Context) cli.Args { + return cli.Args{"--file", "file.json"} +} + +func getArgsWithWrongFlagMock(ctx *cli.Context) cli.Args { + return cli.Args{"--files", "file.json"} +} + +func getArgsWithSubcommandBetweenCommandAndFlagMock(ctx *cli.Context) cli.Args { + return cli.Args{"rdp", "--file", "file.json"} +} + +func getArgsWithSubcommandAfterFlagValueMock(ctx *cli.Context) cli.Args { + return cli.Args{"--file", "file.json", "rdp"} +} + +func getArgsWithSubcommandAfterFlagMock(ctx *cli.Context) cli.Args { + return cli.Args{"--stdin", "rdp"} +} + +func getEmptyArgsMock(ctx *cli.Context) cli.Args { + return cli.Args{} +} + +func convertStrSliceToStrMock(strList []string) string { + return "--file file.json" +} + +func convertStrSliceToStrWithSubcommandBetweenCommandAndFlagMock(strList []string) string { + return "rdp --file file.json" +} + +func convertStrSliceToStrWithSubcommandAfterFlagValueMock(strList []string) string { + return "--file file.json rdp" +} + +func convertStrSliceToStrWithSubcommandAfterFlagMock(strList []string) string { + return "--stdin rdp" +} + +func convertStrSliceToStrWithWrongFlagMock(strList []string) string { + return "--files file.json" +} + +func checkRegexMatchWhenMatchesMock(regexList []string, arguments string) (bool, error) { + return true, nil +} + +func checkRegexMatchWhenDoesNotMatchesMock(regexList []string, arguments string) (bool, error) { + return false, nil +} + +func getSdmCommandMock(appName, commandName, arguments string) string { + return "" +} + +func getAppNameMock(ctx *cli.Context) string { + return "sdm-ext admin servers" +} + +func getCommandNameMock(ctx *cli.Context) string { + return "add" +} + +func commandNotFoundMock(ctx *cli.Context, command string) {} + +func mapCommandArgumentsMock(arguments []string, flags []cli.Flag) map[string]string { + return map[string]string{"--file": "file.json"} +} + +func serversMock(commandName string, mappedOptions map[string]string) error { + return nil +} diff --git a/cli/sdm-ext/cli/test_utils.go b/cli/sdm-ext/cli/test_utils.go new file mode 100644 index 0000000..7944e28 --- /dev/null +++ b/cli/sdm-ext/cli/test_utils.go @@ -0,0 +1,9 @@ +package cli + +import ( + "os/exec" +) + +func NewSdmMock(runCommandMock func(cmd *exec.Cmd)) *sdmImpl { + return &sdmImpl{runCommandMock} +} diff --git a/cli/sdm-ext/go.mod b/cli/sdm-ext/go.mod new file mode 100644 index 0000000..4f92ce3 --- /dev/null +++ b/cli/sdm-ext/go.mod @@ -0,0 +1,17 @@ +module ext + +go 1.17 + +require ( + github.com/stretchr/testify v1.7.1 + github.com/urfave/cli v1.22.5 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/cli/sdm-ext/go.sum b/cli/sdm-ext/go.sum new file mode 100644 index 0000000..94cc993 --- /dev/null +++ b/cli/sdm-ext/go.sum @@ -0,0 +1,21 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/sdm-ext/json-examples/server-list.json b/cli/sdm-ext/json-examples/server-list.json new file mode 100644 index 0000000..86d65e7 --- /dev/null +++ b/cli/sdm-ext/json-examples/server-list.json @@ -0,0 +1,67 @@ +[ + { + "name": "Example Raw TCP", + "hostname": "192.168.101.192", + "port": "49150", + "portOverride": "13392", + "type": "rawtcp", + "tags": { + "key1": "value1", + "key2": "value2" + } + }, + { + "name": "Example RDP", + "hostname": "192.168.101.192", + "port": "3389", + "portOverride": "13391", + "type": "rdp", + "username": "username-example", + "password": "123", + "tags": { + "key1": "value1", + "key2": "value2" + } + }, + { + "allowDeprecatedKeyExchanges": "", + "hostname": "192.168.101.192", + "name": "Example SSH", + "port": "22", + "portForwarding": "", + "publicKey": "", + "type": "ssh", + "username": "username-example", + "tags": { + "key1": "value1", + "key2": "value2" + } + }, + { + "allowDeprecatedKeyExchanges": "", + "hostname": "192.168.101.192", + "name": "Example SSH Cert", + "port": "22", + "portForwarding": "", + "type": "sshCert", + "username": "username-example", + "tags": { + "key1": "value1", + "key2": "value2" + } + }, + { + "allowDeprecatedKeyExchanges": "", + "hostname": "192.168.101.192", + "name": "Example SSH Customer Key", + "port": "22", + "portForwarding": "", + "privateKey": "-----BEGIN RSA PRIVATE KEY-----MIICXAIBAAKBgQCTUmgeG1oqYI6Gn+4ZGS642x9AvxRA5nrM3mLSg38xEuYJ8CZciBEU+wx76umeB4TPK76f8UiLdvC1hvvolix7/dCvQI7wP++LN+C9lTt8vBOlxbscbsdMIOEtbgRA4dNGDsvCUZ9eD+qsaTC697r3x6Vw2zBFUQuypK3TweZeUQIDAQABAoGAS9X30sexum7J73MUVccoze++Ps4d1urN+/feVw9pwhFAaY09shQ49tvkqUVbzQWB0ENsdVj3RxOLBWbe9fOYIczkZFFvgac4xUo2EH3403es+4dZqDw3atvh82gGlORln+iu0JFzaeR14bw1zZpcLZhUzRCMJm/3Ojj92OHCNKECQQD70uyxmwPA/2Zx8l6Qysg2IrbQTqcTk8QDdPHAsRWtfO6iE+JpSbjPZOayqGyhWOeqxicfdF49v4Fh1Tn9X9NDAkEAlcPWLuLH9ciHVpHVbOgLJdbOJOrp3lSQKfPbcSZE6+Umz72SUop+4uoiVeQUqjSYD73MNkqht5ZribCzwMyM2wJAf1rUyZ2T8G856DajDKcBSh+URRUB+iyo3PavrLviMrMUHHPh9U2LYZet9HypM5A62WsNLiMz5haL4GZtxW5I8QJADSfn37SCNkAIS32CDxAPMqK89xc/tg0doOHQDA99jG9TynnvqZqK7On+XCct/YmjNfbJQpepvxPSnITONYiCcwJBALIEgh15XC6+5pBbQPXTi9JsoLzuvNyuvRO+mAayzGnP8W9h6fhDvUx2QSoz0t2IAJdhITC99m8WDQckvO7eFK0=-----END RSA PRIVATE KEY-----", + "type": "ssh-customer-key", + "username": "username-example", + "tags": { + "key1": "value1", + "key2": "value2" + } + } +] \ No newline at end of file diff --git a/cli/sdm-ext/json-examples/server-rawtcp.json b/cli/sdm-ext/json-examples/server-rawtcp.json new file mode 100644 index 0000000..1e53e16 --- /dev/null +++ b/cli/sdm-ext/json-examples/server-rawtcp.json @@ -0,0 +1,11 @@ +{ + "name": "Example Raw TCP", + "hostname": "192.168.101.192", + "port": "49150", + "portOverride": "13392", + "type": "rawtcp", + "tags": { + "key1": "value1", + "key2": "value2" + } +} diff --git a/cli/sdm-ext/json-examples/server-rdp.json b/cli/sdm-ext/json-examples/server-rdp.json new file mode 100644 index 0000000..061f69c --- /dev/null +++ b/cli/sdm-ext/json-examples/server-rdp.json @@ -0,0 +1,13 @@ +{ + "name": "Example RDP", + "hostname": "192.168.101.192", + "port": "3389", + "portOverride": "13391", + "type": "rdp", + "username": "username-example", + "password": "123", + "tags": { + "key1": "value1", + "key2": "value2" + } +} diff --git a/cli/sdm-ext/json-examples/server-ssh-cert.json b/cli/sdm-ext/json-examples/server-ssh-cert.json new file mode 100644 index 0000000..3e66863 --- /dev/null +++ b/cli/sdm-ext/json-examples/server-ssh-cert.json @@ -0,0 +1,13 @@ +{ + "allowDeprecatedKeyExchanges": "", + "hostname": "192.168.101.192", + "name": "Example SSH Cert", + "port": "22", + "portForwarding": "", + "type": "sshCert", + "username": "username-example", + "tags": { + "key1": "value1", + "key2": "value2" + } +} \ No newline at end of file diff --git a/cli/sdm-ext/json-examples/server-ssh-customer-key.json b/cli/sdm-ext/json-examples/server-ssh-customer-key.json new file mode 100644 index 0000000..27689f8 --- /dev/null +++ b/cli/sdm-ext/json-examples/server-ssh-customer-key.json @@ -0,0 +1,14 @@ +{ + "allowDeprecatedKeyExchanges": "", + "hostname": "192.168.101.192", + "name": "Example SSH Customer Key", + "port": "22", + "portForwarding": "", + "privateKey": "-----BEGIN RSA PRIVATE KEY-----MIICXAIBAAKBgQCTUmgeG1oqYI6Gn+4ZGS642x9AvxRA5nrM3mLSg38xEuYJ8CZciBEU+wx76umeB4TPK76f8UiLdvC1hvvolix7/dCvQI7wP++LN+C9lTt8vBOlxbscbsdMIOEtbgRA4dNGDsvCUZ9eD+qsaTC697r3x6Vw2zBFUQuypK3TweZeUQIDAQABAoGAS9X30sexum7J73MUVccoze++Ps4d1urN+/feVw9pwhFAaY09shQ49tvkqUVbzQWB0ENsdVj3RxOLBWbe9fOYIczkZFFvgac4xUo2EH3403es+4dZqDw3atvh82gGlORln+iu0JFzaeR14bw1zZpcLZhUzRCMJm/3Ojj92OHCNKECQQD70uyxmwPA/2Zx8l6Qysg2IrbQTqcTk8QDdPHAsRWtfO6iE+JpSbjPZOayqGyhWOeqxicfdF49v4Fh1Tn9X9NDAkEAlcPWLuLH9ciHVpHVbOgLJdbOJOrp3lSQKfPbcSZE6+Umz72SUop+4uoiVeQUqjSYD73MNkqht5ZribCzwMyM2wJAf1rUyZ2T8G856DajDKcBSh+URRUB+iyo3PavrLviMrMUHHPh9U2LYZet9HypM5A62WsNLiMz5haL4GZtxW5I8QJADSfn37SCNkAIS32CDxAPMqK89xc/tg0doOHQDA99jG9TynnvqZqK7On+XCct/YmjNfbJQpepvxPSnITONYiCcwJBALIEgh15XC6+5pBbQPXTi9JsoLzuvNyuvRO+mAayzGnP8W9h6fhDvUx2QSoz0t2IAJdhITC99m8WDQckvO7eFK0=-----END RSA PRIVATE KEY-----", + "type": "ssh-customer-key", + "username": "username-example", + "tags": { + "key1": "value1", + "key2": "value2" + } +} diff --git a/cli/sdm-ext/json-examples/server-ssh.json b/cli/sdm-ext/json-examples/server-ssh.json new file mode 100644 index 0000000..4c7f822 --- /dev/null +++ b/cli/sdm-ext/json-examples/server-ssh.json @@ -0,0 +1,14 @@ +{ + "allowDeprecatedKeyExchanges": "", + "hostname": "192.168.101.192", + "name": "Example SSH", + "port": "22", + "portForwarding": "", + "publicKey": "", + "type": "ssh", + "username": "username-example", + "tags": { + "key1": "value1", + "key2": "value2" + } +} \ No newline at end of file diff --git a/cli/sdm-ext/sdm-ext.go b/cli/sdm-ext/sdm-ext.go new file mode 100644 index 0000000..80d8f94 --- /dev/null +++ b/cli/sdm-ext/sdm-ext.go @@ -0,0 +1,14 @@ +package main + +import ( + "ext/cli" + "fmt" + "os" +) + +func main() { + err := cli.NewApp().Run(os.Args) + if err != nil { + fmt.Println(err) + } +} diff --git a/cli/sdm-ext/service/admin_servers_add_service.go b/cli/sdm-ext/service/admin_servers_add_service.go new file mode 100644 index 0000000..a75350c --- /dev/null +++ b/cli/sdm-ext/service/admin_servers_add_service.go @@ -0,0 +1,100 @@ +package service + +import ( + "ext/util" + "fmt" + "sort" + "strings" + "unicode" +) + +func (a AdminServiceImpl) AdminServersAdd(options map[string]string) error { + flagList := []string{"--file", "-f", "--stdin", "-i"} + flag := a.findFlag(flagList, options) + + var servers []map[string]interface{} + var err error + + if flag == "--file" || flag == "-f" { + servers, err = a.extractValuesFromJson(options[flag]) + if err != nil { + return err + } + } else if flag == "--stdin" || flag == "-i" { + servers, err = util.GetUserInput() + if err != nil { + return err + } + } + + for _, server := range servers { + serverName := fmt.Sprint(server["name"]) + serverType := fmt.Sprint(server["type"]) + + _, stderr := a.execute( + fmt.Sprintf("admin servers add %s", serverType), + getOptions(server), + serverName, + ) + + if stderr.String() == "" { + fmt.Printf("Server \"%s\" successfully registered\n", serverName) + } else { + fmt.Printf("There was an error registering the \"%s\" server\n", serverName) + } + } + + return nil +} + +func getOptions(server map[string]interface{}) map[string]string { + options := map[string]string{} + + for key, value := range server { + if key != "name" && key != "type" { + key = treatKey(key) + if value != "" { + if key == "tags" { + value = treatTags(value.(map[string]interface{})) + } + if key == "private-key" { + options["--"+key+"="] = fmt.Sprintf(`"%s"`, value) + } else { + options["--"+key] = fmt.Sprint(value) + } + } + } + } + + return options +} + +func treatKey(key string) string { + for _, character := range key { + if character >= 'A' && character <= 'Z' { + key = strings.Replace(key, string(character), "-"+string(unicode.ToLower(character)), -1) + } + } + + return key +} + +func treatTags(tagsMap map[string]interface{}) string { + keys := make([]string, 0, len(tagsMap)) + for key := range tagsMap { + keys = append(keys, key) + } + sort.Strings(keys) + + tags := "" + i := 0 + for _, key := range keys { + tags += fmt.Sprintf("%s=%s", key, tagsMap[key]) + if i < len(tagsMap)-1 { + tags += "," + } + i++ + } + + return tags +} diff --git a/cli/sdm-ext/service/admin_servers_add_service_test.go b/cli/sdm-ext/service/admin_servers_add_service_test.go new file mode 100644 index 0000000..778f85b --- /dev/null +++ b/cli/sdm-ext/service/admin_servers_add_service_test.go @@ -0,0 +1,354 @@ +package service + +import ( + "errors" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAdminServersAdd(t *testing.T) { + tests := adminServersAddTests{} + t.Run("Test AdminServersAdd when it is successful", + tests.testWhenItIsSucessful) + t.Run("Test AdminServersAdd when find flag returns empty", + tests.testWhenFindFlagReturnsEmpty) + t.Run("Test AdminServersAdd when an nonexistent file is passed", + tests.testWhenAnNonexistentFileIsPassed) + t.Run("Test AdminServersAdd when an existent file with a wrong content is passed", + tests.testWhenAnExistentFileWithAWrongContentIsPassed) + t.Run("Test AdminServersAdd when one of the servers is not registered", + tests.testWhenOneOfTheServersIsNotRegistered) +} + +type adminServersAddTests struct{} + +func (tests adminServersAddTests) testWhenItIsSucessful(t *testing.T) { + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagMock) + adminServiceImpl.patchExtractValuesFromJson(extractValuesFromJsonMock) + adminServiceImpl.patchExecute(executeWithSuccessMock) + + options := map[string]string{"--file": "file.json"} + actualErr := adminServiceImpl.AdminServersAdd(options) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddTests) testWhenFindFlagReturnsEmpty(t *testing.T) { + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagReturningEmptyMock) + + options := map[string]string{"--files": "file.json"} + actualErr := adminServiceImpl.AdminServersAdd(options) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddTests) testWhenAnNonexistentFileIsPassed(t *testing.T) { + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagMock) + adminServiceImpl.patchExtractValuesFromJson(extractValuesFromJsonWithABaddlyFormattedJsonMock) + + options := map[string]string{"--file": "file.json"} + + actualErr := adminServiceImpl.AdminServersAdd(options) + + expectedErr := errors.New("invalid character '}' looking for beginning of object key string") + + assert.Equal(t, expectedErr.Error(), actualErr.Error()) +} + +func (tests adminServersAddTests) testWhenAnExistentFileWithAWrongContentIsPassed(t *testing.T) { + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagMock) + adminServiceImpl.patchExtractValuesFromJson(extractValuesFromJsonWithANonExistentJsonFileMock) + + options := map[string]string{"--file": "file.json"} + actualErr := adminServiceImpl.AdminServersAdd(options) + + expectedErr := errors.New("open file.json: no such file or directory") + + assert.Equal(t, expectedErr.Error(), actualErr.Error()) +} + +func (tests adminServersAddTests) testWhenOneOfTheServersIsNotRegistered(t *testing.T) { + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagMock) + adminServiceImpl.patchExtractValuesFromJson(extractValuesFromJsonMock) + adminServiceImpl.patchExecute(executeWithoutSuccessMock) + + options := map[string]string{"--file": "file.json"} + actualErr := adminServiceImpl.AdminServersAdd(options) + + assert.Nil(t, actualErr) +} +func TestGetOptions(t *testing.T) { + tests := getOptionsTests{} + t.Run("Test getOptions when it is successful", + tests.testWhenItIsSucessful) + t.Run("Test getOptions when the server contains tags", + tests.testWhenTheServerContainsTags) + t.Run("Test getOptions when the server contains private key", + tests.testWhenTheServerContainsPrivateKey) + t.Run("Test getOptions when the server not contain a name", + tests.testWhenTheServerNotContainAName) + t.Run("Test getOptions when the server not contain a type", + tests.testWhenTheServerNotContainAType) + t.Run("Test getOptions when the server not contain a name and type", + tests.testWhenTheServerNotContainANameAndType) + t.Run("Test getOptions when the server contain an attribute empty", + tests.testWhenTheServerContainAnAttributeEmpty) + t.Run("Test getOptions when server is empty", + tests.testWhenTheServerIsEmpty) +} + +type getOptionsTests struct{} + +func (tests getOptionsTests) testWhenItIsSucessful(t *testing.T) { + server := getServer() + actualOptionsMap := getOptions(server) + expectedOptionsMap := getOptionsMap() + assert.Equal(t, expectedOptionsMap, actualOptionsMap) +} + +func (tests getOptionsTests) testWhenTheServerContainsTags(t *testing.T) { + server := getServerWithTags() + actualOptionsMap := getOptions(server) + expectedOptionsMap := getOptionsMapWithTags() + assert.Equal(t, expectedOptionsMap, actualOptionsMap) +} + +func (tests getOptionsTests) testWhenTheServerContainsPrivateKey(t *testing.T) { + server := getServerWithPrivateKey() + actualOptionsMap := getOptions(server) + expectedOptionsMap := getOptionsMapWithPrivateKey() + assert.Equal(t, expectedOptionsMap, actualOptionsMap) +} + +func (tests getOptionsTests) testWhenTheServerNotContainAName(t *testing.T) { + server := getServerWithoutName() + actualOptionsMap := getOptions(server) + expectedOptionsMap := getOptionsMap() + assert.Equal(t, expectedOptionsMap, actualOptionsMap) +} + +func (tests getOptionsTests) testWhenTheServerNotContainAType(t *testing.T) { + server := getServerWithoutType() + actualOptionsMap := getOptions(server) + expectedOptionsMap := getOptionsMap() + assert.Equal(t, expectedOptionsMap, actualOptionsMap) +} + +func (tests getOptionsTests) testWhenTheServerNotContainANameAndType(t *testing.T) { + server := getServerWithoutNameAndType() + actualOptionsMap := getOptions(server) + expectedOptionsMap := getOptionsMap() + assert.Equal(t, expectedOptionsMap, actualOptionsMap) +} + +func (tests getOptionsTests) testWhenTheServerContainAnAttributeEmpty(t *testing.T) { + server := getServerWithAnAttributeEmpty() + actualOptionsMap := getOptions(server) + expectedOptionsMap := getOptionsMap() + assert.Equal(t, expectedOptionsMap, actualOptionsMap) +} + +func (tests getOptionsTests) testWhenTheServerIsEmpty(t *testing.T) { + server := map[string]interface{}{} + actualOptionsMap := getOptions(server) + assert.Empty(t, actualOptionsMap) +} + +func TestTreatKey(t *testing.T) { + tests := treatKeyTests{} + t.Run("Test treatKey when it is successful", + tests.testWhenItIsSucessful) + t.Run("Test treatKey when the key is not modified", + tests.testWhenTheKeyIsNotModified) +} + +type treatKeyTests struct{} + +func (tests treatKeyTests) testWhenItIsSucessful(t *testing.T) { + key := "portOverride" + actualKey := treatKey(key) + expectedKey := "port-override" + assert.Equal(t, expectedKey, actualKey) +} + +func (tests treatKeyTests) testWhenTheKeyIsNotModified(t *testing.T) { + key := "hostname" + actualKey := treatKey(key) + expectedKey := "hostname" + assert.Equal(t, expectedKey, actualKey) +} + +func TestTreatTags(t *testing.T) { + tests := treatTagsTests{} + t.Run("Test treatTags when it is successful", + tests.testWhenItIsSucessful) + t.Run("Test treatTags when tags map contain many tags", + tests.testWhenTagsMapContainManyTags) + t.Run("Test treatTags when the tags map is empty", + tests.testWhenTheTagsMapIsEmpty) +} + +type treatTagsTests struct{} + +func (tests treatTagsTests) testWhenItIsSucessful(t *testing.T) { + tagsMap := getTagsMap() + actualTags := treatTags(tagsMap) + expectedTags := "key1=value1" + assert.Equal(t, expectedTags, actualTags) +} + +func (tests treatTagsTests) testWhenTagsMapContainManyTags(t *testing.T) { + tagsMap := getTagsMapWithManyTags() + actualTags := treatTags(tagsMap) + expectedTags := "key1=value1,key2=value2" + assert.Equal(t, expectedTags, actualTags) +} + +func (tests treatTagsTests) testWhenTheTagsMapIsEmpty(t *testing.T) { + tagsMap := map[string]interface{}{} + actualTags := treatTags(tagsMap) + assert.Empty(t, actualTags) +} + +func findFlagMock(flagList []string, optionsMap map[string]string) string { + return "--file" +} + +func findFlagReturningEmptyMock(flagList []string, optionsMap map[string]string) string { + return "" +} + +func extractValuesFromJsonMock(file string) ([]map[string]interface{}, error) { + return getMapDataList(), nil +} + +func extractValuesFromJsonWithANonExistentJsonFileMock(file string) ([]map[string]interface{}, error) { + return nil, errors.New("open file.json: no such file or directory") +} + +func extractValuesFromJsonWithABaddlyFormattedJsonMock(file string) ([]map[string]interface{}, error) { + return nil, errors.New("invalid character '}' looking for beginning of object key string") +} + +func executeWithSuccessMock(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) { + stdout := new(strings.Builder) + stdout.WriteString("") + stderr := new(strings.Builder) + stderr.WriteString("") + return *stdout, *stderr +} + +func executeWithoutSuccessMock(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) { + stdout := new(strings.Builder) + stdout.WriteString("") + stderr := new(strings.Builder) + stderr.WriteString("stderr") + return *stdout, *stderr +} + +func getMapDataList() []map[string]interface{} { + return []map[string]interface{}{ + { + "hostname": "192.168.101.192", + "name": "Example Raw TCP", + "type": "rawtcp", + }, + } +} + +func getServer() map[string]interface{} { + return map[string]interface{}{ + "name": "Example Server", + "hostname": "hostname", + "type": "type", + } +} + +func getServerWithTags() map[string]interface{} { + return map[string]interface{}{ + "name": "Example Server", + "hostname": "hostname", + "type": "type", + "tags": map[string]interface{}{ + "key1": "value1", + }, + } +} + +func getServerWithPrivateKey() map[string]interface{} { + return map[string]interface{}{ + "name": "Example Server", + "hostname": "hostname", + "type": "type", + "privateKey": "private key", + } +} + +func getServerWithoutName() map[string]interface{} { + return map[string]interface{}{ + "hostname": "hostname", + "type": "type", + } +} + +func getServerWithoutType() map[string]interface{} { + return map[string]interface{}{ + "name": "Example Server", + "hostname": "hostname", + } +} + +func getServerWithoutNameAndType() map[string]interface{} { + return map[string]interface{}{ + "hostname": "hostname", + } +} + +func getServerWithAnAttributeEmpty() map[string]interface{} { + return map[string]interface{}{ + "name": "Example Server", + "hostname": "hostname", + "type": "type", + "username": "", + } +} + +func getOptionsMap() map[string]string { + return map[string]string{ + "--hostname": "hostname", + } +} + +func getOptionsMapWithTags() map[string]string { + return map[string]string{ + "--hostname": "hostname", + "--tags": "key1=value1", + } +} + +func getOptionsMapWithPrivateKey() map[string]string { + return map[string]string{ + "--hostname": "hostname", + "--private-key=": `"private key"`, + } +} + +func getTagsMap() map[string]interface{} { + return map[string]interface{}{ + "key1": "value1", + } +} + +func getTagsMapWithManyTags() map[string]interface{} { + return map[string]interface{}{ + "key1": "value1", + "key2": "value2", + } +} diff --git a/cli/sdm-ext/service/admin_service.go b/cli/sdm-ext/service/admin_service.go new file mode 100644 index 0000000..155a0cf --- /dev/null +++ b/cli/sdm-ext/service/admin_service.go @@ -0,0 +1,37 @@ +package service + +import ( + "ext/util" + "strings" +) + +type adminService interface { + execute(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) +} + +type AdminServiceImpl struct { + findFlag func(flagList []string, optionsMap map[string]string) string + extractValuesFromJson func(file string) ([]map[string]interface{}, error) + execute func(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) +} + +func NewAdminService() *AdminServiceImpl { + sdmService := NewSdmService() + return &AdminServiceImpl{ + util.FindFlag, + util.ExtractValuesFromJson, + sdmService.execute, + } +} + +func (s *AdminServiceImpl) patchFindFlag(findFlag func(flagList []string, optionsMap map[string]string) string) { + s.findFlag = findFlag +} + +func (s *AdminServiceImpl) patchExtractValuesFromJson(extractValuesFromJson func(file string) ([]map[string]interface{}, error)) { + s.extractValuesFromJson = extractValuesFromJson +} + +func (s *AdminServiceImpl) patchExecute(execute func(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder)) { + s.execute = execute +} diff --git a/cli/sdm-ext/service/admin_service_test.go b/cli/sdm-ext/service/admin_service_test.go new file mode 100644 index 0000000..5e584a3 --- /dev/null +++ b/cli/sdm-ext/service/admin_service_test.go @@ -0,0 +1,123 @@ +package service + +import ( + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExecute(t *testing.T) { + tests := executeTests{} + t.Run("Test execute when the passed command is valid", + tests.testWhenThePassedCommadIsValid) + t.Run("Test execute when command fails", + tests.testWhenThePassedCommadFails) +} + +type executeTests struct{} + +func (tests executeTests) testWhenThePassedCommadIsValid(t *testing.T) { + sdmServiceImpl := NewSdmServiceMock(runSuccessfulCommandMock) + + commands := getRawTCPServerAddCommand() + options := getOptionsToExecute() + postOptions := getRawTCPServerName() + + actualStdout, actualStderr := sdmServiceImpl.execute(commands, options, postOptions) + + expectedStdout := new(strings.Builder) + expectedStdout.Write([]byte("")) + expectedStderr := new(strings.Builder) + expectedStderr.Write([]byte("")) + + assert.Equal(t, expectedStdout, &actualStdout) + assert.Equal(t, expectedStderr, &actualStderr) +} + +func (tests executeTests) testWhenThePassedCommadFails(t *testing.T) { + sdmServiceImpl := NewSdmServiceMock(runFailedCommandMock) + + commands := getRawTCPServerAddCommand() + options := getOptionsToExecute() + postOptions := getRawTCPServerName() + + actualStdout, actualStderr := sdmServiceImpl.execute(commands, options, postOptions) + + expectedStdout := new(strings.Builder) + expectedStdout.Write([]byte("")) + expectedStderr := new(strings.Builder) + expectedStderr.Write([]byte("stderr")) + + assert.Equal(t, expectedStdout, &actualStdout) + assert.Equal(t, expectedStderr, &actualStderr) +} + +func TestOptionsToArguments(t *testing.T) { + tests := optionsToArgumentsTests{} + t.Run("Test OptionsToArguments when the conversion is successful", + tests.testWhenTheConversionIsSuccessful) + t.Run("Test OptionsToArguments when the options are empty", + tests.testWhenTheOptionsAreEmpty) +} + +type optionsToArgumentsTests struct{} + +func (tests optionsToArgumentsTests) testWhenTheConversionIsSuccessful(t *testing.T) { + optionsMap := getOptionsToExecute() + + actualOptionsList := optionsToArguments(optionsMap) + + expectedOptionsList := getOptionsListToExecute() + + assert.Equal(t, expectedOptionsList, actualOptionsList) +} + +func (tests optionsToArgumentsTests) testWhenTheOptionsAreEmpty(t *testing.T) { + optionsMap := map[string]string{} + + actualOptionsList := optionsToArguments(optionsMap) + + assert.Empty(t, actualOptionsList) +} + +func runSuccessfulCommandMock(cmd *exec.Cmd) { + cmd.Stdout.Write([]byte("")) + cmd.Stderr.Write([]byte("")) +} + +func runFailedCommandMock(cmd *exec.Cmd) { + cmd.Stdout.Write([]byte("")) + cmd.Stderr.Write([]byte("stderr")) +} + +func getOptionsToExecute() map[string]string { + return map[string]string{ + "--hostname": "192.168.101.192", + "--port": "49150", + "--port-override": "13392", + "--tags": "key1=value1,key2=value2", + } +} + +func getRawTCPServerAddCommand() string { + return "admin servers add rawtcp" +} + +func getOptionsListToExecute() []string { + return []string{ + "--hostname", + "192.168.101.192", + "--port", + "49150", + "--port-override", + "13392", + "--tags", + "key1=value1,key2=value2", + } +} + +func getRawTCPServerName() string { + return "Example Raw TCP" +} diff --git a/cli/sdm-ext/service/sdm_service.go b/cli/sdm-ext/service/sdm_service.go new file mode 100644 index 0000000..3e846ca --- /dev/null +++ b/cli/sdm-ext/service/sdm_service.go @@ -0,0 +1,68 @@ +package service + +import ( + "fmt" + "os/exec" + "sort" + "strings" +) + +type sdmService interface { + execute(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) +} + +type sdmServiceImpl struct { + runCommand func(cmd *exec.Cmd) +} + +func NewSdmService() *sdmServiceImpl { + return &sdmServiceImpl{runCommand} +} + +func (i sdmServiceImpl) execute(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) { + opts := append(optionsToArguments(options), postOptions) + commandsAndOptions := append(strings.Split(commands, " "), opts...) + + stdout := new(strings.Builder) + stderr := new(strings.Builder) + + cmd := exec.Command("sdm", commandsAndOptions...) + + cmd.Stdout = stdout + cmd.Stderr = stderr + + i.runCommand(cmd) + + return *stdout, *stderr +} + +func runCommand(cmd *exec.Cmd) { + err := cmd.Run() + if err != nil { + fmt.Println(err) + } +} + +func optionsToArguments(options map[string]string) []string { + strOptions := []string{} + + keys := make([]string, 0, len(options)) + for key := range options { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + fmt.Println(key, options[key]) + if key[len(key)-1:] == "=" { + key += options[key] + options[key] = "" + } + strOptions = append(strOptions, key) + if len(options[key]) > 0 { + strOptions = append(strOptions, options[key]) + } + } + + return strOptions +} diff --git a/cli/sdm-ext/service/test_utils.go b/cli/sdm-ext/service/test_utils.go new file mode 100644 index 0000000..5a6dbcb --- /dev/null +++ b/cli/sdm-ext/service/test_utils.go @@ -0,0 +1,7 @@ +package service + +import "os/exec" + +func NewSdmServiceMock(runCommand func(cmd *exec.Cmd)) *sdmServiceImpl { + return &sdmServiceImpl{runCommand} +} diff --git a/cli/sdm-ext/util/flag_util.go b/cli/sdm-ext/util/flag_util.go new file mode 100644 index 0000000..d35c244 --- /dev/null +++ b/cli/sdm-ext/util/flag_util.go @@ -0,0 +1,31 @@ +package util + +import ( + "github.com/urfave/cli" +) + +func GetAdminServersAddFileFlag() cli.StringFlag { + return cli.StringFlag{ + Name: "file,f", + Usage: "load from a JSON file", + } +} + +func GetAdminServersAddStdinFlag() cli.BoolFlag { + return cli.BoolFlag{ + Name: "stdin,i", + Usage: "load from stdin", + } +} + +func FindFlag(flagList []string, optionsMap map[string]string) string { + for _, flag := range flagList { + for key := range optionsMap { + if flag == key { + return flag + } + } + } + + return "" +} diff --git a/cli/sdm-ext/util/flag_util_test.go b/cli/sdm-ext/util/flag_util_test.go new file mode 100644 index 0000000..4df1fb2 --- /dev/null +++ b/cli/sdm-ext/util/flag_util_test.go @@ -0,0 +1,49 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFindFlag(t *testing.T) { + tests := findFlagTests{} + t.Run("TestFindFlag when find flag is successful", + tests.testWhenFindFlagIsSuccessful) + t.Run("TestFindFlag when the given options are not found", + tests.testWhenTheGivenOptionsAreNotFound) + t.Run("TestFindFlag when options are empty", + tests.testWhenOptionsAreEmpty) +} + +type findFlagTests struct{} + +func (tests findFlagTests) testWhenFindFlagIsSuccessful(t *testing.T) { + flagList := getFlagList() + options := map[string]string{"--file": "file.json"} + flagFound := FindFlag(flagList, options) + + expectedflag := "--file" + + assert.Equal(t, expectedflag, flagFound) +} + +func (tests findFlagTests) testWhenTheGivenOptionsAreNotFound(t *testing.T) { + flagList := getFlagList() + options := map[string]string{"--files": "file.json"} + flagFound := FindFlag(flagList, options) + + assert.Empty(t, flagFound) +} + +func (tests findFlagTests) testWhenOptionsAreEmpty(t *testing.T) { + flagList := getFlagList() + options := map[string]string{} + flagFound := FindFlag(flagList, options) + + assert.Empty(t, flagFound) +} + +func getFlagList() []string { + return []string{"--file", "-f", "--stdin", "-i"} +} diff --git a/cli/sdm-ext/util/util.go b/cli/sdm-ext/util/util.go new file mode 100644 index 0000000..0000822 --- /dev/null +++ b/cli/sdm-ext/util/util.go @@ -0,0 +1,100 @@ +package util + +import ( + "encoding/json" + "io/ioutil" + "os" + "regexp" + "strings" + + "github.com/urfave/cli" +) + +func MapCommandArguments(arguments []string, flags []cli.Flag) map[string]string { + argsMapping := map[string]string{} + + previousArgIsFlag := false + + for index, arg := range arguments { + foundFlag := false + for _, flag := range flags { + if arg[0] == '-' && FlagHasName(flag, arg) { + argsMapping[arg] = "" + foundFlag = true + break + } + } + if !foundFlag && previousArgIsFlag { + argsMapping[arguments[index-1]] = arg + } + previousArgIsFlag = foundFlag + } + + return argsMapping +} + +func FlagHasName(flag cli.Flag, argKey string) bool { + foundFlag := false + for _, flagName := range strings.Split(flag.GetName(), ",") { + if argKey == "-"+flagName || argKey == "--"+flagName { + foundFlag = true + break + } + } + return foundFlag +} + +func ExtractValuesFromJson(file string) ([]map[string]interface{}, error) { + readFile, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + data := []map[string]interface{}{} + err = json.Unmarshal([]byte(readFile), &data) + if err != nil { + return nil, err + } + + return data, nil +} + +func GetUserInput() ([]map[string]interface{}, error) { + var data []map[string]interface{} + + decoder := json.NewDecoder(os.Stdin) + if err := decoder.Decode(&data); err != nil { + return nil, err + } + + return data, nil +} + +func ConvertStrSliceToStr(strList []string) string { + strs := "" + for i, str := range strList { + strs += str + if i < len(strList)-1 { + strs += " " + } + i++ + } + + return strs +} + +func CheckRegexMatch(regexList []string, arguments string) (bool, error) { + var matched bool + var err error + for _, regex := range regexList { + matched, err = regexp.MatchString(regex, strings.TrimSpace(arguments)) + if err != nil { + return false, err + } + if matched { + break + } + } + + return matched, nil +} diff --git a/cli/sdm-ext/util/util_test.go b/cli/sdm-ext/util/util_test.go new file mode 100644 index 0000000..7962929 --- /dev/null +++ b/cli/sdm-ext/util/util_test.go @@ -0,0 +1,339 @@ +package util + +import ( + "errors" + "io/ioutil" + "os" + "regexp/syntax" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +const ( + FILE_REGEX_PATTERN = `^--file [\.\/\w,-]+\.[A-Za-z]+$` + F_REGEX_PATTERN = `^-f [\.\/\w,-]+\.[A-Za-z]+$` + STDIN_REGEX_PATTERN = `^--stdin$` + I_REGEX_PATTERN = `^-i$` + DEFAULT_JSON_FILENAME = "file.json" + DEFAULT_PATTERN_TMPFILE = "tmpfile" +) + +func TestMapCommandArguments(t *testing.T) { + tests := mapCommandArgumentsTests{} + t.Run("Test MapCommandArguments when it is successful", + tests.testWhenItIsSuccessful) + t.Run("Test MapCommandArguments when it returns a map empty", + tests.testWhenItReturnsAnEmptyMap) +} + +type mapCommandArgumentsTests struct{} + +func (tests mapCommandArgumentsTests) testWhenItIsSuccessful(t *testing.T) { + cliFlagList := getCliFlagList() + strList := getCorrectStrList() + actualArgsMapping := MapCommandArguments(strList, cliFlagList) + + expectedArgsMapping := getArgsMapping() + + assert.Equal(t, expectedArgsMapping, actualArgsMapping) +} + +func (tests mapCommandArgumentsTests) testWhenItReturnsAnEmptyMap(t *testing.T) { + cliFlagList := getCliFlagList() + strList := getIncorrectStrList() + actualArgsMapping := MapCommandArguments(strList, cliFlagList) + + assert.Empty(t, actualArgsMapping) +} + +func TestFlagHasName(t *testing.T) { + tests := flagHasNameTests{} + t.Run("Test FlagHasName when it is successful", + tests.testWhenItIsSuccessful) + t.Run("Test FlagHasName when it can not find the flag", + tests.testWhenItCanNotFindTheFlag) +} + +type flagHasNameTests struct{} + +func (tests flagHasNameTests) testWhenItIsSuccessful(t *testing.T) { + cliFlag := getCliFlag() + flag := "--file" + actualFoundFlag := FlagHasName(cliFlag, flag) + + assert.True(t, actualFoundFlag) +} + +func (tests flagHasNameTests) testWhenItCanNotFindTheFlag(t *testing.T) { + cliFlag := getCliFlag() + flag := "--files" + actualFoundFlag := FlagHasName(cliFlag, flag) + + assert.False(t, actualFoundFlag) +} + +func TestExtractValuesFromJson(t *testing.T) { + tests := extractValuesFromJsonTests{} + t.Run("Test ExtractValuesFromJson when the data is successfully extracted", + tests.testWhenTheDataIsSuccessfullyExtracted) + t.Run("Test ExtractValuesFromJson when an nonexistent file is passed", + tests.testWhenAnNonexistentFileIsPassed) + t.Run("Test ExtractValuesFromJson when an existent file with a wrong content is passed", + tests.testWhenAnExistentFileWithAWrongContentIsPassed) +} + +type extractValuesFromJsonTests struct{} + +func (tests extractValuesFromJsonTests) testWhenTheDataIsSuccessfullyExtracted(t *testing.T) { + file, _ := os.Create(DEFAULT_JSON_FILENAME) + defer os.Remove(file.Name()) + file.WriteString(getCorrectJsonFileContent()) + + actualData, actualErr := ExtractValuesFromJson(file.Name()) + + expectedData := getJsonData() + + assert.Equal(t, expectedData, actualData) + assert.Nil(t, actualErr) +} + +func (tests extractValuesFromJsonTests) testWhenAnNonexistentFileIsPassed(t *testing.T) { + actualData, actualErr := ExtractValuesFromJson(DEFAULT_JSON_FILENAME) + + expectedErr := errors.New("open file.json: no such file or directory") + + assert.Nil(t, actualData) + assert.Equal(t, expectedErr.Error(), actualErr.Error()) +} + +func (tests extractValuesFromJsonTests) testWhenAnExistentFileWithAWrongContentIsPassed(t *testing.T) { + file, _ := os.Create(DEFAULT_JSON_FILENAME) + defer os.Remove(file.Name()) + file.WriteString(getIncorrectJsonFileContent()) + + actualData, actualErr := ExtractValuesFromJson(file.Name()) + + expectedErr := errors.New("invalid character '}' looking for beginning of object key string") + + assert.Nil(t, actualData) + assert.Equal(t, expectedErr.Error(), actualErr.Error()) +} + +func TestGetUserInput(t *testing.T) { + tests := getUserInputTests{} + t.Run("Test GetUserInput when the user input is valid", + tests.testWhenTheUserInputIsValid) + t.Run("Test GetUserInput when the user input a badly formatted json", + tests.testWhenTheUserInputABadlyFormattedJson) +} + +type getUserInputTests struct{} + +func (tests getUserInputTests) testWhenTheUserInputIsValid(t *testing.T) { + tmpFile, _ := ioutil.TempFile("", DEFAULT_PATTERN_TMPFILE) + defer func() { + tmpFile.Close() + os.Remove(tmpFile.Name()) + }() + tmpFile.WriteString(getCorrectJsonFileContent()) + tmpFile.Seek(0, 0) + + oldStdin := os.Stdin + defer func() { + os.Stdin = oldStdin + }() + os.Stdin = tmpFile + + actualData, actualErr := GetUserInput() + + expectedData := getJsonData() + + assert.Equal(t, expectedData, actualData) + assert.Nil(t, actualErr) + + tmpFile.Close() +} + +func (tests getUserInputTests) testWhenTheUserInputABadlyFormattedJson(t *testing.T) { + tempFile, err := ioutil.TempFile("", "tempfile") + assert.Nil(t, err) + defer os.Remove(tempFile.Name()) + + _, err = tempFile.WriteString(getIncorrectJsonFileContent()) + assert.Nil(t, err) + + _, err = tempFile.Seek(0, 0) + assert.Nil(t, err) + + oldStdin := os.Stdin + defer func() { + os.Stdin = oldStdin + }() + os.Stdin = tempFile + + actualData, actualErr := GetUserInput() + + expectedErr := errors.New("invalid character '}' looking for beginning of object key string") + + assert.Nil(t, actualData) + assert.Equal(t, expectedErr.Error(), actualErr.Error()) + + err = tempFile.Close() + assert.Nil(t, err) +} + +func TestConvertStrSliceToStr(t *testing.T) { + tests := convertStrSliceToStrTests{} + t.Run("Test ConvertStrSliceToStr when it is successful", + tests.testWhenItIsSuccessful) + t.Run("Test ConvertStrSliceToStr when the given slice is empty", + tests.testWhenTheGivenSliceIsEmpty) +} + +type convertStrSliceToStrTests struct{} + +func (tests convertStrSliceToStrTests) testWhenItIsSuccessful(t *testing.T) { + strList := getCorrectStrList() + actualStr := ConvertStrSliceToStr(strList) + + expectedStr := "--file file.json" + + assert.Equal(t, expectedStr, actualStr) +} + +func (tests convertStrSliceToStrTests) testWhenTheGivenSliceIsEmpty(t *testing.T) { + strList := []string{} + actualStr := ConvertStrSliceToStr(strList) + + assert.Empty(t, actualStr) +} + +func TestCheckRegexMatch(t *testing.T) { + tests := checkRegexMatchTests{} + t.Run("Test CheckRegexMatch when it is successful", + tests.testWhenItIsSuccessful) + t.Run("Test CheckRegexMatch when it does not matches", + tests.testWhenItDoesNotMatches) + t.Run("Test CheckRegexMatch when an invalid regex is passed", + tests.testWhenAnInvalidRegexIsPassed) +} + +type checkRegexMatchTests struct{} + +func (tests checkRegexMatchTests) testWhenItIsSuccessful(t *testing.T) { + regexList := getCorrectRegexList() + arguments := getCorrectArguments() + actualMatched, actualErr := CheckRegexMatch(regexList, arguments) + + assert.True(t, actualMatched) + assert.Nil(t, actualErr) +} + +func (tests checkRegexMatchTests) testWhenItDoesNotMatches(t *testing.T) { + regexList := getCorrectRegexList() + arguments := getIncorrectArguments() + actualMatched, actualErr := CheckRegexMatch(regexList, arguments) + + assert.False(t, actualMatched) + assert.Nil(t, actualErr) +} + +func (tests checkRegexMatchTests) testWhenAnInvalidRegexIsPassed(t *testing.T) { + regexList := getIncorrectRegexList() + arguments := getIncorrectArguments() + actualMatched, actualErr := CheckRegexMatch(regexList, arguments) + + expectedErr := syntax.Error{Code: "invalid escape sequence", Expr: "\\K"} + + assert.False(t, actualMatched) + assert.Equal(t, &expectedErr, actualErr) +} + +func getCliFlag() cli.Flag { + return cli.StringFlag{ + Name: "file,f", + Usage: "load from a JSON file", + } +} + +func getCliFlagList() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: "file,f", + Usage: "load from a JSON file", + }, + cli.BoolFlag{ + Name: "stdin,i", + Usage: "load from stdin", + }, + } +} + +func getCorrectStrList() []string { + return []string{"--file", "file.json"} +} + +func getIncorrectStrList() []string { + return []string{"--files", "file.json"} +} + +func getCorrectJsonFileContent() string { + return ` + [ + { + "name": "Example Raw TCP", + "hostname": "192.168.101.192", + "type": "rawtcp" + } + ] + ` +} + +func getIncorrectJsonFileContent() string { + return ` + [ + { + "name": "Example Raw TCP", + "hostname": "192.168.101.192", + "type": "rawtcp", + } + ] + ` +} + +func getJsonData() []map[string]interface{} { + return []map[string]interface{}{ + { + "hostname": "192.168.101.192", + "name": "Example Raw TCP", + "type": "rawtcp", + }, + } +} + +func getArgsMapping() map[string]string { + return map[string]string{"--file": "file.json"} +} + +func getCorrectRegexList() []string { + return []string{ + FILE_REGEX_PATTERN, + F_REGEX_PATTERN, + STDIN_REGEX_PATTERN, + I_REGEX_PATTERN, + } +} + +func getIncorrectRegexList() []string { + return []string{`^--file \K[\.\/\w,-]+\.[A-Za-z]+$`} +} + +func getCorrectArguments() string { + return "--file file.json" +} + +func getIncorrectArguments() string { + return "--files file.json" +} diff --git a/docs/.empty b/docs/.empty deleted file mode 100644 index 8b13789..0000000 --- a/docs/.empty +++ /dev/null @@ -1 +0,0 @@ - diff --git a/external/README.md b/external/README.md new file mode 100644 index 0000000..3a52990 --- /dev/null +++ b/external/README.md @@ -0,0 +1,9 @@ +# Contributed Automations + +Our users are resourceful and ambitious. The following is a list of automations and tools for interacting with strongDM shared by our customers that we think you might find useful as well. If you have anything that you would like to contribute, don't hesitate to [reach out](mailto:support@strongdm.com?Subject=I%20am%20an%20intelligent%20and%20fantastic%20strongDM%20user%20who%20wants%20to%20contribute%20an%20automation). + +- [Roblox contrib](https://github.com/Roblox/strongdm-contrib). Contribution from [Roblox](https://www.roblox.com/) that shows how to use StrongDM in a Nomad environment. The project includes sample Nomad Jobs specifications and a sample Python utility for automation. +- [Bash script for SDM autocomplete](https://gist.github.com/yolabingo/7bc825d8ecfb455a56a2773ade5e8858). With this script, you can use SDM + Space, SSH + Space + Tab + Tab, and it pulls the hostnames from the SDM admin. Contributed by [dotCMS](https://dotcms.com/). +- [Chef cookbook for automated SSH and gateway provisioning](https://supermarket.chef.io/cookbooks/strongdm), contributed by Applause +- [Importing strongDM logs into an ELK stack](https://borgified.github.io/posts/2018-01-01-sdm-in-elk.html), contributed by an anonymous strongDM user +- [GitHub Action to configure kubectl with strongDM](https://github.com/marketplace/actions/configure-kubectl-with-strongdm), contributed by [Software.com](https://www.software.com/) diff --git a/issue-tracking/jira/.gitignore b/issue-tracking/jira/.gitignore new file mode 100644 index 0000000..fde22f3 --- /dev/null +++ b/issue-tracking/jira/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +set-env.sh +venv/ diff --git a/issue-tracking/jira/Dockerfile b/issue-tracking/jira/Dockerfile new file mode 100644 index 0000000..a4558c6 --- /dev/null +++ b/issue-tracking/jira/Dockerfile @@ -0,0 +1,23 @@ +FROM continuumio/miniconda3 + +ENV JIRA_DIR=/jira + +RUN mkdir -p $JIRA_DIR +WORKDIR $JIRA_DIR + +COPY requirements.txt ./requirements.txt +RUN pip install \ + --no-cache-dir \ + --disable-pip-version-check \ + -r requirements.txt + +COPY access_manager.py . +COPY config_template.py . +COPY exceptions.py . +COPY sdm_service.py . +COPY server.py . + +COPY start.sh /start.sh +RUN chmod +x /start.sh + +ENTRYPOINT ["/start.sh"] diff --git a/issue-tracking/jira/README.md b/issue-tracking/jira/README.md new file mode 100644 index 0000000..1b38961 --- /dev/null +++ b/issue-tracking/jira/README.md @@ -0,0 +1,50 @@ +# Jira Webhook (*proof of concept*) + +Grant temporary access to SDM resources via Jira using webhooks. + +## How it works + +The user interested in getting access to a resource, needs to create an issue with the description: `access to resource-name`. The webhook grants access if the issue is assigned to a valid SDM_ADMIN and closed. + +## Configure + +1. Generate API Token [here](https://id.atlassian.com/manage-profile/security/api-tokens) +2. Enable organization visibility for users emails [here](https://id.atlassian.com/manage-profile/profile-and-visibility) - you need to do this for all users, due to [GDPR](https://community.developer.atlassian.com/t/guidelines-for-requesting-access-to-email-address/27603). +3. Configure webhook: https://``.atlassian.net/plugins/servlet/webhooks + - The script uses [ngrok](https://ngrok.com/) for creating a valid HTTPS endpoint. If you need to deploy in prod you'll need proper certs - cannot use self-signed. + - If you use ngrok, copy the Tunnel URL you get when running the server. For example: `https://ac3b-80-31-185-170.ngrok.io/weebhook`. IMPORTANT: Use HTTPS + +## Run + +### Running locally + +You need to define the following variables: +``` +JIRA_USER= # Usually an email address +JIRA_TOKEN= +JIRA_BASE_URL=https://.atlassian.net +SDM_API_ACCESS_KEY= +SDM_API_SECRET_KEY= +SDM_ADMINS= # A list of email addresses separated by spaces +``` + +After installing all the dependencies listed in [requirements.txt](requirements.txt), start the server: +``` +python3 server.py +``` + +### Running with Docker + +Create a file called `env-file` using [env-file.example](env-file.example) as an example and configure the environment variables necessary for the proper execution of Jira Webhook. + +After that, to create and run a container with the application, run: + +``` +docker-compose up --build +``` + +## Demo video + +Here is a video show how to setup and use the tool. + +https://user-images.githubusercontent.com/82273420/167864566-997049d9-7e24-4d0f-9c6c-7a8e4a0dc841.mp4 diff --git a/issue-tracking/jira/access_manager.py b/issue-tracking/jira/access_manager.py new file mode 100644 index 0000000..3ea2b0b --- /dev/null +++ b/issue-tracking/jira/access_manager.py @@ -0,0 +1,54 @@ +import datetime +from exceptions import NotFoundException, PermissionDeniedException +import logging +import re +import requests + +from config_template import config +from sdm_service import create_sdm_service + +ACCESS_REGEX = r"^access to (.+)$" + +class AccessManager: + def __init__(self): + self.__jira_api_url = f"{config['JIRA_BASE_URL']}/rest/api/2" + self.__http_auth = (config['JIRA_USER'], config['JIRA_TOKEN']) + self.__sdm_service = create_sdm_service() + + def process_issue(self, issue_id): + resp = requests.get( + f"{self.__jira_api_url}/issue/{issue_id}", + auth = self.__http_auth + ) + fields = resp.json()['fields'] + description = fields['description'] + if not re.compile(ACCESS_REGEX).match(description): + return + creator_account_id = fields['creator']['accountId'] + creator_email = self.__get_account_email(creator_account_id) + assignee_email = self.__get_assignee_email(fields['assignee']) + if assignee_email not in config['SDM_ADMINS']: + raise PermissionDeniedException(f"{assignee_email} cannot approve access requests, not an SDM_ADMIN") + resource_name = re.sub(ACCESS_REGEX, "\\1", description) + self.__grant_temporal_access(creator_email, resource_name) + + def __get_account_email(self, account_id): + resp = requests.get( + f"{self.__jira_api_url}/user?accountId={account_id}", + auth = self.__http_auth + ) + data = resp.json() + if 'emailAddress' not in data: + raise NotFoundException("Creator email not available, please check your profile and visibility settings") + return data['emailAddress'] + + def __get_assignee_email(self, assignee): + if 'emailAddress' not in assignee: + raise NotFoundException("Assignee email not available, please check your profile and visibility settings") + return assignee['emailAddress'] + + def __grant_temporal_access(self, account_email, resource_name): + grant_start_from = datetime.datetime.now(datetime.timezone.utc) + grant_valid_until = grant_start_from + datetime.timedelta(minutes = config['GRANT_TIMEOUT']) + self.__sdm_service.grant_temporal_access(account_email, resource_name, grant_start_from, grant_valid_until) + diff --git a/issue-tracking/jira/config_template.py b/issue-tracking/jira/config_template.py new file mode 100644 index 0000000..70c20ad --- /dev/null +++ b/issue-tracking/jira/config_template.py @@ -0,0 +1,11 @@ +import os + +config = { + 'JIRA_USER': os.getenv('JIRA_USER'), + 'JIRA_TOKEN': os.getenv('JIRA_TOKEN'), + 'JIRA_BASE_URL': os.getenv('JIRA_BASE_URL'), + 'SDM_API_ACCESS_KEY': os.getenv('SDM_API_ACCESS_KEY'), + 'SDM_API_SECRET_KEY': os.getenv('SDM_API_SECRET_KEY'), + 'SDM_ADMINS': os.getenv("SDM_ADMINS").split(" "), + 'GRANT_TIMEOUT': int(os.getenv('GRANT_TIMEOUT', '60')), +} \ No newline at end of file diff --git a/issue-tracking/jira/docker-compose.yml b/issue-tracking/jira/docker-compose.yml new file mode 100644 index 0000000..a520a75 --- /dev/null +++ b/issue-tracking/jira/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3.9" +services: + sdm-jira: + build: + dockerfile: ./Dockerfile + context: . + env_file: + - env-file diff --git a/issue-tracking/jira/env-file.example b/issue-tracking/jira/env-file.example new file mode 100644 index 0000000..0ff964d --- /dev/null +++ b/issue-tracking/jira/env-file.example @@ -0,0 +1,9 @@ +JIRA_USER= # Usually an email address +JIRA_TOKEN= # Token generated in the first step of the configuration section +JIRA_BASE_URL=https://.atlassian.net # URL defined during Jira Software registration + +SDM_API_ACCESS_KEY= +SDM_API_SECRET_KEY= +SDM_ADMINS= # A list of email addresses separated by spaces + +NGROK_AUTH_TOKEN= # Token generated when creating an account on the ngrok.com website diff --git a/issue-tracking/jira/exceptions.py b/issue-tracking/jira/exceptions.py new file mode 100644 index 0000000..aac71d0 --- /dev/null +++ b/issue-tracking/jira/exceptions.py @@ -0,0 +1,8 @@ +class NotFoundException(Exception): + pass + +class RoleNotAllowedException(Exception): + pass + +class PermissionDeniedException(Exception): + pass diff --git a/issue-tracking/jira/requirements.txt b/issue-tracking/jira/requirements.txt new file mode 100644 index 0000000..ad00b94 --- /dev/null +++ b/issue-tracking/jira/requirements.txt @@ -0,0 +1,4 @@ +pyngrok +flask +requests +strongdm diff --git a/issue-tracking/jira/sdm_service.py b/issue-tracking/jira/sdm_service.py new file mode 100644 index 0000000..24e41f4 --- /dev/null +++ b/issue-tracking/jira/sdm_service.py @@ -0,0 +1,59 @@ +import logging +import strongdm + +from config_template import config +from exceptions import NotFoundException + +def create_sdm_service(): + client = strongdm.Client(config['SDM_API_ACCESS_KEY'], config['SDM_API_SECRET_KEY']) + return SdmService(client) + +# Class copied from https://github.com/strongdm/accessbot/blob/main/plugins/sdm/lib/sdm_service.py +class SdmService: + def __init__(self, client): + self.__client = client + + def grant_temporal_access(self, account_email, resource_name, start_from, valid_until): + """ + Grant temporary access to a SDM resource for an account + """ + try: + logging.debug( + "##SDM## SdmService.grant_temporary_access resource_id: %s account_id: %s start_from: %s valid_until: %s", + resource_name, account_email, str(start_from), str(valid_until) + ) + sdm_grant = strongdm.AccountGrant( + resource_id = self.get_resource_by_name(resource_name).id, + account_id = self.get_account_by_email(account_email).id, + start_from = start_from, + valid_until = valid_until + ) + self.__client.account_grants.create(sdm_grant) + except Exception as ex: + raise Exception("Grant failed: " + str(ex)) from ex + + def get_resource_by_name(self, name): + """ + Return a SDM resouce by name + """ + try: + logging.debug("##SDM## SdmService.get_resource_by_name name: %s", name) + sdm_resources = list(self.__client.resources.list('name:"{}"'.format(name))) + except Exception as ex: + raise Exception("List resources failed: " + str(ex)) from ex + if len(sdm_resources) == 0: + raise NotFoundException("Sorry, cannot find that resource!") + return sdm_resources[0] + + def get_account_by_email(self, email): + """ + Return a SDM account by email + """ + try: + logging.debug("##SDM## SdmService.get_account_by_email email: %s", email) + sdm_accounts = list(self.__client.accounts.list('email:{}'.format(email))) + except Exception as ex: + raise Exception("List accounts failed: " + str(ex)) from ex + if len(sdm_accounts) == 0: + raise Exception("Sorry, cannot find your account!") + return sdm_accounts[0] diff --git a/issue-tracking/jira/server.py b/issue-tracking/jira/server.py new file mode 100644 index 0000000..72265dd --- /dev/null +++ b/issue-tracking/jira/server.py @@ -0,0 +1,36 @@ +import logging +import os +from flask import Flask, request, Response + +from access_manager import AccessManager + +app = Flask(__name__) + +@app.route('/webhook', methods=['POST']) +def bot(): + data = request.get_json() + if not _is_closed_issue(data): + logging.info("Ignoring issue, since it's comming from a closed status") + return Response(status=200) + issue_id = data['issue']['id'] + try: + logging.info("Processing issue %s", issue_id) + AccessManager().process_issue(issue_id) + return Response(status=200) + except Exception: + logging.error("Error processing issue %s", issue_id, exc_info = True) + return Response(status=500) + +def _is_closed_issue(data): + return data['issue']['fields']['status']['name'] == "Closed" + +def start_ngrok(): + from pyngrok import ngrok + + url = ngrok.connect(5000).public_url + print(' * Tunnel URL:', url) + +if __name__ == '__main__': + if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': + start_ngrok() + app.run(debug=True) diff --git a/issue-tracking/jira/start.sh b/issue-tracking/jira/start.sh new file mode 100644 index 0000000..f54342c --- /dev/null +++ b/issue-tracking/jira/start.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +ngrok authtoken $NGROK_AUTH_TOKEN +python server.py diff --git a/logging/kubernetes/impersonation/README.md b/logging/kubernetes/impersonation/README.md new file mode 100644 index 0000000..c2aa768 --- /dev/null +++ b/logging/kubernetes/impersonation/README.md @@ -0,0 +1,21 @@ +### Important + +This solution is designed to work with multiple access roles. If your strongDM organization has not yet been transitioned to this system, please refer to this [archived folder](https://github.com/strongdm/contrib/tree/main/archive/logging/kubernetes/impersonation) to use the older code for Composite Roles. + +# Convert SDM roles for Kubernetes User Impersonation + +This folder contains a Python script that helps with automation of roles for Kubernetes user impersonation. + +## Requirements +* Python3 +* A [strongDM API key pair](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) with the following permissions: + * Roles: List, Create + * Grants: Write +* An existing Role(s) that you wish to convert and/or add to a Composite Role for k8s user impersonation. + + +## Usage +* Run `pip install -r requirements.txt` +* Run the script with at two role names. This will copy the users in the "r" role to the "m" role one role name: `k8s_auto.py -r CurrentRole`. For example, `k8s_auto.py -r CurrentRoleName -m MapRoleName`. +* When users from the role you passed into the script access a k8s cluster via SDM, the client will pass the user's name and both Role names to the cluster for authorization and auditing. + diff --git a/logging/kubernetes/impersonation/k8s_auto_multi-role.py b/logging/kubernetes/impersonation/k8s_auto_multi-role.py new file mode 100644 index 0000000..573b1be --- /dev/null +++ b/logging/kubernetes/impersonation/k8s_auto_multi-role.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import strongdm, time, os, argparse, sys, logging + +access_key = os.environ['SDM_API_ACCESS_KEY'] +secret_key = os.environ['SDM_API_SECRET_KEY'] + +# Change INFO to ERROR if you don't care about success messages +logging.basicConfig(level = logging.INFO) + +# In this multi-role version, both "r" and "m" keys/values are required +# Add tags to make params required in argparse? + +parser = argparse.ArgumentParser() +parser.add_argument("-r", "--role", help="SDM role name to convert",required=True) +parser.add_argument("-m", "--map", help="SDM mapping role to add",required=True) +args = parser.parse_args() + +client = strongdm.Client(access_key, secret_key) + +def transfer_users(role, id): + # get the role in the arg + role_response = list(client.roles.list('name:\"{role_name}\"'.format(role_name=role))) + if len(role_response) == 0: + print("Could not find Role: " + args.role) + exit(0) + # loop through the list + for r in role_response: + # get the account attachments + attachments = list(client.account_attachments.list('role_id:{id}'.format(id=r.id))) + # loop through that list + for a in attachments: + # create a new account attachment using: user, and "m" role from arg + k8s_attachment = strongdm.AccountAttachment(account_id=a.account_id, role_id=id) + try: + respGrant = client.account_attachments.create(k8s_attachment) + logging.info('Role assignment to' + a.account_id + ' succeeded.') + except Exception as ex: + logging.info('Role assignment to ' + a.account_id + ' failed.') + logging.error("Role assignment role assignment because of error: " + str(ex)) + +def get_k8s_role(role): + k8s_role_id = "" + role_response = list(client.roles.list('name:\"{role_name}\"'.format(role_name=role))) + for r in role_response: + k8s_role_id = r.id + return k8s_role_id + +def main(): + my_id = get_k8s_role(args.map) + try: + transfer_users(args.role, my_id) + logging.info("Script execution is complete.") + except Exception as ex: + logging.error("Script failed to complete, because of error: " + str(ex)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/logging/kubernetes/impersonation/requirements.txt b/logging/kubernetes/impersonation/requirements.txt new file mode 100644 index 0000000..9e6ee6e --- /dev/null +++ b/logging/kubernetes/impersonation/requirements.txt @@ -0,0 +1,2 @@ +strongdm +argparse \ No newline at end of file diff --git a/logging/lifecycle-s3/README.md b/logging/lifecycle-s3/README.md new file mode 100644 index 0000000..99cd03f --- /dev/null +++ b/logging/lifecycle-s3/README.md @@ -0,0 +1,129 @@ +# Export Logs with Lifecycle Hooks + +In this tutorial, you'll learn how to configure lifecycle hooks to export logs from a self-terminating instance to an AWS S3 bucket. Lifecycle hooks tell Amazon EC2 Auto Scaling what action to take when it launches or terminates instances. In this case, we will be configuring lifecycle hooks to allow the instance to export logs before being powered off or destroyed. + +If you don't use S3 buckets, you can easily use the principles explained in this tutorial to export logs to other storage destinations. + +--- + +``This technique is unsupported and is only provided as a potentially useful idea.`` + +--- + +## Prerequisites + +- You must have an AWS account and be familiar with the AWS Console. +- All resources must have AWS CLI installed and keys configured. +- All have to be able to ping AWS resources to access and back them up. + +## Steps + +### Create a Simple Notification System (SNS) topic + +1. In the Services menu, click **Simple Notification System**. +2. Click **Topics**. +3. Click **Create Topic**. +4. Select **Standard** type. +5. Enter a name. +6. Click **Create**. +7. In the topics menu, save the Amazon Resource Name (ARN), the unique identifier of the resource, for later use. Example: `arn:aws:sns:{{region}}:XXXXXXXXXXXX:{{name}}` + +### Create a policy + +1. In the Services menu, click **IAM**. +2. Click **Policies**. +3. Click **Create Policy**. +4. Click the **JSON Editor**. +5. Copy the content of [create_policy.json](./create_policy.json) file and paste it into the JSON Editor. +6. Click **Review**. +7. Enter a name for the policy. +8. Click **Create Policy**. + +### Create an EC2 role + +1. In the IAM menu, click **Roles**. +2. Click **Create Role**. +3. Click on **EC2 Service**. +4. Attach the policy created in [Create a Policy](#create-a-policy). +5. Click **Tags**. +6. Click **Review**. +7. Enter a name for the role. +8. Click **Create Role**. + +### Create a Lambda role + +1. In the IAM menu, click **Roles**. +2. lick **Create Role**. +3. Click on **Lambda Service**. +4. Attach the policy created in [Create a Policy](#create-a-policy). +5. Click **Tags**. +6. Click **Review**. +7. Enter a name for the role. +8. Click **Create Role**. + +### Create a lifecycle hook + +1. In the Services menu, click **EC2**. +2. Click **Auto-Scaling Groups**. +3. Click on the Auto-Scaling Group you want to edit. +4. Click **Instance Management**. +5. Click **Create Lifecycle Hook** and set the following: + 1. **Name**: Enter a name for the lifecycle hook. + 2. **Lifecycle transition**: Set "Instance terminate." + 3. **Heartbeat timeout**: Set "600." + 4. **Default result**: Set to "Continue." +6. Click **Create**. + +### Create an S3 bucket + +1. In the Services menu, click **S3**. +2. Click **Create Bucket**. +3. Enter **Bucket Name**. +4. Select the **Region** for the bucket. +5. Click **Create Bucket**. + +### Create a Systems Manager (SSM) document + +1. In the Services menu, click **System Manager**. +2. Click **Documents**. +3. Click **Owned By Me**. +4. Click **Create Command**. +5. Enter a **Name**. +6. Copy the content of [create_ssm_document.json](./create_ssm_document.json) file and paste it. +7. Click **Create document**. + +### Create a Lambda Function + +1. In the Services menu, click **Lambda**. +2. Click **Create function**. +3. Select **author from scratch** and then set the following: + 1. **Name**: Enter a name. + 2. **Runtime**: Set "Node.js 10.x." + 3. **Role**: Set "use existing role" and select the Lambda role created in [Create a Lambda Role](#create-a-lambda-role). +4. Click **Create function**. +5. Copy the content of [create_lambda_function.js](./create_lambda_function/index.js) file (and replace everything in the window). +6. Go down to **Environment Variables**. +7. Add the variables and the values for your variables using the content of [s3_vars.env](./create_lambda_function/s3_vars.env) as an example. +8. Click **Deploy**. + +### Create a CloudWatch trigger + +1. In Services menu, click **CloudWatch**. +2. Click **Rules**. +3. Click **Create Rule** and set the following: + 1. **Service Name**: Set "Auto-Scaling." + 2. **Event Type**: Set "Instance Launch and Terminate." + 3. **Instance Event**: Edit this to be "specific instance event(s)" and set "EC2 Instance-terminate Lifecycle Action." + 4. **Group**: Edit the group to be '"specific group name(s)" and set it to the auto-scaling group you want to monitor. +4. Click **Add target**. +5. Select the Lambda function you created in [Create a Lambda Function](#create-a-lambda-function). +6. Click **Configure details**. +7. Enter a **Name**. +8. Click **Create rule**. + +### Test that logs are exported to S3 + +1. Go back to **EC2**. +2. Change size of your auto-scaling group in order for it to terminate an instance. **Note: Manual termination doesn't trigger the lifecycle event**. +3. Wait for the instance to terminate and then check S3 for logs. +4. For troubleshooting, you should be able to check the logs in CloudWatch for the Lambda Function and System Manager. diff --git a/logging/lifecycle-s3/create_lambda_function/index.js b/logging/lifecycle-s3/create_lambda_function/index.js new file mode 100644 index 0000000..a3cdb41 --- /dev/null +++ b/logging/lifecycle-s3/create_lambda_function/index.js @@ -0,0 +1,49 @@ +const AWS = require('aws-sdk'); +const ssm = new AWS.SSM(); +const SSM_DOCUMENT_NAME = process.env.SSM_DOCUMENT_NAME; +const S3_BUCKET = process.env.S3_BUCKET; +const SNS_TARGET = process.env.SNS_TARGET; +const BACKUP_DIRECTORY = process.env.BACKUP_DIRECTORY; +const sendCommand = (instanceId, autoScalingGroup, lifecycleHook) => { + var params = { + DocumentName: SSM_DOCUMENT_NAME, + InstanceIds: [instanceId], + Parameters: { + 'ASGNAME': [autoScalingGroup], + 'LIFECYCLEHOOKNAME': [lifecycleHook], + 'BACKUPDIRECTORY': [BACKUP_DIRECTORY], + 'S3BUCKET': [S3_BUCKET], + 'SNSTARGET': [SNS_TARGET], + }, + TimeoutSeconds: 300 + }; + return ssm.sendCommand(params).promise(); +} +exports.handler = async (event) => { + console.log('Received event ', JSON.stringify(event)); + try { + const records = event.Records; + if (!records || !records.length) { + return; + } + for (const record of records) { + if (record.EventSource !== 'aws:sns') { + console.log('Record is not processed because record.EventSource is not aws:sns'); + continue; + } + const message = JSON.parse(record.Sns.Message); + if (message.LifecycleTransition !== 'autoscaling:EC2_INSTANCE_TERMINATING') { + console.log('Record is not processed because message.LifecycleTransition is not autoscaling:EC2_INSTANCE_TERMINATING'); + continue; + } + console.log("processing autoscaling event"); + const autoScalingGroup = message.AutoScalingGroupName; + const instanceId = message.EC2InstanceId; + const lifecycleHook = message.LifecycleHookName; + await sendCommand(instanceId, autoScalingGroup, lifecycleHook); + console.log('sent command'); + } + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/logging/lifecycle-s3/create_lambda_function/s3_vars.env b/logging/lifecycle-s3/create_lambda_function/s3_vars.env new file mode 100644 index 0000000..09bd849 --- /dev/null +++ b/logging/lifecycle-s3/create_lambda_function/s3_vars.env @@ -0,0 +1,10 @@ +**LAMBDA FUNCTION VARS ** +** EXAMPLES OF ENTRIES ** +** CAN BE ANY DIRECTORY THAT YOU DESIGNATE ** +BACKUP_DIRECTORY = '/var/log' +** NAME OF THE BUCKET YOU WANT TO USE ** +S3_BUCKET = 'aws-export-bucket' +** USE THE SNS ARN from earlier ** +SNS_TARGET = 'arn:aws:sns:apxxxxxxxxxxxxxxx' +** NAME OF SYSTEM MANAGER DOCUMENT ** +SSM_DOCUMENT_NAME = 'export-document-name' \ No newline at end of file diff --git a/logging/lifecycle-s3/create_policy.json b/logging/lifecycle-s3/create_policy.json new file mode 100644 index 0000000..ca077b6 --- /dev/null +++ b/logging/lifecycle-s3/create_policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "autoscaling:CompleteLifecycleAction", + "sns:Publish" + ], + "Effect": "Allow", + "Resource": "*" + } + ] +} \ No newline at end of file diff --git a/logging/lifecycle-s3/create_ssm_document.json b/logging/lifecycle-s3/create_ssm_document.json new file mode 100644 index 0000000..f85086f --- /dev/null +++ b/logging/lifecycle-s3/create_ssm_document.json @@ -0,0 +1,56 @@ +{ + "schemaVersion": "1.2", + "description": "Back up logs to S3", + "parameters": { + "ASGNAME": { + "type": "String", + "description": "Auto Scaling group name" + }, + "LIFECYCLEHOOKNAME": { + "type": "String", + "description": "LIFECYCLEHOOK name" + }, + "BACKUPDIRECTORY": { + "type": "String", + "description": "BACKUPDIRECTORY location in server" + }, + "S3BUCKET": { + "type": "String", + "description": "S3BUCKET backup logs" + }, + "SNSTARGET": { + "type": "String", + "description": "SNSTARGET" + } + }, + "runtimeConfig": { + "aws:runShellScript": { + "properties": [ + { + "id": "0.aws:runShellScript", + "runCommand": [ + "", + "#!/bin/bash ", + "INSTANCEID=$(curl http://169.254.169.254/latest/meta-data/instance-id)", + "HOOKRESULT='CONTINUE'", + "REGION=$(curl -s 169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/.$//')", + "dt=*.`date '+%Y-%m-%d'`.log", + "tf=`date '+%Y-%m-%d'`", + "MESSAGE=''", + "", + "if [ -d \"{{BACKUPDIRECTORY}}\" ];", + "then", + "for i in `find \"{{BACKUPDIRECTORY}}\" -name \"$dt\"`; do echo $i; sudo /usr/local/bin/aws s3 cp $i s3://{{S3BUCKET}}/\"$tf\"/\"$INSTANCEID\"/; done", + "else", + " MESSAGE= \"{{BACKUPDIRECTORY}}\" directory Not exits in this server ", + "echo $MESSAGE", + "fi", + "", + "/usr/local/bin/aws sns publish --subject ' Report-Logs_backup-{{ASGNAME}} ' --message \"$MESSAGE\" --target-arn {{SNSTARGET}} --region ${REGION}", + "/usr/local/bin/aws autoscaling complete-lifecycle-action --lifecycle-hook-name {{LIFECYCLEHOOKNAME}} --auto-scaling-group-name {{ASGNAME}} --lifecycle-action-result ${HOOKRESULT} --instance-id ${INSTANCEID} --region ${REGION}" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/logging/ssh/README.md b/logging/ssh/README.md new file mode 100644 index 0000000..174df89 --- /dev/null +++ b/logging/ssh/README.md @@ -0,0 +1,38 @@ +# Extract SSH Logs + +This folder contains a shell/python script that can be used for extracting SSH full logs + +## Requirements +* Python3 +* SDM logs +* A [strongDM admin token](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/admin-tokens) with `SSH Replays` permission + +## Configuration +1. You can adjust the following variables at the top of the script: +* LOG_DIR. Folder where the relay logs are located (absolute path is recommended) +* LOG_PATTERN. Logs name pattern (rarely requires any change) +2. In your local runtime environment, set the token value above as an environment variable named `SDM_ADMIN_TOKEN`. +3. Ideally, configure the script as a CRONJOB. For example (daily config): +``` +0 0 * * * +``` + +## Sample +``` +$ ./extract_ssh_logs.sh 2>/dev/null +session_id,start_time,end_time,user,cmd(new_line=|#|) +s1r6n6RRiECGwflAJ2EPkv3YkBGC,2021-04-13 09:13:51.789592,2021-04-13 09:13:52.200592,Rodolfo Campos,Welcome to OpenSSH Server|#||#|openssh-server:~$ +s1r6n6RRiECGwflAJ2EPkv3YkBGC,2021-04-13 09:13:52.200592,2021-04-13 09:13:55.071592,Rodolfo Campos,ls|#|logs ssh_host_keys sshd.pid|#|openssh-server:~$ +s1r6n6RRiECGwflAJ2EPkv3YkBGC,2021-04-13 09:13:55.071592,2021-04-13 09:13:56.126592,Rodolfo Campos,pwd|#|/config|#|openssh-server:~$ +s1r6n6RRiECGwflAJ2EPkv3YkBGC,2021-04-13 09:13:56.126592,2021-04-13 09:13:59.501592,Rodolfo Campos,echo "hello world"|#|hello world|#|openssh-server:~$ +s1r6n6RRiECGwflAJ2EPkv3YkBGC,2021-04-13 09:13:59.501592,2021-04-13 09:14:02.427592,Rodolfo Campos,ls|#|logs ssh_host_keys sshd.pid|#|openssh-server:~$ +s1r6n6RRiECGwflAJ2EPkv3YkBGC,2021-04-13 09:14:02.427592,2021-04-13 09:14:02.831592,Rodolfo Campos,ls|#|logs ssh_host_keys sshd.pid|#|openssh-server:~$ +s1r6n6RRiECGwflAJ2EPkv3YkBGC,2021-04-13 09:14:02.831592,2021-04-13 09:14:04.111592,Rodolfo Campos,exit|#|logout|#| +s1rWwpo8PrK5nJU9GnI5y3hDGYF9,2021-04-22 15:28:58.497968,2021-04-22 15:28:58.871968,Rodolfo Campos,Welcome to OpenSSH Server|#||#| +s1rWwpo8PrK5nJU9GnI5y3hDGYF9,2021-04-22 15:28:58.871968,2021-04-22 15:29:02.232968,Rodolfo Campos,openssh-server:~$ exit|#|logout|#| +``` + +Considerations: +* Commands with several lines are delimited by `|#|` +* SSH session files will be stored, and deleted in the next iteration, in the folder where the script gets executed +* If you use the JSON format for local logging, change line 30 to `sdm ssh split $line --json 1>&2` diff --git a/logging/ssh/extract_ssh_logs.sh b/logging/ssh/extract_ssh_logs.sh new file mode 100755 index 0000000..513a0dd --- /dev/null +++ b/logging/ssh/extract_ssh_logs.sh @@ -0,0 +1,100 @@ +#!/usr/bin/python3 +# strongDM - Daily SSH session commands extract (from strongDM servers) +# +# Extracts all previous day ssh events from strongDM logs +# Creates a stdout file for each ssh session-id +# +# Cron daily, at midnight. crontab 0 0 * * * + +import base64 +import datetime +import os +import re +import subprocess +import sys + +LOG_DIR="logs" +LOG_PATTERN="relay*.log" + +def main(): + successful = generate_ssh_session_files() + if not successful: + print(f"Error splitting log files from {LOG_DIR}") + exit(-1) + print_logs() + +def generate_ssh_session_files(): + ssh_split_cmd = f""" + rm *.ssh + find {LOG_DIR} -name '{LOG_PATTERN}' | while read line; do + sdm ssh split $line 1>&2 + done + ls *.ssh &> /dev/null + """ + result = os.system(ssh_split_cmd) + return result == 0 + +def print_logs(): + init_date = get_init_date() + print("session_id,start_time,end_time,user,cmd(new_line=|#|)") + for count, line in enumerate(run_command(f"sdm audit ssh --from {init_date}")): + if count == 0: + continue + + is_relay, session_id, start_time, user = extract_session_info(line) + if is_relay != "true": + continue + + if not os.path.isfile(f"{session_id}.ssh"): + print("Session: $session_id is not present in the provided log", file=sys.stderr) + continue + + print_session_logs(session_id, start_time, user) + +def get_init_date(): + yesterday = datetime.datetime.now() - datetime.timedelta(days = 1) + return yesterday.strftime("%Y-%m-%d") + +def run_command(command): + p = subprocess.Popen(command.split(), stdout = subprocess.PIPE, stderr = subprocess.STDOUT) + return iter(p.stdout.readline, b'') + +def extract_session_info(line): + session_info = line.decode("utf-8").split(",") + return session_info[8], session_info[6], session_info[0], session_info[4] + +def print_session_logs(session_id, start_time, user): + full_cmd_entry = "" + total_elapsed_millis = 0 + start_time_regular = zulu_date_to_regular(start_time) + for line in run_command(f"cat {session_id}.ssh"): + elapsed_millis, cmd_entry = extract_cmd_entry_info(line) + one_line_cmd_entry = cmd_entry.replace("\r", "").replace("\n", "|#|") + full_cmd_entry = f"{full_cmd_entry}{one_line_cmd_entry}" + total_elapsed_millis += elapsed_millis + + if not end_of_line(cmd_entry): + continue + + end_time_regular = add_millis(start_time_regular, total_elapsed_millis) + print(f"{session_id},{start_time_regular},{end_time_regular},{user},{full_cmd_entry}") + + full_cmd_entry = "" + total_elapsed_millis = 0 + start_time_regular = end_time_regular + +def zulu_date_to_regular(input_date): + return re.sub(r"[0-9]{,3} \+[0-9]{,4} UTC$", "", input_date) + +def extract_cmd_entry_info(line): + cmd_entry_info = line.decode("utf-8").split(",") + return int(cmd_entry_info[0]), base64.b64decode(cmd_entry_info[1]).decode("utf-8") + +def end_of_line(cmd_entry): + return True if "\n" in cmd_entry else False + +def add_millis(input_date, input_millis): + new_date = datetime.datetime.strptime(input_date, "%Y-%m-%d %H:%M:%S.%f") + datetime.timedelta(milliseconds = input_millis) + return str(new_date) + +main() diff --git a/monitoring/gateways/README.md b/monitoring/gateways/README.md new file mode 100644 index 0000000..52ac999 --- /dev/null +++ b/monitoring/gateways/README.md @@ -0,0 +1,11 @@ +### Overview +This shell script is intended to help troubleshoot the use of system resources on Unix machines running a strongDM Gateway or Relay. It may be useful in particular when a Gateway is failing, and you are unable to obtain logs from the given machine. + +NB: for long-term monitoring, we recommend a more robust third-party tool like [Node Exporter](https://github.com/prometheus/node_exporter). + +#### Requirements: +- An S3 bucket in AWS +- AWS credentials that allow writing to that bucket. + +#### Notes: +- This script was tested on Ubuntu 18.04 and 20.04. diff --git a/monitoring/gateways/gateway-monitor.sh b/monitoring/gateways/gateway-monitor.sh new file mode 100644 index 0000000..164ddbd --- /dev/null +++ b/monitoring/gateways/gateway-monitor.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# day, hour, minute timestamp +TIMESTAMP=`date +'%Y%m%d%H%M'` + +S3NAME=strongdm-gw-health-$TIMESTAMP.gz +S3PATH=s3://s3-bucket-name # no trailing slash + +export AWS_ACCESS_KEY_ID=id +export AWS_SECRET_ACCESS_KEY=secret + +(free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }' ; \ +df -h | awk '$NF=="/"{printf "Disk Usage: %d/%dGB (%s)\n", $3,$2,$5}' ; \ +top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}' ; \ +lsof | wc -l | awk '{printf "Open files: " $1"\n"}') | \ +gzip | aws s3 cp - $S3PATH/$S3NAME \ No newline at end of file diff --git a/monitoring/resources/README.md b/monitoring/resources/README.md new file mode 100644 index 0000000..cf83124 --- /dev/null +++ b/monitoring/resources/README.md @@ -0,0 +1,13 @@ +### Overview +This Python script uses the strongDM SDK to find unhealthy resources (those that appear yellow in the UI) and "tags" them with a timestamp. The tagging process forces a health check on each resource, just as if you had clicked the `Check Now` button in the Admin UI. + +This process doesn't affect any other tags you might use. + +#### Requirements: +- [SDM API keys](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) defined in runtime environment +- Python 3 +- strongDM Python module + +#### Notes: +- This script can take some time to run if you have many unhealthy resources. +- We strongly recommend that you run this script single-threaded. \ No newline at end of file diff --git a/monitoring/resources/datasource_healthcheck.py b/monitoring/resources/datasource_healthcheck.py new file mode 100644 index 0000000..9bd1c9e --- /dev/null +++ b/monitoring/resources/datasource_healthcheck.py @@ -0,0 +1,40 @@ +import strongdm, time, os + +access_key=os.getenv("SDM_API_ACCESS_KEY") +secret_key=os.getenv("SDM_API_SECRET_KEY") + +# Create SDM client +client = strongdm.Client(access_key, secret_key) + +# Get time details for key update +import time +seconds = int( time.time() ) +humanTime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(seconds)) + +# Get all resources of type DATASOURCE with healthy=FALSE +resources = list(client.resources.list('category:datasource, healthy:false')) +print("\nThe following Datasources are NOT healthy, and will be updated with a fresh tag:\n") +for i in resources: + print("Name:", i.name," | Healthy?", i.healthy) + # create the tag locally, with current timestamp + i.tags = {"lastHealthcheck": "{}".format(seconds)} + # this will add the tag if not present, or update if so + response = client.resources.update(i) + +# Wait 5 seconds per unhealthy resource +mTime = len(resources) * 5 +print('~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=') +print('Running healthchecks ...') +time.sleep(mTime) +print('Work complete.') +print('~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=') + +# Get a new list of resources, to see what changes the update above made +resources = list(client.resources.list('category:datasource, healthy:false')) +print("The following Datasources are still unhealthy:\n") +for i in resources: + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(1347517370)) + print("Name:", i.name," | Healthy?", i.healthy, " | Last check:", humanTime ) + +print("\nDone.") + diff --git a/monitoring/sdm_health_exporter/README.md b/monitoring/sdm_health_exporter/README.md new file mode 100644 index 0000000..865623c --- /dev/null +++ b/monitoring/sdm_health_exporter/README.md @@ -0,0 +1,70 @@ +# Purpose + +This script serves as an example exporter that can monitor the health of resources ("Infrastructure") and nodes ("Gateways/Relays"). The script uses the following workflow: + +1. Make an API call to strongDM's API to retrive information about resources and nodes (configurable by updating `update_interval` variable in `main()`) + +2. Collect data about any resource or node that is tagged with the `alert_tag` in strongDM (`alert_tag` variable is configurable in `main()`) + +3. Export metrics to a prometheus endpoint as a "Gauge" (0 for healthy, 1 for unhealthy) + +*IMPORTANT NOTE*: Currently, the resources and nodes only perform automatic health checks every 12 hours, or when the check is manually initiated through the UI. There is a feature request in place to lower that automatic health check interval, and/or make it possible to initiate a manual check through the API. + + +# Setup + +- Create a new strongDM API key + + https://www.strongdm.com/docs/admin-ui-guide/access/api-keys + +- Configure the environment variables:` + + `export SDM_API_ID=""` + + `export SDM_API_SECRET=""` + +- Create a new virtual environment + + `python3 -m venv venv` + +- Activate the new environment + + `source venv/bin/activate` + +- Install requirements with `pip` + + `pip install -r requirements.txt` + + +# Sample `/metrics` data + +After starting the exporter, the new metrics will be available on `http://:8337/metrics`. Here is an example of what the exported metrics will look: + +``` +# HELP example_server1 health of resource +# TYPE example_server1 gauge +example_server1{id="rs-116",name="example_server1",tags="{'lab-infra': '', 'alert': ''}",task="health_check"} 0.0 +# HELP example_server2 health of resource +# TYPE example_server2 gauge +example_server2{id="rs-5c6",name="example_server2",tags="{'lab-infra': '', 'alert': ''}",task="health_check"} 0.0 +# HELP example_server3 health of resource +# TYPE example_server3 gauge +example_server3{id="n-181",name="example_server3",tags="{'alert': ''}",task="health_check"} 0.0 +``` + +# Scraping the metrics with Prometheus + +Adding the following job to the `prometheus.yml` file will scrape the metrics from our new endpoint (replace `localhost` with the appropriate IP or hostname): + +``` + - job_name: "sdm_health_exporter" + static_configs: + - targets: ["localhost:8337"] +``` + + +# Alerting on failures + +If you use Alert Manager with Prometheus, you can use a PromQL expression similar to the one below to alert on unhealthy resources/nodes: + +`{job="sdm_health_exporter", task="health_check"} == 1` diff --git a/monitoring/sdm_health_exporter/requirements.txt b/monitoring/sdm_health_exporter/requirements.txt new file mode 100644 index 0000000..f9ddb81 --- /dev/null +++ b/monitoring/sdm_health_exporter/requirements.txt @@ -0,0 +1,8 @@ +googleapis-common-protos==1.52.0 +grpcio==1.42.0 +prometheus-client==0.13.1 +protobuf==3.19.4 +pydantic==1.9.0 +six==1.16.0 +strongdm==1.0.35 +typing-extensions==4.1.0 diff --git a/monitoring/sdm_health_exporter/sdm_health_exporter.py b/monitoring/sdm_health_exporter/sdm_health_exporter.py new file mode 100755 index 0000000..a5c11b3 --- /dev/null +++ b/monitoring/sdm_health_exporter/sdm_health_exporter.py @@ -0,0 +1,179 @@ +import os +import time +from typing import Any +from pydantic import BaseModel +import strongdm +from prometheus_client import start_http_server, Gauge + +""" +sdm_exporter.py + +This script serves as an example exporter that can monitor the +health of resources ("Infrastructure") and nodes ("Gateways/Relays"). + +The script uses the following workflow: + +- Make an API call to strongDM's API to retrive information about resources +and nodes. The frequency of the API call is configurable by updating the +"update_interval" variable in "main()" + +- Collect data about any resource or node that is tagged with "alert" in strongDM. +This tag is configurable by updating the "alert_tag" variable in "main()" + +- Export metrics to a prometheus endpoint as a gauge (0 for healthy, 1 for unhealthy) +""" + +class SdmObject(BaseModel): + """ + Pydantic class with type enforcement + Prometheus object of type "Gauge" + Possible values: healthy: 0, unhealthy: 1 + """ + health_metric: Gauge + + class Config: + validate_assignment = True + arbitrary_types_allowed = True + + +def strip_invalid_chars(name): + """ + Dashes are invalid for prometheus metric but allowed in strongDM + """ + return name.replace('-','_') + + +def get_sdm_keys(): + """ + Loads API keys from environment variables. + Raises: KeyError if environment variables are not found + Returns: strings for API ID and secret + """ + + try: + api_id = os.environ['SDM_API_ACCESS_KEY'] + api_secret = os.environ['SDM_API_SECRET_KEY'] + except KeyError as ke: + print(f'FATAL: Missing env variable: {ke}. Exiting...') + exit() + + return api_id, api_secret + + +def get_sdm_resources(client, alert_tag, sdm_objects): + """ + Issue strongDM API calls every to collect information on resources + Export any resource that's tagged with in strongDM to a prometheus exporter + Returns: dictionary of SdmObject instances, keyed by resource name + """ + + # prometheus labels that will be attached to the exported metric + labels = ['id','healthy','name','tags','task'] + + for resource in client.resources.list(''): + + # make sure there are no characters that are invalid for the prometheus exporter + resource_name = strip_invalid_chars(resource.name) + + # only process resources that are tagged with in strongDM + if alert_tag not in resource.tags: + continue + + # only register a new prometheus collector if it's not an existing object + # create a new "Gauge" and define the labels that will be used for the metric + # create new SdmObject with this information and add it to "sdm_objects" dictionary + if resource_name not in sdm_objects: + health_metric = Gauge(resource_name, 'health of resource', labelnames=labels) + sdm_objects[resource_name] = SdmObject(health_metric=health_metric) + + # resource health is returned as True or False + # set the label values based on information retrieved from the API call + # set the metric to 0 (healthy) or 1 (unhealthy) + sdm_objects[resource_name].health_metric.labels( + id=resource.id, + healthy=resource.healthy, + name=resource_name, + tags=resource.tags, + task='health_check' + ).set(0 if resource.healthy else 1) + + return sdm_objects + + +def get_sdm_nodes(client, alert_tag, sdm_objects): + """ + Issue strongDM API calls every to collect information on nodes + Export any resource that's tagged with in strongDM to a prometheus exporter + Returns: dictionary of SdmObject instances, keyed by node name + """ + + # prometheus labels that will be attached to the exported metric + labels = ['id','name','state','tags','task'] + + for node in client.nodes.list(''): + + # make sure there are no characters that are invalid for the prometheus exporter + node_name = strip_invalid_chars(node.name) + + # only process nodes that are tagged with in strongDM + if alert_tag not in node.tags: + continue + + # only register a new prometheus collector if it's not an existing object + # create a new "Gauge" and define the labels that will be used for the metric + # create new SdmObject with this information and add it to "sdm_objects" dictionary + if node_name not in sdm_objects: + health_metric = Gauge(node_name, 'health of resource', labelnames=labels) + sdm_objects[node_name] = SdmObject(health_metric=health_metric) + + # node health is returned as "started", "stopped", or "new" + # anything other than "stated" is considered unhealthy + if node.state not in "started": + + # set the label values based on information retrieved from the API call + # set the metric to 0 (healthy) or 1 (unhealthy) + sdm_objects[node_name].health_metric.labels( + id=node.id, + name=node_name, + state=node.state, + tags=node.tags, + task='health_check' + ).set(0 if node.state == "started" else 1) + + return sdm_objects + + +def main(): + + # frequency to issue API call to strongDM + update_interval = 60 + + # filter strongDM results to resources that are tagged with in strongDM + alert_tag = 'alert' + + # retrieve strongDM API keys + api_id, api_secret = get_sdm_keys() + + # keeps track of SdmObject instances, keyed by resource name + sdm_objects = {} + + # start prometheus endpoint + start_http_server(8337) + + # strongDM API client + client = strongdm.Client(api_id, api_secret) + + # issue API calls to obtain health status indefinitely with a frequency of + while True: + + # collect resources and objects and write them out to a Prometheus exporter + # returns dictionary of SdmObject instances, keyed by resource or node name + sdm_objects = get_sdm_resources(client, alert_tag, sdm_objects) + sdm_objects = get_sdm_nodes(client, alert_tag, sdm_objects) + + # wait "update_interval" amount of time before issuing the next API call + time.sleep(update_interval) + + +if __name__ == '__main__': + main() diff --git a/terraform/resources/ssh/README.md b/terraform/resources/ssh/README.md new file mode 100644 index 0000000..d965dac --- /dev/null +++ b/terraform/resources/ssh/README.md @@ -0,0 +1,7 @@ +### Overview +This Terraform script uses the AWS provider to create an EC2 instance, and the strongDM provider to create an [SSH-certificate server resource](https://www.strongdm.com/docs/admin-ui-guide/settings/ssh/ssh-certificate-auth) that points to it. + +#### Requirements: +- [AWS credentials configured locally](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html) +- [SDM API keys](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) defined in runtime environment +- Terraform 14 or later \ No newline at end of file diff --git a/terraform/resources/ssh/main.tf b/terraform/resources/ssh/main.tf new file mode 100644 index 0000000..c2bb9b9 --- /dev/null +++ b/terraform/resources/ssh/main.tf @@ -0,0 +1,99 @@ +# Queries from SDM CA Public Key +data "sdm_ssh_ca_pubkey" "ssh_pubkey_query" { +} + +# Queries latest Ubuntu AMI +data "aws_ami" "ubuntu" { + most_recent = true + owners = ["099720109477"] # Canonical + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + +} + +resource "aws_default_vpc" "default" { +} + +resource "aws_default_subnet" "default_az1" { + availability_zone = "${var.aws_region}a" +} + +# Create SDM Security Group +resource "aws_security_group" "ssh_sg" { + + name = var.sgName + vpc_id = aws_default_vpc.default.id + description = "Sec Group for SDM SSH" + +} + +# Adds ingress for SDM to be able to SSH +resource "aws_security_group_rule" "ingress_rules" { + + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + description = "ssh" + cidr_blocks = [aws_default_vpc.default.cidr_block] + security_group_id = aws_security_group.ssh_sg.id + +} + +# Adds egress for all instances to reach internet +resource "aws_security_group_rule" "egress_rules" { + + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.ssh_sg.id + +} + +# Create the EC2 instance +resource "aws_instance" "linux_instance" { + ami = data.aws_ami.ubuntu.id + subnet_id = aws_default_subnet.default_az1.id + + vpc_security_group_ids = [aws_security_group.ssh_sg.id] + instance_type = "t2.micro" + + # Run commands to configure server with strongDM CA + user_data = data.template_file.sdm_ssh_install.rendered + + tags = { + Name = var.instanceName + } + + volume_tags = { + Name = var.instanceName + } +} + +# Create the SDM server resource +resource "sdm_resource" "sshCA" { + ssh_cert { + name = var.instanceName + hostname = aws_instance.linux_instance.private_dns + username = "ubuntu" + port = 22 + } +} + +# Renders script into base64 encoded template +data "template_file" "sdm_ssh_install" { + template = file("${path.module}/template/sdm_ssh_install/install.sh.tpl") + vars = { + SSH_PUB_KEY = "${data.sdm_ssh_ca_pubkey.ssh_pubkey_query.public_key}" + } +} \ No newline at end of file diff --git a/terraform/resources/ssh/providers.tf b/terraform/resources/ssh/providers.tf new file mode 100644 index 0000000..7f608fa --- /dev/null +++ b/terraform/resources/ssh/providers.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + sdm = { + source = "strongdm/sdm" + } + } +} + +provider "aws" { + region = var.aws_region +} diff --git a/terraform/resources/ssh/template/sdm_ssh_install/install.sh.tpl b/terraform/resources/ssh/template/sdm_ssh_install/install.sh.tpl new file mode 100644 index 0000000..afc008c --- /dev/null +++ b/terraform/resources/ssh/template/sdm_ssh_install/install.sh.tpl @@ -0,0 +1,4 @@ +#!/bin/bash +echo "${SSH_PUB_KEY}" | sudo tee -a /etc/ssh/sdm_ca.pub +echo "TrustedUserCAKeys /etc/ssh/sdm_ca.pub" | sudo tee -a /etc/ssh/sshd_config +sudo systemctl restart ssh \ No newline at end of file diff --git a/terraform/resources/ssh/variables.tf b/terraform/resources/ssh/variables.tf new file mode 100644 index 0000000..069d3cf --- /dev/null +++ b/terraform/resources/ssh/variables.tf @@ -0,0 +1,11 @@ +variable "aws_region" { + default = "us-west-2" +} + +variable "instanceName" { + default = "your-instance-name" +} + +variable "sgName" { + default = "your-sg-name" +} \ No newline at end of file diff --git a/webinars/21-12-08_PagerDuty/README.md b/webinars/21-12-08_PagerDuty/README.md new file mode 100644 index 0000000..148d68b --- /dev/null +++ b/webinars/21-12-08_PagerDuty/README.md @@ -0,0 +1,23 @@ +### Overview +If you're using PagerDuty, then you already have on-call schedules mapped out for critical roles. But when someone is on-call, they may need more database or server access than they'd otherwise use. This is where strongDM temporary grants come in: you can integrate your PagerDuty on-call schedule with strongDM to automatically grant strongDM users access to additional resources during their on-call shifts. This Python example shows a simple way of managing the process. + +The script has two major portions: first, look up who is on call for a specific schedule over a certain time period; second, parse these assignments with the strongDM SDK to grant temporary access to a datasource or server. One wrinkle is that two API calls are necessary to PagerDuty: first, getting the list of who is on call will give a list of users and user IDs, but not email addresses. Second, specific user lookups get us the email addresses of who is on call. + +#### Requirements: +- [SDM API keys](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) defined in runtime environment +- Python 3 +- strongDM Python module + +#### Steps: +To get this script working in your environment, you'll need the following: + +- A strongDM API key and secret with Datasource list and grant and User assign and list rights +- A strongDM Resource name +- A PagerDuty API key with read-only rights +- The schedule ID of a PagerDuty schedule you wish to use as the basis of the temporary grants + +#### Notes: +- In order for this automation to work, your users will need to be identified by the same email addresses in PagerDuty and in strongDM. + +_This script was presented in a strongDM webinar on 8 December 2021. You can view the recording on-demand via [this link](https://strongdm.zoom.us/rec/share/1CMvoeTMCLwLZaaqHWymvHVy28IRlQIxZjCbPocUweSbKcZmfhG9gcouKihgNK3c.aomQI3rl1mSuC5gG?startTime=1638986428000)._ + diff --git a/webinars/21-12-08_PagerDuty/pager_duty_grant.py b/webinars/21-12-08_PagerDuty/pager_duty_grant.py new file mode 100644 index 0000000..0ee8ebc --- /dev/null +++ b/webinars/21-12-08_PagerDuty/pager_duty_grant.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +import requests,json,datetime,subprocess,strongdm,re +from datetime import timezone + +# PagerDuty API key +API_KEY = 'PD_API_KEY' + +# strongDM API keys: requires [datasource: list,grant] and [user: list,assign] +access_key = "SDM_ACCESS_KEY" +secret_key = "SDM_SECRET_KEY" + +# Name of strongDM Datasource to which you are granting access, from Admin UI +DATASOURCE = 'RESOURCE_NAME' + +# Set your time zone for PagerDuty +TIME_ZONE = 'UTC' +# Get this ID from the PagerDuty admin UI, or via their 'schedules' API endpoint +SCHEDULE_IDS = ['ID'] +# for the PD API requests. Modify UNTIL with the proper time offset +UNTIL = (datetime.timedelta(days=1) + datetime.datetime.utcnow()).isoformat() + 'Z' + +def get_oncalls(): + url = 'https://api.pagerduty.com/oncalls' + headers = { + 'Accept': 'application/vnd.pagerduty+json;version=2', + 'Authorization': 'Token token={token}'.format(token=API_KEY) + } + payload = { + 'time_zone': TIME_ZONE, + 'schedule_ids[]': SCHEDULE_IDS, + 'until': UNTIL, + } + + r = requests.get(url, headers=headers, params=payload) + struct = r.json() + output = [] + + for record in struct["oncalls"]: + # get user's email address + r = requests.get(record["user"]["self"], headers=headers) + output.append({"email" : r.json()["user"]["email"], + "from" : record["start"], + "to" : record["end"]}) + return output + +def grant_access(access_list): + + client = strongdm.Client(access_key, secret_key) + + # Get Datasource(s) + resources = list(client.resources.list('name:{}'.format(DATASOURCE)) ) + resourceID = resources[0].id + + # Cycle through the output from PagerDuty + for item in access_list: + # Use the PD email address to get the user from SDM + print('Current PD user is: ' + item["email"]) + users = list(client.accounts.list('email:{}'.format(item["email"]))) + if len(users) > 0: + print('SDM user found!') + myUserID = users[0].id + # Convert the date strings from PD into a datetime object + s = datetime.datetime.strptime(item["from"], '%Y-%m-%dT%H:%M:%SZ') + e = datetime.datetime.strptime(item["to"], '%Y-%m-%dT%H:%M:%SZ') + # Make both objects 'aware' (with TZ) as required by the strongDM SDK + start = s.replace(tzinfo=timezone.utc) + end = e.replace(tzinfo=timezone.utc) + # Create the grant object + myGrant = strongdm.AccountGrant(resource_id='{}'.format(resourceID),account_id='{}'.format(myUserID), + start_from=start, valid_until=end) + # Perform the grant + try: + respGrant = client.account_grants.create(myGrant) + except Exception as ex: + print("\nSkipping user " + item["email"] + " on account of error: " + str(ex)) + else: + print("\nGrant succeeded for user " + item["email"] + " to resource " + DATASOURCE + " from {} to {}".format(start,end)) + print('---\n') + +def main(): + access = get_oncalls() + grant_access(access) + +main() \ No newline at end of file diff --git a/webinars/21-12-15_terraform_resource_import/README.md b/webinars/21-12-15_terraform_resource_import/README.md new file mode 100644 index 0000000..e05b497 --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/README.md @@ -0,0 +1,25 @@ +# Importing resources into strongDM using Terraform. + +This is a small demo script showing how to add resources into strongDM if you are spinning them up in Terraform. This module will create a whole new VPC with public and private subnet and create a PSQL database and ssh resource. This is purely referencial and not for production environments. + +1. Download this repo + +2. Navigate into it this folder in your terminal + +3. Run `terraform init` + +4. You'll need to edit/create the `terraform.tfvars` file with your required variables. To generate an API key log in to the [strongDM admin UI](https://app.strongdm.com/) and generate a new API Key. You can see this docs page for instructions on how to [generate an API Key](https://www.strongdm.com/docs/admin-ui-guide/access/api-keys). File contentes example below: + + ```HCL + sdm_access_key = "YOURSDMAPIACCESSKEY" + sdm_secret_key = "YOURSDMAPISECRETKEY" + region = "us-west-1" + ``` + +5. Run `terraform plan` to verify all the changes and any errors. + +6. Run `terraform apply` to deploy the terraform. Give about 15-20 minutes to execute as the Relay and PSQL instance wait until the NAT Gateway has come online to validate network connectivity and installation script to run. + +7. Check your [strongDM Admin Panel](https://app.strongdm.com/) and verify resources are online. + +8. If you wish to test resources and connectivity assign a role that has permissions to the PSQL resource and ssh resources. \ No newline at end of file diff --git a/webinars/21-12-15_terraform_resource_import/main.tf b/webinars/21-12-15_terraform_resource_import/main.tf new file mode 100644 index 0000000..3d92382 --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/main.tf @@ -0,0 +1,315 @@ +# Locals + +locals { + required_tags = { + ExpiryDate = "2021-12-16" + } +} + +# VPC + +resource "aws_vpc" "Main" { + cidr_block = var.main_vpc_cidr + instance_tenancy = "default" + tags = merge(local.required_tags, var.resource_tags) +} + +resource "aws_internet_gateway" "IGW" { + vpc_id = aws_vpc.Main.id +} + +resource "aws_subnet" "publicsubnets" { + vpc_id = aws_vpc.Main.id + cidr_block = var.public_subnets + tags = merge(local.required_tags, var.resource_tags) +} + +resource "aws_subnet" "privatesubnets" { + vpc_id = aws_vpc.Main.id + cidr_block = var.private_subnets + tags = merge(local.required_tags, var.resource_tags) +} + +resource "aws_route_table" "PublicRT" { + vpc_id = aws_vpc.Main.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.IGW.id + } + tags = merge(local.required_tags, var.resource_tags) +} + +resource "aws_route_table" "PrivateRT" { + vpc_id = aws_vpc.Main.id + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.NATgw.id + } + tags = merge(local.required_tags, var.resource_tags) +} + +resource "aws_route_table_association" "PublicRTassociation" { + subnet_id = aws_subnet.publicsubnets.id + route_table_id = aws_route_table.PublicRT.id +} + +resource "aws_route_table_association" "PrivateRTassociation" { + subnet_id = aws_subnet.privatesubnets.id + route_table_id = aws_route_table.PrivateRT.id +} + +resource "aws_eip" "natIP" { + vpc = true +} + +resource "aws_nat_gateway" "NATgw" { + allocation_id = aws_eip.natIP.id + subnet_id = aws_subnet.publicsubnets.id +} + +# AMI + +data "aws_ami" "ubuntu" { + most_recent = true + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["099720109477"] # Canonical +} + +# Security Groups + +resource "aws_security_group" "gateway_sg" { + + name = "sdm_gateway_sg" + vpc_id = aws_vpc.Main.id + description = "Sec Group for SDM Gateway" + +} + +resource "aws_security_group_rule" "ingress_rules" { + + count = length(var.gateway_sg_ingress_rules) + + type = "ingress" + from_port = var.gateway_sg_ingress_rules[count.index].from_port + to_port = var.gateway_sg_ingress_rules[count.index].to_port + protocol = var.gateway_sg_ingress_rules[count.index].protocol + cidr_blocks = [var.gateway_sg_ingress_rules[count.index].cidr_block] + description = var.gateway_sg_ingress_rules[count.index].description + security_group_id = aws_security_group.gateway_sg.id + +} + +resource "aws_security_group_rule" "egress_rules" { + + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.gateway_sg.id + +} + +resource "aws_security_group" "relay_sg" { + + name = "sdm_relay_sg" + vpc_id = aws_vpc.Main.id + description = "Sec Group for SDM Gateway" + +} + +resource "aws_security_group_rule" "relay_ingress_rule" { + + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + description = "ssh" + source_security_group_id = aws_security_group.gateway_sg.id + security_group_id = aws_security_group.relay_sg.id + +} + +resource "aws_security_group_rule" "relay_egress_rules" { + + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.relay_sg.id + +} + +resource "aws_security_group" "psql_sg" { + + name = "sdm_psql_sg" + vpc_id = aws_vpc.Main.id + description = "Sec Group for PSQL" + +} + +resource "aws_security_group_rule" "psql_ingress_rules" { + + count = length(var.psql_sg_ingress_rules) + + type = "ingress" + from_port = var.psql_sg_ingress_rules[count.index].from_port + to_port = var.psql_sg_ingress_rules[count.index].to_port + protocol = var.psql_sg_ingress_rules[count.index].protocol + cidr_blocks = [var.psql_sg_ingress_rules[count.index].cidr_block] + description = var.psql_sg_ingress_rules[count.index].description + security_group_id = aws_security_group.psql_sg.id + +} + +resource "aws_security_group_rule" "psql_egress_rules" { + + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.psql_sg.id + +} + +# SDM Gateway Instance + +resource "sdm_node" "gateway" { + gateway { + name = "sdm-gateway-01" + listen_address = "${aws_eip.gateway.public_ip}:5000" + bind_address = "0.0.0.0:5000" + } +} + +output "gateway_token" { + value = sdm_node.gateway.gateway[0].token + sensitive = true +} + +resource "aws_eip" "gateway" { + network_interface = aws_network_interface.gateway.id +} + +resource "aws_network_interface" "gateway" { + subnet_id = aws_subnet.publicsubnets.id + security_groups = [aws_security_group.gateway_sg.id] + + tags = merge(local.required_tags, var.resource_tags) +} + +resource "aws_instance" "gateway" { + + ami = data.aws_ami.ubuntu.image_id + instance_type = "t2.micro" + key_name = aws_key_pair.terraform_key.key_name + + user_data = templatefile("${path.module}/template/sdm_gateway_install/install.sh.tpl", { SDM_GATEWAY_TOKEN = "${sdm_node.gateway.gateway[0].token}" }) + tags = merge(local.required_tags, var.resource_tags) + + network_interface { + network_interface_id = aws_network_interface.gateway.id + device_index = 0 + } + +} + +# SDM Relay Instance + +resource "sdm_node" "relay" { + relay { + name = "sdm-relay-01" + } +} + +output "relay_token" { + value = sdm_node.relay.relay[0].token + sensitive = true +} + +resource "aws_instance" "relay" { + depends_on = [aws_nat_gateway.NATgw] + ami = data.aws_ami.ubuntu.image_id + instance_type = "t2.micro" + subnet_id = aws_subnet.privatesubnets.id + vpc_security_group_ids = [aws_security_group.relay_sg.id] + key_name = aws_key_pair.terraform_key.key_name + + user_data = templatefile("${path.module}/template/sdm_relay_install/install.sh.tpl", { SDM_RELAY_TOKEN = "${sdm_node.relay.relay[0].token}", SSH_PUB_KEY = "${data.sdm_ssh_ca_pubkey.ssh_pubkey_query.public_key}" }) + tags = merge(local.required_tags, var.resource_tags) + +} + +resource "sdm_resource" "relay_ssh" { + ssh_cert { + name = "sdm-relay-ssh" + username = "ubuntu" + hostname = aws_instance.relay.private_ip + port = 22 + tags = merge(local.required_tags, var.resource_tags) + } +} + +# PSQL Instance + +resource "aws_instance" "psql" { + depends_on = [aws_nat_gateway.NATgw] + ami = data.aws_ami.ubuntu.id + instance_type = "t3.small" + vpc_security_group_ids = [aws_security_group.psql_sg.id] + subnet_id = aws_subnet.privatesubnets.id + user_data = templatefile("${path.module}/template/psql/install.sh.tpl", { SSH_PUB_KEY = "${data.sdm_ssh_ca_pubkey.ssh_pubkey_query.public_key}" }) + tags = merge(local.required_tags, var.resource_tags) +} + +resource "sdm_resource" "psql_admin" { + postgres { + name = "sdm-psql-admin" + hostname = aws_instance.psql.private_ip + database = "dvdrental" + username = "postgres" + password = "notastrongpassword123" + port = 5432 + + tags = merge(local.required_tags, var.resource_tags) + } +} + +resource "sdm_resource" "psql_ssh" { + ssh_cert { + name = "sdm-psql-ssh" + username = "ubuntu" + hostname = aws_instance.psql.private_ip + port = 22 + tags = merge(local.required_tags, var.resource_tags) + } +} + +# SDM Public Key + +data "sdm_ssh_ca_pubkey" "ssh_pubkey_query" { +} + +# SSH Key + +resource "tls_private_key" "terraform_key" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "aws_key_pair" "terraform_key" { + key_name = var.key_name + public_key = tls_private_key.terraform_key.public_key_openssh +} \ No newline at end of file diff --git a/webinars/21-12-15_terraform_resource_import/providers.tf b/webinars/21-12-15_terraform_resource_import/providers.tf new file mode 100644 index 0000000..e4c9aa4 --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/providers.tf @@ -0,0 +1,21 @@ +# Provider + +terraform { + required_version = ">= 0.15.0" + required_providers { + aws = ">= 3.0.0" + sdm = { + source = "strongdm/sdm" + version = ">= 1.0.12" + } + } +} + +provider "aws" { + region = var.region +} + +provider "sdm" { + api_access_key = var.sdm_access_key + api_secret_key = var.sdm_secret_key +} diff --git a/webinars/21-12-15_terraform_resource_import/template/psql/install.sh.tpl b/webinars/21-12-15_terraform_resource_import/template/psql/install.sh.tpl new file mode 100644 index 0000000..6570df4 --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/template/psql/install.sh.tpl @@ -0,0 +1,18 @@ +#!/bin/bash +echo "${SSH_PUB_KEY}" | sudo tee -a /etc/ssh/sdm_ca.pub +echo "TrustedUserCAKeys /etc/ssh/sdm_ca.pub" | sudo tee -a /etc/ssh/sshd_config +sudo systemctl restart ssh +sudo apt-get update && sudo apt-get upgrade -y +sudo apt-get install -y unzip curl wget +sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' +wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - +sudo apt-get install -y postgresql +echo "listen_addresses = '*'" | sudo tee -a /etc/postgresql/*/main/postgresql.conf +echo "host all all 10.0.2.0/16 md5" | sudo tee -a /etc/postgresql/*/main/pg_hba.conf +sudo systemctl start postgresql && sudo systemctl enable postgresql +curl -O https://www.postgresqltutorial.com/wp-content/uploads/2019/05/dvdrental.zip +unzip ./dvd*.zip && rm -rf dvdrental.zip +sudo -u postgres createdb dvdrental +sudo -u postgres pg_restore --no-owner --role=postgres -U postgres --dbname dvdrental --verbose dvdrental.tar +sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'notastrongpassword123';" +sudo systemctl restart postgresql \ No newline at end of file diff --git a/webinars/21-12-15_terraform_resource_import/template/sdm_gateway_install/install.sh.tpl b/webinars/21-12-15_terraform_resource_import/template/sdm_gateway_install/install.sh.tpl new file mode 100644 index 0000000..50e1d75 --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/template/sdm_gateway_install/install.sh.tpl @@ -0,0 +1,4 @@ +#!/bin/bash +sudo apt install unzip -y +curl -J -O -L https://app.strongdm.com/releases/cli/linux && unzip sdmcli* && rm -f sdmcli* +sudo ./sdm install --relay --token="${SDM_GATEWAY_TOKEN}" \ No newline at end of file diff --git a/webinars/21-12-15_terraform_resource_import/template/sdm_relay_install/install.sh.tpl b/webinars/21-12-15_terraform_resource_import/template/sdm_relay_install/install.sh.tpl new file mode 100644 index 0000000..0cc960f --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/template/sdm_relay_install/install.sh.tpl @@ -0,0 +1,8 @@ +#!/bin/bash +echo "${SSH_PUB_KEY}" | sudo tee -a /etc/ssh/sdm_ca.pub +echo "TrustedUserCAKeys /etc/ssh/sdm_ca.pub" | sudo tee -a /etc/ssh/sshd_config +sudo systemctl restart ssh +sudo apt update && sudo apt upgrade -y +sudo apt install unzip -y +curl -J -O -L https://app.strongdm.com/releases/cli/linux && unzip sdmcli* && rm -f sdmcli* +sudo ./sdm install --relay --token="${SDM_RELAY_TOKEN}" \ No newline at end of file diff --git a/webinars/21-12-15_terraform_resource_import/variables.tf b/webinars/21-12-15_terraform_resource_import/variables.tf new file mode 100644 index 0000000..b40632e --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/variables.tf @@ -0,0 +1,105 @@ +variable "region" { + description = "AWS Region To Deploy" +} + +variable "sdm_access_key" { + description = "SDM API Access Key" +} + +variable "sdm_secret_key" { + description = "SDM API Secret Key" +} + +variable "project_name" { + type = string + default = "sdm-dev" +} + +variable "main_vpc_cidr" { + default = "10.0.0.0/16" + description = "VPC CIDR Range" +} + +variable "public_subnets" { + default = "10.0.1.0/24" + description = "Public Subnet CIDR Range" +} + +variable "private_subnets" { + default = "10.0.2.0/24" + description = "Private Subnet CIDR Range" +} + +variable "resource_tags" { + type = map(string) + default = { + Terraform = "true" + env = "dev" + } +} + +variable "psql_sg_ingress_rules" { + + type = list(object({ + + from_port = number + to_port = number + protocol = string + cidr_block = string + description = string + + })) + + default = [ + { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_block = "10.0.2.0/24" + description = "psql" + }, + { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_block = "10.0.2.0/24" + description = "ssh" + } + ] + +} + +variable "gateway_sg_ingress_rules" { + + type = list(object({ + + from_port = number + to_port = number + protocol = string + cidr_block = string + description = string + + })) + + default = [ + { + from_port = 5000 + to_port = 5000 + protocol = "tcp" + cidr_block = "0.0.0.0/0" + description = "sdm" + }, + { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_block = "0.0.0.0/0" + description = "ssh" + } + ] + +} + +variable "key_name" { + default = "dev-ssh-terraform-key" +} \ No newline at end of file diff --git a/webinars/22-01-05_resource-grant/README.md b/webinars/22-01-05_resource-grant/README.md new file mode 100644 index 0000000..9fed82c --- /dev/null +++ b/webinars/22-01-05_resource-grant/README.md @@ -0,0 +1,19 @@ +### Overview +This Python script (python-sdk-grants.py) uses the strongDM SDK to assign resources with in a role to a user for set time period. Follow the steps below to add in the details for your environment to get started. + +#### Requirements: +- [SDM API keys](https://www.strongdm.com/docs/admin-ui-guide/settings/admin-tokens/api-keys) defined in runtime environment +- Python 3 +- strongDM Python module + +#### Steps: +You will need to update the following lines with entries that exist in your environment. +- line 9: Your SDM API key +- line 10: Your SDM Secret +- line 13: The role you wish to assign to a user +- line 15: The user you wish to assign the resources too, defined by their email address as it exists with in SDM +- Line 17 & 18: The start date and time for the access to begin +- Line 19 & 20: The end date and time for the access to be removed + +#### Notes: +- [SDM API Documentation](https://www.strongdm.com/docs/api) diff --git a/webinars/22-01-05_resource-grant/python-sdk-grants.py b/webinars/22-01-05_resource-grant/python-sdk-grants.py new file mode 100644 index 0000000..a3cfa2a --- /dev/null +++ b/webinars/22-01-05_resource-grant/python-sdk-grants.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +import strongdm, datetime +from datetime import timezone + +# Global Variables + +# API Key/Secret +api_key = "Your-API-Key-Here" +secret_key = "Your-Secret-Here" + +# Role to grant access to +roleName = "YourRoleName" +# User to grant the role to +user = 'YourEmail@Address.com' +# Grant start and end date/time +startDate = "2022-01-05" +startTime = "00:01:00" +endDate = "2022-01-06" +endTime = "00:01:00" + +def grant_access(): + # Gets SDM client based on the provided api key + client = strongdm.Client(api_key, secret_key) + # Gets the SDM Role specified in the roleName global variable + roleResponse = client.roles.list("name:{role}".format(role=roleName)) + # Sets the start and end date/time for the grant based on the global variables + start_date = startDate+"T"+startTime+"Z" + end_date = endDate+"T"+endTime+"Z" + s = datetime.datetime.strptime(start_date, '%Y-%m-%dT%H:%M:%SZ') + e = datetime.datetime.strptime(end_date, '%Y-%m-%dT%H:%M:%SZ') + start = s.replace(tzinfo=timezone.utc) + end = e.replace(tzinfo=timezone.utc) + #Get all SDM users and filter based on the user global variable + users = list(client.accounts.list('email:{}'.format(user))) + #Select that user from the returned users list + myUserID = users[0].id + # Adds each resource from the role to the user + for r in roleResponse: + # Using the Role, User id, gets a list of associated resource grants + rgResponse = client.role_grants.list( + 'role_id:{id}'.format(id=r.id)) + # Cycle through that list + for g in rgResponse: + # Create a temporary grant for each one + myGrant = strongdm.AccountGrant(resource_id='{}'.format(g.resource_id), account_id='{}'.format(myUserID), + start_from=start, valid_until=end) + # Assign the grant by "creating" it + try: + respGrant = client.account_grants.create(myGrant) + except Exception as ex: + print("\nSkipping user " + user + " because of error: " + str(ex)) + else: + print("\nGrant succeeded for user " + user + " to a resource in role " + + r.name + " from {} to {}".format(start, end)) + print('---\n') + +def main(): + grant_access() + +main() \ No newline at end of file