From b12356e600f950a716502a22f497569370f6f734 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Tue, 18 May 2021 09:05:59 -0400 Subject: [PATCH 01/87] Update SUPPORT.md First draft --- SUPPORT.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/SUPPORT.md b/SUPPORT.md index 1de6cef..e8be7f8 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,8 +1,5 @@ -# Garden Support +# Garden-contrib Support -Here's a list of options you could try: +This repository contains code and scripts written by strongDM staff, customers, and others. This code is 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. From 354f3f759e61ea314017251d0a3edde86eed65fc Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Tue, 18 May 2021 09:06:58 -0400 Subject: [PATCH 02/87] Update SUPPORT.md --- SUPPORT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SUPPORT.md b/SUPPORT.md index e8be7f8..5d26503 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -2,4 +2,4 @@ This repository contains code and scripts written by strongDM staff, customers, and others. This code is 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. -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. +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. From 90dd52a5c1c44a8ff3b0b382281e919a2649bf21 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Tue, 18 May 2021 09:09:27 -0400 Subject: [PATCH 03/87] Update SUPPORT.md --- SUPPORT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SUPPORT.md b/SUPPORT.md index 5d26503..b4e95a2 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,4 +1,4 @@ -# Garden-contrib Support +# strongDM Code Garden Contrib Support This repository contains code and scripts written by strongDM staff, customers, and others. This code is 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. From a369ddc80c97ba0290fcb47a6ee8b1362352a394 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Tue, 18 May 2021 09:13:02 -0400 Subject: [PATCH 04/87] Update SUPPORT.md --- SUPPORT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SUPPORT.md b/SUPPORT.md index b4e95a2..56f8489 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,5 +1,5 @@ -# strongDM Code Garden Contrib Support +# Garden Contrib Support -This repository contains code and scripts written by strongDM staff, customers, and others. This code is 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. +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. 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. From 816a2d28625aa748f0db291aedc057f826ea183c Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Tue, 18 May 2021 09:19:16 -0400 Subject: [PATCH 05/87] Update README-sample.md First update --- README-sample.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/README-sample.md b/README-sample.md index cc98e8e..d656bb6 100644 --- a/README-sample.md +++ b/README-sample.md @@ -1,22 +1,14 @@ -# Project Name +# Garden Contrib -Here you should include a description of the project and the problem it's solving using strongDM. +This repository is a "(code) garden" that is tended to by strongDM staff, customers, and others. This code may include scripts, code snippets written against the strongDM SDK's, Terraform templates, etc. ## 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. +We welcome contributions from customers, enthusiasts, or anyone interested in strongDM tech! Please refer to the [contributing](CONTRIBUTING.md) page. ## Support -Refer to the [support](SUPPORT.md) guidelines or dump part of the information here. +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. From 5ed7c68ab493cea27e9e59867d232cdb35f1d4bf Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Thu, 20 May 2021 15:01:37 -0400 Subject: [PATCH 06/87] Update README.md Update for public consumption --- README.md | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) 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. From e09c2b7cbd3fffdf24d22033760aa77d2248ea3a Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Thu, 20 May 2021 15:02:10 -0400 Subject: [PATCH 07/87] Delete README-sample.md --- README-sample.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 README-sample.md diff --git a/README-sample.md b/README-sample.md deleted file mode 100644 index d656bb6..0000000 --- a/README-sample.md +++ /dev/null @@ -1,14 +0,0 @@ -# Garden Contrib - -This repository is a "(code) garden" that is tended to by strongDM staff, customers, and others. This code may include scripts, code snippets written against the strongDM SDK's, Terraform templates, etc. - -## Table of Contents -* [Contributing](#contributing) -* [Support](#support) - -## Contributing -We welcome contributions from customers, enthusiasts, or anyone interested in strongDM tech! 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. - From 9a0358a47b4eb516c7a97d0388707b9a042626c3 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Thu, 20 May 2021 15:04:27 -0400 Subject: [PATCH 08/87] Update SUPPORT.md Changing language from garden > strongDM --- SUPPORT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SUPPORT.md b/SUPPORT.md index 56f8489..da670f4 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,4 +1,4 @@ -# Garden Contrib Support +# strongDM Contrib 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. From a89075690bd2093618c512f08eabd0da92e1bc33 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Thu, 20 May 2021 15:05:48 -0400 Subject: [PATCH 09/87] Update CONTRIBUTING.md First edit --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fea2377..99af670 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 From aa5e7ee48e27ea44760d49df4151f6058f15b5b1 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Thu, 20 May 2021 15:06:31 -0400 Subject: [PATCH 10/87] Update CONTRIBUTING.md Removed Auth0 link --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99af670..f580259 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. From 77d14281befacd3e35a0638d2231e08cf115e95e Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Thu, 20 May 2021 15:16:10 -0400 Subject: [PATCH 11/87] Delete docs directory Not needed here --- docs/.empty | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/.empty diff --git a/docs/.empty b/docs/.empty deleted file mode 100644 index 8b13789..0000000 --- a/docs/.empty +++ /dev/null @@ -1 +0,0 @@ - From 4953d97f8c63183420c135a8304bb95884b256c6 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Thu, 20 May 2021 15:46:20 -0400 Subject: [PATCH 12/87] Add Okta shim script --- authentication/okta/README.md | 41 ++ .../okta/okta-sync-update/matchers.yml | 10 + .../okta-sync-provisioning.go | 662 ++++++++++++++++++ 3 files changed, 713 insertions(+) create mode 100644 authentication/okta/README.md create mode 100644 authentication/okta/okta-sync-update/matchers.yml create mode 100644 authentication/okta/okta-sync-update/okta-sync-provisioning.go diff --git a/authentication/okta/README.md b/authentication/okta/README.md new file mode 100644 index 0000000..5644363 --- /dev/null +++ b/authentication/okta/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/authentication/okta/okta-sync-update/matchers.yml b/authentication/okta/okta-sync-update/matchers.yml new file mode 100644 index 0000000..e2a2adc --- /dev/null +++ b/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/authentication/okta/okta-sync-update/okta-sync-provisioning.go b/authentication/okta/okta-sync-update/okta-sync-provisioning.go new file mode 100644 index 0000000..b5e6b59 --- /dev/null +++ b/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) +} From bfbec01463b6e1946d80afa50a3260841da365a8 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Thu, 20 May 2021 15:49:51 -0400 Subject: [PATCH 13/87] Update to Okta folder docs --- authentication/okta/README.md | 42 +------------------ .../okta/okta-sync-update/README.md | 41 ++++++++++++++++++ 2 files changed, 42 insertions(+), 41 deletions(-) create mode 100644 authentication/okta/okta-sync-update/README.md diff --git a/authentication/okta/README.md b/authentication/okta/README.md index 5644363..85c339d 100644 --- a/authentication/okta/README.md +++ b/authentication/okta/README.md @@ -1,41 +1 @@ -## 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 +This folder contains code for use with strongDM and Okta. \ No newline at end of file diff --git a/authentication/okta/okta-sync-update/README.md b/authentication/okta/okta-sync-update/README.md new file mode 100644 index 0000000..5644363 --- /dev/null +++ b/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 From 87171ed639d772ae592d11ece316eaed3adab066 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Fri, 28 May 2021 15:45:45 -0400 Subject: [PATCH 14/87] Add script for extracting SSH logs --- logging/ssh/README.md | 36 ++++++++++++ logging/ssh/extract_ssh_logs.sh | 99 +++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 logging/ssh/README.md create mode 100755 logging/ssh/extract_ssh_logs.sh diff --git a/logging/ssh/README.md b/logging/ssh/README.md new file mode 100644 index 0000000..149dd0d --- /dev/null +++ b/logging/ssh/README.md @@ -0,0 +1,36 @@ +# Extract SSH Logs + +This folder contains a shell/python script that can be used for extracting SSH full logs + +## Requirements +* Python3 +* SDM logs + +## Configuration +You can adjust the following variables at the top of the script: +* LOG_DIR. Folder where the relay logs are located +* LOG_PATTERN. Logs name pattern + +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 diff --git a/logging/ssh/extract_ssh_logs.sh b/logging/ssh/extract_ssh_logs.sh new file mode 100755 index 0000000..9af626e --- /dev/null +++ b/logging/ssh/extract_ssh_logs.sh @@ -0,0 +1,99 @@ +#!/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}" # TODO Fix cmd_entry + total_elapsed_millis += elapsed_millis + + if not end_of_line(cmd_entry): + continue + + end_time_regular = str(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): + return datetime.datetime.strptime(input_date, "%Y-%m-%d %H:%M:%S.%f") + datetime.timedelta(milliseconds = input_millis) + +main() From 022859edeb27eb9b3105ac041074cc4f29d35ae4 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Sun, 30 May 2021 23:06:24 -0400 Subject: [PATCH 15/87] Update extract_ssh_logs.sh --- logging/ssh/extract_ssh_logs.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/logging/ssh/extract_ssh_logs.sh b/logging/ssh/extract_ssh_logs.sh index 9af626e..cbdb3fa 100755 --- a/logging/ssh/extract_ssh_logs.sh +++ b/logging/ssh/extract_ssh_logs.sh @@ -70,13 +70,13 @@ def print_session_logs(session_id, start_time, user): 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}" # TODO Fix cmd_entry + 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 = str(add_millis(start_time_regular, total_elapsed_millis)) + 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 = "" @@ -94,6 +94,7 @@ def end_of_line(cmd_entry): return True if "\n" in cmd_entry else False def add_millis(input_date, input_millis): - return datetime.datetime.strptime(input_date, "%Y-%m-%d %H:%M:%S.%f") + datetime.timedelta(milliseconds = 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() From d15356e62064251617062b56d52f30a149ff0da5 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Wed, 2 Jun 2021 11:38:48 -0400 Subject: [PATCH 16/87] Update README.md Added notes about admin token, and change required if your logging uses JSON --- logging/ssh/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/logging/ssh/README.md b/logging/ssh/README.md index 149dd0d..174df89 100644 --- a/logging/ssh/README.md +++ b/logging/ssh/README.md @@ -5,13 +5,14 @@ This folder contains a shell/python script that can be used for extracting SSH f ## 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 -You can adjust the following variables at the top of the script: -* LOG_DIR. Folder where the relay logs are located -* LOG_PATTERN. Logs name pattern - -Ideally, configure the script as a CRONJOB. For example (daily config): +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 * * * ``` @@ -34,3 +35,4 @@ s1rWwpo8PrK5nJU9GnI5y3hDGYF9,2021-04-22 15:28:58.871968,2021-04-22 15:29:02.2329 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` From 81dc344fa52880efbadf9eac32a8022f83904083 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 2 Jun 2021 14:33:22 -0400 Subject: [PATCH 17/87] Adding TF script for ssh_pub --- .gitignore | 2 ++ terraform/resources/ssh/README.md | 7 ++++ terraform/resources/ssh/main.tf | 51 ++++++++++++++++++++++++++++ terraform/resources/ssh/providers.tf | 11 ++++++ terraform/resources/ssh/variables.tf | 7 ++++ 5 files changed, 78 insertions(+) create mode 100644 .gitignore create mode 100644 terraform/resources/ssh/README.md create mode 100644 terraform/resources/ssh/main.tf create mode 100644 terraform/resources/ssh/providers.tf create mode 100644 terraform/resources/ssh/variables.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8084be8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +terraform/resources/.DS_Store +terraform/.DS_Store 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..599b8e4 --- /dev/null +++ b/terraform/resources/ssh/main.tf @@ -0,0 +1,51 @@ +data "sdm_ssh_ca_pubkey" "ssh_pubkey_query" { + # returns the SDM-generated CA for your org +} + +data "aws_ami" "ubuntu" { + most_recent = true + owners = ["099720109477"] # Canonical +} + +resource "aws_default_vpc" "default" { +} + +resource "aws_default_subnet" "default_az1" { + availability_zone = "${var.aws_region}a" +} + +# Create the EC2 instance +resource "aws_instance" "linux_instance" { + ami = data.aws_ami.ubuntu.id + subnet_id = aws_default_subnet.default_az1.id + + # security_groups = var.securityGroups + instance_type = "t2.micro" + + # Run commands to configure server with strongDM CA + user_data = <> /etc/ssh/sdm_ca.pub +chmod 0600 /etc/ssh/sdm_ca.pub +echo 'TrustedUserCAKeys /etc/ssh/sdm_ca.pub' | tee -a /etc/ssh/sshd_config +service ssh restart +USERDATA + + 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.public_dns + username = "ubuntu" + port = 22 + } +} \ 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/variables.tf b/terraform/resources/ssh/variables.tf new file mode 100644 index 0000000..ff4bd12 --- /dev/null +++ b/terraform/resources/ssh/variables.tf @@ -0,0 +1,7 @@ +variable "aws_region" { + default = "us-west-2" +} +variable "instanceName" { + default = "your-instance-name" +} + From dccc774890d191d87840538b051564c7591c08b0 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Thu, 3 Jun 2021 16:44:35 -0400 Subject: [PATCH 18/87] Adding health check script --- .gitignore | 1 + monitoring/resources/README.md | 13 ++++++ .../resources/datasource_healthcheck.py | 40 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 monitoring/resources/README.md create mode 100644 monitoring/resources/datasource_healthcheck.py diff --git a/.gitignore b/.gitignore index 8084be8..54032fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ terraform/resources/.DS_Store terraform/.DS_Store +.DS_Store 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.") + From 345f23d960967bb38ebf3b5d7256d84945e32067 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Fri, 4 Jun 2021 14:13:25 -0400 Subject: [PATCH 19/87] Remove "and" operator when generating ssh session files --- logging/ssh/extract_ssh_logs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logging/ssh/extract_ssh_logs.sh b/logging/ssh/extract_ssh_logs.sh index cbdb3fa..513a0dd 100755 --- a/logging/ssh/extract_ssh_logs.sh +++ b/logging/ssh/extract_ssh_logs.sh @@ -25,7 +25,7 @@ def main(): def generate_ssh_session_files(): ssh_split_cmd = f""" - rm *.ssh && \ + rm *.ssh find {LOG_DIR} -name '{LOG_PATTERN}' | while read line; do sdm ssh split $line 1>&2 done From 1c5603e418856ac7f1576002c9d37f9938c90d96 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Fri, 4 Jun 2021 14:30:07 -0400 Subject: [PATCH 20/87] Add okta sync multi group --- .../okta/okta-sync-multi-group/.gitignore | 1 + .../okta/okta-sync-multi-group/README.md | 32 + .../okta/okta-sync-multi-group/go.mod | 10 + .../okta/okta-sync-multi-group/go.sum | 121 ++++ .../okta/okta-sync-multi-group/main.go | 639 ++++++++++++++++++ .../okta/okta-sync-multi-group/matchers.yml | 14 + 6 files changed, 817 insertions(+) create mode 100644 authentication/okta/okta-sync-multi-group/.gitignore create mode 100644 authentication/okta/okta-sync-multi-group/README.md create mode 100644 authentication/okta/okta-sync-multi-group/go.mod create mode 100644 authentication/okta/okta-sync-multi-group/go.sum create mode 100644 authentication/okta/okta-sync-multi-group/main.go create mode 100644 authentication/okta/okta-sync-multi-group/matchers.yml diff --git a/authentication/okta/okta-sync-multi-group/.gitignore b/authentication/okta/okta-sync-multi-group/.gitignore new file mode 100644 index 0000000..3a8ce9c --- /dev/null +++ b/authentication/okta/okta-sync-multi-group/.gitignore @@ -0,0 +1 @@ +set-env.sh diff --git a/authentication/okta/okta-sync-multi-group/README.md b/authentication/okta/okta-sync-multi-group/README.md new file mode 100644 index 0000000..fa239c2 --- /dev/null +++ b/authentication/okta/okta-sync-multi-group/README.md @@ -0,0 +1,32 @@ +# 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 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. + +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. + +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.) + +## 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. diff --git a/authentication/okta/okta-sync-multi-group/go.mod b/authentication/okta/okta-sync-multi-group/go.mod new file mode 100644 index 0000000..c22d6db --- /dev/null +++ b/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/authentication/okta/okta-sync-multi-group/go.sum b/authentication/okta/okta-sync-multi-group/go.sum new file mode 100644 index 0000000..b2e5e17 --- /dev/null +++ b/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/authentication/okta/okta-sync-multi-group/main.go b/authentication/okta/okta-sync-multi-group/main.go new file mode 100644 index 0000000..708775d --- /dev/null +++ b/authentication/okta/okta-sync-multi-group/main.go @@ -0,0 +1,639 @@ +// 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" +) + +// modify this Okta query filter with any valid Okta API parameters to control user creation in SDM +var oktaQueryString = "profile.login sw \"rodolfo\" and (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)) + + 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) + + 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 = login + 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 + } + } + 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 { + 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 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/authentication/okta/okta-sync-multi-group/matchers.yml b/authentication/okta/okta-sync-multi-group/matchers.yml new file mode 100644 index 0000000..8f4fecc --- /dev/null +++ b/authentication/okta/okta-sync-multi-group/matchers.yml @@ -0,0 +1,14 @@ +--- +groups: + - + name: Other + resources: + - type:mysql + - + name: rodo-group-support + resources: + - type:mysql + - + name: rodo-group-engineering + resources: + - type:postgres From 00ce622bdfd4c2b8b6b1387177e0ef499073a4a0 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Fri, 4 Jun 2021 15:35:26 -0400 Subject: [PATCH 21/87] Add missing okta user information --- authentication/okta/okta-sync-multi-group/main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/authentication/okta/okta-sync-multi-group/main.go b/authentication/okta/okta-sync-multi-group/main.go index 708775d..cc6dc9b 100644 --- a/authentication/okta/okta-sync-multi-group/main.go +++ b/authentication/okta/okta-sync-multi-group/main.go @@ -231,7 +231,7 @@ func loadOktaUsers(ctx context.Context) (oktaUserList, error) { var users []oktaUser for _, u := range apiUsers { - login := (*u.Profile)["login"].(string) + profile := (*u.Profile) groups, _, err := client.User.ListUserGroups(u.Id, nil) if err != nil { @@ -244,7 +244,9 @@ func loadOktaUsers(ctx context.Context) (oktaUserList, error) { } var u oktaUser - u.Login = login + u.Login = profile["login"].(string) + u.FirstName = profile["firstName"].(string) + u.LastName = profile["lastName"].(string) u.Groups = groupNames users = append(users, u) } From 6ffe2e4e9d0013094faafcfc38ed3e5c3f6ae663 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Thu, 17 Jun 2021 06:43:37 -0400 Subject: [PATCH 22/87] Add create users only when assigned to a group listed in matchers.yml --- .../okta/okta-sync-multi-group/main.go | 22 +++++++++++++++++-- .../okta/okta-sync-multi-group/matchers.yml | 4 ++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/authentication/okta/okta-sync-multi-group/main.go b/authentication/okta/okta-sync-multi-group/main.go index cc6dc9b..957bb7f 100644 --- a/authentication/okta/okta-sync-multi-group/main.go +++ b/authentication/okta/okta-sync-multi-group/main.go @@ -31,8 +31,10 @@ import ( "gopkg.in/yaml.v2" ) +const OKTA_USERS_LIMIT = 500 + // modify this Okta query filter with any valid Okta API parameters to control user creation in SDM -var oktaQueryString = "profile.login sw \"rodolfo\" and (status eq \"ACTIVE\")" +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") @@ -222,7 +224,7 @@ func loadOktaUsers(ctx context.Context) (oktaUserList, error) { if err != nil { return nil, errors.Wrap(err, "invalid Okta configuration") } - search := query.NewQueryParams(query.WithSearch(oktaQueryString)) + search := query.NewQueryParams(query.WithSearch(oktaQueryString), query.WithLimit(OKTA_USERS_LIMIT)) apiUsers, _, err := client.User.ListUsers(search) if err != nil { @@ -344,6 +346,7 @@ func matchEntitlements(ctx context.Context, client *sdm.Client, matchers *Matche 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) } @@ -448,6 +451,10 @@ func syncUsers(ctx context.Context, client *sdm.Client, initialUsers userList, r 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) { @@ -495,6 +502,17 @@ func createMatchingUsers(ctx context.Context, client *sdm.Client, roles roleList 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 { diff --git a/authentication/okta/okta-sync-multi-group/matchers.yml b/authentication/okta/okta-sync-multi-group/matchers.yml index 8f4fecc..3eab7b2 100644 --- a/authentication/okta/okta-sync-multi-group/matchers.yml +++ b/authentication/okta/okta-sync-multi-group/matchers.yml @@ -12,3 +12,7 @@ groups: name: rodo-group-engineering resources: - type:postgres + - + name: rodo-group-data + resources: + - type:athena \ No newline at end of file From bd5907f508174d7b3cd091b30d560a77f663fac2 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Thu, 17 Jun 2021 10:56:25 -0400 Subject: [PATCH 23/87] Update docs --- authentication/okta/okta-sync-multi-group/README.md | 5 ++--- authentication/okta/okta-sync-multi-group/main.go | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/authentication/okta/okta-sync-multi-group/README.md b/authentication/okta/okta-sync-multi-group/README.md index fa239c2..aa45f6a 100644 --- a/authentication/okta/okta-sync-multi-group/README.md +++ b/authentication/okta/okta-sync-multi-group/README.md @@ -8,7 +8,7 @@ This script reads a separate JSON file, [matchers.yml](matchers.yml), which maps 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 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. +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. @@ -20,8 +20,6 @@ The script won't remove any Roles or Users in SDM, unless you use the flags: `-d > For example, the sample file in this folder would create strongDM Roles with access to all `mysql` and `postgres` resources in your organization. -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.) - ## Sample ``` $ go run . -delete-roles-not-in-okta -delete-users-not-in-okta @@ -30,3 +28,4 @@ $ go run . -delete-roles-not-in-okta -delete-users-not-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/authentication/okta/okta-sync-multi-group/main.go b/authentication/okta/okta-sync-multi-group/main.go index 957bb7f..016cd42 100644 --- a/authentication/okta/okta-sync-multi-group/main.go +++ b/authentication/okta/okta-sync-multi-group/main.go @@ -33,7 +33,6 @@ import ( const OKTA_USERS_LIMIT = 500 -// modify this Okta query filter with any valid Okta API parameters to control user creation in SDM var oktaQueryString = "(status eq \"ACTIVE\")" var jsonFlag = flag.Bool("json", false, "dump a JSON report for debugging") From de2de8c22cd9b89bad4853cff0c1e9604474c210 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Mon, 21 Jun 2021 11:58:29 -0400 Subject: [PATCH 24/87] Add user/roles sync with multiple Identity Providers - Include support for Google --- .../multi-idp/sync-update/.gitignore | 4 + .../multi-idp/sync-update/README.md | 67 ++ authentication/multi-idp/sync-update/go.mod | 12 + authentication/multi-idp/sync-update/go.sum | 527 ++++++++++++++ .../multi-idp/sync-update/google.go | 134 ++++ authentication/multi-idp/sync-update/main.go | 671 ++++++++++++++++++ .../multi-idp/sync-update/matchers.yml | 20 + authentication/multi-idp/sync-update/okta.go | 55 ++ 8 files changed, 1490 insertions(+) create mode 100644 authentication/multi-idp/sync-update/.gitignore create mode 100644 authentication/multi-idp/sync-update/README.md create mode 100644 authentication/multi-idp/sync-update/go.mod create mode 100644 authentication/multi-idp/sync-update/go.sum create mode 100644 authentication/multi-idp/sync-update/google.go create mode 100644 authentication/multi-idp/sync-update/main.go create mode 100644 authentication/multi-idp/sync-update/matchers.yml create mode 100644 authentication/multi-idp/sync-update/okta.go diff --git a/authentication/multi-idp/sync-update/.gitignore b/authentication/multi-idp/sync-update/.gitignore new file mode 100644 index 0000000..de3b0e2 --- /dev/null +++ b/authentication/multi-idp/sync-update/.gitignore @@ -0,0 +1,4 @@ +set-env.sh +credentials.json +token.json +__debug_bin diff --git a/authentication/multi-idp/sync-update/README.md b/authentication/multi-idp/sync-update/README.md new file mode 100644 index 0000000..5a64b2e --- /dev/null +++ b/authentication/multi-idp/sync-update/README.md @@ -0,0 +1,67 @@ +# Synchronize Users from different Identity Providers with Multiple Groups +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 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 +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 + -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 +``` + +Okta: +``` +$ 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 + +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`. \ No newline at end of file diff --git a/authentication/multi-idp/sync-update/go.mod b/authentication/multi-idp/sync-update/go.mod new file mode 100644 index 0000000..dd76bde --- /dev/null +++ b/authentication/multi-idp/sync-update/go.mod @@ -0,0 +1,12 @@ +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/strongdm/strongdm-sdk-go v0.9.30 + 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..4fc87ba --- /dev/null +++ b/authentication/multi-idp/sync-update/go.sum @@ -0,0 +1,527 @@ +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/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.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/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-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..31f8b92 --- /dev/null +++ b/authentication/multi-idp/sync-update/google.go @@ -0,0 +1,134 @@ +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" +) + +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..c112d86 --- /dev/null +++ b/authentication/multi-idp/sync-update/main.go @@ -0,0 +1,671 @@ +// 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/pkg/errors" + sdm "github.com/strongdm/strongdm-sdk-go" + "gopkg.in/yaml.v2" +) + +const USERS_LIMIT = 500 + +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") + +// 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) + } +} + +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() + + matchers, err := loadMatchers() + if err != nil { + fmt.Fprintf(os.Stderr, "error loading Matchers users: %v\n", err) + os.Exit(1) + return + } + rpt.Matchers = matchers + + idpUsers, err := loadIdpUsers(ctx, matchers) // TODO Calculate root!! and return error when multiple root + 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) + + 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.SDMRolesInIdP = matchingRoles + rpt.SDMRoleInIdPCount = len(matchingRoles) + rpt.SDMRolesNotInIdP = unmatchingRoles + rpt.SDMRoleNotInIdPCount = len(unmatchingRoles) + + 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) + } + + 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() + 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) { + fmt.Fprintf(os.Stderr, "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) { + fmt.Fprintf(os.Stderr, "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..98d1836 --- /dev/null +++ b/authentication/multi-idp/sync-update/matchers.yml @@ -0,0 +1,20 @@ +--- +groups: + - + name: General + # Only one element can be root + root: true + 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/authentication/multi-idp/sync-update/okta.go b/authentication/multi-idp/sync-update/okta.go new file mode 100644 index 0000000..bf24d09 --- /dev/null +++ b/authentication/multi-idp/sync-update/okta.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "os" + + "github.com/okta/okta-sdk-golang/okta" + "github.com/okta/okta-sdk-golang/okta/query" + "github.com/pkg/errors" +) + +const QUERY_STRING = "(status eq \"ACTIVE\")" + +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") + } + search := query.NewQueryParams(query.WithSearch(QUERY_STRING), query.WithLimit(USERS_LIMIT)) + + apiUsers, _, err := client.User.ListUsers(search) + if err != nil { + return nil, errors.Wrap(err, "unable to retrieve okta users") + } + + var users []idpUser + 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 idpUser + 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 +} From 27bfcb85d8818cd4a02c7f1e0e6475e481084cda Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Mon, 21 Jun 2021 12:07:51 -0400 Subject: [PATCH 25/87] Update README.md --- authentication/multi-idp/sync-update/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authentication/multi-idp/sync-update/README.md b/authentication/multi-idp/sync-update/README.md index 5a64b2e..e300375 100644 --- a/authentication/multi-idp/sync-update/README.md +++ b/authentication/multi-idp/sync-update/README.md @@ -18,7 +18,7 @@ The script won't remove any Roles or Users in SDM, unless you use the flags: `-d 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 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. +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. @@ -64,4 +64,4 @@ Considerations: 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`. \ No newline at end of file +* Google uses paths for OrgUnits. For indicating `/` use the flag Root in `matchers.yml`. From 1253346e9c3a0b797bc0d7df0a6ad6f1f45e5ea4 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Mon, 21 Jun 2021 12:10:21 -0400 Subject: [PATCH 26/87] Update README.md --- authentication/multi-idp/sync-update/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authentication/multi-idp/sync-update/README.md b/authentication/multi-idp/sync-update/README.md index e300375..9c8437b 100644 --- a/authentication/multi-idp/sync-update/README.md +++ b/authentication/multi-idp/sync-update/README.md @@ -1,4 +1,4 @@ -# Synchronize Users from different Identity Providers with Multiple Groups +# Synchronize Users from different Identity Providers 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 @@ -46,7 +46,7 @@ $ go run . -okta -delete-unmatching-roles -delete-unmatching-users 5 IdP users, 3 strongDM users in IdP, 3 strongDM roles in Idp ``` -Okta: +Google: ``` $ go run . -google -delete-unmatching-roles -delete-unmatching-users 5 IdP users, 3 strongDM users in IdP, 3 strongDM roles in Idp From e45f5a3cd8504e85635c7c50fb2572fbd359d03d Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Mon, 21 Jun 2021 12:11:18 -0400 Subject: [PATCH 27/87] Update README.md --- authentication/multi-idp/sync-update/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authentication/multi-idp/sync-update/README.md b/authentication/multi-idp/sync-update/README.md index 9c8437b..8832589 100644 --- a/authentication/multi-idp/sync-update/README.md +++ b/authentication/multi-idp/sync-update/README.md @@ -60,7 +60,7 @@ Considerations: 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 + * A user can only be assigned to one OrgUnit at a time Considerations: * Reference: https://developers.google.com/admin-sdk/directory/v1/quickstart/go From 7ce4e613c1c621299b56998aa03911f30d7149c8 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Mon, 21 Jun 2021 14:12:05 -0400 Subject: [PATCH 28/87] Remove stale TODO comment --- authentication/multi-idp/sync-update/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authentication/multi-idp/sync-update/main.go b/authentication/multi-idp/sync-update/main.go index c112d86..d78849b 100644 --- a/authentication/multi-idp/sync-update/main.go +++ b/authentication/multi-idp/sync-update/main.go @@ -131,7 +131,7 @@ func main() { } rpt.Matchers = matchers - idpUsers, err := loadIdpUsers(ctx, matchers) // TODO Calculate root!! and return error when multiple root + idpUsers, err := loadIdpUsers(ctx, matchers) if err != nil { fmt.Fprintf(os.Stderr, "error loading users from IdP: %v\n", err) os.Exit(1) From 5012976a12b15f3b150073e9bf47e93acc6989e1 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Tue, 22 Jun 2021 05:57:51 -0400 Subject: [PATCH 29/87] Restructure repo layout - Create archive folder --- archive/README.md | 5 +++++ {authentication => archive/authentication}/okta/README.md | 0 .../authentication}/okta/okta-sync-multi-group/.gitignore | 0 .../authentication}/okta/okta-sync-multi-group/README.md | 0 .../authentication}/okta/okta-sync-multi-group/go.mod | 0 .../authentication}/okta/okta-sync-multi-group/go.sum | 0 .../authentication}/okta/okta-sync-multi-group/main.go | 0 .../authentication}/okta/okta-sync-multi-group/matchers.yml | 0 .../authentication}/okta/okta-sync-update/README.md | 0 .../authentication}/okta/okta-sync-update/matchers.yml | 0 .../okta/okta-sync-update/okta-sync-provisioning.go | 0 authentication/README.md | 3 +++ 12 files changed, 8 insertions(+) create mode 100644 archive/README.md rename {authentication => archive/authentication}/okta/README.md (100%) rename {authentication => archive/authentication}/okta/okta-sync-multi-group/.gitignore (100%) rename {authentication => archive/authentication}/okta/okta-sync-multi-group/README.md (100%) rename {authentication => archive/authentication}/okta/okta-sync-multi-group/go.mod (100%) rename {authentication => archive/authentication}/okta/okta-sync-multi-group/go.sum (100%) rename {authentication => archive/authentication}/okta/okta-sync-multi-group/main.go (100%) rename {authentication => archive/authentication}/okta/okta-sync-multi-group/matchers.yml (100%) rename {authentication => archive/authentication}/okta/okta-sync-update/README.md (100%) rename {authentication => archive/authentication}/okta/okta-sync-update/matchers.yml (100%) rename {authentication => archive/authentication}/okta/okta-sync-update/okta-sync-provisioning.go (100%) create mode 100644 authentication/README.md diff --git a/archive/README.md b/archive/README.md new file mode 100644 index 0000000..b57fa4d --- /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 for you. diff --git a/authentication/okta/README.md b/archive/authentication/okta/README.md similarity index 100% rename from authentication/okta/README.md rename to archive/authentication/okta/README.md diff --git a/authentication/okta/okta-sync-multi-group/.gitignore b/archive/authentication/okta/okta-sync-multi-group/.gitignore similarity index 100% rename from authentication/okta/okta-sync-multi-group/.gitignore rename to archive/authentication/okta/okta-sync-multi-group/.gitignore diff --git a/authentication/okta/okta-sync-multi-group/README.md b/archive/authentication/okta/okta-sync-multi-group/README.md similarity index 100% rename from authentication/okta/okta-sync-multi-group/README.md rename to archive/authentication/okta/okta-sync-multi-group/README.md diff --git a/authentication/okta/okta-sync-multi-group/go.mod b/archive/authentication/okta/okta-sync-multi-group/go.mod similarity index 100% rename from authentication/okta/okta-sync-multi-group/go.mod rename to archive/authentication/okta/okta-sync-multi-group/go.mod diff --git a/authentication/okta/okta-sync-multi-group/go.sum b/archive/authentication/okta/okta-sync-multi-group/go.sum similarity index 100% rename from authentication/okta/okta-sync-multi-group/go.sum rename to archive/authentication/okta/okta-sync-multi-group/go.sum diff --git a/authentication/okta/okta-sync-multi-group/main.go b/archive/authentication/okta/okta-sync-multi-group/main.go similarity index 100% rename from authentication/okta/okta-sync-multi-group/main.go rename to archive/authentication/okta/okta-sync-multi-group/main.go diff --git a/authentication/okta/okta-sync-multi-group/matchers.yml b/archive/authentication/okta/okta-sync-multi-group/matchers.yml similarity index 100% rename from authentication/okta/okta-sync-multi-group/matchers.yml rename to archive/authentication/okta/okta-sync-multi-group/matchers.yml diff --git a/authentication/okta/okta-sync-update/README.md b/archive/authentication/okta/okta-sync-update/README.md similarity index 100% rename from authentication/okta/okta-sync-update/README.md rename to archive/authentication/okta/okta-sync-update/README.md diff --git a/authentication/okta/okta-sync-update/matchers.yml b/archive/authentication/okta/okta-sync-update/matchers.yml similarity index 100% rename from authentication/okta/okta-sync-update/matchers.yml rename to archive/authentication/okta/okta-sync-update/matchers.yml diff --git a/authentication/okta/okta-sync-update/okta-sync-provisioning.go b/archive/authentication/okta/okta-sync-update/okta-sync-provisioning.go similarity index 100% rename from authentication/okta/okta-sync-update/okta-sync-provisioning.go rename to archive/authentication/okta/okta-sync-update/okta-sync-provisioning.go 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. From d5255d6b47b4961e40cef0b16769b8d2804ae9ae Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Tue, 22 Jun 2021 06:01:00 -0400 Subject: [PATCH 30/87] Update README.md --- archive/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archive/README.md b/archive/README.md index b57fa4d..6b81d35 100644 --- a/archive/README.md +++ b/archive/README.md @@ -2,4 +2,4 @@ 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 for you. +All changes are reflected in the Git history, we just want to make easier the finding process. From b242c1af9044c4dd00b5851f58ff0db4565094d4 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Mon, 28 Jun 2021 07:25:24 -0400 Subject: [PATCH 31/87] Add pagination to LoadOktaUsers --- .../multi-idp/sync-update/.gitignore | 1 + authentication/multi-idp/sync-update/go.mod | 1 + authentication/multi-idp/sync-update/go.sum | 2 + .../multi-idp/sync-update/google.go | 3 + authentication/multi-idp/sync-update/main.go | 4 +- authentication/multi-idp/sync-update/okta.go | 69 +++++++++++++------ 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/authentication/multi-idp/sync-update/.gitignore b/authentication/multi-idp/sync-update/.gitignore index de3b0e2..a7ede26 100644 --- a/authentication/multi-idp/sync-update/.gitignore +++ b/authentication/multi-idp/sync-update/.gitignore @@ -2,3 +2,4 @@ set-env.sh credentials.json token.json __debug_bin +.vscode/ diff --git a/authentication/multi-idp/sync-update/go.mod b/authentication/multi-idp/sync-update/go.mod index dd76bde..e8f3cdb 100644 --- a/authentication/multi-idp/sync-update/go.mod +++ b/authentication/multi-idp/sync-update/go.mod @@ -6,6 +6,7 @@ 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.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 index 4fc87ba..b30236d 100644 --- a/authentication/multi-idp/sync-update/go.sum +++ b/authentication/multi-idp/sync-update/go.sum @@ -168,6 +168,8 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd 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= diff --git a/authentication/multi-idp/sync-update/google.go b/authentication/multi-idp/sync-update/google.go index 31f8b92..ce5063c 100644 --- a/authentication/multi-idp/sync-update/google.go +++ b/authentication/multi-idp/sync-update/google.go @@ -18,6 +18,9 @@ import ( "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 { diff --git a/authentication/multi-idp/sync-update/main.go b/authentication/multi-idp/sync-update/main.go index d78849b..c9da117 100644 --- a/authentication/multi-idp/sync-update/main.go +++ b/authentication/multi-idp/sync-update/main.go @@ -29,8 +29,6 @@ import ( "gopkg.in/yaml.v2" ) -const USERS_LIMIT = 500 - 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") @@ -131,7 +129,7 @@ func main() { } rpt.Matchers = matchers - idpUsers, err := loadIdpUsers(ctx, matchers) + idpUsers, err := loadIdpUsers(ctx, matchers) if err != nil { fmt.Fprintf(os.Stderr, "error loading users from IdP: %v\n", err) os.Exit(1) diff --git a/authentication/multi-idp/sync-update/okta.go b/authentication/multi-idp/sync-update/okta.go index bf24d09..1a6f3c6 100644 --- a/authentication/multi-idp/sync-update/okta.go +++ b/authentication/multi-idp/sync-update/okta.go @@ -2,14 +2,18 @@ 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") == "" { @@ -23,33 +27,58 @@ func LoadOktaUsers(ctx context.Context) (idpUserList, error) { if err != nil { return nil, errors.Wrap(err, "invalid Okta configuration") } - search := query.NewQueryParams(query.WithSearch(QUERY_STRING), query.WithLimit(USERS_LIMIT)) - - apiUsers, _, err := client.User.ListUsers(search) - if err != nil { - return nil, errors.Wrap(err, "unable to retrieve okta users") - } var users []idpUser - for _, u := range apiUsers { - profile := (*u.Profile) - - groups, _, err := client.User.ListUserGroups(u.Id, nil) + 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 user groups") + return nil, errors.Wrap(err, "unable to retrieve okta users") } - var groupNames []string - for _, g := range groups { - groupNames = append(groupNames, g.Profile.Name) + 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, + }) } - var u idpUser - u.Login = profile["login"].(string) - u.FirstName = profile["firstName"].(string) - u.LastName = profile["lastName"].(string) - u.Groups = groupNames - users = append(users, u) + 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 +} From 8db87568ba0395870afa59696f40b359c92330b0 Mon Sep 17 00:00:00 2001 From: John Turner Date: Mon, 28 Jun 2021 13:28:07 -0500 Subject: [PATCH 32/87] Adding in some Ansible examples for SDM and AWS --- ansible/aws_playbooks/README.md | 55 +++++++++++++ .../aws-self-register-gateway.yml | 56 +++++++++++++ .../aws-self-register-ssh.yml | 58 ++++++++++++++ .../scripts/sdm-gatewayadd.sh | 10 +++ .../scripts/sdm-sshadd.sh | 12 +++ .../playbooks/sdm_gateway_install.yml | 60 ++++++++++++++ .../playbooks/sdm_pub_cert_ssh_install.yml | 80 +++++++++++++++++++ .../playbooks/sdm_relay_install.yml | 62 ++++++++++++++ .../playbooks/sdm_ssh_install.yml | 66 +++++++++++++++ 9 files changed, 459 insertions(+) create mode 100644 ansible/aws_playbooks/README.md create mode 100644 ansible/aws_playbooks/aws_self_register_playbooks/aws-self-register-gateway.yml create mode 100644 ansible/aws_playbooks/aws_self_register_playbooks/aws-self-register-ssh.yml create mode 100644 ansible/aws_playbooks/aws_self_register_playbooks/scripts/sdm-gatewayadd.sh create mode 100644 ansible/aws_playbooks/aws_self_register_playbooks/scripts/sdm-sshadd.sh create mode 100644 ansible/aws_playbooks/playbooks/sdm_gateway_install.yml create mode 100644 ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml create mode 100644 ansible/aws_playbooks/playbooks/sdm_relay_install.yml create mode 100644 ansible/aws_playbooks/playbooks/sdm_ssh_install.yml 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..2c5d595 --- /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/latestmeta-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 From b68ad47b2b22888069fcb9fd4867461476c00af7 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Tue, 6 Jul 2021 16:56:49 -0400 Subject: [PATCH 33/87] Adding k8s impersonation automation script --- logging/kubernetes/impersonation/README.md | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 logging/kubernetes/impersonation/README.md diff --git a/logging/kubernetes/impersonation/README.md b/logging/kubernetes/impersonation/README.md new file mode 100644 index 0000000..73b3d49 --- /dev/null +++ b/logging/kubernetes/impersonation/README.md @@ -0,0 +1,23 @@ +# 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 +* SDM logs +* 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. From cad34137d02912d0275d45b6f08861dd8d898450 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Tue, 6 Jul 2021 16:57:11 -0400 Subject: [PATCH 34/87] Add'l files for k8s impersonation --- logging/kubernetes/impersonation/k8s_auto.py | 79 +++++++++++++++++++ .../kubernetes/impersonation/requirements.txt | 2 + 2 files changed, 81 insertions(+) create mode 100644 logging/kubernetes/impersonation/k8s_auto.py create mode 100644 logging/kubernetes/impersonation/requirements.txt diff --git a/logging/kubernetes/impersonation/k8s_auto.py b/logging/kubernetes/impersonation/k8s_auto.py new file mode 100644 index 0000000..9aee954 --- /dev/null +++ b/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/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 From 9d8f798384abec054036fcc936b8fd85d5ea3750 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Tue, 6 Jul 2021 16:58:02 -0400 Subject: [PATCH 35/87] Update README.md --- logging/kubernetes/impersonation/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/logging/kubernetes/impersonation/README.md b/logging/kubernetes/impersonation/README.md index 73b3d49..53db434 100644 --- a/logging/kubernetes/impersonation/README.md +++ b/logging/kubernetes/impersonation/README.md @@ -4,7 +4,6 @@ This folder contains a Python script that helps with automation of roles for Kub ## Requirements * Python3 -* SDM logs * 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 From 0dbe3912a38adf4321dae4cfaac8f6dd727584f5 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 21 Jul 2021 11:31:20 -0400 Subject: [PATCH 36/87] Add script to list role resources --- audit/roles/role_resources.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 audit/roles/role_resources.py diff --git a/audit/roles/role_resources.py b/audit/roles/role_resources.py new file mode 100644 index 0000000..831a8b3 --- /dev/null +++ b/audit/roles/role_resources.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +import strongdm, os + +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_details(): + roleResponse = list(client.roles.list("")) + print_border() + for r in roleResponse: + print("Role name: \"" + r.name + "\" includes the following resources:") + rgResponse = client.role_grants.list('role_id:{id}'.format(id=r.id) ) + + for g in rgResponse: + res = list(client.resources.list("id:{}".format(g.resource_id))) + print("\t" + res[0].name) + print_border() + +def print_border(): + print("~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=") + +def main(): + get_role_details() + +if __name__ == "__main__": + main() + + + + + + From 6a478ece93f9ff5ffe547656e0bafcfae74e690c Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 21 Jul 2021 11:37:56 -0400 Subject: [PATCH 37/87] Add README for role list script --- audit/roles/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 audit/roles/README.md diff --git a/audit/roles/README.md b/audit/roles/README.md new file mode 100644 index 0000000..35fa18a --- /dev/null +++ b/audit/roles/README.md @@ -0,0 +1,13 @@ +# 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 + +## Usage +* Run `pip install strongdm` +* Run the script e.g. `python role_resources.py`. From 1d2265fc57b567e1ee530a512b60385d2eb862d8 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Wed, 21 Jul 2021 11:53:43 -0400 Subject: [PATCH 38/87] Add check for beta resources when getting resources --- authentication/multi-idp/sync-update/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/authentication/multi-idp/sync-update/main.go b/authentication/multi-idp/sync-update/main.go index c9da117..2881f2e 100644 --- a/authentication/multi-idp/sync-update/main.go +++ b/authentication/multi-idp/sync-update/main.go @@ -348,6 +348,9 @@ func matchEntitlements(ctx context.Context, client *sdm.Client, matchers *Matche } 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 { From 336e9aab8bd97e2b1950c8cad1643231d744a189 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 21 Jul 2021 12:00:12 -0400 Subject: [PATCH 39/87] fixed case issues --- audit/roles/role_resources.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/audit/roles/role_resources.py b/audit/roles/role_resources.py index 831a8b3..9b497db 100644 --- a/audit/roles/role_resources.py +++ b/audit/roles/role_resources.py @@ -8,13 +8,13 @@ client = strongdm.Client(access_key, secret_key) def get_role_details(): - roleResponse = list(client.roles.list("")) + role_response = list(client.roles.list("")) print_border() - for r in roleResponse: + for r in role_response: print("Role name: \"" + r.name + "\" includes the following resources:") - rgResponse = client.role_grants.list('role_id:{id}'.format(id=r.id) ) + rg_response = client.role_grants.list('role_id:{id}'.format(id=r.id) ) - for g in rgResponse: + for g in rg_response: res = list(client.resources.list("id:{}".format(g.resource_id))) print("\t" + res[0].name) print_border() From 1da6077a9e562e85a4c96a16560161692ed4cc1a Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Thu, 29 Jul 2021 10:59:14 -0400 Subject: [PATCH 40/87] Add logging flag --- .../multi-idp/sync-update/README.md | 2 ++ authentication/multi-idp/sync-update/go.mod | 1 + authentication/multi-idp/sync-update/go.sum | 4 ++++ authentication/multi-idp/sync-update/main.go | 21 +++++++++++++++++-- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/authentication/multi-idp/sync-update/README.md b/authentication/multi-idp/sync-update/README.md index 8832589..b0d4874 100644 --- a/authentication/multi-idp/sync-update/README.md +++ b/authentication/multi-idp/sync-update/README.md @@ -34,6 +34,8 @@ $ go run . -help use Google as IdP -json dump a JSON report for debugging + -log + include logging information -okta use Okta as IdP -plan diff --git a/authentication/multi-idp/sync-update/go.mod b/authentication/multi-idp/sync-update/go.mod index e8f3cdb..4994078 100644 --- a/authentication/multi-idp/sync-update/go.mod +++ b/authentication/multi-idp/sync-update/go.mod @@ -5,6 +5,7 @@ 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 diff --git a/authentication/multi-idp/sync-update/go.sum b/authentication/multi-idp/sync-update/go.sum index b30236d..b7f732b 100644 --- a/authentication/multi-idp/sync-update/go.sum +++ b/authentication/multi-idp/sync-update/go.sum @@ -158,10 +158,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb 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= @@ -297,6 +300,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w 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= diff --git a/authentication/multi-idp/sync-update/main.go b/authentication/multi-idp/sync-update/main.go index 2881f2e..981e518 100644 --- a/authentication/multi-idp/sync-update/main.go +++ b/authentication/multi-idp/sync-update/main.go @@ -24,6 +24,8 @@ import ( "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/pkg/errors" sdm "github.com/strongdm/strongdm-sdk-go" "gopkg.in/yaml.v2" @@ -31,6 +33,7 @@ import ( 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") @@ -46,6 +49,10 @@ func init() { 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 { @@ -120,6 +127,7 @@ func main() { var rpt syncReport rpt.Start = time.Now() + log.Info("Starting sync...") matchers, err := loadMatchers() if err != nil { @@ -128,6 +136,7 @@ func main() { return } rpt.Matchers = matchers + log.Infof("Loaded %d matchers", len(matchers.Groups)) idpUsers, err := loadIdpUsers(ctx, matchers) if err != nil { @@ -137,6 +146,7 @@ func main() { } rpt.IdPUsers = idpUsers rpt.IdPUserCount = len(idpUsers) + log.Infof("Loaded %d users from IdP", rpt.IdPUserCount) initialRoles, err := loadRoles(ctx, client) if err != nil { @@ -144,6 +154,7 @@ func main() { os.Exit(1) return } + log.Infof("Loaded %d initial roles from SDM", len(initialRoles)) initialUsers, err := loadUsers(ctx, client) if err != nil { @@ -151,8 +162,10 @@ func main() { 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) @@ -163,6 +176,8 @@ func main() { 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 { @@ -174,6 +189,8 @@ func main() { 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() @@ -463,13 +480,13 @@ func createMatchingUsers(ctx context.Context, client *sdm.Client, roles roleList matchingUsers := userList{} for _, idpUser := range idpUsers { if !idpUserHasMatchingGroup(idpUser, matchers) { - fmt.Fprintf(os.Stderr, "ignoring user %s - no group in matchers assigned to it\n", idpUser.Login) + 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) { - fmt.Fprintf(os.Stderr, "ignoring user %s - might be assigned to a different org\n", idpUser.Login) + log.Infof("ignoring user %s - might be assigned to a different org\n", idpUser.Login) continue } if err != nil { From 20c094508cd2da4d23b24e7a2eeb120f2c274106 Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Thu, 29 Jul 2021 11:46:34 -0400 Subject: [PATCH 41/87] Add comments to matchers file --- authentication/multi-idp/sync-update/matchers.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/authentication/multi-idp/sync-update/matchers.yml b/authentication/multi-idp/sync-update/matchers.yml index 98d1836..d0c7763 100644 --- a/authentication/multi-idp/sync-update/matchers.yml +++ b/authentication/multi-idp/sync-update/matchers.yml @@ -1,10 +1,12 @@ --- +# 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 From 9827e93e7f07520cdd02c88519d408895bd31701 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 15 Sep 2021 12:37:26 -0400 Subject: [PATCH 42/87] Adding gateway resources script --- monitoring/gateways/README.md | 11 +++++++++++ monitoring/gateways/gateway-monitor.sh | 15 +++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 monitoring/gateways/README.md create mode 100644 monitoring/gateways/gateway-monitor.sh 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 From bcc95035b28c5ce639ea34dd43dbf1edb439a095 Mon Sep 17 00:00:00 2001 From: wallrony Date: Mon, 20 Sep 2021 16:30:17 -0300 Subject: [PATCH 43/87] Add contributed_automations --- automation/contributed_automations/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 automation/contributed_automations/README.md diff --git a/automation/contributed_automations/README.md b/automation/contributed_automations/README.md new file mode 100644 index 0000000..dcaf396 --- /dev/null +++ b/automation/contributed_automations/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). + +- [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 +- [Export Logs with Lifecycle Hooks](https://www.strongdm.com/docs/automation/contributed/export-logs-lifecycle-hooks), contributed by John Turner +- [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/) From 1bd9cbbc3d84313a86211a756ff2fb971683c8f2 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Mon, 20 Sep 2021 16:41:57 -0400 Subject: [PATCH 44/87] Updating k8s UI script to multi-role --- .../kubernetes/impersonation/README.md | 26 +++++++++ .../kubernetes/impersonation/k8s_auto.py | 0 .../kubernetes/impersonation/requirements.txt | 2 + logging/kubernetes/impersonation/README.md | 4 ++ .../impersonation/k8s_auto_multi-role.py | 55 +++++++++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 archive/logging/kubernetes/impersonation/README.md rename {logging => archive/logging}/kubernetes/impersonation/k8s_auto.py (100%) create mode 100644 archive/logging/kubernetes/impersonation/requirements.txt create mode 100644 logging/kubernetes/impersonation/k8s_auto_multi-role.py diff --git a/archive/logging/kubernetes/impersonation/README.md b/archive/logging/kubernetes/impersonation/README.md new file mode 100644 index 0000000..e0089d6 --- /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](../../../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/logging/kubernetes/impersonation/k8s_auto.py b/archive/logging/kubernetes/impersonation/k8s_auto.py similarity index 100% rename from logging/kubernetes/impersonation/k8s_auto.py rename to archive/logging/kubernetes/impersonation/k8s_auto.py 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/logging/kubernetes/impersonation/README.md b/logging/kubernetes/impersonation/README.md index 53db434..2f34a8a 100644 --- a/logging/kubernetes/impersonation/README.md +++ b/logging/kubernetes/impersonation/README.md @@ -1,3 +1,7 @@ +### 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](../../archive/logging/kubernetes) 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. 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..96e7ae4 --- /dev/null +++ b/logging/kubernetes/impersonation/k8s_auto_multi-role.py @@ -0,0 +1,55 @@ +#!/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'] + +logging.basicConfig(level = logging.INFO) + +# In this multi-role version, both "r" and "m" keys/values are required +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) + print("Role assignment succeeded") + except Exception as ex: + print("Skipping role assignment because of error: " + str(ex)) + raise 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) + print("Copying users from Role " + args.role + " to Role "+ args.map + " succeeded.") + except Exception as ex: + print("Copying users from Role " + args.role + " to Role "+ args.map + " failed!") + +if __name__ == "__main__": + main() \ No newline at end of file From 59fb5c4bf5af378b98f319cadb1bc186aea1b437 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Mon, 20 Sep 2021 16:45:41 -0400 Subject: [PATCH 45/87] updated README links --- archive/logging/kubernetes/impersonation/README.md | 2 +- logging/kubernetes/impersonation/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/archive/logging/kubernetes/impersonation/README.md b/archive/logging/kubernetes/impersonation/README.md index e0089d6..0b970c7 100644 --- a/archive/logging/kubernetes/impersonation/README.md +++ b/archive/logging/kubernetes/impersonation/README.md @@ -1,6 +1,6 @@ # 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](../../../logging/kubernetes/impersonation). If your strongDM organization has not yet been transitioned to this system, you may continue to use this script. +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 diff --git a/logging/kubernetes/impersonation/README.md b/logging/kubernetes/impersonation/README.md index 2f34a8a..0c00e6f 100644 --- a/logging/kubernetes/impersonation/README.md +++ b/logging/kubernetes/impersonation/README.md @@ -1,6 +1,6 @@ ### 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](../../archive/logging/kubernetes) to use the older code for Composite Roles. +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 From 26a8189048db18ba0c958b3e01bc4b7d2783b505 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Mon, 20 Sep 2021 16:50:18 -0400 Subject: [PATCH 46/87] README update --- logging/kubernetes/impersonation/README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/logging/kubernetes/impersonation/README.md b/logging/kubernetes/impersonation/README.md index 0c00e6f..c2aa768 100644 --- a/logging/kubernetes/impersonation/README.md +++ b/logging/kubernetes/impersonation/README.md @@ -16,11 +16,6 @@ This folder contains a Python script that helps with automation of roles for Kub ## 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. +* 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. -## Considerations: -* If the Composite Role already exists, we assume this script has been run before. The script will warn and exit. From 5746ecab5c128304bffed9570861c49c28ddaffc Mon Sep 17 00:00:00 2001 From: wallrony Date: Tue, 21 Sep 2021 11:34:02 -0300 Subject: [PATCH 47/87] Change automations folder name to external --- {automation/contributed_automations => external}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {automation/contributed_automations => external}/README.md (100%) diff --git a/automation/contributed_automations/README.md b/external/README.md similarity index 100% rename from automation/contributed_automations/README.md rename to external/README.md From 5b5dacde4e7232f0a5c17cd4769d2b2bdc55a25a Mon Sep 17 00:00:00 2001 From: David Petersen Date: Tue, 21 Sep 2021 12:35:54 -0400 Subject: [PATCH 48/87] Better error handling for k8s MR script --- .../kubernetes/impersonation/k8s_auto_multi-role.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/logging/kubernetes/impersonation/k8s_auto_multi-role.py b/logging/kubernetes/impersonation/k8s_auto_multi-role.py index 96e7ae4..573b1be 100644 --- a/logging/kubernetes/impersonation/k8s_auto_multi-role.py +++ b/logging/kubernetes/impersonation/k8s_auto_multi-role.py @@ -5,9 +5,12 @@ 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) @@ -31,10 +34,10 @@ def transfer_users(role, id): k8s_attachment = strongdm.AccountAttachment(account_id=a.account_id, role_id=id) try: respGrant = client.account_attachments.create(k8s_attachment) - print("Role assignment succeeded") + logging.info('Role assignment to' + a.account_id + ' succeeded.') except Exception as ex: - print("Skipping role assignment because of error: " + str(ex)) - raise 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 = "" @@ -47,9 +50,9 @@ def main(): my_id = get_k8s_role(args.map) try: transfer_users(args.role, my_id) - print("Copying users from Role " + args.role + " to Role "+ args.map + " succeeded.") + logging.info("Script execution is complete.") except Exception as ex: - print("Copying users from Role " + args.role + " to Role "+ args.map + " failed!") + logging.error("Script failed to complete, because of error: " + str(ex)) if __name__ == "__main__": main() \ No newline at end of file From 51dba185fd1915ba2e612e2e02b97f05fb112dba Mon Sep 17 00:00:00 2001 From: wallrony Date: Tue, 21 Sep 2021 14:48:53 -0300 Subject: [PATCH 49/87] =?UTF-8?q?Add=20John=E2=80=99s=20contrib=20instruct?= =?UTF-8?q?ions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- logging/export-log/README.md | 129 ++++++++++++++++++ .../create_lambda_function/index.js | 49 +++++++ .../create_lambda_function/s3_vars.env | 10 ++ logging/export-log/create_policy.json | 13 ++ logging/export-log/create_ssm_document.json | 56 ++++++++ 5 files changed, 257 insertions(+) create mode 100644 logging/export-log/README.md create mode 100644 logging/export-log/create_lambda_function/index.js create mode 100644 logging/export-log/create_lambda_function/s3_vars.env create mode 100644 logging/export-log/create_policy.json create mode 100644 logging/export-log/create_ssm_document.json diff --git a/logging/export-log/README.md b/logging/export-log/README.md new file mode 100644 index 0000000..99cd03f --- /dev/null +++ b/logging/export-log/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/export-log/create_lambda_function/index.js b/logging/export-log/create_lambda_function/index.js new file mode 100644 index 0000000..a3cdb41 --- /dev/null +++ b/logging/export-log/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/export-log/create_lambda_function/s3_vars.env b/logging/export-log/create_lambda_function/s3_vars.env new file mode 100644 index 0000000..09bd849 --- /dev/null +++ b/logging/export-log/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/export-log/create_policy.json b/logging/export-log/create_policy.json new file mode 100644 index 0000000..ca077b6 --- /dev/null +++ b/logging/export-log/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/export-log/create_ssm_document.json b/logging/export-log/create_ssm_document.json new file mode 100644 index 0000000..f85086f --- /dev/null +++ b/logging/export-log/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 From 313fec25b48ae78b348edd8ccfb18272c1ee756e Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Wed, 22 Sep 2021 11:41:47 +0200 Subject: [PATCH 50/87] Add Jira Webhook PoC --- issue-tracking/jira/.gitignore | 3 ++ issue-tracking/jira/README.md | 32 ++++++++++++++ issue-tracking/jira/access_manager.py | 54 +++++++++++++++++++++++ issue-tracking/jira/config_template.py | 11 +++++ issue-tracking/jira/exceptions.py | 8 ++++ issue-tracking/jira/requirements.txt | 4 ++ issue-tracking/jira/sdm_service.py | 59 ++++++++++++++++++++++++++ issue-tracking/jira/server.py | 36 ++++++++++++++++ 8 files changed, 207 insertions(+) create mode 100644 issue-tracking/jira/.gitignore create mode 100644 issue-tracking/jira/README.md create mode 100644 issue-tracking/jira/access_manager.py create mode 100644 issue-tracking/jira/config_template.py create mode 100644 issue-tracking/jira/exceptions.py create mode 100644 issue-tracking/jira/requirements.txt create mode 100644 issue-tracking/jira/sdm_service.py create mode 100644 issue-tracking/jira/server.py 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/README.md b/issue-tracking/jira/README.md new file mode 100644 index 0000000..4e2a0f5 --- /dev/null +++ b/issue-tracking/jira/README.md @@ -0,0 +1,32 @@ +# 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 + +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 +``` 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/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) From 6dc6cc2706e1da24e42c3dede3c47c78b7962d1d Mon Sep 17 00:00:00 2001 From: wallrony Date: Wed, 22 Sep 2021 08:51:09 -0300 Subject: [PATCH 51/87] Change John's contrib folder to export-s3 and remove strongDM docs references in external --- external/README.md | 2 +- logging/{export-log => export-s3}/README.md | 0 .../{export-log => export-s3}/create_lambda_function/index.js | 0 .../create_lambda_function/s3_vars.env | 0 logging/{export-log => export-s3}/create_policy.json | 0 logging/{export-log => export-s3}/create_ssm_document.json | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename logging/{export-log => export-s3}/README.md (100%) rename logging/{export-log => export-s3}/create_lambda_function/index.js (100%) rename logging/{export-log => export-s3}/create_lambda_function/s3_vars.env (100%) rename logging/{export-log => export-s3}/create_policy.json (100%) rename logging/{export-log => export-s3}/create_ssm_document.json (100%) diff --git a/external/README.md b/external/README.md index dcaf396..5afd9e6 100644 --- a/external/README.md +++ b/external/README.md @@ -4,6 +4,6 @@ Our users are resourceful and ambitious. The following is a list of automations - [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 -- [Export Logs with Lifecycle Hooks](https://www.strongdm.com/docs/automation/contributed/export-logs-lifecycle-hooks), contributed by John Turner +- [Export Logs with Lifecycle Hooks](../logging/export-s3/README.md), contributed by John Turner - [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/logging/export-log/README.md b/logging/export-s3/README.md similarity index 100% rename from logging/export-log/README.md rename to logging/export-s3/README.md diff --git a/logging/export-log/create_lambda_function/index.js b/logging/export-s3/create_lambda_function/index.js similarity index 100% rename from logging/export-log/create_lambda_function/index.js rename to logging/export-s3/create_lambda_function/index.js diff --git a/logging/export-log/create_lambda_function/s3_vars.env b/logging/export-s3/create_lambda_function/s3_vars.env similarity index 100% rename from logging/export-log/create_lambda_function/s3_vars.env rename to logging/export-s3/create_lambda_function/s3_vars.env diff --git a/logging/export-log/create_policy.json b/logging/export-s3/create_policy.json similarity index 100% rename from logging/export-log/create_policy.json rename to logging/export-s3/create_policy.json diff --git a/logging/export-log/create_ssm_document.json b/logging/export-s3/create_ssm_document.json similarity index 100% rename from logging/export-log/create_ssm_document.json rename to logging/export-s3/create_ssm_document.json From 76d60802e475c1665655f5c9e315f5864ab01e6e Mon Sep 17 00:00:00 2001 From: wallrony Date: Wed, 22 Sep 2021 10:37:03 -0300 Subject: [PATCH 52/87] Remove export log from external readme --- external/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/external/README.md b/external/README.md index 5afd9e6..07c3bba 100644 --- a/external/README.md +++ b/external/README.md @@ -4,6 +4,5 @@ Our users are resourceful and ambitious. The following is a list of automations - [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 -- [Export Logs with Lifecycle Hooks](../logging/export-s3/README.md), contributed by John Turner - [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/) From e6bb6fadc8ecf4acc3a4c8e9a5bf237ee1674b12 Mon Sep 17 00:00:00 2001 From: wallrony Date: Wed, 22 Sep 2021 12:45:08 -0300 Subject: [PATCH 53/87] Changed folder name to lifecycle-s3 --- logging/{export-s3 => lifecycle-s3}/README.md | 0 .../{export-s3 => lifecycle-s3}/create_lambda_function/index.js | 0 .../create_lambda_function/s3_vars.env | 0 logging/{export-s3 => lifecycle-s3}/create_policy.json | 0 logging/{export-s3 => lifecycle-s3}/create_ssm_document.json | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename logging/{export-s3 => lifecycle-s3}/README.md (100%) rename logging/{export-s3 => lifecycle-s3}/create_lambda_function/index.js (100%) rename logging/{export-s3 => lifecycle-s3}/create_lambda_function/s3_vars.env (100%) rename logging/{export-s3 => lifecycle-s3}/create_policy.json (100%) rename logging/{export-s3 => lifecycle-s3}/create_ssm_document.json (100%) diff --git a/logging/export-s3/README.md b/logging/lifecycle-s3/README.md similarity index 100% rename from logging/export-s3/README.md rename to logging/lifecycle-s3/README.md diff --git a/logging/export-s3/create_lambda_function/index.js b/logging/lifecycle-s3/create_lambda_function/index.js similarity index 100% rename from logging/export-s3/create_lambda_function/index.js rename to logging/lifecycle-s3/create_lambda_function/index.js diff --git a/logging/export-s3/create_lambda_function/s3_vars.env b/logging/lifecycle-s3/create_lambda_function/s3_vars.env similarity index 100% rename from logging/export-s3/create_lambda_function/s3_vars.env rename to logging/lifecycle-s3/create_lambda_function/s3_vars.env diff --git a/logging/export-s3/create_policy.json b/logging/lifecycle-s3/create_policy.json similarity index 100% rename from logging/export-s3/create_policy.json rename to logging/lifecycle-s3/create_policy.json diff --git a/logging/export-s3/create_ssm_document.json b/logging/lifecycle-s3/create_ssm_document.json similarity index 100% rename from logging/export-s3/create_ssm_document.json rename to logging/lifecycle-s3/create_ssm_document.json From 53256a170fe14e5cfb0926d5303d78b1b36d84db Mon Sep 17 00:00:00 2001 From: David Petersen Date: Fri, 24 Sep 2021 12:23:15 -0400 Subject: [PATCH 54/87] Adding new audit users script --- audit/user_audit/README.md | 19 ++++++++ audit/user_audit/audit_sdm_users.py | 70 +++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 audit/user_audit/README.md create mode 100644 audit/user_audit/audit_sdm_users.py diff --git a/audit/user_audit/README.md b/audit/user_audit/README.md new file mode 100644 index 0000000..b946560 --- /dev/null +++ b/audit/user_audit/README.md @@ -0,0 +1,19 @@ +# 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 + +## Usage +* Run `pip install strongdm` +* Run the script e.g. `python audit_sdm_users`. +* The script will create a 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..fd421fd --- /dev/null +++ b/audit/user_audit/audit_sdm_users.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +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.environ['SDM_API_ACCESS_KEY'] +secret_key = os.environ['SDM_API_SECRET_KEY'] + +client = strongdm.Client(access_key, secret_key) + +def get_data(): + user_response = client.accounts.list('type:user') + # Loop through the user list + for u in user_response: + # Create/reset role and resource lists at user level + role_list = [] + resource_list = [] + # Use the account.id to get ACCOUNT ATTACHMENTS + account_attach = client.account_attachments.list('account_id:{}'.format(u.id)) + # Loop 2 + for r in account_attach: + roles = list(client.roles.list('id:{}'.format(r.role_id))) + # Loop through the roles to which this user is assigned + for o in roles: + # Append the name to role_list + role_list.append(o.name) + role_list.append("/") + # Using the role_id, get role_attachments + role_grants = client.role_grants.list('role_id:{}'.format(o.id)) + for a in role_grants: + resources = list(client.resources.list('id:{}'.format(a.resource_id))) + for e in resources: + # Append the name to resource_list + resource_list.append(e.name) + resource_list.append("/") + + # Store the userID and email, for printing later + audit_dict[u.id] = [u.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': + subprocess.call(('open', filepath)) + elif platform.system() == 'Windows': # Windows + os.startfile(filepath) + else: # linux variants + subprocess.call(('xdg-open', filepath)) + + + + +def main(): + get_data() + print_csv() + open_csv() + +if __name__ == "__main__": + main() \ No newline at end of file From 815c9cecc2f8d107a6fea5089682f10c16aea925 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Fri, 24 Sep 2021 12:24:17 -0400 Subject: [PATCH 55/87] Update README.md --- audit/user_audit/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audit/user_audit/README.md b/audit/user_audit/README.md index b946560..029c123 100644 --- a/audit/user_audit/README.md +++ b/audit/user_audit/README.md @@ -11,8 +11,8 @@ This folder contains a Python script that lists all users, with their Role(s) an ## Usage * Run `pip install strongdm` -* Run the script e.g. `python audit_sdm_users`. -* The script will create a CSV file (path defined at top of 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 From 5668205877058a647c0e10cce0098712696a5bb2 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Fri, 24 Sep 2021 13:16:57 -0400 Subject: [PATCH 56/87] Update README.md --- audit/user_audit/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/audit/user_audit/README.md b/audit/user_audit/README.md index 029c123..e5fa15c 100644 --- a/audit/user_audit/README.md +++ b/audit/user_audit/README.md @@ -8,6 +8,7 @@ This folder contains a Python script that lists all users, with their Role(s) an * Roles: List * Datasources: List * Users: List + * Grants: List ## Usage * Run `pip install strongdm` From 0313086ce814d60d19573dd91897f287be2f7ea1 Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Fri, 24 Sep 2021 13:21:42 -0400 Subject: [PATCH 57/87] Update README.md --- audit/user_audit/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/audit/user_audit/README.md b/audit/user_audit/README.md index e5fa15c..6f60489 100644 --- a/audit/user_audit/README.md +++ b/audit/user_audit/README.md @@ -12,6 +12,7 @@ This folder contains a Python script that lists all users, with their Role(s) an ## 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. From d011990dce3131388dcec7fb8c4142afa58bfeaf Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Wed, 29 Sep 2021 12:35:26 -0400 Subject: [PATCH 58/87] Update README.md --- audit/user_audit/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audit/user_audit/README.md b/audit/user_audit/README.md index 6f60489..8528cc8 100644 --- a/audit/user_audit/README.md +++ b/audit/user_audit/README.md @@ -18,4 +18,4 @@ This folder contains a Python script that lists all users, with their Role(s) an * 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!. +* This script does not list permanent or temporary direct grants -- though it could be modified to do so! From 315b648755675dacc01a109bf7bc5ff21eebfe3f Mon Sep 17 00:00:00 2001 From: Rodolfo Campos Date: Fri, 15 Oct 2021 10:58:14 +0200 Subject: [PATCH 59/87] Update README.md --- external/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/external/README.md b/external/README.md index 07c3bba..3a52990 100644 --- a/external/README.md +++ b/external/README.md @@ -2,6 +2,7 @@ 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 From 01bf6f0d0518e7164384d5674944bf5bf1bed0bc Mon Sep 17 00:00:00 2001 From: John turner Date: Thu, 21 Oct 2021 15:30:32 -0400 Subject: [PATCH 60/87] Updating to adding sg, update instance, ami, and more .gitignore files --- .gitignore | 51 +++++++++++ terraform/resources/ssh/main.tf | 86 +++++++++++++++---- .../template/sdm_ssh_install/install.sh.tpl | 4 + terraform/resources/ssh/variables.tf | 8 +- 4 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 terraform/resources/ssh/template/sdm_ssh_install/install.sh.tpl diff --git a/.gitignore b/.gitignore index 54032fc..af6a008 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,54 @@ 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* diff --git a/terraform/resources/ssh/main.tf b/terraform/resources/ssh/main.tf index 599b8e4..c2bb9b9 100644 --- a/terraform/resources/ssh/main.tf +++ b/terraform/resources/ssh/main.tf @@ -1,10 +1,22 @@ +# Queries from SDM CA Public Key data "sdm_ssh_ca_pubkey" "ssh_pubkey_query" { - # returns the SDM-generated CA for your org } +# Queries latest Ubuntu AMI data "aws_ami" "ubuntu" { - most_recent = true - owners = ["099720109477"] # Canonical + 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" { @@ -14,38 +26,74 @@ 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 + ami = data.aws_ami.ubuntu.id + subnet_id = aws_default_subnet.default_az1.id - # security_groups = var.securityGroups - instance_type = "t2.micro" + vpc_security_group_ids = [aws_security_group.ssh_sg.id] + instance_type = "t2.micro" # Run commands to configure server with strongDM CA - user_data = <> /etc/ssh/sdm_ca.pub -chmod 0600 /etc/ssh/sdm_ca.pub -echo 'TrustedUserCAKeys /etc/ssh/sdm_ca.pub' | tee -a /etc/ssh/sshd_config -service ssh restart -USERDATA - + 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.public_dns + 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/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 index ff4bd12..069d3cf 100644 --- a/terraform/resources/ssh/variables.tf +++ b/terraform/resources/ssh/variables.tf @@ -1,7 +1,11 @@ variable "aws_region" { - default = "us-west-2" + default = "us-west-2" } + variable "instanceName" { - default = "your-instance-name" + default = "your-instance-name" } +variable "sgName" { + default = "your-sg-name" +} \ No newline at end of file From d144a51e3765734a29b7944f66894842db3010ca Mon Sep 17 00:00:00 2001 From: David Petersen <61467616+wrenhunter@users.noreply.github.com> Date: Thu, 2 Dec 2021 12:48:00 -0500 Subject: [PATCH 61/87] Update README.md --- authentication/multi-idp/sync-update/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/authentication/multi-idp/sync-update/README.md b/authentication/multi-idp/sync-update/README.md index b0d4874..ea972e8 100644 --- a/authentication/multi-idp/sync-update/README.md +++ b/authentication/multi-idp/sync-update/README.md @@ -1,4 +1,7 @@ # 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 From f598691f36903c163b4f546c2cdcb8a54c0187ce Mon Sep 17 00:00:00 2001 From: David Petersen Date: Tue, 4 Jan 2022 17:17:40 -0500 Subject: [PATCH 62/87] Added Shane webinar files --- webinars/role-resource-grant/README.md | 19 ++++++ .../role-resource-grant/python-sdk-grants.py | 61 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 webinars/role-resource-grant/README.md create mode 100644 webinars/role-resource-grant/python-sdk-grants.py diff --git a/webinars/role-resource-grant/README.md b/webinars/role-resource-grant/README.md new file mode 100644 index 0000000..9fed82c --- /dev/null +++ b/webinars/role-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/role-resource-grant/python-sdk-grants.py b/webinars/role-resource-grant/python-sdk-grants.py new file mode 100644 index 0000000..a3cfa2a --- /dev/null +++ b/webinars/role-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 From 869af51040d6a6eeb3fb7ddbfbb976ada1aa5047 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Tue, 4 Jan 2022 17:23:32 -0500 Subject: [PATCH 63/87] Changed folder name to include date --- .../{role-resource-grant => 22-01-05_resource-grant}/README.md | 0 .../python-sdk-grants.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename webinars/{role-resource-grant => 22-01-05_resource-grant}/README.md (100%) rename webinars/{role-resource-grant => 22-01-05_resource-grant}/python-sdk-grants.py (100%) diff --git a/webinars/role-resource-grant/README.md b/webinars/22-01-05_resource-grant/README.md similarity index 100% rename from webinars/role-resource-grant/README.md rename to webinars/22-01-05_resource-grant/README.md diff --git a/webinars/role-resource-grant/python-sdk-grants.py b/webinars/22-01-05_resource-grant/python-sdk-grants.py similarity index 100% rename from webinars/role-resource-grant/python-sdk-grants.py rename to webinars/22-01-05_resource-grant/python-sdk-grants.py From f3961a3460de5dd1ef7fe180e6b4c4811f31cabf Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 5 Jan 2022 09:27:31 -0500 Subject: [PATCH 64/87] Add PagerDuty webinar files --- webinars/22-12-08_PagerDuty/README.md | 23 +++++ .../22-12-08_PagerDuty/pager_duty_grant.py | 85 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 webinars/22-12-08_PagerDuty/README.md create mode 100644 webinars/22-12-08_PagerDuty/pager_duty_grant.py diff --git a/webinars/22-12-08_PagerDuty/README.md b/webinars/22-12-08_PagerDuty/README.md new file mode 100644 index 0000000..7b5bbda --- /dev/null +++ b/webinars/22-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._ + diff --git a/webinars/22-12-08_PagerDuty/pager_duty_grant.py b/webinars/22-12-08_PagerDuty/pager_duty_grant.py new file mode 100644 index 0000000..0ee8ebc --- /dev/null +++ b/webinars/22-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 From eeaab1789b2456f0d279787b3a9f1a8fd67bf770 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 5 Jan 2022 09:39:10 -0500 Subject: [PATCH 65/87] Updated PD/webinar readme --- webinars/22-12-08_PagerDuty/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinars/22-12-08_PagerDuty/README.md b/webinars/22-12-08_PagerDuty/README.md index 7b5bbda..148d68b 100644 --- a/webinars/22-12-08_PagerDuty/README.md +++ b/webinars/22-12-08_PagerDuty/README.md @@ -19,5 +19,5 @@ To get this script working in your environment, you'll need the following: #### 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._ +_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)._ From 59ce37dd259867d638eb08ad1ea73fb5729ba29c Mon Sep 17 00:00:00 2001 From: John Turner Date: Wed, 5 Jan 2022 11:07:05 -0500 Subject: [PATCH 66/87] Uploading 21-12-15 terraform webinar --- .../README.md | 23 ++ .../main.tf | 344 ++++++++++++++++++ .../providers.tf | 21 ++ .../template/psql/install.sh.tpl | 18 + .../sdm_gateway_install/install.sh.tpl | 4 + .../template/sdm_relay_install/install.sh.tpl | 8 + .../variables.tf | 149 ++++++++ 7 files changed, 567 insertions(+) create mode 100644 webinars/21-12-15_terraform_resource_import/README.md create mode 100644 webinars/21-12-15_terraform_resource_import/main.tf create mode 100644 webinars/21-12-15_terraform_resource_import/providers.tf create mode 100644 webinars/21-12-15_terraform_resource_import/template/psql/install.sh.tpl create mode 100644 webinars/21-12-15_terraform_resource_import/template/sdm_gateway_install/install.sh.tpl create mode 100644 webinars/21-12-15_terraform_resource_import/template/sdm_relay_install/install.sh.tpl create mode 100644 webinars/21-12-15_terraform_resource_import/variables.tf 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..9aea078 --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/README.md @@ -0,0 +1,23 @@ +# 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: + + >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..7ea3bf3 --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/main.tf @@ -0,0 +1,344 @@ +# 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_role_grant" "admin_grant_psql_admin" { + role_id = sdm_role.admins.id + resource_id = sdm_resource.psql_admin.id +} + +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) + } +} + +resource "sdm_role_grant" "admin_grant_psql_ssh" { + role_id = sdm_role.admins.id + resource_id = sdm_resource.psql_ssh.id +} + +# SDM Roles + +resource "sdm_role" "admins" { + name = "sdm-admin-role" +} +resource "sdm_account" "admin_users" { + count = length(var.admin_users) + user { + first_name = split("@", var.admin_users[count.index])[0] + last_name = "Onboarding" + email = var.admin_users[count.index] + } +} +resource "sdm_account_attachment" "admin_attachment" { + count = length(var.admin_users) + account_id = sdm_account.admin_users[count.index].id + role_id = sdm_role.admins.id +} + +# 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..b598d09 --- /dev/null +++ b/webinars/21-12-15_terraform_resource_import/variables.tf @@ -0,0 +1,149 @@ +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" + Creator = "John T." + } +} + +variable "admin_users" { + type = list(string) + default = [] + description = "A list of email addresses that will be granted access to all resources." +} + +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 "vault_sg_ingress_rules" { + + type = list(object({ + + from_port = number + to_port = number + protocol = string + cidr_block = string + description = string + + })) + + default = [ + { + from_port = 8200 + to_port = 8201 + protocol = "tcp" + cidr_block = "10.0.2.0/24" + description = "vault" + }, + { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_block = "10.0.2.0/24" + description = "ssh" + }, + { + from_port = 8500 + to_port = 8501 + protocol = "tcp" + cidr_block = "10.0.2.0/24" + description = "consul" + } + ] + +} + +variable "key_name" { + default = "jturner-terraform-key" +} \ No newline at end of file From b81a5c0c5f10c042baa5d6d13562a3479d938660 Mon Sep 17 00:00:00 2001 From: John Turner Date: Wed, 5 Jan 2022 11:09:23 -0500 Subject: [PATCH 67/87] Editing 21-12-15 terraform webinar --- webinars/21-12-15_terraform_resource_import/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/webinars/21-12-15_terraform_resource_import/README.md b/webinars/21-12-15_terraform_resource_import/README.md index 9aea078..e05b497 100644 --- a/webinars/21-12-15_terraform_resource_import/README.md +++ b/webinars/21-12-15_terraform_resource_import/README.md @@ -10,9 +10,11 @@ This is a small demo script showing how to add resources into strongDM if you ar 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: - >sdm_access_key = "YOURSDMAPIACCESSKEY" - >sdm_secret_key = "YOURSDMAPISECRETKEY" - >region = "us-west-1" + ```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. From f7b4d94b136f24fc84698a9a8343a4f26e8f8cc6 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 5 Jan 2022 11:12:00 -0500 Subject: [PATCH 68/87] Bad dates --- webinars/{22-12-08_PagerDuty => 21-12-08_PagerDuty}/README.md | 0 .../pager_duty_grant.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename webinars/{22-12-08_PagerDuty => 21-12-08_PagerDuty}/README.md (100%) rename webinars/{22-12-08_PagerDuty => 21-12-08_PagerDuty}/pager_duty_grant.py (100%) diff --git a/webinars/22-12-08_PagerDuty/README.md b/webinars/21-12-08_PagerDuty/README.md similarity index 100% rename from webinars/22-12-08_PagerDuty/README.md rename to webinars/21-12-08_PagerDuty/README.md diff --git a/webinars/22-12-08_PagerDuty/pager_duty_grant.py b/webinars/21-12-08_PagerDuty/pager_duty_grant.py similarity index 100% rename from webinars/22-12-08_PagerDuty/pager_duty_grant.py rename to webinars/21-12-08_PagerDuty/pager_duty_grant.py From 6854479a3f38e46ffd11b6f177aa913ee4b6fba4 Mon Sep 17 00:00:00 2001 From: John Turner Date: Wed, 5 Jan 2022 11:13:35 -0500 Subject: [PATCH 69/87] Editing 21-12-15 terraform webinar by removing other TF artifacts --- .../variables.tf | 48 +------------------ 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/webinars/21-12-15_terraform_resource_import/variables.tf b/webinars/21-12-15_terraform_resource_import/variables.tf index b598d09..b40632e 100644 --- a/webinars/21-12-15_terraform_resource_import/variables.tf +++ b/webinars/21-12-15_terraform_resource_import/variables.tf @@ -34,16 +34,10 @@ variable "resource_tags" { type = map(string) default = { Terraform = "true" - Creator = "John T." + env = "dev" } } -variable "admin_users" { - type = list(string) - default = [] - description = "A list of email addresses that will be granted access to all resources." -} - variable "psql_sg_ingress_rules" { type = list(object({ @@ -106,44 +100,6 @@ variable "gateway_sg_ingress_rules" { } -variable "vault_sg_ingress_rules" { - - type = list(object({ - - from_port = number - to_port = number - protocol = string - cidr_block = string - description = string - - })) - - default = [ - { - from_port = 8200 - to_port = 8201 - protocol = "tcp" - cidr_block = "10.0.2.0/24" - description = "vault" - }, - { - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_block = "10.0.2.0/24" - description = "ssh" - }, - { - from_port = 8500 - to_port = 8501 - protocol = "tcp" - cidr_block = "10.0.2.0/24" - description = "consul" - } - ] - -} - variable "key_name" { - default = "jturner-terraform-key" + default = "dev-ssh-terraform-key" } \ No newline at end of file From 454f3feec479d34fa2b65ad01336161b2d97b174 Mon Sep 17 00:00:00 2001 From: John Turner Date: Wed, 5 Jan 2022 11:23:35 -0500 Subject: [PATCH 70/87] Editing 21-12-15 terraform webinar by removing other TF artifacts --- .../main.tf | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/webinars/21-12-15_terraform_resource_import/main.tf b/webinars/21-12-15_terraform_resource_import/main.tf index 7ea3bf3..3d92382 100644 --- a/webinars/21-12-15_terraform_resource_import/main.tf +++ b/webinars/21-12-15_terraform_resource_import/main.tf @@ -287,11 +287,6 @@ resource "sdm_resource" "psql_admin" { } } -resource "sdm_role_grant" "admin_grant_psql_admin" { - role_id = sdm_role.admins.id - resource_id = sdm_resource.psql_admin.id -} - resource "sdm_resource" "psql_ssh" { ssh_cert { name = "sdm-psql-ssh" @@ -302,30 +297,6 @@ resource "sdm_resource" "psql_ssh" { } } -resource "sdm_role_grant" "admin_grant_psql_ssh" { - role_id = sdm_role.admins.id - resource_id = sdm_resource.psql_ssh.id -} - -# SDM Roles - -resource "sdm_role" "admins" { - name = "sdm-admin-role" -} -resource "sdm_account" "admin_users" { - count = length(var.admin_users) - user { - first_name = split("@", var.admin_users[count.index])[0] - last_name = "Onboarding" - email = var.admin_users[count.index] - } -} -resource "sdm_account_attachment" "admin_attachment" { - count = length(var.admin_users) - account_id = sdm_account.admin_users[count.index].id - role_id = sdm_role.admins.id -} - # SDM Public Key data "sdm_ssh_ca_pubkey" "ssh_pubkey_query" { From 4512734a5cb126e835b8d8b6bc775e8ac9c8f610 Mon Sep 17 00:00:00 2001 From: Craig Goldstein Date: Fri, 18 Feb 2022 11:07:11 -0500 Subject: [PATCH 71/87] sdm_health_exporter.py - v0.0.1 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 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) --- monitoring/sdm_health_exporter/README.md | 70 +++++++ .../sdm_health_exporter/requirements.txt | 8 + .../sdm_health_exporter.py | 178 ++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 monitoring/sdm_health_exporter/README.md create mode 100644 monitoring/sdm_health_exporter/requirements.txt create mode 100755 monitoring/sdm_health_exporter/sdm_health_exporter.py 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..5b3d387 --- /dev/null +++ b/monitoring/sdm_health_exporter/sdm_health_exporter.py @@ -0,0 +1,178 @@ +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_ID'] + api_secret = os.environ['SDM_API_SECRET'] + 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','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, + 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','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" or "stopped" + # make sure that the "state" value we received is expected + if node.state not in ("started", "stopped"): + raise ValueError(f'Received unexpected state: {node.state}') + + # 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, + 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() From d105508bf03fdaf97efff5d1d6b8659a1c5f519b Mon Sep 17 00:00:00 2001 From: Craig Goldstein Date: Wed, 23 Feb 2022 09:58:35 -0500 Subject: [PATCH 72/87] sdm_health_check.py - PR changes - Renamed API key/secret environment variables - Removed validation check for states being "started" or "stopped" - Added the "health" and "state" prometheus labels for resources and nodes respectively to make it easy to see the current health in prometheus --- .../sdm_health_exporter.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/monitoring/sdm_health_exporter/sdm_health_exporter.py b/monitoring/sdm_health_exporter/sdm_health_exporter.py index 5b3d387..a5c11b3 100755 --- a/monitoring/sdm_health_exporter/sdm_health_exporter.py +++ b/monitoring/sdm_health_exporter/sdm_health_exporter.py @@ -51,8 +51,8 @@ def get_sdm_keys(): """ try: - api_id = os.environ['SDM_API_ID'] - api_secret = os.environ['SDM_API_SECRET'] + 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() @@ -68,7 +68,7 @@ def get_sdm_resources(client, alert_tag, sdm_objects): """ # prometheus labels that will be attached to the exported metric - labels = ['id','name','tags','task'] + labels = ['id','healthy','name','tags','task'] for resource in client.resources.list(''): @@ -91,6 +91,7 @@ def get_sdm_resources(client, alert_tag, sdm_objects): # 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' @@ -107,7 +108,7 @@ def get_sdm_nodes(client, alert_tag, sdm_objects): """ # prometheus labels that will be attached to the exported metric - labels = ['id','name','tags','task'] + labels = ['id','name','state','tags','task'] for node in client.nodes.list(''): @@ -125,19 +126,19 @@ def get_sdm_nodes(client, alert_tag, 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" or "stopped" - # make sure that the "state" value we received is expected - if node.state not in ("started", "stopped"): - raise ValueError(f'Received unexpected state: {node.state}') - - # 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, - tags=node.tags, - task='health_check' - ).set(0 if node.state == "started" else 1) + # 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 From 94ae0745d459c412fdc5cd816e6667489a594081 Mon Sep 17 00:00:00 2001 From: steevebisson Date: Sun, 13 Mar 2022 11:26:53 -0400 Subject: [PATCH 73/87] Added a permission for running role_resources.py --- audit/roles/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/audit/roles/README.md b/audit/roles/README.md index 35fa18a..d875c3b 100644 --- a/audit/roles/README.md +++ b/audit/roles/README.md @@ -7,6 +7,7 @@ This folder contains a Python script that lists all Roles and the Resources link * 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` From 85eac23b9768044b5ce193d964287c2589bbe953 Mon Sep 17 00:00:00 2001 From: felipesantosds Date: Wed, 16 Mar 2022 12:27:24 -0300 Subject: [PATCH 74/87] Create Jira Webhook Docker image and update docs Co-authored-by: gaabrieljesuss --- issue-tracking/jira/Dockerfile | 23 +++++++++++++++++++++++ issue-tracking/jira/README.md | 12 ++++++++++++ issue-tracking/jira/docker-compose.yml | 8 ++++++++ issue-tracking/jira/env-file.example | 9 +++++++++ issue-tracking/jira/start.sh | 4 ++++ 5 files changed, 56 insertions(+) create mode 100644 issue-tracking/jira/Dockerfile create mode 100644 issue-tracking/jira/docker-compose.yml create mode 100644 issue-tracking/jira/env-file.example create mode 100644 issue-tracking/jira/start.sh 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 index 4e2a0f5..868a809 100644 --- a/issue-tracking/jira/README.md +++ b/issue-tracking/jira/README.md @@ -16,6 +16,8 @@ The user interested in getting access to a resource, needs to create an issue wi ## Run +### Running locally + You need to define the following variables: ``` JIRA_USER= # Usually an email address @@ -30,3 +32,13 @@ After installing all the dependencies listed in [requirements.txt](requirements. ``` 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 +``` 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/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 From aa8b694b8c0b11c541d8a4680f36fdb6c6125760 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Fri, 18 Mar 2022 17:16:34 -0400 Subject: [PATCH 75/87] Adding AWS ASG scripts --- automations/AWS-autoscaling/README.md | 25 ++++++++++++++++ automations/AWS-autoscaling/gw.py | 36 ++++++++++++++++++++++++ automations/AWS-autoscaling/user-data.sh | 26 +++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 automations/AWS-autoscaling/README.md create mode 100644 automations/AWS-autoscaling/gw.py create mode 100644 automations/AWS-autoscaling/user-data.sh 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 From 95552cee51ed8b93cf515d67acdaa59a868c0805 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Thu, 24 Mar 2022 16:59:06 -0400 Subject: [PATCH 76/87] Adding comp_users script --- audit/composite_role_audit/README.md | 22 ++++++++++ audit/composite_role_audit/comp_users.py | 51 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 audit/composite_role_audit/README.md create mode 100644 audit/composite_role_audit/comp_users.py 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 From 8857e5ce57741dd78abf400d94d43ea721355f69 Mon Sep 17 00:00:00 2001 From: felipesantoos Date: Wed, 30 Mar 2022 11:04:16 -0300 Subject: [PATCH 77/87] Add sdm-ext cli files Co-authored-by: gaabrieljesuss --- cli/sdm-ext/.env.example | 2 + cli/sdm-ext/README.Md | 42 +++ cli/sdm-ext/adapter/adapter.go | 17 + cli/sdm-ext/adapter/adapter_test.go | 41 +++ cli/sdm-ext/cli/app.go | 64 ++++ cli/sdm-ext/cli/app_test.go | 75 ++++ cli/sdm-ext/cli/servers.go | 93 +++++ cli/sdm-ext/cli/servers_test.go | 126 +++++++ cli/sdm-ext/go.mod | 20 + cli/sdm-ext/go.sum | 27 ++ cli/sdm-ext/json-examples/server-list.json | 67 ++++ cli/sdm-ext/json-examples/server-rawtcp.json | 11 + cli/sdm-ext/json-examples/server-rdp.json | 13 + .../json-examples/server-ssh-cert.json | 13 + .../server-ssh-customer-key.json | 14 + cli/sdm-ext/json-examples/server-ssh.json | 14 + cli/sdm-ext/sdm-ext.go | 14 + .../service/admin_servers_add_service.go | 93 +++++ .../service/admin_servers_add_service_test.go | 313 ++++++++++++++++ cli/sdm-ext/service/admin_service.go | 54 +++ cli/sdm-ext/service/admin_service_test.go | 126 +++++++ cli/sdm-ext/util/flag_util.go | 31 ++ cli/sdm-ext/util/flag_util_test.go | 52 +++ cli/sdm-ext/util/util.go | 110 ++++++ cli/sdm-ext/util/util_test.go | 348 ++++++++++++++++++ 25 files changed, 1780 insertions(+) create mode 100644 cli/sdm-ext/.env.example create mode 100644 cli/sdm-ext/README.Md create mode 100644 cli/sdm-ext/adapter/adapter.go create mode 100644 cli/sdm-ext/adapter/adapter_test.go create mode 100644 cli/sdm-ext/cli/app.go create mode 100644 cli/sdm-ext/cli/app_test.go create mode 100644 cli/sdm-ext/cli/servers.go create mode 100644 cli/sdm-ext/cli/servers_test.go create mode 100644 cli/sdm-ext/go.mod create mode 100644 cli/sdm-ext/go.sum create mode 100644 cli/sdm-ext/json-examples/server-list.json create mode 100644 cli/sdm-ext/json-examples/server-rawtcp.json create mode 100644 cli/sdm-ext/json-examples/server-rdp.json create mode 100644 cli/sdm-ext/json-examples/server-ssh-cert.json create mode 100644 cli/sdm-ext/json-examples/server-ssh-customer-key.json create mode 100644 cli/sdm-ext/json-examples/server-ssh.json create mode 100644 cli/sdm-ext/sdm-ext.go create mode 100644 cli/sdm-ext/service/admin_servers_add_service.go create mode 100644 cli/sdm-ext/service/admin_servers_add_service_test.go create mode 100644 cli/sdm-ext/service/admin_service.go create mode 100644 cli/sdm-ext/service/admin_service_test.go create mode 100644 cli/sdm-ext/util/flag_util.go create mode 100644 cli/sdm-ext/util/flag_util_test.go create mode 100644 cli/sdm-ext/util/util.go create mode 100644 cli/sdm-ext/util/util_test.go diff --git a/cli/sdm-ext/.env.example b/cli/sdm-ext/.env.example new file mode 100644 index 0000000..9e6f4cd --- /dev/null +++ b/cli/sdm-ext/.env.example @@ -0,0 +1,2 @@ +SDM_API_ACCESS_KEY= +SDM_API_SECRET_KEY= diff --git a/cli/sdm-ext/README.Md b/cli/sdm-ext/README.Md new file mode 100644 index 0000000..e814699 --- /dev/null +++ b/cli/sdm-ext/README.Md @@ -0,0 +1,42 @@ +# Docs + +# 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 use the SDM-EXT CLI, you need to provide the following mandatory variables: + +- SDM_API_ACCESS_KEY: SDM API access key +- SDM_API_SECRET_KEY: SDM API Access Key Secret + +Ps.: you can use .env.example to create your .env file with variables. + +To run the SDM-EXT CLI, after downloading the project, going to your directory and setting the variables above, 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 + +## Comandos exclusivos da SDM-EXT CLI + +- `sdm-ext admin servers add --file ./json-examples/server-list.json` +- `sdm-ext admin servers add --f ./json-examples/server-list.json` +- `sdm-ext admin servers add --stdin` +- `sdm-ext admin servers add -i` + +## Comandos da SDM CLI executados com a SDM-EXT CLI + +- `sdm-ext admin servers list` \ No newline at end of file diff --git a/cli/sdm-ext/adapter/adapter.go b/cli/sdm-ext/adapter/adapter.go new file mode 100644 index 0000000..6487971 --- /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..5e22ac4 --- /dev/null +++ b/cli/sdm-ext/adapter/adapter_test.go @@ -0,0 +1,41 @@ +package adapter + +import ( + "errors" + "testing" + + "bou.ke/monkey" + "github.com/stretchr/testify/assert" +) + +func TestServers(t *testing.T) { + defer monkey.UnpatchAll() + + 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..4ca20ac --- /dev/null +++ b/cli/sdm-ext/cli/app.go @@ -0,0 +1,64 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/urfave/cli" +) + +// Version is set by the build system. +var Version = "" + +func Main() { + err := newApp().Run(os.Args) + if err != nil { + fmt.Println(err) + } +} + +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 executeWithSdm() (*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 + + runCommand(cmd) + + return stdout, stderr +} + +func runCommand(cmd *exec.Cmd) { + err := cmd.Run() + if err != nil { + fmt.Println(err) + } +} + +func commandNotFound(ctx *cli.Context, command string) { + stdout, stderr := executeWithSdm() + 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..c0bc347 --- /dev/null +++ b/cli/sdm-ext/cli/app_test.go @@ -0,0 +1,75 @@ +package cli + +import ( + "os/exec" + "strings" + "testing" + + "bou.ke/monkey" + "github.com/stretchr/testify/assert" +) + +func TestExecuteWithSdm(t *testing.T) { + monkey.UnpatchAll() + tests := executeWithSdmTests{} + t.Run("TestExecuteWithSdm when command is successful", + tests.testWhenSdmCommandIsSuccessful) + t.Run("TestExecuteWithSdm when command is failed", + tests.testWhenSdmCommandIsFailed) +} + +type executeWithSdmTests struct{} + +func (tests executeWithSdmTests) testWhenSdmCommandIsSuccessful(t *testing.T) { + monkey.Patch(runCommand, runSuccessfulCommandMock) + + actualStdout, actualStderr := executeWithSdm() + + 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) testWhenSdmCommandIsFailed(t *testing.T) { + monkey.Patch(runCommand, runFailedCommandMock) + + actualStdout, actualStderr := executeWithSdm() + + 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/servers.go b/cli/sdm-ext/cli/servers.go new file mode 100644 index 0000000..d1029db --- /dev/null +++ b/cli/sdm-ext/cli/servers.go @@ -0,0 +1,93 @@ +package cli + +import ( + "ext/adapter" + "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: adminServersAddAction, + SkipFlagParsing: true, +} + +var adminServersAddFlags = []cli.Flag{ + util.GetAdminServersAddFileFlag(), + util.GetAdminServersAddStdinFlag(), +} + +func adminServersAddAction(ctx *cli.Context) error { + argumentList := getArgs(ctx) + arguments := util.ConvertStrSliceToStr(argumentList) + + matched, err := util.CheckRegexMatch(getRegexList(), arguments) + if err != nil { + return err + } + + if !matched { + sdmCommand := getSdmCommand(ctx.App.Name, ctx.Command.Name, arguments) + commandNotFound(ctx, sdmCommand) + + return nil + } + + mappedArguments := util.MapCommandArguments(argumentList, adminServersAddFlags) + err = adapter.Servers(ctx.Command.Name, mappedArguments) + if err != nil { + return err + } + + return nil +} + +func getArgs(ctx *cli.Context) cli.Args { + if ctx == nil { + fmt.Println() // Needed because mock + } + return ctx.Args() +} + +func getSdmCommand(appName, commandName, arguments string) string { + newAppName := removeSdmExt(appName) + return fmt.Sprintf("%s %s %s", newAppName, commandName, arguments) +} + +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..3b66068 --- /dev/null +++ b/cli/sdm-ext/cli/servers_test.go @@ -0,0 +1,126 @@ +package cli + +import ( + "ext/adapter" + "ext/util" + "testing" + + "bou.ke/monkey" + "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) +} + +type adminServersAddActionTests struct{} + +func (tests adminServersAddActionTests) testWhenThePassedCommandIsValid(t *testing.T) { + defer monkey.UnpatchAll() + + monkey.Patch(getArgs, getArgsMock) + monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrMock) + monkey.Patch(util.CheckRegexMatch, checkRegexMatchMock) + monkey.Patch(util.MapCommandArguments, mapCommandArgumentsMock) + monkey.Patch(adapter.Servers, serversMock) + + actualErr := adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) + monkey.UnpatchAll() +} + +func (tests adminServersAddActionTests) testWhenThereIsNoArguments(t *testing.T) { + defer monkey.UnpatchAll() + + monkey.Patch(getArgs, getEmptyArgsMock) + monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrMock) + monkey.Patch(util.CheckRegexMatch, checkRegexMatchMock) + monkey.Patch(util.MapCommandArguments, mapCommandArgumentsMock) + monkey.Patch(adapter.Servers, serversMock) + + actualErr := adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) + monkey.UnpatchAll() +} + +func getArgsMock(ctx *cli.Context) cli.Args { + return cli.Args{"--file", "file.json"} +} + +func getEmptyArgsMock(ctx *cli.Context) cli.Args { + return cli.Args{} +} + +func convertStrSliceToStrMock(strList []string) string { + return "--file file.json" +} + +func checkRegexMatchMock(regexList []string, arguments string) (bool, error) { + return true, nil +} + +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 +} + +func TestGetSdmCommand(t *testing.T) { + defer monkey.UnpatchAll() + + 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) { + defer monkey.UnpatchAll() + + 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) +} diff --git a/cli/sdm-ext/go.mod b/cli/sdm-ext/go.mod new file mode 100644 index 0000000..98bbbaa --- /dev/null +++ b/cli/sdm-ext/go.mod @@ -0,0 +1,20 @@ +module ext + +go 1.17 + +require ( + github.com/joho/godotenv v1.4.0 + github.com/urfave/cli v1.22.5 +) + +require ( + bou.ke/monkey v1.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // 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 + github.com/stretchr/testify v1.7.1 // 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..e65c20e --- /dev/null +++ b/cli/sdm-ext/go.sum @@ -0,0 +1,27 @@ +bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= +bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= +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/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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/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..3a2c164 --- /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..61c2ffc --- /dev/null +++ b/cli/sdm-ext/sdm-ext.go @@ -0,0 +1,14 @@ +package main + +import ( + "ext/cli" + "ext/util" + "fmt" + + "github.com/joho/godotenv" +) + +func main() { + godotenv.Load(fmt.Sprintf("%s/.env", util.GetBasePath())) + cli.Main() +} 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..ce0e3a6 --- /dev/null +++ b/cli/sdm-ext/service/admin_servers_add_service.go @@ -0,0 +1,93 @@ +package service + +import ( + "ext/util" + "fmt" + "strings" + "unicode" +) + +func (a AdminService) AdminServersAdd(options map[string]string) error { + flagList := []string{"--file", "-f", "--stdin", "-i"} + flag := util.FindFlag(flagList, options) + + var servers []map[string]interface{} + var err error + + if flag == "--file" || flag == "-f" { + servers, err = util.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 := 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 { + tags := "" + i := 0 + for key, value := range tagsMap { + tags += fmt.Sprintf("%s=%s", key, value) + 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..5539459 --- /dev/null +++ b/cli/sdm-ext/service/admin_servers_add_service_test.go @@ -0,0 +1,313 @@ +package service + +import ( + "errors" + "ext/util" + "strings" + "testing" + + "bou.ke/monkey" + "github.com/stretchr/testify/assert" +) + +func TestAdminServersAdd(t *testing.T) { + defer monkey.UnpatchAll() + 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) { + monkey.Patch(util.FindFlag, findFlagMock) + monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonMock) + monkey.Patch(execute, executeWithSuccessMock) + options := map[string]string{"--file": "file.json"} + service := NewAdminService() + actualErr := service.AdminServersAdd(options) + assert.Nil(t, actualErr) +} +func (tests adminServersAddTests) testWhenFindFlagReturnsEmpty(t *testing.T) { + monkey.Patch(util.FindFlag, findFlagReturningEmptyMock) + options := map[string]string{"--files": "file.json"} + service := NewAdminService() + actualErr := service.AdminServersAdd(options) + assert.Nil(t, actualErr) +} +func (tests adminServersAddTests) testWhenAnNonexistentFileIsPassed(t *testing.T) { + monkey.Patch(util.FindFlag, findFlagMock) + monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonWithABaddlyFormattedJsonMock) + options := map[string]string{"--file": "file.json"} + service := NewAdminService() + actualErr := service.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) { + monkey.Patch(util.FindFlag, findFlagMock) + monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonWithANonExistentJsonFileMock) + options := map[string]string{"--file": "file.json"} + service := NewAdminService() + actualErr := service.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) { + monkey.Patch(util.FindFlag, findFlagMock) + monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonMock) + monkey.Patch(execute, executeWithoutSuccessMock) + options := map[string]string{"--file": "file.json"} + service := NewAdminService() + actualErr := service.AdminServersAdd(options) + assert.Nil(t, actualErr) +} +func TestGetOptions(t *testing.T) { + defer monkey.UnpatchAll() + 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) { + defer monkey.UnpatchAll() + 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) { + defer monkey.UnpatchAll() + 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..051c94c --- /dev/null +++ b/cli/sdm-ext/service/admin_service.go @@ -0,0 +1,54 @@ +package service + +import ( + "fmt" + "os/exec" + "strings" +) + +type AdminService struct{} + +func NewAdminService() *AdminService { + return &AdminService{} +} + +func 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 + + 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{} + + for key, value := range options { + if key[len(key)-1:] == "=" { + key += value + value = "" + } + strOptions = append(strOptions, key) + if len(value) > 0 { + strOptions = append(strOptions, value) + } + } + + return strOptions +} 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..7a0fc6c --- /dev/null +++ b/cli/sdm-ext/service/admin_service_test.go @@ -0,0 +1,126 @@ +package service + +import ( + "os/exec" + "strings" + "testing" + + "bou.ke/monkey" + "github.com/stretchr/testify/assert" +) + +func TestExecute(t *testing.T) { + defer monkey.UnpatchAll() + + tests := executeTests{} + t.Run("Test execute when the passed command is valid", + tests.testWhenThePassedCommadIsValid) + t.Run("Test execute when command is failed", + tests.testWhenThePassedCommadIsFailed) +} + +type executeTests struct{} + +func (tests executeTests) testWhenThePassedCommadIsValid(t *testing.T) { + monkey.Patch(runCommand, runSuccessfulCommandMock) + + commands := getRawTCPServerAddCommand() + options := getOptionsToExecute() + postOptions := getRawTCPServerName() + + actualStdout, actualStderr := 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) testWhenThePassedCommadIsFailed(t *testing.T) { + monkey.Patch(runCommand, runFailedCommandMock) + + commands := getRawTCPServerAddCommand() + options := getOptionsToExecute() + postOptions := getRawTCPServerName() + + actualStdout, actualStderr := 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 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 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 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/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..0f14a3e --- /dev/null +++ b/cli/sdm-ext/util/flag_util_test.go @@ -0,0 +1,52 @@ +package util + +import ( + "testing" + + "bou.ke/monkey" + "github.com/stretchr/testify/assert" +) + +func TestFindFlag(t *testing.T) { + defer monkey.UnpatchAll() + + 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..dc148b2 --- /dev/null +++ b/cli/sdm-ext/util/util.go @@ -0,0 +1,110 @@ +package util + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/urfave/cli" +) + +func GetBasePath() string { + _, file, _, _ := runtime.Caller(0) + basepath := filepath.Join(filepath.Dir(file), "../") + return basepath +} + +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 { + fmt.Print() // Needed because mock + 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..7d305ac --- /dev/null +++ b/cli/sdm-ext/util/util_test.go @@ -0,0 +1,348 @@ +package util + +import ( + "errors" + "io/ioutil" + "os" + "regexp/syntax" + "testing" + + "bou.ke/monkey" + "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) { + defer monkey.UnpatchAll() + + 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) { + defer monkey.UnpatchAll() + + 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) { + defer monkey.UnpatchAll() + + 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) { + defer monkey.UnpatchAll() + + 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" +} From ade4c654a373510f62a286f71688788767a7e01c Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 30 Mar 2022 11:15:36 -0300 Subject: [PATCH 78/87] Update README.Md --- cli/sdm-ext/README.Md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cli/sdm-ext/README.Md b/cli/sdm-ext/README.Md index e814699..bae96fb 100644 --- a/cli/sdm-ext/README.Md +++ b/cli/sdm-ext/README.Md @@ -1,5 +1,3 @@ -# Docs - # 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". @@ -10,16 +8,18 @@ You need to install SDM CLI and configure it on your machine. See [Installing th To use the SDM-EXT CLI, you need to provide the following mandatory variables: -- SDM_API_ACCESS_KEY: SDM API access key -- SDM_API_SECRET_KEY: SDM API Access Key Secret +- SDM_API_ACCESS_KEY: SDM API access key; +- SDM_API_SECRET_KEY: SDM API Access Key Secret. Ps.: you can use .env.example to create your .env file with variables. -To run the SDM-EXT CLI, after downloading the project, going to your directory and setting the variables above, 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. +![Screenshot from 2022-03-30 10-34-23](https://user-images.githubusercontent.com/49795183/160855074-0dee81d6-fa8d-4292-9d6d-04b3d76f773f.png) + +To run the SDM-EXT CLI, after downloading the project, going to its directory and setting the variables above, 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. +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: @@ -30,13 +30,13 @@ You can use the following sdm-ext flags: # Examples -## Comandos exclusivos da SDM-EXT CLI +## Exclusive SDM-EXT CLI Commands - `sdm-ext admin servers add --file ./json-examples/server-list.json` - `sdm-ext admin servers add --f ./json-examples/server-list.json` - `sdm-ext admin servers add --stdin` - `sdm-ext admin servers add -i` -## Comandos da SDM CLI executados com a SDM-EXT CLI +## SDM CLI commands executed with the SDM-EXT CLI -- `sdm-ext admin servers list` \ No newline at end of file +- `sdm-ext admin servers list` From ca55a42840acf31ae3c8bda0567fde64a8e04ef8 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 30 Mar 2022 11:20:57 -0300 Subject: [PATCH 79/87] Update README.Md --- cli/sdm-ext/README.Md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/sdm-ext/README.Md b/cli/sdm-ext/README.Md index bae96fb..5efe44c 100644 --- a/cli/sdm-ext/README.Md +++ b/cli/sdm-ext/README.Md @@ -33,10 +33,15 @@ You can use the following sdm-ext flags: ## 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. From 2b4ba79d663551f6efe5b8f760339a469f15d61d Mon Sep 17 00:00:00 2001 From: felipesantoos Date: Thu, 31 Mar 2022 16:03:19 -0300 Subject: [PATCH 80/87] Fix tests, remove .env file and update doc s Co-authored-by: gaabrieljesuss --- cli/sdm-ext/.env.example | 2 - cli/sdm-ext/README.Md | 85 ++-- cli/sdm-ext/adapter/adapter.go | 34 +- cli/sdm-ext/adapter/adapter_test.go | 79 ++-- cli/sdm-ext/cli/app.go | 121 +++--- cli/sdm-ext/cli/app_test.go | 153 ++++---- cli/sdm-ext/cli/servers.go | 200 +++++----- cli/sdm-ext/cli/servers_test.go | 368 ++++++++++++------ .../server-ssh-customer-key.json | 28 +- cli/sdm-ext/sdm-ext.go | 28 +- .../service/admin_servers_add_service_test.go | 59 ++- cli/sdm-ext/service/admin_service.go | 108 ++--- cli/sdm-ext/service/admin_service_test.go | 254 ++++++------ cli/sdm-ext/util/flag_util_test.go | 3 - cli/sdm-ext/util/util.go | 212 +++++----- cli/sdm-ext/util/util_test.go | 9 - 16 files changed, 944 insertions(+), 799 deletions(-) delete mode 100644 cli/sdm-ext/.env.example diff --git a/cli/sdm-ext/.env.example b/cli/sdm-ext/.env.example deleted file mode 100644 index 9e6f4cd..0000000 --- a/cli/sdm-ext/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -SDM_API_ACCESS_KEY= -SDM_API_SECRET_KEY= diff --git a/cli/sdm-ext/README.Md b/cli/sdm-ext/README.Md index 5efe44c..894ad71 100644 --- a/cli/sdm-ext/README.Md +++ b/cli/sdm-ext/README.Md @@ -1,47 +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 use the SDM-EXT CLI, you need to provide the following mandatory variables: - -- SDM_API_ACCESS_KEY: SDM API access key; -- SDM_API_SECRET_KEY: SDM API Access Key Secret. - -Ps.: you can use .env.example to create your .env file with variables. - -![Screenshot from 2022-03-30 10-34-23](https://user-images.githubusercontent.com/49795183/160855074-0dee81d6-fa8d-4292-9d6d-04b3d76f773f.png) - -To run the SDM-EXT CLI, after downloading the project, going to its directory and setting the variables above, 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. +# 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 index 6487971..056380a 100644 --- a/cli/sdm-ext/adapter/adapter.go +++ b/cli/sdm-ext/adapter/adapter.go @@ -1,17 +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) - } -} +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 index 5e22ac4..b854ee3 100644 --- a/cli/sdm-ext/adapter/adapter_test.go +++ b/cli/sdm-ext/adapter/adapter_test.go @@ -1,41 +1,38 @@ -package adapter - -import ( - "errors" - "testing" - - "bou.ke/monkey" - "github.com/stretchr/testify/assert" -) - -func TestServers(t *testing.T) { - defer monkey.UnpatchAll() - - 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) -} +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 index 4ca20ac..07109e0 100644 --- a/cli/sdm-ext/cli/app.go +++ b/cli/sdm-ext/cli/app.go @@ -1,64 +1,57 @@ -package cli - -import ( - "fmt" - "os" - "os/exec" - "strings" - - "github.com/urfave/cli" -) - -// Version is set by the build system. -var Version = "" - -func Main() { - err := newApp().Run(os.Args) - if err != nil { - fmt.Println(err) - } -} - -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 executeWithSdm() (*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 - - runCommand(cmd) - - return stdout, stderr -} - -func runCommand(cmd *exec.Cmd) { - err := cmd.Run() - if err != nil { - fmt.Println(err) - } -} - -func commandNotFound(ctx *cli.Context, command string) { - stdout, stderr := executeWithSdm() - fmt.Print(stdout.String()) - fmt.Print(stderr.String()) -} +package cli + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "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 executeWithSdm() (*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 + + runCommand(cmd) + + return stdout, stderr +} + +func runCommand(cmd *exec.Cmd) { + err := cmd.Run() + if err != nil { + fmt.Println(err) + } +} + +func commandNotFound(ctx *cli.Context, command string) { + stdout, stderr := executeWithSdm() + 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 index c0bc347..cc2226b 100644 --- a/cli/sdm-ext/cli/app_test.go +++ b/cli/sdm-ext/cli/app_test.go @@ -1,75 +1,78 @@ -package cli - -import ( - "os/exec" - "strings" - "testing" - - "bou.ke/monkey" - "github.com/stretchr/testify/assert" -) - -func TestExecuteWithSdm(t *testing.T) { - monkey.UnpatchAll() - tests := executeWithSdmTests{} - t.Run("TestExecuteWithSdm when command is successful", - tests.testWhenSdmCommandIsSuccessful) - t.Run("TestExecuteWithSdm when command is failed", - tests.testWhenSdmCommandIsFailed) -} - -type executeWithSdmTests struct{} - -func (tests executeWithSdmTests) testWhenSdmCommandIsSuccessful(t *testing.T) { - monkey.Patch(runCommand, runSuccessfulCommandMock) - - actualStdout, actualStderr := executeWithSdm() - - 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) testWhenSdmCommandIsFailed(t *testing.T) { - monkey.Patch(runCommand, runFailedCommandMock) - - actualStdout, actualStderr := executeWithSdm() - - 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("") -} +package cli + +import ( + "os/exec" + "strings" + "testing" + + "bou.ke/monkey" + "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) { + defer monkey.UnpatchAll() + + monkey.Patch(runCommand, runSuccessfulCommandMock) + + actualStdout, actualStderr := executeWithSdm() + + 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) { + defer monkey.UnpatchAll() + + monkey.Patch(runCommand, runFailedCommandMock) + + actualStdout, actualStderr := executeWithSdm() + + 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/servers.go b/cli/sdm-ext/cli/servers.go index d1029db..8351c36 100644 --- a/cli/sdm-ext/cli/servers.go +++ b/cli/sdm-ext/cli/servers.go @@ -1,93 +1,107 @@ -package cli - -import ( - "ext/adapter" - "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: adminServersAddAction, - SkipFlagParsing: true, -} - -var adminServersAddFlags = []cli.Flag{ - util.GetAdminServersAddFileFlag(), - util.GetAdminServersAddStdinFlag(), -} - -func adminServersAddAction(ctx *cli.Context) error { - argumentList := getArgs(ctx) - arguments := util.ConvertStrSliceToStr(argumentList) - - matched, err := util.CheckRegexMatch(getRegexList(), arguments) - if err != nil { - return err - } - - if !matched { - sdmCommand := getSdmCommand(ctx.App.Name, ctx.Command.Name, arguments) - commandNotFound(ctx, sdmCommand) - - return nil - } - - mappedArguments := util.MapCommandArguments(argumentList, adminServersAddFlags) - err = adapter.Servers(ctx.Command.Name, mappedArguments) - if err != nil { - return err - } - - return nil -} - -func getArgs(ctx *cli.Context) cli.Args { - if ctx == nil { - fmt.Println() // Needed because mock - } - return ctx.Args() -} - -func getSdmCommand(appName, commandName, arguments string) string { - newAppName := removeSdmExt(appName) - return fmt.Sprintf("%s %s %s", newAppName, commandName, arguments) -} - -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, - } -} +package cli + +import ( + "ext/adapter" + "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: adminServersAddAction, + SkipFlagParsing: true, +} + +var adminServersAddFlags = []cli.Flag{ + util.GetAdminServersAddFileFlag(), + util.GetAdminServersAddStdinFlag(), +} + +func adminServersAddAction(ctx *cli.Context) error { + argumentList := getArgs(ctx) + arguments := util.ConvertStrSliceToStr(argumentList) + + matched, err := util.CheckRegexMatch(getRegexList(), arguments) + if err != nil { + return err + } + + if !matched { + sdmCommand := getSdmCommand(getAppName(ctx), getCommandName(ctx), arguments) + commandNotFound(ctx, sdmCommand) + + return nil + } + + mappedArguments := util.MapCommandArguments(argumentList, adminServersAddFlags) + err = adapter.Servers(ctx.Command.Name, mappedArguments) + if err != nil { + return err + } + + return nil +} + +func getArgs(ctx *cli.Context) cli.Args { + if ctx == nil { + fmt.Println() // Needed because mock + } + 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 { + if ctx == nil { + fmt.Println() // Needed because mock + } + return ctx.App.Name +} + +func getCommandName(ctx *cli.Context) string { + if ctx == nil { + fmt.Println() // Needed because mock + } + 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 index 3b66068..dc5c25c 100644 --- a/cli/sdm-ext/cli/servers_test.go +++ b/cli/sdm-ext/cli/servers_test.go @@ -1,126 +1,242 @@ -package cli - -import ( - "ext/adapter" - "ext/util" - "testing" - - "bou.ke/monkey" - "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) -} - -type adminServersAddActionTests struct{} - -func (tests adminServersAddActionTests) testWhenThePassedCommandIsValid(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(getArgs, getArgsMock) - monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrMock) - monkey.Patch(util.CheckRegexMatch, checkRegexMatchMock) - monkey.Patch(util.MapCommandArguments, mapCommandArgumentsMock) - monkey.Patch(adapter.Servers, serversMock) - - actualErr := adminServersAddAction(&cli.Context{}) - - assert.Nil(t, actualErr) - monkey.UnpatchAll() -} - -func (tests adminServersAddActionTests) testWhenThereIsNoArguments(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(getArgs, getEmptyArgsMock) - monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrMock) - monkey.Patch(util.CheckRegexMatch, checkRegexMatchMock) - monkey.Patch(util.MapCommandArguments, mapCommandArgumentsMock) - monkey.Patch(adapter.Servers, serversMock) - - actualErr := adminServersAddAction(&cli.Context{}) - - assert.Nil(t, actualErr) - monkey.UnpatchAll() -} - -func getArgsMock(ctx *cli.Context) cli.Args { - return cli.Args{"--file", "file.json"} -} - -func getEmptyArgsMock(ctx *cli.Context) cli.Args { - return cli.Args{} -} - -func convertStrSliceToStrMock(strList []string) string { - return "--file file.json" -} - -func checkRegexMatchMock(regexList []string, arguments string) (bool, error) { - return true, nil -} - -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 -} - -func TestGetSdmCommand(t *testing.T) { - defer monkey.UnpatchAll() - - 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) { - defer monkey.UnpatchAll() - - 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) -} +package cli + +import ( + "ext/adapter" + "ext/util" + "testing" + + "bou.ke/monkey" + "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) { + defer monkey.UnpatchAll() + + monkey.Patch(getArgs, getArgsMock) + monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrMock) + monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenMatchesMock) + monkey.Patch(util.MapCommandArguments, mapCommandArgumentsMock) + monkey.Patch(adapter.Servers, serversMock) + + actualErr := adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenThereIsNoArguments(t *testing.T) { + defer monkey.UnpatchAll() + + monkey.Patch(getArgs, getEmptyArgsMock) + monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrMock) + monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenMatchesMock) + monkey.Patch(util.MapCommandArguments, mapCommandArgumentsMock) + monkey.Patch(adapter.Servers, serversMock) + + actualErr := adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenThePassedFlagDoesNotExistInSdmExtCli(t *testing.T) { + defer monkey.UnpatchAll() + + monkey.Patch(getArgs, getArgsWithWrongFlagMock) + monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrWithWrongFlagMock) + monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenDoesNotMatchesMock) + monkey.Patch(getSdmCommand, getSdmCommandMock) + monkey.Patch(getAppName, getAppNameMock) + monkey.Patch(getCommandName, getCommandNameMock) + monkey.Patch(commandNotFound, commandNotFoundMock) + + actualErr := adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenASubcommandIsPassedBetweenAddCommandAndFlag(t *testing.T) { + defer monkey.UnpatchAll() + + monkey.Patch(getArgs, getArgsWithSubcommandBetweenCommandAndFlagMock) + monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrWithSubcommandBetweenCommandAndFlagMock) + monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenDoesNotMatchesMock) + monkey.Patch(getSdmCommand, getSdmCommandMock) + monkey.Patch(getAppName, getAppNameMock) + monkey.Patch(getCommandName, getCommandNameMock) + monkey.Patch(commandNotFound, commandNotFoundMock) + + actualErr := adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenTheSubcommandIsPassedAfterFlagValue(t *testing.T) { + defer monkey.UnpatchAll() + + monkey.Patch(getArgs, getArgsWithSubcommandAfterFlagValueMock) + monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrWithSubcommandAfterFlagValueMock) + monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenDoesNotMatchesMock) + monkey.Patch(getSdmCommand, getSdmCommandMock) + monkey.Patch(getAppName, getAppNameMock) + monkey.Patch(getCommandName, getCommandNameMock) + monkey.Patch(commandNotFound, commandNotFoundMock) + + actualErr := adminServersAddAction(&cli.Context{}) + + assert.Nil(t, actualErr) +} + +func (tests adminServersAddActionTests) testWhenTheSubcommandIsPassedAfterFlag(t *testing.T) { + defer monkey.UnpatchAll() + + monkey.Patch(getArgs, getArgsWithSubcommandAfterFlagMock) + monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrWithSubcommandAfterFlagMock) + monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenDoesNotMatchesMock) + monkey.Patch(getSdmCommand, getSdmCommandMock) + monkey.Patch(getAppName, getAppNameMock) + monkey.Patch(getCommandName, getCommandNameMock) + monkey.Patch(commandNotFound, commandNotFoundMock) + + actualErr := 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/json-examples/server-ssh-customer-key.json b/cli/sdm-ext/json-examples/server-ssh-customer-key.json index 3a2c164..27689f8 100644 --- a/cli/sdm-ext/json-examples/server-ssh-customer-key.json +++ b/cli/sdm-ext/json-examples/server-ssh-customer-key.json @@ -1,14 +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" - } -} +{ + "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/sdm-ext.go b/cli/sdm-ext/sdm-ext.go index 61c2ffc..80d8f94 100644 --- a/cli/sdm-ext/sdm-ext.go +++ b/cli/sdm-ext/sdm-ext.go @@ -1,14 +1,14 @@ -package main - -import ( - "ext/cli" - "ext/util" - "fmt" - - "github.com/joho/godotenv" -) - -func main() { - godotenv.Load(fmt.Sprintf("%s/.env", util.GetBasePath())) - cli.Main() -} +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_test.go b/cli/sdm-ext/service/admin_servers_add_service_test.go index 5539459..47a8921 100644 --- a/cli/sdm-ext/service/admin_servers_add_service_test.go +++ b/cli/sdm-ext/service/admin_servers_add_service_test.go @@ -11,7 +11,6 @@ import ( ) func TestAdminServersAdd(t *testing.T) { - defer monkey.UnpatchAll() tests := adminServersAddTests{} t.Run("Test AdminServersAdd when it is successful", tests.testWhenItIsSucessful) @@ -28,50 +27,74 @@ func TestAdminServersAdd(t *testing.T) { type adminServersAddTests struct{} func (tests adminServersAddTests) testWhenItIsSucessful(t *testing.T) { + defer monkey.UnpatchAll() + monkey.Patch(util.FindFlag, findFlagMock) monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonMock) monkey.Patch(execute, executeWithSuccessMock) + options := map[string]string{"--file": "file.json"} service := NewAdminService() actualErr := service.AdminServersAdd(options) + assert.Nil(t, actualErr) } + func (tests adminServersAddTests) testWhenFindFlagReturnsEmpty(t *testing.T) { + defer monkey.UnpatchAll() + monkey.Patch(util.FindFlag, findFlagReturningEmptyMock) + options := map[string]string{"--files": "file.json"} service := NewAdminService() actualErr := service.AdminServersAdd(options) + assert.Nil(t, actualErr) } + func (tests adminServersAddTests) testWhenAnNonexistentFileIsPassed(t *testing.T) { + defer monkey.UnpatchAll() + monkey.Patch(util.FindFlag, findFlagMock) monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonWithABaddlyFormattedJsonMock) + options := map[string]string{"--file": "file.json"} service := NewAdminService() actualErr := service.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) { + defer monkey.UnpatchAll() + monkey.Patch(util.FindFlag, findFlagMock) monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonWithANonExistentJsonFileMock) + options := map[string]string{"--file": "file.json"} service := NewAdminService() actualErr := service.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) { + defer monkey.UnpatchAll() + monkey.Patch(util.FindFlag, findFlagMock) monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonMock) monkey.Patch(execute, executeWithoutSuccessMock) + options := map[string]string{"--file": "file.json"} service := NewAdminService() actualErr := service.AdminServersAdd(options) + assert.Nil(t, actualErr) } func TestGetOptions(t *testing.T) { - defer monkey.UnpatchAll() tests := getOptionsTests{} t.Run("Test getOptions when it is successful", tests.testWhenItIsSucessful) @@ -99,42 +122,49 @@ func (tests getOptionsTests) testWhenItIsSucessful(t *testing.T) { 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) @@ -142,7 +172,6 @@ func (tests getOptionsTests) testWhenTheServerIsEmpty(t *testing.T) { } func TestTreatKey(t *testing.T) { - defer monkey.UnpatchAll() tests := treatKeyTests{} t.Run("Test treatKey when it is successful", tests.testWhenItIsSucessful) @@ -158,14 +187,15 @@ func (tests treatKeyTests) testWhenItIsSucessful(t *testing.T) { 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) { - defer monkey.UnpatchAll() tests := treatTagsTests{} t.Run("Test treatTags when it is successful", tests.testWhenItIsSucessful) @@ -183,12 +213,14 @@ func (tests treatTagsTests) testWhenItIsSucessful(t *testing.T) { 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) @@ -198,18 +230,23 @@ func (tests treatTagsTests) testWhenTheTagsMapIsEmpty(t *testing.T) { 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("") @@ -217,6 +254,7 @@ func executeWithSuccessMock(commands string, options map[string]string, postOpti 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("") @@ -224,6 +262,7 @@ func executeWithoutSuccessMock(commands string, options map[string]string, postO stderr.WriteString("stderr") return *stdout, *stderr } + func getMapDataList() []map[string]interface{} { return []map[string]interface{}{ { @@ -233,6 +272,7 @@ func getMapDataList() []map[string]interface{} { }, } } + func getServer() map[string]interface{} { return map[string]interface{}{ "name": "Example Server", @@ -240,6 +280,7 @@ func getServer() map[string]interface{} { "type": "type", } } + func getServerWithTags() map[string]interface{} { return map[string]interface{}{ "name": "Example Server", @@ -250,6 +291,7 @@ func getServerWithTags() map[string]interface{} { }, } } + func getServerWithPrivateKey() map[string]interface{} { return map[string]interface{}{ "name": "Example Server", @@ -258,23 +300,27 @@ func getServerWithPrivateKey() map[string]interface{} { "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", @@ -283,28 +329,33 @@ func getServerWithAnAttributeEmpty() map[string]interface{} { "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", diff --git a/cli/sdm-ext/service/admin_service.go b/cli/sdm-ext/service/admin_service.go index 051c94c..c14dbc4 100644 --- a/cli/sdm-ext/service/admin_service.go +++ b/cli/sdm-ext/service/admin_service.go @@ -1,54 +1,54 @@ -package service - -import ( - "fmt" - "os/exec" - "strings" -) - -type AdminService struct{} - -func NewAdminService() *AdminService { - return &AdminService{} -} - -func 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 - - 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{} - - for key, value := range options { - if key[len(key)-1:] == "=" { - key += value - value = "" - } - strOptions = append(strOptions, key) - if len(value) > 0 { - strOptions = append(strOptions, value) - } - } - - return strOptions -} +package service + +import ( + "fmt" + "os/exec" + "strings" +) + +type AdminService struct{} + +func NewAdminService() *AdminService { + return &AdminService{} +} + +func 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 + + 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{} + + for key, value := range options { + if key[len(key)-1:] == "=" { + key += value + value = "" + } + strOptions = append(strOptions, key) + if len(value) > 0 { + strOptions = append(strOptions, value) + } + } + + return strOptions +} diff --git a/cli/sdm-ext/service/admin_service_test.go b/cli/sdm-ext/service/admin_service_test.go index 7a0fc6c..08cc7fd 100644 --- a/cli/sdm-ext/service/admin_service_test.go +++ b/cli/sdm-ext/service/admin_service_test.go @@ -1,126 +1,128 @@ -package service - -import ( - "os/exec" - "strings" - "testing" - - "bou.ke/monkey" - "github.com/stretchr/testify/assert" -) - -func TestExecute(t *testing.T) { - defer monkey.UnpatchAll() - - tests := executeTests{} - t.Run("Test execute when the passed command is valid", - tests.testWhenThePassedCommadIsValid) - t.Run("Test execute when command is failed", - tests.testWhenThePassedCommadIsFailed) -} - -type executeTests struct{} - -func (tests executeTests) testWhenThePassedCommadIsValid(t *testing.T) { - monkey.Patch(runCommand, runSuccessfulCommandMock) - - commands := getRawTCPServerAddCommand() - options := getOptionsToExecute() - postOptions := getRawTCPServerName() - - actualStdout, actualStderr := 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) testWhenThePassedCommadIsFailed(t *testing.T) { - monkey.Patch(runCommand, runFailedCommandMock) - - commands := getRawTCPServerAddCommand() - options := getOptionsToExecute() - postOptions := getRawTCPServerName() - - actualStdout, actualStderr := 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 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 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 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" -} +package service + +import ( + "os/exec" + "strings" + "testing" + + "bou.ke/monkey" + "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) { + defer monkey.UnpatchAll() + + monkey.Patch(runCommand, runSuccessfulCommandMock) + + commands := getRawTCPServerAddCommand() + options := getOptionsToExecute() + postOptions := getRawTCPServerName() + + actualStdout, actualStderr := 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) { + defer monkey.UnpatchAll() + + monkey.Patch(runCommand, runFailedCommandMock) + + commands := getRawTCPServerAddCommand() + options := getOptionsToExecute() + postOptions := getRawTCPServerName() + + actualStdout, actualStderr := 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/util/flag_util_test.go b/cli/sdm-ext/util/flag_util_test.go index 0f14a3e..4df1fb2 100644 --- a/cli/sdm-ext/util/flag_util_test.go +++ b/cli/sdm-ext/util/flag_util_test.go @@ -3,13 +3,10 @@ package util import ( "testing" - "bou.ke/monkey" "github.com/stretchr/testify/assert" ) func TestFindFlag(t *testing.T) { - defer monkey.UnpatchAll() - tests := findFlagTests{} t.Run("TestFindFlag when find flag is successful", tests.testWhenFindFlagIsSuccessful) diff --git a/cli/sdm-ext/util/util.go b/cli/sdm-ext/util/util.go index dc148b2..eedeef2 100644 --- a/cli/sdm-ext/util/util.go +++ b/cli/sdm-ext/util/util.go @@ -1,110 +1,102 @@ -package util - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "runtime" - "strings" - - "github.com/urfave/cli" -) - -func GetBasePath() string { - _, file, _, _ := runtime.Caller(0) - basepath := filepath.Join(filepath.Dir(file), "../") - return basepath -} - -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 { - fmt.Print() // Needed because mock - 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 -} +package util + +import ( + "encoding/json" + "fmt" + "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 { + fmt.Print() // Needed because mock + 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 index 7d305ac..7962929 100644 --- a/cli/sdm-ext/util/util_test.go +++ b/cli/sdm-ext/util/util_test.go @@ -7,7 +7,6 @@ import ( "regexp/syntax" "testing" - "bou.ke/monkey" "github.com/stretchr/testify/assert" "github.com/urfave/cli" ) @@ -22,8 +21,6 @@ const ( ) func TestMapCommandArguments(t *testing.T) { - defer monkey.UnpatchAll() - tests := mapCommandArgumentsTests{} t.Run("Test MapCommandArguments when it is successful", tests.testWhenItIsSuccessful) @@ -52,8 +49,6 @@ func (tests mapCommandArgumentsTests) testWhenItReturnsAnEmptyMap(t *testing.T) } func TestFlagHasName(t *testing.T) { - defer monkey.UnpatchAll() - tests := flagHasNameTests{} t.Run("Test FlagHasName when it is successful", tests.testWhenItIsSuccessful) @@ -190,8 +185,6 @@ func (tests getUserInputTests) testWhenTheUserInputABadlyFormattedJson(t *testin } func TestConvertStrSliceToStr(t *testing.T) { - defer monkey.UnpatchAll() - tests := convertStrSliceToStrTests{} t.Run("Test ConvertStrSliceToStr when it is successful", tests.testWhenItIsSuccessful) @@ -218,8 +211,6 @@ func (tests convertStrSliceToStrTests) testWhenTheGivenSliceIsEmpty(t *testing.T } func TestCheckRegexMatch(t *testing.T) { - defer monkey.UnpatchAll() - tests := checkRegexMatchTests{} t.Run("Test CheckRegexMatch when it is successful", tests.testWhenItIsSuccessful) From 1e23507bade11626dbf01ce1d855b7e6dd144616 Mon Sep 17 00:00:00 2001 From: felipesantoos Date: Thu, 7 Apr 2022 08:29:39 -0300 Subject: [PATCH 81/87] Remove monkey-patching Co-authored-by: gaabrieljesuss --- cli/sdm-ext/adapter/adapter.go | 2 +- cli/sdm-ext/cli/app.go | 4 +- cli/sdm-ext/cli/app_test.go | 17 +- cli/sdm-ext/cli/servers.go | 8 +- cli/sdm-ext/cli/servers_test.go | 273 ++++++++++++++---- cli/sdm-ext/go.mod | 39 ++- cli/sdm-ext/go.sum | 52 ++-- .../service/admin_servers_add_service_test.go | 83 ++++-- cli/sdm-ext/service/admin_service.go | 4 +- cli/sdm-ext/service/admin_service_test.go | 17 +- cli/sdm-ext/util/flag_util.go | 2 +- cli/sdm-ext/util/util.go | 8 +- 12 files changed, 361 insertions(+), 148 deletions(-) diff --git a/cli/sdm-ext/adapter/adapter.go b/cli/sdm-ext/adapter/adapter.go index 056380a..a4e42df 100644 --- a/cli/sdm-ext/adapter/adapter.go +++ b/cli/sdm-ext/adapter/adapter.go @@ -5,7 +5,7 @@ import ( "fmt" ) -func Servers(commandName string, mappedOptions map[string]string) error { +var Servers = func(commandName string, mappedOptions map[string]string) error { adminService := service.NewAdminService() switch commandName { diff --git a/cli/sdm-ext/cli/app.go b/cli/sdm-ext/cli/app.go index 07109e0..17d5c8d 100644 --- a/cli/sdm-ext/cli/app.go +++ b/cli/sdm-ext/cli/app.go @@ -43,14 +43,14 @@ func executeWithSdm() (*strings.Builder, *strings.Builder) { return stdout, stderr } -func runCommand(cmd *exec.Cmd) { +var runCommand = func(cmd *exec.Cmd) { err := cmd.Run() if err != nil { fmt.Println(err) } } -func commandNotFound(ctx *cli.Context, command string) { +var commandNotFound = func(ctx *cli.Context, command string) { stdout, stderr := executeWithSdm() 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 index cc2226b..6b42611 100644 --- a/cli/sdm-ext/cli/app_test.go +++ b/cli/sdm-ext/cli/app_test.go @@ -5,7 +5,6 @@ import ( "strings" "testing" - "bou.ke/monkey" "github.com/stretchr/testify/assert" ) @@ -20,9 +19,11 @@ func TestExecuteWithSdm(t *testing.T) { type executeWithSdmTests struct{} func (tests executeWithSdmTests) testWhenSdmCommandIsSuccessful(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(runCommand, runSuccessfulCommandMock) + runCommandBackup := runCommand + runCommand = runSuccessfulCommandMock + defer func() { + runCommand = runCommandBackup + }() actualStdout, actualStderr := executeWithSdm() @@ -36,9 +37,11 @@ func (tests executeWithSdmTests) testWhenSdmCommandIsSuccessful(t *testing.T) { } func (tests executeWithSdmTests) testWhenSdmCommandFails(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(runCommand, runFailedCommandMock) + runCommandBackup := runCommand + runCommand = runFailedCommandMock + defer func() { + runCommand = runCommandBackup + }() actualStdout, actualStderr := executeWithSdm() diff --git a/cli/sdm-ext/cli/servers.go b/cli/sdm-ext/cli/servers.go index 8351c36..45edf99 100644 --- a/cli/sdm-ext/cli/servers.go +++ b/cli/sdm-ext/cli/servers.go @@ -67,26 +67,26 @@ func adminServersAddAction(ctx *cli.Context) error { return nil } -func getArgs(ctx *cli.Context) cli.Args { +var getArgs = func(ctx *cli.Context) cli.Args { if ctx == nil { fmt.Println() // Needed because mock } return ctx.Args() } -func getSdmCommand(appName, commandName, arguments string) string { +var getSdmCommand = func(appName, commandName, arguments string) string { newAppName := removeSdmExt(appName) return fmt.Sprintf("%s %s %s", newAppName, commandName, arguments) } -func getAppName(ctx *cli.Context) string { +var getAppName = func(ctx *cli.Context) string { if ctx == nil { fmt.Println() // Needed because mock } return ctx.App.Name } -func getCommandName(ctx *cli.Context) string { +var getCommandName = func(ctx *cli.Context) string { if ctx == nil { fmt.Println() // Needed because mock } diff --git a/cli/sdm-ext/cli/servers_test.go b/cli/sdm-ext/cli/servers_test.go index dc5c25c..a3411ec 100644 --- a/cli/sdm-ext/cli/servers_test.go +++ b/cli/sdm-ext/cli/servers_test.go @@ -5,7 +5,6 @@ import ( "ext/util" "testing" - "bou.ke/monkey" "github.com/stretchr/testify/assert" "github.com/urfave/cli" ) @@ -29,13 +28,35 @@ func TestAdminServersAddAction(t *testing.T) { type adminServersAddActionTests struct{} func (tests adminServersAddActionTests) testWhenThePassedCommandIsValid(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(getArgs, getArgsMock) - monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrMock) - monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenMatchesMock) - monkey.Patch(util.MapCommandArguments, mapCommandArgumentsMock) - monkey.Patch(adapter.Servers, serversMock) + getArgsBackup := getArgs + getArgs = getArgsMock + defer func() { + getArgs = getArgsBackup + }() + + convertStrSliceToStrBackup := util.ConvertStrSliceToStr + util.ConvertStrSliceToStr = convertStrSliceToStrMock + defer func() { + util.ConvertStrSliceToStr = convertStrSliceToStrBackup + }() + + checkRegexMatchBackup := util.CheckRegexMatch + util.CheckRegexMatch = checkRegexMatchWhenMatchesMock + defer func() { + util.CheckRegexMatch = checkRegexMatchBackup + }() + + mapCommandArgumentsBackup := util.MapCommandArguments + util.MapCommandArguments = mapCommandArgumentsMock + defer func() { + util.MapCommandArguments = mapCommandArgumentsBackup + }() + + serversBackup := adapter.Servers + adapter.Servers = serversMock + defer func() { + adapter.Servers = serversBackup + }() actualErr := adminServersAddAction(&cli.Context{}) @@ -43,13 +64,35 @@ func (tests adminServersAddActionTests) testWhenThePassedCommandIsValid(t *testi } func (tests adminServersAddActionTests) testWhenThereIsNoArguments(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(getArgs, getEmptyArgsMock) - monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrMock) - monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenMatchesMock) - monkey.Patch(util.MapCommandArguments, mapCommandArgumentsMock) - monkey.Patch(adapter.Servers, serversMock) + getArgsBackup := getArgs + getArgs = getEmptyArgsMock + defer func() { + getArgs = getArgsBackup + }() + + convertStrSliceToStrBackup := util.ConvertStrSliceToStr + util.ConvertStrSliceToStr = convertStrSliceToStrMock + defer func() { + util.ConvertStrSliceToStr = convertStrSliceToStrBackup + }() + + checkRegexMatchBackup := util.CheckRegexMatch + util.CheckRegexMatch = checkRegexMatchWhenMatchesMock + defer func() { + util.CheckRegexMatch = checkRegexMatchBackup + }() + + mapCommandArgumentsBackup := util.MapCommandArguments + util.MapCommandArguments = mapCommandArgumentsMock + defer func() { + util.MapCommandArguments = mapCommandArgumentsBackup + }() + + serversBackup := adapter.Servers + adapter.Servers = serversMock + defer func() { + adapter.Servers = serversBackup + }() actualErr := adminServersAddAction(&cli.Context{}) @@ -57,15 +100,47 @@ func (tests adminServersAddActionTests) testWhenThereIsNoArguments(t *testing.T) } func (tests adminServersAddActionTests) testWhenThePassedFlagDoesNotExistInSdmExtCli(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(getArgs, getArgsWithWrongFlagMock) - monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrWithWrongFlagMock) - monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenDoesNotMatchesMock) - monkey.Patch(getSdmCommand, getSdmCommandMock) - monkey.Patch(getAppName, getAppNameMock) - monkey.Patch(getCommandName, getCommandNameMock) - monkey.Patch(commandNotFound, commandNotFoundMock) + getArgsBackup := getArgs + getArgs = getArgsWithWrongFlagMock + defer func() { + getArgs = getArgsBackup + }() + + convertStrSliceToStrBackup := util.ConvertStrSliceToStr + util.ConvertStrSliceToStr = convertStrSliceToStrWithWrongFlagMock + defer func() { + util.ConvertStrSliceToStr = convertStrSliceToStrBackup + }() + + checkRegexMatchBackup := util.CheckRegexMatch + util.CheckRegexMatch = checkRegexMatchWhenDoesNotMatchesMock + defer func() { + util.CheckRegexMatch = checkRegexMatchBackup + }() + + getSdmCommandBackup := getSdmCommand + getSdmCommand = getSdmCommandMock + defer func() { + getSdmCommand = getSdmCommandBackup + }() + + getAppNameBackup := getAppName + getAppName = getAppNameMock + defer func() { + getAppName = getAppNameBackup + }() + + getCommandNameBackup := getCommandName + getCommandName = getCommandNameMock + defer func() { + getCommandName = getCommandNameBackup + }() + + commandNotFoundBackup := commandNotFound + commandNotFound = commandNotFoundMock + defer func() { + commandNotFound = commandNotFoundBackup + }() actualErr := adminServersAddAction(&cli.Context{}) @@ -73,15 +148,47 @@ func (tests adminServersAddActionTests) testWhenThePassedFlagDoesNotExistInSdmEx } func (tests adminServersAddActionTests) testWhenASubcommandIsPassedBetweenAddCommandAndFlag(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(getArgs, getArgsWithSubcommandBetweenCommandAndFlagMock) - monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrWithSubcommandBetweenCommandAndFlagMock) - monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenDoesNotMatchesMock) - monkey.Patch(getSdmCommand, getSdmCommandMock) - monkey.Patch(getAppName, getAppNameMock) - monkey.Patch(getCommandName, getCommandNameMock) - monkey.Patch(commandNotFound, commandNotFoundMock) + getArgsBackup := getArgs + getArgs = getArgsWithSubcommandBetweenCommandAndFlagMock + defer func() { + getArgs = getArgsBackup + }() + + convertStrSliceToStrBackup := util.ConvertStrSliceToStr + util.ConvertStrSliceToStr = convertStrSliceToStrWithSubcommandBetweenCommandAndFlagMock + defer func() { + util.ConvertStrSliceToStr = convertStrSliceToStrBackup + }() + + checkRegexMatchBackup := util.CheckRegexMatch + util.CheckRegexMatch = checkRegexMatchWhenDoesNotMatchesMock + defer func() { + util.CheckRegexMatch = checkRegexMatchBackup + }() + + getSdmCommandBackup := getSdmCommand + getSdmCommand = getSdmCommandMock + defer func() { + getSdmCommand = getSdmCommandBackup + }() + + getAppNameBackup := getAppName + getAppName = getAppNameMock + defer func() { + getAppName = getAppNameBackup + }() + + getCommandNameBackup := getCommandName + getCommandName = getCommandNameMock + defer func() { + getCommandName = getCommandNameBackup + }() + + commandNotFoundBackup := commandNotFound + commandNotFound = commandNotFoundMock + defer func() { + commandNotFound = commandNotFoundBackup + }() actualErr := adminServersAddAction(&cli.Context{}) @@ -89,15 +196,47 @@ func (tests adminServersAddActionTests) testWhenASubcommandIsPassedBetweenAddCom } func (tests adminServersAddActionTests) testWhenTheSubcommandIsPassedAfterFlagValue(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(getArgs, getArgsWithSubcommandAfterFlagValueMock) - monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrWithSubcommandAfterFlagValueMock) - monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenDoesNotMatchesMock) - monkey.Patch(getSdmCommand, getSdmCommandMock) - monkey.Patch(getAppName, getAppNameMock) - monkey.Patch(getCommandName, getCommandNameMock) - monkey.Patch(commandNotFound, commandNotFoundMock) + getArgsBackup := getArgs + getArgs = getArgsWithSubcommandAfterFlagValueMock + defer func() { + getArgs = getArgsBackup + }() + + convertStrSliceToStrBackup := util.ConvertStrSliceToStr + util.ConvertStrSliceToStr = convertStrSliceToStrWithSubcommandAfterFlagValueMock + defer func() { + util.ConvertStrSliceToStr = convertStrSliceToStrBackup + }() + + checkRegexMatchBackup := util.CheckRegexMatch + util.CheckRegexMatch = checkRegexMatchWhenDoesNotMatchesMock + defer func() { + util.CheckRegexMatch = checkRegexMatchBackup + }() + + getSdmCommandBackup := getSdmCommand + getSdmCommand = getSdmCommandMock + defer func() { + getSdmCommand = getSdmCommandBackup + }() + + getAppNameBackup := getAppName + getAppName = getAppNameMock + defer func() { + getAppName = getAppNameBackup + }() + + getCommandNameBackup := getCommandName + getCommandName = getCommandNameMock + defer func() { + getCommandName = getCommandNameBackup + }() + + commandNotFoundBackup := commandNotFound + commandNotFound = commandNotFoundMock + defer func() { + commandNotFound = commandNotFoundBackup + }() actualErr := adminServersAddAction(&cli.Context{}) @@ -105,15 +244,47 @@ func (tests adminServersAddActionTests) testWhenTheSubcommandIsPassedAfterFlagVa } func (tests adminServersAddActionTests) testWhenTheSubcommandIsPassedAfterFlag(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(getArgs, getArgsWithSubcommandAfterFlagMock) - monkey.Patch(util.ConvertStrSliceToStr, convertStrSliceToStrWithSubcommandAfterFlagMock) - monkey.Patch(util.CheckRegexMatch, checkRegexMatchWhenDoesNotMatchesMock) - monkey.Patch(getSdmCommand, getSdmCommandMock) - monkey.Patch(getAppName, getAppNameMock) - monkey.Patch(getCommandName, getCommandNameMock) - monkey.Patch(commandNotFound, commandNotFoundMock) + getArgsBackup := getArgs + getArgs = getArgsWithSubcommandAfterFlagMock + defer func() { + getArgs = getArgsBackup + }() + + convertStrSliceToStrBackup := util.ConvertStrSliceToStr + util.ConvertStrSliceToStr = convertStrSliceToStrWithSubcommandAfterFlagMock + defer func() { + util.ConvertStrSliceToStr = convertStrSliceToStrBackup + }() + + checkRegexMatchBackup := util.CheckRegexMatch + util.CheckRegexMatch = checkRegexMatchWhenDoesNotMatchesMock + defer func() { + util.CheckRegexMatch = checkRegexMatchBackup + }() + + getSdmCommandBackup := getSdmCommand + getSdmCommand = getSdmCommandMock + defer func() { + getSdmCommand = getSdmCommandBackup + }() + + getAppNameBackup := getAppName + getAppName = getAppNameMock + defer func() { + getAppName = getAppNameBackup + }() + + getCommandNameBackup := getCommandName + getCommandName = getCommandNameMock + defer func() { + getCommandName = getCommandNameBackup + }() + + commandNotFoundBackup := commandNotFound + commandNotFound = commandNotFoundMock + defer func() { + commandNotFound = commandNotFoundBackup + }() actualErr := adminServersAddAction(&cli.Context{}) diff --git a/cli/sdm-ext/go.mod b/cli/sdm-ext/go.mod index 98bbbaa..bb17d95 100644 --- a/cli/sdm-ext/go.mod +++ b/cli/sdm-ext/go.mod @@ -1,20 +1,19 @@ -module ext - -go 1.17 - -require ( - github.com/joho/godotenv v1.4.0 - github.com/urfave/cli v1.22.5 -) - -require ( - bou.ke/monkey v1.0.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect - github.com/davecgh/go-spew v1.1.0 // indirect - github.com/mailru/easyjson v0.7.7 // 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 - github.com/stretchr/testify v1.7.1 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect -) +module ext + +go 1.17 + +require ( + github.com/joho/godotenv v1.4.0 + 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/mailru/easyjson v0.7.7 // 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 + github.com/stretchr/testify v1.7.1 // 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 index e65c20e..5694c3a 100644 --- a/cli/sdm-ext/go.sum +++ b/cli/sdm-ext/go.sum @@ -1,27 +1,25 @@ -bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= -bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= -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/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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/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= +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/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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/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/service/admin_servers_add_service_test.go b/cli/sdm-ext/service/admin_servers_add_service_test.go index 47a8921..7cafaa7 100644 --- a/cli/sdm-ext/service/admin_servers_add_service_test.go +++ b/cli/sdm-ext/service/admin_servers_add_service_test.go @@ -6,7 +6,6 @@ import ( "strings" "testing" - "bou.ke/monkey" "github.com/stretchr/testify/assert" ) @@ -27,11 +26,23 @@ func TestAdminServersAdd(t *testing.T) { type adminServersAddTests struct{} func (tests adminServersAddTests) testWhenItIsSucessful(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(util.FindFlag, findFlagMock) - monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonMock) - monkey.Patch(execute, executeWithSuccessMock) + findFlagBackup := util.FindFlag + util.FindFlag = findFlagMock + defer func() { + util.FindFlag = findFlagBackup + }() + + extractValuesFromJsonBackup := util.ExtractValuesFromJson + util.ExtractValuesFromJson = extractValuesFromJsonMock + defer func() { + util.ExtractValuesFromJson = extractValuesFromJsonBackup + }() + + executeBackup := execute + execute = executeWithSuccessMock + defer func() { + execute = executeBackup + }() options := map[string]string{"--file": "file.json"} service := NewAdminService() @@ -41,9 +52,11 @@ func (tests adminServersAddTests) testWhenItIsSucessful(t *testing.T) { } func (tests adminServersAddTests) testWhenFindFlagReturnsEmpty(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(util.FindFlag, findFlagReturningEmptyMock) + findFlagBackup := util.FindFlag + util.FindFlag = findFlagReturningEmptyMock + defer func() { + util.FindFlag = findFlagBackup + }() options := map[string]string{"--files": "file.json"} service := NewAdminService() @@ -53,10 +66,17 @@ func (tests adminServersAddTests) testWhenFindFlagReturnsEmpty(t *testing.T) { } func (tests adminServersAddTests) testWhenAnNonexistentFileIsPassed(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(util.FindFlag, findFlagMock) - monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonWithABaddlyFormattedJsonMock) + findFlagBackup := util.FindFlag + util.FindFlag = findFlagMock + defer func() { + util.FindFlag = findFlagBackup + }() + + extractValuesFromJsonBackup := util.ExtractValuesFromJson + util.ExtractValuesFromJson = extractValuesFromJsonWithABaddlyFormattedJsonMock + defer func() { + util.ExtractValuesFromJson = extractValuesFromJsonBackup + }() options := map[string]string{"--file": "file.json"} service := NewAdminService() @@ -67,10 +87,17 @@ func (tests adminServersAddTests) testWhenAnNonexistentFileIsPassed(t *testing.T assert.Equal(t, expectedErr.Error(), actualErr.Error()) } func (tests adminServersAddTests) testWhenAnExistentFileWithAWrongContentIsPassed(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(util.FindFlag, findFlagMock) - monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonWithANonExistentJsonFileMock) + findFlagBackup := util.FindFlag + util.FindFlag = findFlagMock + defer func() { + util.FindFlag = findFlagBackup + }() + + extractValuesFromJsonBackup := util.ExtractValuesFromJson + util.ExtractValuesFromJson = extractValuesFromJsonWithANonExistentJsonFileMock + defer func() { + util.ExtractValuesFromJson = extractValuesFromJsonBackup + }() options := map[string]string{"--file": "file.json"} service := NewAdminService() @@ -82,11 +109,23 @@ func (tests adminServersAddTests) testWhenAnExistentFileWithAWrongContentIsPasse } func (tests adminServersAddTests) testWhenOneOfTheServersIsNotRegistered(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(util.FindFlag, findFlagMock) - monkey.Patch(util.ExtractValuesFromJson, extractValuesFromJsonMock) - monkey.Patch(execute, executeWithoutSuccessMock) + findFlagBackup := util.FindFlag + util.FindFlag = findFlagMock + defer func() { + util.FindFlag = findFlagBackup + }() + + extractValuesFromJsonBackup := util.ExtractValuesFromJson + util.ExtractValuesFromJson = extractValuesFromJsonMock + defer func() { + util.ExtractValuesFromJson = extractValuesFromJsonBackup + }() + + executeBackup := execute + execute = executeWithoutSuccessMock + defer func() { + execute = executeBackup + }() options := map[string]string{"--file": "file.json"} service := NewAdminService() diff --git a/cli/sdm-ext/service/admin_service.go b/cli/sdm-ext/service/admin_service.go index c14dbc4..21d1331 100644 --- a/cli/sdm-ext/service/admin_service.go +++ b/cli/sdm-ext/service/admin_service.go @@ -12,7 +12,7 @@ func NewAdminService() *AdminService { return &AdminService{} } -func execute(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) { +var execute = func(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) { opts := append(optionsToArguments(options), postOptions) commandsAndOptions := append(strings.Split(commands, " "), opts...) @@ -29,7 +29,7 @@ func execute(commands string, options map[string]string, postOptions string) (st return *stdout, *stderr } -func runCommand(cmd *exec.Cmd) { +var runCommand = func(cmd *exec.Cmd) { err := cmd.Run() if err != nil { fmt.Println(err) diff --git a/cli/sdm-ext/service/admin_service_test.go b/cli/sdm-ext/service/admin_service_test.go index 08cc7fd..6a3ff22 100644 --- a/cli/sdm-ext/service/admin_service_test.go +++ b/cli/sdm-ext/service/admin_service_test.go @@ -5,7 +5,6 @@ import ( "strings" "testing" - "bou.ke/monkey" "github.com/stretchr/testify/assert" ) @@ -20,9 +19,11 @@ func TestExecute(t *testing.T) { type executeTests struct{} func (tests executeTests) testWhenThePassedCommadIsValid(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(runCommand, runSuccessfulCommandMock) + runCommandBackup := runCommand + runCommand = runSuccessfulCommandMock + defer func() { + runCommand = runCommandBackup + }() commands := getRawTCPServerAddCommand() options := getOptionsToExecute() @@ -40,9 +41,11 @@ func (tests executeTests) testWhenThePassedCommadIsValid(t *testing.T) { } func (tests executeTests) testWhenThePassedCommadFails(t *testing.T) { - defer monkey.UnpatchAll() - - monkey.Patch(runCommand, runFailedCommandMock) + runCommandBackup := runCommand + runCommand = runFailedCommandMock + defer func() { + runCommand = runCommandBackup + }() commands := getRawTCPServerAddCommand() options := getOptionsToExecute() diff --git a/cli/sdm-ext/util/flag_util.go b/cli/sdm-ext/util/flag_util.go index d35c244..c06f44e 100644 --- a/cli/sdm-ext/util/flag_util.go +++ b/cli/sdm-ext/util/flag_util.go @@ -18,7 +18,7 @@ func GetAdminServersAddStdinFlag() cli.BoolFlag { } } -func FindFlag(flagList []string, optionsMap map[string]string) string { +var FindFlag = func(flagList []string, optionsMap map[string]string) string { for _, flag := range flagList { for key := range optionsMap { if flag == key { diff --git a/cli/sdm-ext/util/util.go b/cli/sdm-ext/util/util.go index eedeef2..c89a1f3 100644 --- a/cli/sdm-ext/util/util.go +++ b/cli/sdm-ext/util/util.go @@ -11,7 +11,7 @@ import ( "github.com/urfave/cli" ) -func MapCommandArguments(arguments []string, flags []cli.Flag) map[string]string { +var MapCommandArguments = func(arguments []string, flags []cli.Flag) map[string]string { argsMapping := map[string]string{} previousArgIsFlag := false @@ -45,7 +45,7 @@ func FlagHasName(flag cli.Flag, argKey string) bool { return foundFlag } -func ExtractValuesFromJson(file string) ([]map[string]interface{}, error) { +var ExtractValuesFromJson = func(file string) ([]map[string]interface{}, error) { readFile, err := ioutil.ReadFile(file) if err != nil { return nil, err @@ -71,7 +71,7 @@ func GetUserInput() ([]map[string]interface{}, error) { return data, nil } -func ConvertStrSliceToStr(strList []string) string { +var ConvertStrSliceToStr = func(strList []string) string { fmt.Print() // Needed because mock strs := "" for i, str := range strList { @@ -85,7 +85,7 @@ func ConvertStrSliceToStr(strList []string) string { return strs } -func CheckRegexMatch(regexList []string, arguments string) (bool, error) { +var CheckRegexMatch = func(regexList []string, arguments string) (bool, error) { var matched bool var err error for _, regex := range regexList { From cae9c136fe1459ccd3f8818445307e857c843ea8 Mon Sep 17 00:00:00 2001 From: felipesantoos Date: Fri, 8 Apr 2022 15:05:51 -0300 Subject: [PATCH 82/87] Update mock strategy Co-authored-by: gaabrieljesuss --- cli/sdm-ext/adapter/adapter.go | 2 +- cli/sdm-ext/cli/app.go | 30 +- cli/sdm-ext/cli/app_test.go | 16 +- cli/sdm-ext/cli/sdm.go | 42 +++ cli/sdm-ext/cli/sdm_ext.go | 74 +++++ cli/sdm-ext/cli/servers.go | 41 +-- cli/sdm-ext/cli/servers_test.go | 298 ++++-------------- cli/sdm-ext/cli/test_utils.go | 9 + cli/sdm-ext/go.mod | 36 +-- cli/sdm-ext/go.sum | 46 ++- .../service/admin_servers_add_service.go | 19 +- .../service/admin_servers_add_service_test.go | 95 ++---- cli/sdm-ext/service/admin_service.go | 59 ++-- cli/sdm-ext/service/admin_service_test.go | 16 +- cli/sdm-ext/service/sdm_service.go | 68 ++++ cli/sdm-ext/service/test_utils.go | 7 + cli/sdm-ext/util/flag_util.go | 2 +- cli/sdm-ext/util/util.go | 10 +- 18 files changed, 389 insertions(+), 481 deletions(-) create mode 100644 cli/sdm-ext/cli/sdm.go create mode 100644 cli/sdm-ext/cli/sdm_ext.go create mode 100644 cli/sdm-ext/cli/test_utils.go create mode 100644 cli/sdm-ext/service/sdm_service.go create mode 100644 cli/sdm-ext/service/test_utils.go diff --git a/cli/sdm-ext/adapter/adapter.go b/cli/sdm-ext/adapter/adapter.go index a4e42df..056380a 100644 --- a/cli/sdm-ext/adapter/adapter.go +++ b/cli/sdm-ext/adapter/adapter.go @@ -5,7 +5,7 @@ import ( "fmt" ) -var Servers = func(commandName string, mappedOptions map[string]string) error { +func Servers(commandName string, mappedOptions map[string]string) error { adminService := service.NewAdminService() switch commandName { diff --git a/cli/sdm-ext/cli/app.go b/cli/sdm-ext/cli/app.go index 17d5c8d..85124ef 100644 --- a/cli/sdm-ext/cli/app.go +++ b/cli/sdm-ext/cli/app.go @@ -2,9 +2,6 @@ package cli import ( "fmt" - "os" - "os/exec" - "strings" "github.com/urfave/cli" ) @@ -28,30 +25,9 @@ func NewApp() *cli.App { return app } -func executeWithSdm() (*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 - - runCommand(cmd) - - return stdout, stderr -} - -var runCommand = func(cmd *exec.Cmd) { - err := cmd.Run() - if err != nil { - fmt.Println(err) - } -} - -var commandNotFound = func(ctx *cli.Context, command string) { - stdout, stderr := executeWithSdm() +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 index 6b42611..7de059f 100644 --- a/cli/sdm-ext/cli/app_test.go +++ b/cli/sdm-ext/cli/app_test.go @@ -19,13 +19,9 @@ func TestExecuteWithSdm(t *testing.T) { type executeWithSdmTests struct{} func (tests executeWithSdmTests) testWhenSdmCommandIsSuccessful(t *testing.T) { - runCommandBackup := runCommand - runCommand = runSuccessfulCommandMock - defer func() { - runCommand = runCommandBackup - }() + sdmImpl := NewSdmMock(runSuccessfulCommandMock) - actualStdout, actualStderr := executeWithSdm() + actualStdout, actualStderr := sdmImpl.execute() expectedStdout := new(strings.Builder) expectedStdout.Write(getFilledStdout()) @@ -37,13 +33,9 @@ func (tests executeWithSdmTests) testWhenSdmCommandIsSuccessful(t *testing.T) { } func (tests executeWithSdmTests) testWhenSdmCommandFails(t *testing.T) { - runCommandBackup := runCommand - runCommand = runFailedCommandMock - defer func() { - runCommand = runCommandBackup - }() + sdmImpl := NewSdmMock(runFailedCommandMock) - actualStdout, actualStderr := executeWithSdm() + actualStdout, actualStderr := sdmImpl.execute() expectedStdout := new(strings.Builder) expectedStdout.Write(getEmptyStdout()) 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 index 45edf99..ee1ecf6 100644 --- a/cli/sdm-ext/cli/servers.go +++ b/cli/sdm-ext/cli/servers.go @@ -1,7 +1,6 @@ package cli import ( - "ext/adapter" "ext/util" "fmt" "strings" @@ -10,8 +9,8 @@ import ( ) const ( - FILE_REGEX_PATTERN = `^--file [\.\/\w,-]+\.[A-Za-z]+$` - F_REGEX_PATTERN = `^-f [\.\/\w,-]+\.[A-Za-z]+$` + FILE_REGEX_PATTERN = `^--file [\.:\/\\\w,-]+\.[A-Za-z]+$` + F_REGEX_PATTERN = `^-f [\.:\/\\\w,-]+\.[A-Za-z]+$` STDIN_REGEX_PATTERN = `^--stdin$` I_REGEX_PATTERN = `^-i$` ) @@ -33,7 +32,7 @@ var adminServersAddCommand = cli.Command{ Aliases: []string{"create"}, Usage: "add one or more server", Flags: adminServersAddFlags, - Action: adminServersAddAction, + Action: NewSdmExt().adminServersAddAction, SkipFlagParsing: true, } @@ -42,24 +41,23 @@ var adminServersAddFlags = []cli.Flag{ util.GetAdminServersAddStdinFlag(), } -func adminServersAddAction(ctx *cli.Context) error { - argumentList := getArgs(ctx) - arguments := util.ConvertStrSliceToStr(argumentList) - - matched, err := util.CheckRegexMatch(getRegexList(), arguments) +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 := getSdmCommand(getAppName(ctx), getCommandName(ctx), arguments) - commandNotFound(ctx, sdmCommand) + sdmCommand := i.getSdmCommand(i.getAppName(ctx), i.getCommandName(ctx), arguments) + i.commandNotFound(ctx, sdmCommand) return nil } - mappedArguments := util.MapCommandArguments(argumentList, adminServersAddFlags) - err = adapter.Servers(ctx.Command.Name, mappedArguments) + mappedArguments := i.mapCommandArguments(argumentList, adminServersAddFlags) + err = i.servers(ctx.Command.Name, mappedArguments) if err != nil { return err } @@ -67,29 +65,20 @@ func adminServersAddAction(ctx *cli.Context) error { return nil } -var getArgs = func(ctx *cli.Context) cli.Args { - if ctx == nil { - fmt.Println() // Needed because mock - } +func getArgs(ctx *cli.Context) cli.Args { return ctx.Args() } -var getSdmCommand = func(appName, commandName, arguments string) string { +func getSdmCommand(appName, commandName, arguments string) string { newAppName := removeSdmExt(appName) return fmt.Sprintf("%s %s %s", newAppName, commandName, arguments) } -var getAppName = func(ctx *cli.Context) string { - if ctx == nil { - fmt.Println() // Needed because mock - } +func getAppName(ctx *cli.Context) string { return ctx.App.Name } -var getCommandName = func(ctx *cli.Context) string { - if ctx == nil { - fmt.Println() // Needed because mock - } +func getCommandName(ctx *cli.Context) string { return ctx.Command.Name } diff --git a/cli/sdm-ext/cli/servers_test.go b/cli/sdm-ext/cli/servers_test.go index a3411ec..1c737b5 100644 --- a/cli/sdm-ext/cli/servers_test.go +++ b/cli/sdm-ext/cli/servers_test.go @@ -1,8 +1,6 @@ package cli import ( - "ext/adapter" - "ext/util" "testing" "github.com/stretchr/testify/assert" @@ -28,265 +26,93 @@ func TestAdminServersAddAction(t *testing.T) { type adminServersAddActionTests struct{} func (tests adminServersAddActionTests) testWhenThePassedCommandIsValid(t *testing.T) { - getArgsBackup := getArgs - getArgs = getArgsMock - defer func() { - getArgs = getArgsBackup - }() - - convertStrSliceToStrBackup := util.ConvertStrSliceToStr - util.ConvertStrSliceToStr = convertStrSliceToStrMock - defer func() { - util.ConvertStrSliceToStr = convertStrSliceToStrBackup - }() - - checkRegexMatchBackup := util.CheckRegexMatch - util.CheckRegexMatch = checkRegexMatchWhenMatchesMock - defer func() { - util.CheckRegexMatch = checkRegexMatchBackup - }() - - mapCommandArgumentsBackup := util.MapCommandArguments - util.MapCommandArguments = mapCommandArgumentsMock - defer func() { - util.MapCommandArguments = mapCommandArgumentsBackup - }() - - serversBackup := adapter.Servers - adapter.Servers = serversMock - defer func() { - adapter.Servers = serversBackup - }() - - actualErr := adminServersAddAction(&cli.Context{}) + 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) { - getArgsBackup := getArgs - getArgs = getEmptyArgsMock - defer func() { - getArgs = getArgsBackup - }() - - convertStrSliceToStrBackup := util.ConvertStrSliceToStr - util.ConvertStrSliceToStr = convertStrSliceToStrMock - defer func() { - util.ConvertStrSliceToStr = convertStrSliceToStrBackup - }() - - checkRegexMatchBackup := util.CheckRegexMatch - util.CheckRegexMatch = checkRegexMatchWhenMatchesMock - defer func() { - util.CheckRegexMatch = checkRegexMatchBackup - }() - - mapCommandArgumentsBackup := util.MapCommandArguments - util.MapCommandArguments = mapCommandArgumentsMock - defer func() { - util.MapCommandArguments = mapCommandArgumentsBackup - }() - - serversBackup := adapter.Servers - adapter.Servers = serversMock - defer func() { - adapter.Servers = serversBackup - }() - - actualErr := adminServersAddAction(&cli.Context{}) + 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) { - getArgsBackup := getArgs - getArgs = getArgsWithWrongFlagMock - defer func() { - getArgs = getArgsBackup - }() - - convertStrSliceToStrBackup := util.ConvertStrSliceToStr - util.ConvertStrSliceToStr = convertStrSliceToStrWithWrongFlagMock - defer func() { - util.ConvertStrSliceToStr = convertStrSliceToStrBackup - }() - - checkRegexMatchBackup := util.CheckRegexMatch - util.CheckRegexMatch = checkRegexMatchWhenDoesNotMatchesMock - defer func() { - util.CheckRegexMatch = checkRegexMatchBackup - }() - - getSdmCommandBackup := getSdmCommand - getSdmCommand = getSdmCommandMock - defer func() { - getSdmCommand = getSdmCommandBackup - }() - - getAppNameBackup := getAppName - getAppName = getAppNameMock - defer func() { - getAppName = getAppNameBackup - }() - - getCommandNameBackup := getCommandName - getCommandName = getCommandNameMock - defer func() { - getCommandName = getCommandNameBackup - }() - - commandNotFoundBackup := commandNotFound - commandNotFound = commandNotFoundMock - defer func() { - commandNotFound = commandNotFoundBackup - }() - - actualErr := adminServersAddAction(&cli.Context{}) + 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) { - getArgsBackup := getArgs - getArgs = getArgsWithSubcommandBetweenCommandAndFlagMock - defer func() { - getArgs = getArgsBackup - }() - - convertStrSliceToStrBackup := util.ConvertStrSliceToStr - util.ConvertStrSliceToStr = convertStrSliceToStrWithSubcommandBetweenCommandAndFlagMock - defer func() { - util.ConvertStrSliceToStr = convertStrSliceToStrBackup - }() - - checkRegexMatchBackup := util.CheckRegexMatch - util.CheckRegexMatch = checkRegexMatchWhenDoesNotMatchesMock - defer func() { - util.CheckRegexMatch = checkRegexMatchBackup - }() - - getSdmCommandBackup := getSdmCommand - getSdmCommand = getSdmCommandMock - defer func() { - getSdmCommand = getSdmCommandBackup - }() - - getAppNameBackup := getAppName - getAppName = getAppNameMock - defer func() { - getAppName = getAppNameBackup - }() - - getCommandNameBackup := getCommandName - getCommandName = getCommandNameMock - defer func() { - getCommandName = getCommandNameBackup - }() - - commandNotFoundBackup := commandNotFound - commandNotFound = commandNotFoundMock - defer func() { - commandNotFound = commandNotFoundBackup - }() - - actualErr := adminServersAddAction(&cli.Context{}) + 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) { - getArgsBackup := getArgs - getArgs = getArgsWithSubcommandAfterFlagValueMock - defer func() { - getArgs = getArgsBackup - }() - - convertStrSliceToStrBackup := util.ConvertStrSliceToStr - util.ConvertStrSliceToStr = convertStrSliceToStrWithSubcommandAfterFlagValueMock - defer func() { - util.ConvertStrSliceToStr = convertStrSliceToStrBackup - }() - - checkRegexMatchBackup := util.CheckRegexMatch - util.CheckRegexMatch = checkRegexMatchWhenDoesNotMatchesMock - defer func() { - util.CheckRegexMatch = checkRegexMatchBackup - }() - - getSdmCommandBackup := getSdmCommand - getSdmCommand = getSdmCommandMock - defer func() { - getSdmCommand = getSdmCommandBackup - }() - - getAppNameBackup := getAppName - getAppName = getAppNameMock - defer func() { - getAppName = getAppNameBackup - }() - - getCommandNameBackup := getCommandName - getCommandName = getCommandNameMock - defer func() { - getCommandName = getCommandNameBackup - }() - - commandNotFoundBackup := commandNotFound - commandNotFound = commandNotFoundMock - defer func() { - commandNotFound = commandNotFoundBackup - }() - - actualErr := adminServersAddAction(&cli.Context{}) + 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) { - getArgsBackup := getArgs - getArgs = getArgsWithSubcommandAfterFlagMock - defer func() { - getArgs = getArgsBackup - }() - - convertStrSliceToStrBackup := util.ConvertStrSliceToStr - util.ConvertStrSliceToStr = convertStrSliceToStrWithSubcommandAfterFlagMock - defer func() { - util.ConvertStrSliceToStr = convertStrSliceToStrBackup - }() - - checkRegexMatchBackup := util.CheckRegexMatch - util.CheckRegexMatch = checkRegexMatchWhenDoesNotMatchesMock - defer func() { - util.CheckRegexMatch = checkRegexMatchBackup - }() - - getSdmCommandBackup := getSdmCommand - getSdmCommand = getSdmCommandMock - defer func() { - getSdmCommand = getSdmCommandBackup - }() - - getAppNameBackup := getAppName - getAppName = getAppNameMock - defer func() { - getAppName = getAppNameBackup - }() - - getCommandNameBackup := getCommandName - getCommandName = getCommandNameMock - defer func() { - getCommandName = getCommandNameBackup - }() - - commandNotFoundBackup := commandNotFound - commandNotFound = commandNotFoundMock - defer func() { - commandNotFound = commandNotFoundBackup - }() - - actualErr := adminServersAddAction(&cli.Context{}) + 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) } 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 index bb17d95..4f92ce3 100644 --- a/cli/sdm-ext/go.mod +++ b/cli/sdm-ext/go.mod @@ -1,19 +1,17 @@ -module ext - -go 1.17 - -require ( - github.com/joho/godotenv v1.4.0 - 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/mailru/easyjson v0.7.7 // 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 - github.com/stretchr/testify v1.7.1 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect -) +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 index 5694c3a..94cc993 100644 --- a/cli/sdm-ext/go.sum +++ b/cli/sdm-ext/go.sum @@ -1,25 +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/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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/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= +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/service/admin_servers_add_service.go b/cli/sdm-ext/service/admin_servers_add_service.go index ce0e3a6..a75350c 100644 --- a/cli/sdm-ext/service/admin_servers_add_service.go +++ b/cli/sdm-ext/service/admin_servers_add_service.go @@ -3,19 +3,20 @@ package service import ( "ext/util" "fmt" + "sort" "strings" "unicode" ) -func (a AdminService) AdminServersAdd(options map[string]string) error { +func (a AdminServiceImpl) AdminServersAdd(options map[string]string) error { flagList := []string{"--file", "-f", "--stdin", "-i"} - flag := util.FindFlag(flagList, options) + flag := a.findFlag(flagList, options) var servers []map[string]interface{} var err error if flag == "--file" || flag == "-f" { - servers, err = util.ExtractValuesFromJson(options[flag]) + servers, err = a.extractValuesFromJson(options[flag]) if err != nil { return err } @@ -30,7 +31,7 @@ func (a AdminService) AdminServersAdd(options map[string]string) error { serverName := fmt.Sprint(server["name"]) serverType := fmt.Sprint(server["type"]) - _, stderr := execute( + _, stderr := a.execute( fmt.Sprintf("admin servers add %s", serverType), getOptions(server), serverName, @@ -79,10 +80,16 @@ func treatKey(key string) string { } 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, value := range tagsMap { - tags += fmt.Sprintf("%s=%s", key, value) + for _, key := range keys { + tags += fmt.Sprintf("%s=%s", key, tagsMap[key]) if i < len(tagsMap)-1 { 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 index 7cafaa7..778f85b 100644 --- a/cli/sdm-ext/service/admin_servers_add_service_test.go +++ b/cli/sdm-ext/service/admin_servers_add_service_test.go @@ -2,7 +2,6 @@ package service import ( "errors" - "ext/util" "strings" "testing" @@ -26,82 +25,48 @@ func TestAdminServersAdd(t *testing.T) { type adminServersAddTests struct{} func (tests adminServersAddTests) testWhenItIsSucessful(t *testing.T) { - findFlagBackup := util.FindFlag - util.FindFlag = findFlagMock - defer func() { - util.FindFlag = findFlagBackup - }() - - extractValuesFromJsonBackup := util.ExtractValuesFromJson - util.ExtractValuesFromJson = extractValuesFromJsonMock - defer func() { - util.ExtractValuesFromJson = extractValuesFromJsonBackup - }() - - executeBackup := execute - execute = executeWithSuccessMock - defer func() { - execute = executeBackup - }() + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagMock) + adminServiceImpl.patchExtractValuesFromJson(extractValuesFromJsonMock) + adminServiceImpl.patchExecute(executeWithSuccessMock) options := map[string]string{"--file": "file.json"} - service := NewAdminService() - actualErr := service.AdminServersAdd(options) + actualErr := adminServiceImpl.AdminServersAdd(options) assert.Nil(t, actualErr) } func (tests adminServersAddTests) testWhenFindFlagReturnsEmpty(t *testing.T) { - findFlagBackup := util.FindFlag - util.FindFlag = findFlagReturningEmptyMock - defer func() { - util.FindFlag = findFlagBackup - }() + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagReturningEmptyMock) options := map[string]string{"--files": "file.json"} - service := NewAdminService() - actualErr := service.AdminServersAdd(options) + actualErr := adminServiceImpl.AdminServersAdd(options) assert.Nil(t, actualErr) } func (tests adminServersAddTests) testWhenAnNonexistentFileIsPassed(t *testing.T) { - findFlagBackup := util.FindFlag - util.FindFlag = findFlagMock - defer func() { - util.FindFlag = findFlagBackup - }() - - extractValuesFromJsonBackup := util.ExtractValuesFromJson - util.ExtractValuesFromJson = extractValuesFromJsonWithABaddlyFormattedJsonMock - defer func() { - util.ExtractValuesFromJson = extractValuesFromJsonBackup - }() + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagMock) + adminServiceImpl.patchExtractValuesFromJson(extractValuesFromJsonWithABaddlyFormattedJsonMock) options := map[string]string{"--file": "file.json"} - service := NewAdminService() - actualErr := service.AdminServersAdd(options) + + 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) { - findFlagBackup := util.FindFlag - util.FindFlag = findFlagMock - defer func() { - util.FindFlag = findFlagBackup - }() - - extractValuesFromJsonBackup := util.ExtractValuesFromJson - util.ExtractValuesFromJson = extractValuesFromJsonWithANonExistentJsonFileMock - defer func() { - util.ExtractValuesFromJson = extractValuesFromJsonBackup - }() + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagMock) + adminServiceImpl.patchExtractValuesFromJson(extractValuesFromJsonWithANonExistentJsonFileMock) options := map[string]string{"--file": "file.json"} - service := NewAdminService() - actualErr := service.AdminServersAdd(options) + actualErr := adminServiceImpl.AdminServersAdd(options) expectedErr := errors.New("open file.json: no such file or directory") @@ -109,27 +74,13 @@ func (tests adminServersAddTests) testWhenAnExistentFileWithAWrongContentIsPasse } func (tests adminServersAddTests) testWhenOneOfTheServersIsNotRegistered(t *testing.T) { - findFlagBackup := util.FindFlag - util.FindFlag = findFlagMock - defer func() { - util.FindFlag = findFlagBackup - }() - - extractValuesFromJsonBackup := util.ExtractValuesFromJson - util.ExtractValuesFromJson = extractValuesFromJsonMock - defer func() { - util.ExtractValuesFromJson = extractValuesFromJsonBackup - }() - - executeBackup := execute - execute = executeWithoutSuccessMock - defer func() { - execute = executeBackup - }() + adminServiceImpl := NewAdminService() + adminServiceImpl.patchFindFlag(findFlagMock) + adminServiceImpl.patchExtractValuesFromJson(extractValuesFromJsonMock) + adminServiceImpl.patchExecute(executeWithoutSuccessMock) options := map[string]string{"--file": "file.json"} - service := NewAdminService() - actualErr := service.AdminServersAdd(options) + actualErr := adminServiceImpl.AdminServersAdd(options) assert.Nil(t, actualErr) } diff --git a/cli/sdm-ext/service/admin_service.go b/cli/sdm-ext/service/admin_service.go index 21d1331..155a0cf 100644 --- a/cli/sdm-ext/service/admin_service.go +++ b/cli/sdm-ext/service/admin_service.go @@ -1,54 +1,37 @@ package service import ( - "fmt" - "os/exec" + "ext/util" "strings" ) -type AdminService struct{} - -func NewAdminService() *AdminService { - return &AdminService{} +type adminService interface { + execute(commands string, options map[string]string, postOptions string) (strings.Builder, strings.Builder) } -var execute = func(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 - - runCommand(cmd) - - return *stdout, *stderr +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) } -var runCommand = func(cmd *exec.Cmd) { - err := cmd.Run() - if err != nil { - fmt.Println(err) +func NewAdminService() *AdminServiceImpl { + sdmService := NewSdmService() + return &AdminServiceImpl{ + util.FindFlag, + util.ExtractValuesFromJson, + sdmService.execute, } } -func optionsToArguments(options map[string]string) []string { - strOptions := []string{} +func (s *AdminServiceImpl) patchFindFlag(findFlag func(flagList []string, optionsMap map[string]string) string) { + s.findFlag = findFlag +} - for key, value := range options { - if key[len(key)-1:] == "=" { - key += value - value = "" - } - strOptions = append(strOptions, key) - if len(value) > 0 { - strOptions = append(strOptions, value) - } - } +func (s *AdminServiceImpl) patchExtractValuesFromJson(extractValuesFromJson func(file string) ([]map[string]interface{}, error)) { + s.extractValuesFromJson = extractValuesFromJson +} - return strOptions +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 index 6a3ff22..5e584a3 100644 --- a/cli/sdm-ext/service/admin_service_test.go +++ b/cli/sdm-ext/service/admin_service_test.go @@ -19,17 +19,13 @@ func TestExecute(t *testing.T) { type executeTests struct{} func (tests executeTests) testWhenThePassedCommadIsValid(t *testing.T) { - runCommandBackup := runCommand - runCommand = runSuccessfulCommandMock - defer func() { - runCommand = runCommandBackup - }() + sdmServiceImpl := NewSdmServiceMock(runSuccessfulCommandMock) commands := getRawTCPServerAddCommand() options := getOptionsToExecute() postOptions := getRawTCPServerName() - actualStdout, actualStderr := execute(commands, options, postOptions) + actualStdout, actualStderr := sdmServiceImpl.execute(commands, options, postOptions) expectedStdout := new(strings.Builder) expectedStdout.Write([]byte("")) @@ -41,17 +37,13 @@ func (tests executeTests) testWhenThePassedCommadIsValid(t *testing.T) { } func (tests executeTests) testWhenThePassedCommadFails(t *testing.T) { - runCommandBackup := runCommand - runCommand = runFailedCommandMock - defer func() { - runCommand = runCommandBackup - }() + sdmServiceImpl := NewSdmServiceMock(runFailedCommandMock) commands := getRawTCPServerAddCommand() options := getOptionsToExecute() postOptions := getRawTCPServerName() - actualStdout, actualStderr := execute(commands, options, postOptions) + actualStdout, actualStderr := sdmServiceImpl.execute(commands, options, postOptions) expectedStdout := new(strings.Builder) expectedStdout.Write([]byte("")) 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 index c06f44e..d35c244 100644 --- a/cli/sdm-ext/util/flag_util.go +++ b/cli/sdm-ext/util/flag_util.go @@ -18,7 +18,7 @@ func GetAdminServersAddStdinFlag() cli.BoolFlag { } } -var FindFlag = func(flagList []string, optionsMap map[string]string) string { +func FindFlag(flagList []string, optionsMap map[string]string) string { for _, flag := range flagList { for key := range optionsMap { if flag == key { diff --git a/cli/sdm-ext/util/util.go b/cli/sdm-ext/util/util.go index c89a1f3..0000822 100644 --- a/cli/sdm-ext/util/util.go +++ b/cli/sdm-ext/util/util.go @@ -2,7 +2,6 @@ package util import ( "encoding/json" - "fmt" "io/ioutil" "os" "regexp" @@ -11,7 +10,7 @@ import ( "github.com/urfave/cli" ) -var MapCommandArguments = func(arguments []string, flags []cli.Flag) map[string]string { +func MapCommandArguments(arguments []string, flags []cli.Flag) map[string]string { argsMapping := map[string]string{} previousArgIsFlag := false @@ -45,7 +44,7 @@ func FlagHasName(flag cli.Flag, argKey string) bool { return foundFlag } -var ExtractValuesFromJson = func(file string) ([]map[string]interface{}, error) { +func ExtractValuesFromJson(file string) ([]map[string]interface{}, error) { readFile, err := ioutil.ReadFile(file) if err != nil { return nil, err @@ -71,8 +70,7 @@ func GetUserInput() ([]map[string]interface{}, error) { return data, nil } -var ConvertStrSliceToStr = func(strList []string) string { - fmt.Print() // Needed because mock +func ConvertStrSliceToStr(strList []string) string { strs := "" for i, str := range strList { strs += str @@ -85,7 +83,7 @@ var ConvertStrSliceToStr = func(strList []string) string { return strs } -var CheckRegexMatch = func(regexList []string, arguments string) (bool, error) { +func CheckRegexMatch(regexList []string, arguments string) (bool, error) { var matched bool var err error for _, regex := range regexList { From ee06b3687e4c395afd781e4070e1006cda9e2f7f Mon Sep 17 00:00:00 2001 From: David Petersen Date: Thu, 28 Apr 2022 12:04:03 -0400 Subject: [PATCH 83/87] Audit: temp change --- audit/roles/role_resources.py | 35 --------------- audit/user_audit/audit_sdm_users.py | 70 ----------------------------- 2 files changed, 105 deletions(-) diff --git a/audit/roles/role_resources.py b/audit/roles/role_resources.py index 9b497db..e69de29 100644 --- a/audit/roles/role_resources.py +++ b/audit/roles/role_resources.py @@ -1,35 +0,0 @@ -#!/usr/bin/env python - -import strongdm, os - -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_details(): - role_response = list(client.roles.list("")) - print_border() - for r in role_response: - print("Role name: \"" + r.name + "\" includes the following resources:") - rg_response = client.role_grants.list('role_id:{id}'.format(id=r.id) ) - - for g in rg_response: - res = list(client.resources.list("id:{}".format(g.resource_id))) - print("\t" + res[0].name) - print_border() - -def print_border(): - print("~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=") - -def main(): - get_role_details() - -if __name__ == "__main__": - main() - - - - - - diff --git a/audit/user_audit/audit_sdm_users.py b/audit/user_audit/audit_sdm_users.py index fd421fd..e69de29 100644 --- a/audit/user_audit/audit_sdm_users.py +++ b/audit/user_audit/audit_sdm_users.py @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 - -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.environ['SDM_API_ACCESS_KEY'] -secret_key = os.environ['SDM_API_SECRET_KEY'] - -client = strongdm.Client(access_key, secret_key) - -def get_data(): - user_response = client.accounts.list('type:user') - # Loop through the user list - for u in user_response: - # Create/reset role and resource lists at user level - role_list = [] - resource_list = [] - # Use the account.id to get ACCOUNT ATTACHMENTS - account_attach = client.account_attachments.list('account_id:{}'.format(u.id)) - # Loop 2 - for r in account_attach: - roles = list(client.roles.list('id:{}'.format(r.role_id))) - # Loop through the roles to which this user is assigned - for o in roles: - # Append the name to role_list - role_list.append(o.name) - role_list.append("/") - # Using the role_id, get role_attachments - role_grants = client.role_grants.list('role_id:{}'.format(o.id)) - for a in role_grants: - resources = list(client.resources.list('id:{}'.format(a.resource_id))) - for e in resources: - # Append the name to resource_list - resource_list.append(e.name) - resource_list.append("/") - - # Store the userID and email, for printing later - audit_dict[u.id] = [u.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': - subprocess.call(('open', filepath)) - elif platform.system() == 'Windows': # Windows - os.startfile(filepath) - else: # linux variants - subprocess.call(('xdg-open', filepath)) - - - - -def main(): - get_data() - print_csv() - open_csv() - -if __name__ == "__main__": - main() \ No newline at end of file From 775acc6163613c575009be636a8a4cc66e85d113 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Thu, 28 Apr 2022 12:08:38 -0400 Subject: [PATCH 84/87] Audit: script updates for role-grant deprecation --- .gitignore | 1 + audit/roles/role_resources.py | 89 +++++++++++++++++++++ audit/user_audit/audit_sdm_users.py | 118 ++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) diff --git a/.gitignore b/.gitignore index af6a008..2dbaa45 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ operations/variable-scripts/variable.json package.json .vscode/settings.json *tfstate* +audit.csv diff --git a/audit/roles/role_resources.py b/audit/roles/role_resources.py index e69de29..ad5b5f0 100644 --- a/audit/roles/role_resources.py +++ 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/audit_sdm_users.py b/audit/user_audit/audit_sdm_users.py index e69de29..ced98c3 100644 --- a/audit/user_audit/audit_sdm_users.py +++ 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() + + From 7a3f67e863152aed2d0f4b64db32c3568609861e Mon Sep 17 00:00:00 2001 From: Gabriel de Jesus <82273420+gaabrieljesuss@users.noreply.github.com> Date: Wed, 11 May 2022 10:48:48 -0300 Subject: [PATCH 85/87] Add jira webhook demo video Co-authored-by: vassalo --- issue-tracking/jira/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/issue-tracking/jira/README.md b/issue-tracking/jira/README.md index 868a809..1b38961 100644 --- a/issue-tracking/jira/README.md +++ b/issue-tracking/jira/README.md @@ -42,3 +42,9 @@ 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 From 84729f2fd398a3cbdac182a598fc7d3133c0b2a6 Mon Sep 17 00:00:00 2001 From: John Turner Date: Wed, 11 May 2022 12:01:23 -0400 Subject: [PATCH 86/87] Doing a fix for the ansible playbook --- ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml b/ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml index 2c5d595..4af05f7 100644 --- a/ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml +++ b/ansible/aws_playbooks/playbooks/sdm_pub_cert_ssh_install.yml @@ -63,7 +63,7 @@ args: executable: /bin/bash - name: Get SDM Gateway Token And Install - shell: ./sdm admin servers add sshCert --hostname `curl http://169.254.169.254/latestmeta-data/local-ipv4` --username $USER --port 22 + 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 From bd0f0a32c4ca1a3b8696c58b6fe15914b2092d45 Mon Sep 17 00:00:00 2001 From: John Hutchison Date: Mon, 1 Aug 2022 10:34:44 -0400 Subject: [PATCH 87/87] Add script to audit account logins for past 90 days --- audit/account_logins_past_90_days/README | 10 ++ audit/account_logins_past_90_days/main.py | 141 ++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 audit/account_logins_past_90_days/README create mode 100644 audit/account_logins_past_90_days/main.py 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()