├── util.go ├── scripts ├── gofmtcheck.sh └── build.sh ├── .circleci └── config.yml ├── cmd └── vault-plugin-secrets-mongodbatlas │ └── main.go ├── go.mod ├── path_roles_list.go ├── .gitignore ├── docker-test.sh ├── client.go ├── Makefile ├── README.md ├── backend.go ├── path_roles_test.go ├── path_credentials.go ├── path_config_test.go ├── path_config.go ├── acceptance_test.go ├── website └── source │ ├── api-docs │ └── secrets │ │ └── mongodbatlas │ │ └── index.html.md │ └── docs │ └── secrets │ └── mongodbatlas │ └── index.html.md ├── README-orig.md ├── test_env.go ├── path_roles.go ├── secret_programmatic_api_keys.go ├── go.sum └── LICENSE /util.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | func isOrgKey(orgID, projectID string) bool { 4 | return len(orgID) > 0 && len(projectID) == 0 5 | } 6 | 7 | func isProjectKey(orgID, projectID string) bool { 8 | return len(orgID) == 0 && len(projectID) > 0 9 | } 10 | 11 | func isAssignedToProject(orgID, projectID string) bool { 12 | return len(orgID) > 0 && len(projectID) > 0 13 | } 14 | -------------------------------------------------------------------------------- /scripts/gofmtcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "==> Checking that code complies with gofmt requirements..." 4 | 5 | gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`) 6 | if [[ -n ${gofmt_files} ]]; then 7 | echo 'gofmt needs running on the following files:' 8 | echo "${gofmt_files}" 9 | echo "You can use the command: \`make fmt\` to reformat code." 10 | exit 1 11 | fi -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.12 6 | working_directory: /go/src/github.com/hashicorp/vault-plugin-secrets-mongodbatlas 7 | steps: 8 | - checkout 9 | - run: 10 | name: "Setup Environment" 11 | command: | 12 | echo 'export GO111MODULE=on' >> $BASH_ENV 13 | - run: 14 | name: "Run Tests" 15 | command: make test 16 | - run: 17 | name: "Install Gox" 18 | command: go get github.com/mitchellh/gox 19 | - run: 20 | name: "Run Build" 21 | command: make dev 22 | -------------------------------------------------------------------------------- /cmd/vault-plugin-secrets-mongodbatlas/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | atlas "github.com/mongodb/vault-plugin-secrets-mongodbatlas" 7 | 8 | hclog "github.com/hashicorp/go-hclog" 9 | "github.com/hashicorp/vault/api" 10 | "github.com/hashicorp/vault/sdk/plugin" 11 | ) 12 | 13 | func main() { 14 | apiClientMeta := &api.PluginAPIClientMeta{} 15 | flags := apiClientMeta.FlagSet() 16 | flags.Parse(os.Args[1:]) 17 | 18 | tlsConfig := apiClientMeta.GetTLSConfig() 19 | tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) 20 | 21 | if err := plugin.Serve(&plugin.ServeOpts{ 22 | BackendFactoryFunc: atlas.Factory, 23 | TLSProviderFunc: tlsProviderFunc, 24 | }); err != nil { 25 | logger := hclog.New(&hclog.LoggerOptions{}) 26 | 27 | logger.Error("plugin shutting down", "error", err) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mongodb/vault-plugin-secrets-mongodbatlas 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a 7 | github.com/armon/go-radix v1.0.0 // indirect 8 | github.com/go-test/deep v1.0.2 9 | github.com/hashicorp/errwrap v1.0.0 10 | github.com/hashicorp/go-hclog v0.9.2 11 | github.com/hashicorp/go-version v1.2.0 // indirect 12 | github.com/hashicorp/vault/api v1.0.5-0.20190805220215-b4347d553834 13 | github.com/hashicorp/vault/sdk v0.1.14-0.20190805214312-16112a336457 14 | github.com/mitchellh/mapstructure v1.1.2 15 | github.com/mongodb/go-client-mongodb-atlas v0.1.1 16 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f // indirect 17 | golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5 // indirect 18 | golang.org/x/text v0.3.2 // indirect 19 | google.golang.org/genproto v0.0.0-20190513181449-d00d292a067c // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /path_roles_list.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/vault/sdk/framework" 7 | "github.com/hashicorp/vault/sdk/logical" 8 | ) 9 | 10 | func (b *Backend) pathRolesList() *framework.Path { 11 | return &framework.Path{ 12 | Pattern: "roles/?$", 13 | 14 | Callbacks: map[logical.Operation]framework.OperationFunc{ 15 | logical.ListOperation: b.operationListRoles, 16 | }, 17 | 18 | HelpSynopsis: pathRolesListHelpSyn, 19 | HelpDescription: pathRolesListHelpDesc, 20 | } 21 | } 22 | 23 | func (b *Backend) operationListRoles(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 24 | entries, err := req.Storage.List(ctx, "roles/") 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return logical.ListResponse(entries), nil 30 | } 31 | 32 | const pathRolesListHelpSyn = `List the existing roles in this backend` 33 | const pathRolesListHelpDesc = `Roles will be listed by the role name.` 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,macos 3 | # Edit at https://www.gitignore.io/?templates=go,macos 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | pkg* 13 | bin* 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | ### Go Patch ### 22 | /vendor/ 23 | /Godeps/ 24 | 25 | ### macOS ### 26 | # General 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | ### vsCode ### 32 | .vscode 33 | 34 | # Icon must end with two \r 35 | Icon 36 | 37 | # Thumbnails 38 | ._* 39 | 40 | # Files that might appear in the root of a volume 41 | .DocumentRevisions-V100 42 | .fseventsd 43 | .Spotlight-V100 44 | .TemporaryItems 45 | .Trashes 46 | .VolumeIcon.icns 47 | .com.apple.timemachine.donotpresent 48 | 49 | # Directories potentially created on remote AFP share 50 | .AppleDB 51 | .AppleDesktop 52 | Network Trash Folder 53 | Temporary Items 54 | .apdisk 55 | 56 | .idea/ 57 | # End of https://www.gitignore.io/api/go,macos 58 | -------------------------------------------------------------------------------- /docker-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | make dockerbuild 6 | 7 | docker kill vaultplg 2>/dev/null || true 8 | tmpdir=$(mktemp -d vaultplgXXXXXX) 9 | mkdir "$tmpdir/data" 10 | docker run --rm -d -p8200:8200 --name vaultplg -v "$(pwd)/$tmpdir/data":/data -v $(pwd)/bin:/example --cap-add=IPC_LOCK -e 'VAULT_LOCAL_CONFIG= 11 | { 12 | "backend": {"file": {"path": "/data"}}, 13 | "listener": [{"tcp": {"address": "0.0.0.0:8200", "tls_disable": true}}], 14 | "plugin_directory": "/example", 15 | "log_level": "debug", 16 | "disable_mlock": true, 17 | "api_addr": "http://localhost:8200" 18 | } 19 | ' vault server 20 | sleep 1 21 | 22 | export VAULT_ADDR=http://localhost:8200 23 | 24 | initoutput=$(vault operator init -key-shares=1 -key-threshold=1 -format=json) 25 | vault operator unseal $(echo "$initoutput" | jq -r .unseal_keys_hex[0]) 26 | 27 | export VAULT_TOKEN=$(echo "$initoutput" | jq -r .root_token) 28 | 29 | vault write sys/plugins/catalog/secret/vault-plugin-secrets-mongodbatlas \ 30 | sha_256=$(shasum -a 256 bin/vault-plugin-secrets-mongodbatlas | cut -d' ' -f1) \ 31 | command="vault-plugin-secrets-mongodbatlas" 32 | 33 | vault secrets enable \ 34 | -path="mongodbatlas" \ 35 | -plugin-name="vault-plugin-secrets-mongodbatlas" plugin 36 | 37 | vault write sys/plugins/catalog/database/mongodbatlas-database-plugin \ 38 | sha256=$(shasum -a 256 bin/mongodbatlas-database-plugin | cut -d' ' -f1) \ 39 | command="mongodbatlas-database-plugin" 40 | 41 | vault secrets enable database 42 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/Sectorbob/mlab-ns2/gae/ns/digest" 8 | "github.com/hashicorp/errwrap" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas" 11 | ) 12 | 13 | func (b *Backend) clientMongo(ctx context.Context, s logical.Storage) (*mongodbatlas.Client, error) { 14 | b.clientMutex.Lock() 15 | defer b.clientMutex.Unlock() 16 | 17 | // if the client is already created, just return it 18 | if b.client != nil { 19 | return b.client, nil 20 | } 21 | 22 | client, err := nonCachedClient(ctx, s) 23 | if err != nil { 24 | return nil, err 25 | } 26 | b.client = client 27 | 28 | return b.client, nil 29 | } 30 | 31 | func nonCachedClient(ctx context.Context, s logical.Storage) (*mongodbatlas.Client, error) { 32 | 33 | config, err := getRootConfig(ctx, s) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | transport := digest.NewTransport(config.PublicKey, config.PrivateKey) 39 | 40 | client, err := transport.Client() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return mongodbatlas.NewClient(client), nil 46 | } 47 | 48 | func getRootConfig(ctx context.Context, s logical.Storage) (*config, error) { 49 | 50 | entry, err := s.Get(ctx, "config") 51 | if err != nil { 52 | return nil, err 53 | } 54 | if entry != nil { 55 | var config config 56 | if err := entry.DecodeJSON(&config); err != nil { 57 | return nil, errwrap.Wrapf("error reading root configuration: {{err}}", err) 58 | } 59 | 60 | // return the config, we are done 61 | return &config, nil 62 | 63 | } 64 | 65 | return nil, errors.New("empty config entry") 66 | } 67 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TOOL?=vault-plugin-secrets-mongodbatlas 2 | TEST?=$$(go list ./... | grep -v /vendor/ | grep -v teamcity) 3 | VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr 4 | EXTERNAL_TOOLS=\ 5 | github.com/mitchellh/gox \ 6 | github.com/golang/dep/cmd/dep 7 | BUILD_TAGS?=${TOOL} 8 | GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor) 9 | 10 | # bin generates the releaseable binaries for this plugin 11 | bin: fmtcheck generate 12 | @CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/build.sh'" 13 | 14 | default: dev 15 | 16 | # dev creates binaries for testing Vault locally. These are put 17 | # into ./bin/ as well as $GOPATH/bin. 18 | dev: fmtcheck generate 19 | @CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' VAULT_DEV_BUILD=1 sh -c "'$(CURDIR)/scripts/build.sh'" 20 | 21 | # test runs the unit tests and vets the code 22 | test: fmtcheck generate 23 | CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test -v -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=20m -parallel=4 24 | 25 | testcompile: fmtcheck generate 26 | @for pkg in $(TEST) ; do \ 27 | go test -v -c -tags='$(BUILD_TAGS)' $$pkg -parallel=4 ; \ 28 | done 29 | 30 | # generate runs `go generate` to build the dynamically generated 31 | # source files. 32 | generate: 33 | go generate $(go list ./... | grep -v /vendor/) 34 | 35 | # bootstrap the build by downloading additional tools 36 | bootstrap: 37 | @for tool in $(EXTERNAL_TOOLS) ; do \ 38 | echo "Installing/Updating $$tool" ; \ 39 | go get -u $$tool; \ 40 | done 41 | 42 | fmtcheck: 43 | @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" 44 | 45 | fmt: 46 | gofmt -w $(GOFMT_FILES) 47 | 48 | proto: 49 | protoc *.proto --go_out=plugins=grpc:. 50 | 51 | .PHONY: bin default generate test vet bootstrap fmt fmtcheck -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HashiCorp Vault MongoDB Atlas Secrets Engine 2 | 3 | The MongoDB Atlas Secrets Engine is a plugin for HashiCorp Vault which generates unique, ephemeral [Programmatic API](https://docs.atlas.mongodb.com/reference/api/apiKeys/) keys for MongoDB Atlas. 4 | 5 | **The plugin is included in version 1.4 of Vault.** 6 | 7 | This repository will be archived in the near future as the code developed here is now hosted within HashiCorp's repo: https://github.com/hashicorp/vault-plugin-secrets-mongodbatlas. Any PRs or issues should be filed there. Please see Vault's main README for more information - https://github.com/hashicorp/vault. 8 | 9 | ## Support, Bugs and Feature Requests 10 | Support for the HashiCorp Vault MongoDB Atlas Secrets Engine is provided under MongoDB Atlas support plans. Please submit support questions within the Atlas UI. Vault support is via HashiCorp. 11 | 12 | Bugs should be filed under the Issues section of the Vault repo - https://github.com/hashicorp/vault. 13 | 14 | Feature requests can be submitted at https://feedback.mongodb.com/forums/924145-atlas - just select the Vault plugin as the category or vote for an already suggested feature. 15 | 16 | ## Quick Links 17 | - [MongoDB Atlas Secrets Engine - Docs](https://www.vaultproject.io/docs/secrets/mongodbatlas) 18 | - [MongoDB Atlas Secrets Engine - API Docs](https://www.vaultproject.io/api-docs/secret/mongodbatlas/) 19 | - [MongoDB Atlas Website](https://www.mongodb.com/cloud/atlas) 20 | - [Vault Website](https://www.vaultproject.io) 21 | 22 | **Please note**: Hashicorp takes Vault's security and their users' trust very seriously, as does MongoDB. 23 | 24 | If you believe you have found a security issue in Vault or with this plugin, _please responsibly disclose_ by 25 | contacting HashiCorp at [security@hashicorp.com](mailto:security@hashicorp.com) and contact MongoDB 26 | directly via [security@mongodb.com](mailto:security@mongodb.com) or 27 | [open a ticket](https://jira.mongodb.org/plugins/servlet/samlsso?redirectTo=%2Fbrowse%2FSECURITY) (link is external). 28 | -------------------------------------------------------------------------------- /backend.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/hashicorp/vault/sdk/framework" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas" 12 | ) 13 | 14 | func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 15 | b := NewBackend(conf.System) 16 | if err := b.Setup(ctx, conf); err != nil { 17 | return nil, err 18 | } 19 | return b, nil 20 | } 21 | 22 | func NewBackend(system logical.SystemView) *Backend { 23 | var b Backend 24 | b.Backend = &framework.Backend{ 25 | Help: strings.TrimSpace(backendHelp), 26 | 27 | PathsSpecial: &logical.Paths{ 28 | LocalStorage: []string{ 29 | framework.WALPrefix, 30 | }, 31 | SealWrapStorage: []string{ 32 | "config", 33 | }, 34 | }, 35 | 36 | Paths: []*framework.Path{ 37 | b.pathRolesList(), 38 | b.pathRoles(), 39 | b.pathConfig(), 40 | b.pathCredentials(), 41 | }, 42 | 43 | Secrets: []*framework.Secret{ 44 | b.programmaticAPIKeys(), 45 | }, 46 | 47 | WALRollback: b.pathProgrammaticAPIKeyRollback, 48 | WALRollbackMinAge: minUserRollbackAge, 49 | BackendType: logical.TypeLogical, 50 | } 51 | b.system = system 52 | return &b 53 | } 54 | 55 | type Backend struct { 56 | *framework.Backend 57 | 58 | credentialMutex sync.RWMutex 59 | clientMutex sync.RWMutex 60 | 61 | client *mongodbatlas.Client 62 | 63 | system logical.SystemView 64 | } 65 | 66 | const backendHelp = ` 67 | The MongoDB Atlas backend dynamically generates API keys for a set of 68 | Organization or Project roles. The API keys have a configurable lease 69 | set and are automatically revoked at the end of the lease. 70 | 71 | After mounting this backend, the Public and Private keys to generate 72 | API keys must be configured with the "config" path and roles must be 73 | written using the "roles/" endpoints before any API keys can be generated. 74 | 75 | ` 76 | const minUserRollbackAge = 5 * time.Minute 77 | -------------------------------------------------------------------------------- /path_roles_test.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/hashicorp/vault/sdk/logical" 9 | ) 10 | 11 | func TestBackend_PathListCredentials(t *testing.T) { 12 | var resp *logical.Response 13 | var err error 14 | config := logical.TestBackendConfig() 15 | config.StorageView = &logical.InmemStorage{} 16 | config.System = logical.TestSystemView() 17 | 18 | b := NewBackend(config.System) 19 | if err := b.Setup(context.Background(), config); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | credData := map[string]interface{}{ 24 | "credential_type": "org_programmatic_api_key", 25 | "organization_id": "aspergues", 26 | "roles": []string{"me", "domine"}, 27 | } 28 | 29 | credReq := &logical.Request{ 30 | Operation: logical.UpdateOperation, 31 | Storage: config.StorageView, 32 | Data: credData, 33 | } 34 | 35 | for i := 1; i <= 10; i++ { 36 | credReq.Path = "roles/testcred" + strconv.Itoa(i) 37 | resp, err = b.HandleRequest(context.Background(), credReq) 38 | if err != nil || (resp != nil && resp.IsError()) { 39 | t.Fatalf("bad: credential creation failed:. resp:%#v err:%v", resp, err) 40 | } 41 | 42 | } 43 | 44 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 45 | Operation: logical.ListOperation, 46 | Path: "roles/", 47 | Storage: config.StorageView, 48 | }) 49 | if err != nil || (resp != nil && resp.IsError()) { 50 | t.Fatalf("bad: listing credentials failed. resp:%#v\n err:%v", resp, err) 51 | } 52 | 53 | if len(resp.Data["keys"].([]string)) != 10 { 54 | t.Fatalf("failed to list all 10 credentials") 55 | } 56 | 57 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 58 | Operation: logical.ListOperation, 59 | Path: "roles/", 60 | Storage: config.StorageView, 61 | }) 62 | if err != nil || (resp != nil && resp.IsError()) { 63 | t.Fatalf("bad: listing credentials failed. resp:%#v\n err:%v", resp, err) 64 | } 65 | 66 | if len(resp.Data["keys"].([]string)) != 10 { 67 | t.Fatalf("failed to list all 10 credentials") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /path_credentials.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | 9 | "github.com/hashicorp/errwrap" 10 | "github.com/hashicorp/vault/sdk/framework" 11 | "github.com/hashicorp/vault/sdk/helper/base62" 12 | "github.com/hashicorp/vault/sdk/logical" 13 | ) 14 | 15 | var displayNameRegex = regexp.MustCompile("[^a-zA-Z0-9+=,.@_-]") 16 | 17 | func (b *Backend) pathCredentials() *framework.Path { 18 | return &framework.Path{ 19 | Pattern: "creds/" + framework.GenericNameRegex("name"), 20 | Fields: map[string]*framework.FieldSchema{ 21 | "name": { 22 | Type: framework.TypeLowerCaseString, 23 | Description: "Name of the role", 24 | Required: true, 25 | }, 26 | }, 27 | Callbacks: map[logical.Operation]framework.OperationFunc{ 28 | logical.ReadOperation: b.pathCredentialsRead, 29 | logical.UpdateOperation: b.pathCredentialsRead, 30 | }, 31 | 32 | HelpSynopsis: pathCredentialsHelpSyn, 33 | HelpDescription: pathCredentialsHelpDesc, 34 | } 35 | 36 | } 37 | 38 | func (b *Backend) pathCredentialsRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 39 | userName := d.Get("name").(string) 40 | 41 | cred, err := b.credentialRead(ctx, req.Storage, userName) 42 | if err != nil { 43 | return nil, errwrap.Wrapf("error retrieving credential: {{err}}", err) 44 | } 45 | 46 | if cred == nil { 47 | return nil, errors.New("error retrieving credential: credential is nil") 48 | } 49 | 50 | return b.programmaticAPIKeyCreate(ctx, req.Storage, userName, cred) 51 | 52 | } 53 | 54 | type walEntry struct { 55 | UserName string 56 | ProjectID string 57 | OrganizationID string 58 | ProgrammaticAPIKeyID string 59 | } 60 | 61 | func genUsername(displayName string) (string, error) { 62 | 63 | midString := displayNameRegex.ReplaceAllString(displayName, "_") 64 | 65 | id, err := base62.Random(20) 66 | if err != nil { 67 | return "", err 68 | } 69 | ret := fmt.Sprintf("vault-%s-%s", midString, id) 70 | return ret, nil 71 | } 72 | 73 | const pathCredentialsHelpSyn = ` 74 | Generate MongoDB Atlas Programmatic API from a specific Vault role. 75 | ` 76 | const pathCredentialsHelpDesc = ` 77 | This path reads generates MongoDB Atlas Programmatic API Keys for 78 | a particular role. Atlas Programmatic API Keys will be 79 | generated on demand and will be automatically revoked when 80 | the lease is up. 81 | ` 82 | -------------------------------------------------------------------------------- /path_config_test.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-test/deep" 8 | "github.com/hashicorp/vault/sdk/logical" 9 | ) 10 | 11 | func TestBackend_PathConfig(t *testing.T) { 12 | var resp *logical.Response 13 | var err error 14 | config := logical.TestBackendConfig() 15 | config.StorageView = &logical.InmemStorage{} 16 | 17 | b := NewBackend(config.System) 18 | if err := b.Setup(context.Background(), config); err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | // Test write operation 23 | configData := map[string]interface{}{ 24 | "public_key": "my_public_key", 25 | "private_key": "my_private_key", 26 | } 27 | 28 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 29 | Operation: logical.UpdateOperation, 30 | Path: "config", 31 | Data: configData, 32 | Storage: config.StorageView, 33 | }) 34 | 35 | if err != nil || (resp != nil && resp.IsError()) { 36 | t.Fatalf("config write failed:. resp:%#v err:%v", resp, err) 37 | } 38 | 39 | // Test read operation 40 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 41 | Operation: logical.ReadOperation, 42 | Path: "config", 43 | Data: configData, 44 | Storage: config.StorageView, 45 | }) 46 | 47 | if err != nil || (resp != nil && resp.IsError()) { 48 | t.Fatalf("config write failed:. resp:%#v err:%v", resp, err) 49 | } 50 | 51 | expected := map[string]interface{}{ 52 | "public_key": "my_public_key", 53 | } 54 | 55 | if diff := deep.Equal(expected, resp.Data); diff != nil { 56 | t.Fatalf("bad response. expected %v, got: %v", expected, resp.Data) 57 | } 58 | 59 | // Test bad data on write 60 | 61 | // Missing public key 62 | configData = map[string]interface{}{ 63 | "private_key": "my_private_key", 64 | } 65 | 66 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 67 | Operation: logical.UpdateOperation, 68 | Path: "config", 69 | Data: configData, 70 | Storage: config.StorageView, 71 | }) 72 | 73 | if err == nil { 74 | t.Fatal("expect error response but got nil") 75 | } 76 | 77 | // Missing private key 78 | configData = map[string]interface{}{ 79 | "public_key": "my_public_key", 80 | } 81 | 82 | resp, err = b.HandleRequest(context.Background(), &logical.Request{ 83 | Operation: logical.UpdateOperation, 84 | Path: "config", 85 | Data: configData, 86 | Storage: config.StorageView, 87 | }) 88 | 89 | if err == nil { 90 | t.Fatal("expect error response but got nil") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /path_config.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/hashicorp/vault/sdk/framework" 8 | "github.com/hashicorp/vault/sdk/logical" 9 | ) 10 | 11 | func (b *Backend) pathConfig() *framework.Path { 12 | return &framework.Path{ 13 | Pattern: "config", 14 | Fields: map[string]*framework.FieldSchema{ 15 | "public_key": { 16 | Type: framework.TypeString, 17 | Description: "MongoDB Atlas Programmatic Public Key", 18 | Required: true, 19 | }, 20 | "private_key": { 21 | Type: framework.TypeString, 22 | Description: "MongoDB Atlas Programmatic Private Key", 23 | Required: true, 24 | DisplayAttrs: &framework.DisplayAttributes{ 25 | Sensitive: true, 26 | }, 27 | }, 28 | }, 29 | Callbacks: map[logical.Operation]framework.OperationFunc{ 30 | logical.UpdateOperation: b.pathConfigWrite, 31 | logical.ReadOperation: b.pathConfigRead, 32 | }, 33 | HelpSynopsis: pathConfigHelpSyn, 34 | HelpDescription: pathConfigHelpDesc, 35 | } 36 | } 37 | 38 | func (b *Backend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 39 | publicKey := data.Get("public_key").(string) 40 | if publicKey == "" { 41 | return nil, errors.New("public_key is empty") 42 | } 43 | 44 | privateKey := data.Get("private_key").(string) 45 | if privateKey == "" { 46 | return nil, errors.New("private_key is empty") 47 | } 48 | 49 | entry, err := logical.StorageEntryJSON("config", config{ 50 | PublicKey: publicKey, 51 | PrivateKey: privateKey, 52 | }) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | if err := req.Storage.Put(ctx, entry); err != nil { 58 | return nil, err 59 | } 60 | 61 | // Clean cached client (if any) 62 | b.client = nil 63 | 64 | return nil, nil 65 | } 66 | 67 | func (b *Backend) pathConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 68 | cfg, err := getRootConfig(ctx, req.Storage) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return &logical.Response{ 74 | Data: map[string]interface{}{ 75 | "public_key": cfg.PublicKey, 76 | }, 77 | }, nil 78 | } 79 | 80 | type config struct { 81 | PrivateKey string `json:"private_key"` 82 | PublicKey string `json:"public_key"` 83 | } 84 | 85 | const pathConfigHelpSyn = ` 86 | Configure the credentials that are used to manage Database Users. 87 | ` 88 | 89 | const pathConfigHelpDesc = ` 90 | Before doing anything, the Atlas backend needs credentials that are able 91 | to manage databaseusers, access keys, etc. This endpoint is used to 92 | configure those credentials. 93 | ` 94 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TOOL=vault-plugin-secrets-mongodbatlas 4 | # 5 | # This script builds the application from source for multiple platforms. 6 | set -e 7 | 8 | # Get the parent directory of where this script is. 9 | SOURCE="${BASH_SOURCE[0]}" 10 | while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done 11 | DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" 12 | 13 | # Change into that directory 14 | cd "$DIR" 15 | 16 | # Set build tags 17 | BUILD_TAGS="${BUILD_TAGS}:-${TOOL}" 18 | 19 | # Get the git commit 20 | GIT_COMMIT="$(git rev-parse HEAD)" 21 | GIT_DIRTY="$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)" 22 | 23 | # Determine the arch/os combos we're building for 24 | XC_ARCH=${XC_ARCH:-"386 amd64"} 25 | XC_OS=${XC_OS:-linux darwin windows freebsd openbsd netbsd solaris} 26 | XC_OSARCH=${XC_OSARCH:-"linux/386 linux/amd64 linux/arm linux/arm64 darwin/386 darwin/amd64 windows/386 windows/amd64 freebsd/386 freebsd/amd64 freebsd/arm openbsd/386 openbsd/amd64 openbsd/arm netbsd/386 netbsd/amd64 netbsd/arm solaris/amd64"} 27 | 28 | GOPATH=${GOPATH:-$(go env GOPATH)} 29 | case $(uname) in 30 | CYGWIN*) 31 | GOPATH="$(cygpath $GOPATH)" 32 | ;; 33 | esac 34 | 35 | # Delete the old dir 36 | echo "==> Removing old directory..." 37 | rm -f bin/* 38 | rm -rf pkg/* 39 | mkdir -p bin/ 40 | 41 | # If its dev mode, only build for our self 42 | if [ "${VAULT_DEV_BUILD}x" != "x" ]; then 43 | XC_OS=$(go env GOOS) 44 | XC_ARCH=$(go env GOARCH) 45 | XC_OSARCH=$(go env GOOS)/$(go env GOARCH) 46 | fi 47 | 48 | 49 | # The main method we need for building is in the cmd directory 50 | cd "${DIR}/cmd/" 51 | 52 | # Build! 53 | echo "==> Building..." 54 | gox \ 55 | -osarch="${XC_OSARCH}" \ 56 | -ldflags "-X github.com/mongodb/${TOOL}/version.GitCommit='${GIT_COMMIT}${GIT_DIRTY}'" \ 57 | -output "${DIR}/pkg/{{.OS}}_{{.Arch}}/${TOOL}" \ 58 | -tags="${BUILD_TAGS}" \ 59 | . 60 | 61 | # Return to the home directory 62 | cd "$DIR" 63 | 64 | # Move all the compiled things to the $GOPATH/bin 65 | OLDIFS=$IFS 66 | IFS=: MAIN_GOPATH=($GOPATH) 67 | IFS=$OLDIFS 68 | 69 | # Copy our OS/Arch to the bin/ directory 70 | DEV_PLATFORM="./pkg/$(go env GOOS)_$(go env GOARCH)" 71 | for F in $(find ${DEV_PLATFORM} -mindepth 1 -maxdepth 1 -type f); do 72 | cp ${F} bin/ 73 | cp ${F} ${MAIN_GOPATH}/bin/ 74 | done 75 | 76 | if [ "${VAULT_DEV_BUILD}x" = "x" ]; then 77 | # Zip and copy to the dist dir 78 | echo "==> Packaging..." 79 | for PLATFORM in $(find ./pkg -mindepth 1 -maxdepth 1 -type d); do 80 | OSARCH=$(basename ${PLATFORM}) 81 | echo "--> ${OSARCH}" 82 | 83 | pushd $PLATFORM >/dev/null 2>&1 84 | zip ../${OSARCH}.zip ./* 85 | popd >/dev/null 2>&1 86 | done 87 | fi 88 | 89 | # Done! 90 | echo 91 | echo "==> Results:" 92 | ls -hl bin/ -------------------------------------------------------------------------------- /acceptance_test.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | log "github.com/hashicorp/go-hclog" 10 | "github.com/hashicorp/vault/sdk/helper/logging" 11 | "github.com/hashicorp/vault/sdk/logical" 12 | ) 13 | 14 | const ( 15 | envVarRunAccTests = "VAULT_ACC" 16 | envVarPrivateKey = "ATLAS_PRIVATE_KEY" 17 | envVarPublicKey = "ATLAS_PUBLIC_KEY" 18 | envVarProjectID = "ATLAS_PROJECT_ID" 19 | envVarOrganizationID = "ATLAS_ORGANIZATION_ID" 20 | ) 21 | 22 | var runAcceptanceTests = os.Getenv(envVarRunAccTests) == "1" 23 | 24 | func TestAcceptanceProgrammaticAPIKey(t *testing.T) { 25 | if !runAcceptanceTests { 26 | t.SkipNow() 27 | } 28 | 29 | acceptanceTestEnv, err := newAcceptanceTestEnv() 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | t.Run("add config", acceptanceTestEnv.AddConfig) 35 | t.Run("add programmatic API Key role", acceptanceTestEnv.AddProgrammaticAPIKeyRole) 36 | t.Run("read programmatic API key cred", acceptanceTestEnv.ReadProgrammaticAPIKeyRule) 37 | t.Run("renew programmatic API key creds", acceptanceTestEnv.RenewProgrammaticAPIKeys) 38 | t.Run("revoke programmatic API key creds", acceptanceTestEnv.RevokeProgrammaticAPIKeys) 39 | 40 | } 41 | 42 | func TestAcceptanceProgrammaticAPIKey_WithProjectID(t *testing.T) { 43 | if !runAcceptanceTests { 44 | t.SkipNow() 45 | } 46 | 47 | acceptanceTestEnv, err := newAcceptanceTestEnv() 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | t.Run("add config", acceptanceTestEnv.AddConfig) 53 | t.Run("add programmatic API Key role", acceptanceTestEnv.AddProgrammaticAPIKeyRoleWithProjectID) 54 | t.Run("read programmatic API key cred", acceptanceTestEnv.ReadProgrammaticAPIKeyRule) 55 | t.Run("renew programmatic API key creds", acceptanceTestEnv.RenewProgrammaticAPIKeys) 56 | t.Run("revoke programmatic API key creds", acceptanceTestEnv.RevokeProgrammaticAPIKeys) 57 | 58 | } 59 | 60 | func TestAcceptanceProgrammaticAPIKey_ProjectWithIPWhitelist(t *testing.T) { 61 | if !runAcceptanceTests { 62 | t.SkipNow() 63 | } 64 | 65 | acceptanceTestEnv, err := newAcceptanceTestEnv() 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | t.Run("add config", acceptanceTestEnv.AddConfig) 71 | t.Run("add programmatic API Key role", acceptanceTestEnv.AddProgrammaticAPIKeyRoleProjectWithIP) 72 | t.Run("read programmatic API key cred", acceptanceTestEnv.ReadProgrammaticAPIKeyRule) 73 | t.Run("renew programmatic API key creds", acceptanceTestEnv.RenewProgrammaticAPIKeys) 74 | t.Run("revoke programmatic API key creds", acceptanceTestEnv.RevokeProgrammaticAPIKeys) 75 | 76 | } 77 | 78 | func TestAcceptanceProgrammaticAPIKey_WithIPWhitelist(t *testing.T) { 79 | if !runAcceptanceTests { 80 | t.SkipNow() 81 | } 82 | 83 | acceptanceTestEnv, err := newAcceptanceTestEnv() 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | t.Run("add config", acceptanceTestEnv.AddConfig) 89 | t.Run("add programmatic API Key role", acceptanceTestEnv.AddProgrammaticAPIKeyRoleWithIP) 90 | t.Run("read programmatic API key cred", acceptanceTestEnv.ReadProgrammaticAPIKeyRule) 91 | t.Run("renew programmatic API key creds", acceptanceTestEnv.RenewProgrammaticAPIKeys) 92 | t.Run("revoke programmatic API key creds", acceptanceTestEnv.RevokeProgrammaticAPIKeys) 93 | 94 | } 95 | 96 | func TestAcceptanceProgrammaticAPIKey_WithCIDRWhitelist(t *testing.T) { 97 | if !runAcceptanceTests { 98 | t.SkipNow() 99 | } 100 | 101 | acceptanceTestEnv, err := newAcceptanceTestEnv() 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | t.Run("add config", acceptanceTestEnv.AddConfig) 107 | t.Run("add programmatic API Key role", acceptanceTestEnv.AddProgrammaticAPIKeyRoleWithCIDR) 108 | t.Run("read programmatic API key cred", acceptanceTestEnv.ReadProgrammaticAPIKeyRule) 109 | t.Run("renew programmatic API key creds", acceptanceTestEnv.RenewProgrammaticAPIKeys) 110 | t.Run("revoke programmatic API key creds", acceptanceTestEnv.RevokeProgrammaticAPIKeys) 111 | 112 | } 113 | 114 | func TestAcceptanceProgrammaticAPIKey_WithCIDRAndIPWhitelist(t *testing.T) { 115 | if !runAcceptanceTests { 116 | t.SkipNow() 117 | } 118 | 119 | acceptanceTestEnv, err := newAcceptanceTestEnv() 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | t.Run("add config", acceptanceTestEnv.AddConfig) 125 | t.Run("add programmatic API Key role", acceptanceTestEnv.AddProgrammaticAPIKeyRoleWithCIDRAndIP) 126 | t.Run("read programmatic API key cred", acceptanceTestEnv.ReadProgrammaticAPIKeyRule) 127 | t.Run("renew programmatic API key creds", acceptanceTestEnv.RenewProgrammaticAPIKeys) 128 | t.Run("revoke programmatic API key creds", acceptanceTestEnv.RevokeProgrammaticAPIKeys) 129 | 130 | } 131 | 132 | func TestAcceptanceProgrammaticAPIKey_AssignToProject(t *testing.T) { 133 | if !runAcceptanceTests { 134 | t.SkipNow() 135 | } 136 | 137 | acceptanceTestEnv, err := newAcceptanceTestEnv() 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | 142 | t.Run("add config", acceptanceTestEnv.AddConfig) 143 | t.Run("add programmatic API Key role", acceptanceTestEnv.AddProgrammaticAPIKeyRoleWithProjectIDAndOrgID) 144 | t.Run("read programmatic API key cred", acceptanceTestEnv.ReadProgrammaticAPIKeyRule) 145 | t.Run("renew programmatic API key creds", acceptanceTestEnv.RenewProgrammaticAPIKeys) 146 | t.Run("revoke programmatic API key creds", acceptanceTestEnv.RevokeProgrammaticAPIKeys) 147 | 148 | } 149 | 150 | func TestAcceptanceProgrammaticAPIKey_WithTTL(t *testing.T) { 151 | if !runAcceptanceTests { 152 | t.SkipNow() 153 | } 154 | 155 | acceptanceTestEnv, err := newAcceptanceTestEnv() 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | 160 | t.Run("add config", acceptanceTestEnv.AddConfig) 161 | t.Run("add programmatic API Key role with TTL", acceptanceTestEnv.AddProgrammaticAPIKeyRoleWithTTL) 162 | t.Run("read programmatic API key cred", acceptanceTestEnv.ReadProgrammaticAPIKeyRule) 163 | t.Run("check lease for programmatic API key cred", acceptanceTestEnv.CheckLease) 164 | t.Run("renew programmatic API key creds", acceptanceTestEnv.RenewProgrammaticAPIKeys) 165 | t.Run("revoke programmatic API key creds", acceptanceTestEnv.RevokeProgrammaticAPIKeys) 166 | 167 | } 168 | 169 | func newAcceptanceTestEnv() (*testEnv, error) { 170 | ctx := context.Background() 171 | 172 | maxLease, _ := time.ParseDuration("60s") 173 | defaultLease, _ := time.ParseDuration("30s") 174 | conf := &logical.BackendConfig{ 175 | System: &logical.StaticSystemView{ 176 | DefaultLeaseTTLVal: defaultLease, 177 | MaxLeaseTTLVal: maxLease, 178 | }, 179 | Logger: logging.NewVaultLogger(log.Debug), 180 | } 181 | b, err := Factory(ctx, conf) 182 | if err != nil { 183 | return nil, err 184 | } 185 | return &testEnv{ 186 | PublicKey: os.Getenv(envVarPublicKey), 187 | PrivateKey: os.Getenv(envVarPrivateKey), 188 | ProjectID: os.Getenv(envVarProjectID), 189 | OrganizationID: os.Getenv(envVarOrganizationID), 190 | Backend: b, 191 | Context: ctx, 192 | Storage: &logical.InmemStorage{}, 193 | }, nil 194 | } 195 | -------------------------------------------------------------------------------- /website/source/api-docs/secrets/mongodbatlas/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "api" 3 | page_title: "MongoDB Atlas - Secrets Engines - HTTP API" 4 | sidebar_title: "MongoDB Atlas" 5 | sidebar_current: "docs-secrets-engines-mongodbatlas" 6 | description: |- 7 | The MongoDB Atlas Secrets Engine for Vault generates MongoDB Atlas Programmatic API Keys dynamically. 8 | --- 9 | 10 | # MongoDB Atlas Secrets Engine 11 | 12 | The MongoDB Atlas Secrets Engine generates Programmatic API keys for MongoDB Atlas. This allows one to manage the lifecycle of these MongoDB Atlas secrets programmatically. The created MongoDB Atlas secrets are 13 | time-based and are automatically revoked when the Vault lease expires, unless renewed. Vault will create a Programmatic API key for each lease scoped to the MongoDB Atlas project or organization denoted with the included role(s). An IP Whitelist may also be configured for the Programmatic API key with desired IPs and/or CIDR blocks. 14 | 15 | The MongoDB Atlas Programmatic API Key Public and 16 | Private Key is returned to the caller. To learn more about Programmatic API Keys visit the [Programmatic API Keys Doc](https://docs.atlas.mongodb.com/reference/api/apiKeys/). 17 | 18 | ## Configure Connection 19 | 20 | In addition to the parameters defined by the Secrets Engines Backend, this plugin has a number of parameters to further configure a connection. 21 | 22 | | Method | Path | 23 | | :--------------------------- | :--------------------- | 24 | | `POST` | `/mongodbatlas/config` | 25 | 26 | 27 | ## Parameters 28 | 29 | - `public_key` `(string: )` – The Public Programmatic API Key used to authenticate with the MongoDB Atlas API. 30 | - `private_key` `(string: )` - The Private Programmatic API Key used to connect with MongoDB Atlas API. 31 | 32 | ### Sample Payload 33 | 34 | ```json 35 | { 36 | "public_key": "aPublicKey", 37 | "private_key": "aPrivateKey", 38 | } 39 | ``` 40 | 41 | ### Sample Request 42 | ```bash 43 | $ curl \ 44 | --header "X-Vault-Token: ..." \ 45 | --request POST \ 46 | --data @payload.json \ 47 | http://127.0.0.1:8200/mongodbatlas/config` 48 | ``` 49 | 50 | ## Create/Update Programmatic API Key role 51 | Programmatic API Key credential types create a Vault role to generate a Programmatic API Key at 52 | either the MongoDB Atlas Organization or Project level with the designated role(s) for programmatic access. If a role with the name does not exist, it will be created. If the role exists, it will be updated with the new attributes. 53 | 54 | | Method | Path | 55 | | :--------------------------- | :--------------------- | 56 | | `POST` | `/roles/:name` | 57 | 58 | 59 | ## Parameters 60 | 61 | `name` `(string )` - Unique identifier name of the role name 62 | `project_id` `(string )` - Unique identifier for the organization to which the target API Key belongs. Use the /orgs endpoint to retrieve all organizations to which the authenticated user has access. 63 | `roles` `(list [string] )` - List of roles that the API Key needs to have. If the roles array is provided: 64 | 65 | -> **IMPORTANT:** Provide at least one role. Make sure all roles must be valid for the Organization or Project. 66 | 67 | -> **NOTE:** Include all roles that you want this API Key to have. Any roles not in this array are removed. 68 | 69 | - The Organization roles are: 70 | - `ORG_OWNER` 71 | - `ORG_MEMBER` 72 | - `ORG_GROUP_CREATOR` 73 | - `ORG_BILLING_ADMIN` 74 | - `ORG_READ_ONLY` 75 | 76 | - The Project roles are: 77 | - `GROUP_CHARTS_ADMIN` 78 | - `GROUP_CLUSTER_MANAGER` 79 | - `GROUP_DATA_ACCESS_ADMIN` 80 | - `GROUP_DATA_ACCESS_READ_ONLY` 81 | - `GROUP_DATA_ACCESS_READ_WRITE` 82 | - `GROUP_OWNER` 83 | - `GROUP_READ_ONLY` 84 | 85 | 86 | `ip_addresses` `(list [string] )` - IP address to be added to the whitelist for the API key. This field is mutually exclusive with the cidrBlock field. 87 | `cidr_blocks` `(list [string] )` - Whitelist entry in CIDR notation to be added for the API key. This field is mutually exclusive with the ipAddress field. 88 | 89 | ### Sample Payload 90 | 91 | ```json 92 | { 93 | "project_id": "5cf5a45a9ccf6400e60981b6", 94 | "roles": ["GROUP_CLUSTER_MANAGER"], 95 | "cidr_blocks": ["192.168.1.3/32"], 96 | "ip_addresses": ["192.168.1.3", "192.168.1.4"] 97 | } 98 | ``` 99 | 100 | ```bash 101 | $ curl \ 102 | --header "X-Vault-Token: ..." \ 103 | --request POST \ 104 | --data @payload.json \ 105 | http://127.0.0.1:8200/mongodbatlas/roles/test-programmatic-key 106 | ``` 107 | 108 | ### Sample Response 109 | ```json 110 | { 111 | "project_id": "5cf5a45a9ccf6400e60981b6", 112 | "roles": ["GROUP_CLUSTER_MANAGER"], 113 | "cidr_blocks": ["192.168.1.3/32"], 114 | "ip_addresses": ["192.168.1.3", "192.168.1.4"], 115 | "organization_id": "7cf5a45a9ccf6400e60981b7", 116 | "ttl": "0s", 117 | "max_ttl": "0s" 118 | } 119 | 120 | ``` 121 | 122 | ## Read Programmatic API Key role 123 | 124 | | Method | Path | 125 | | :--------------------------- | :--------------------- | 126 | | `Get` | `/roles/:name` | 127 | 128 | 129 | ## Parameters 130 | 131 | `name` `(string )` - Unique identifier name of the role name 132 | 133 | ### Sample Payload 134 | 135 | ```bash 136 | $ curl \ 137 | --header "X-Vault-Token: ..." \ 138 | --request GET \ 139 | --data @payload.json \ 140 | http://127.0.0.1:8200/mongodbatlas/roles/test-programmatic-key 141 | ``` 142 | 143 | ### Sample Response 144 | ```json 145 | { 146 | "project_id": "5cf5a45a9ccf6400e60981b6", 147 | "roles": ["GROUP_CLUSTER_MANAGER"], 148 | "cidr_blocks": ["192.168.1.3/32"], 149 | "ip_addresses": ["192.168.1.3", "192.168.1.4"], 150 | "organization_id": "7cf5a45a9ccf6400e60981b7", 151 | "ttl": "0s", 152 | "max_ttl": "0s" 153 | } 154 | ``` 155 | 156 | ## List Programmatic API Key role 157 | 158 | | Method | Path | 159 | | :--------------------------- | :--------------------- | 160 | | `Get` | `/roles` | 161 | 162 | 163 | ### Sample Payload 164 | 165 | ```bash 166 | $ curl \ 167 | --header "X-Vault-Token: ..." \ 168 | --request GET \ 169 | --data @payload.json \ 170 | http://127.0.0.1:8200/mongodbatlas/roles 171 | ``` 172 | 173 | ### Sample Response 174 | ```json 175 | [ 176 | { 177 | "project_id": "5cf5a45a9ccf6400e60981b6", 178 | "roles": ["GROUP_CLUSTER_MANAGER"], 179 | "cidr_blocks": ["192.168.1.3/32"], 180 | "ip_addresses": ["192.168.1.3", "192.168.1.4"], 181 | "organization_id": "7cf5a45a9ccf6400e60981b7", 182 | "ttl": "0s", 183 | "max_ttl": "0s" 184 | }, 185 | { 186 | "project_id": "5cf5a45a9ccf6400e60981b6", 187 | "roles": ["READ"], 188 | "cidr_blocks": ["192.168.1.3/35"], 189 | "ip_addresses": ["192.168.1.5", "192.168.1.6"], 190 | "organization_id": "7cf5a45a9ccf6400e60981b7", 191 | "ttl": "0s", 192 | "max_ttl": "0s" 193 | } 194 | ] 195 | 196 | ``` 197 | 198 | ## Delete Programmatic API Key role 199 | 200 | | Method | Path | 201 | | :--------------------------- | :--------------------- | 202 | | `DELETE` | `/roles/:name` | 203 | 204 | 205 | ## Parameters 206 | 207 | `name` `(string )` - Unique identifier name of the role name 208 | 209 | ### Sample Payload 210 | 211 | ```bash 212 | $ curl \ 213 | --header "X-Vault-Token: ..." \ 214 | --request DELETE \ 215 | --data @payload.json \ 216 | http://127.0.0.1:8200/mongodbatlas/roles/test-programmatic-key 217 | ``` 218 | 219 | ### Sample Response 220 | ```json 221 | {} 222 | ``` 223 | 224 | ## Read Credential 225 | 226 | ### Sample Request 227 | 228 | | Method | Path | 229 | | :--------------------------- | :--------------------- | 230 | | `GET` | `/creds/:name` | 231 | 232 | ## Parameters 233 | `name` `(string )` - Unique identifier name of the credential 234 | 235 | ```bash 236 | $ curl \ 237 | --header "X-Vault-Token: ..." \ 238 | http://127.0.0.1:8200/mongodbatlas/creds/0fLBv1c2YDzPlJB1PwsRRKHR 239 | ``` 240 | 241 | ### Sample Response 242 | ```json 243 | { 244 | "lease_duration": "20s", 245 | "lease_renewable": true, 246 | "description": "vault-test-1563980947-1318", 247 | "private_key": "905ae89e-6ee8-40rd-ab12-613t8e3fe836", 248 | "public_key": "klpruxce" 249 | } 250 | ``` -------------------------------------------------------------------------------- /README-orig.md: -------------------------------------------------------------------------------- 1 | # Vault Plugins: MongoDB Atlas Secrets Engine and Database Secrets Engine for MongoDB Atlas plugin 2 | 3 | **VERY IMPORTANT INFO: This plugin is currently under development.** 4 | 5 | **The README below is also in development and has been written to be accurate once development is complete.** 6 | 7 | **Feel free to test it out following the instructions under the Developing section below, however consider this beta until it is verified by HashiCorp, which is in progress.** 8 | 9 | **Once verified and released versions will be documented in a CHANGELOG** 10 | 11 | This contains two Secrets Engines specific to MongoDB Atlas for use with [Hashicorp Vault](https://github.com/hashicorp/vault). 12 | The first is the MongoDB Atlas Secrets Engine which generates unique, ephemeral [Programmatic API](https://docs.atlas.mongodb.com/reference/api/apiKeys/) keys for MongoDB Atlas. 13 | The second is an extension of the existing Database Secrets Engine and allows generation of unique, ephemeral 14 | programmatic MongoDB [Database User](https://docs.atlas.mongodb.com/reference/api/database-users/) credentials in MongoDB Atlas, thus we refer to it as the Database Secrets 15 | Engine for MongoDB Atlas. 16 | 17 | The plugins are located in the following directories: 18 | - **MongoDB Atlas Secrets Engine:** `plugins/logical/mongodbatlas/` 19 | - **Database Secrets Engine for MongoDB Atlas plugin:** `plugins/database/mongodbatlas` 20 | 21 | **Please note**: Hashicorp takes Vault's security and their users' trust very seriously, as does MongoDB. 22 | 23 | If you believe you have found a security issue in Vault or with this plugin, _please responsibly disclose_ by 24 | contacting us at [security@hashicorp.com](mailto:security@hashicorp.com) and contact MongoDB 25 | directly via [security@mongodb.com](mailto:security@mongodb.com) or 26 | [open a ticket](https://jira.mongodb.org/plugins/servlet/samlsso?redirectTo=%2Fbrowse%2FSECURITY) (link is external). 27 | 28 | ## Quick Links 29 | - [Vault Website](https://www.vaultproject.io) 30 | - [MongoDB Atlas Website](https://www.mongodb.com/cloud/atlas) 31 | - [MongoDB Atlas Secrets Engine Docs](https://www.vaultproject.io/docs/secrets/mongodbatlas/index.html) 32 | - [Database Secrets Engine for MongoDB Atlas](https://www.vaultproject.io/docs/secrets/databases/mongodbatlas.html) 33 | - [Vault Github](https://www.github.com/hashicorp/vault) 34 | - [Vault General Announcement List](https://groups.google.com/forum/#!forum/hashicorp-announce) 35 | - [Vault Discussion List](https://groups.google.com/forum/#!forum/vault-tool) 36 | 37 | 38 | ## Usage 39 | 40 | **The following will be accurate after review and approval by Hashicorp, which is in progress. Until then follow the instructions in the developing section that follows:** 41 | 42 | These are a [Vault specific plugins (aka Secrets Engines/Backends)](https://www.vaultproject.io/docs/internals/plugins.html). This guide assumes you have already installed Vault 43 | and have a basic understanding of how Vault works. Otherwise, first read this guide on 44 | how to [get started with Vault](https://www.vaultproject.io/intro/getting-started/install.html). 45 | 46 | If you are using Vault 11.0.1 or above, both plugins are packaged with Vault. The MongoDB Atlas Secrets Engine can be enabled by running: 47 | 48 | The MongoDB Atlas Secrets Engine can be enabled by running: 49 | 50 | ```sh 51 | 52 | $ vault secrets enable mongodbatlas 53 | 54 | Success! Enabled the mongodbatlas secrets engine at: mongodbatlas/ 55 | 56 | ``` 57 | 58 | Then, write the configuration for the plugin and the lease, this is an example: 59 | 60 | ```sh 61 | 62 | vault write mongodbatlas/config \ 63 | public_key="a-public-key" \ 64 | private_key="a-private-key" 65 | 66 | vault write mongodbatlas/config/lease \ 67 | ttl=300 \ 68 | max_ttl=4800 69 | 70 | ``` 71 | 72 | The Database Secrets Engine for MongoDB Atlas can be enabled by running: 73 | 74 | ```sh 75 | 76 | $ vault secrets enable database 77 | 78 | Success! Enabled the database secrets engine at: database/ 79 | 80 | ``` 81 | 82 | Then, write the configuration for the plugin, for example: 83 | 84 | ```sh 85 | $ vault write database/config/my-mongodbatlas-database \ 86 | plugin_name=mongodbatlas-database-plugin \ 87 | allowed_roles="my-role" \ 88 | public_key="a-public-key" \ 89 | private_key="a-private-key!" \ 90 | project_id="a-project-id" 91 | 92 | ``` 93 | 94 | If you are testing this plugin in an earlier version of Vault or 95 | want to develop, see the next section. 96 | 97 | ## Developing 98 | 99 | If you wish to work on either plugin, you'll first need [Go](https://www.golang.org) 100 | installed on your machine (whichever version is required by Vault). 101 | 102 | Make sure Go is properly installed, including setting up a [GOPATH](https://golang.org/doc/code.html#GOPATH). 103 | 104 | ### Get Plugin 105 | 106 | Clone this repository: 107 | 108 | ``` 109 | 110 | mkdir $GOPATH/src/github.com/mongodb/vault-plugin-secrets-mongodbatlas` 111 | cd $GOPATH/src/github.com/mongodb/ 112 | git clone git@github.com:mongodb/vault-plugin-secrets-mongodbatlas.git 113 | go mod download 114 | 115 | ``` 116 | (or use `go get github.com/mongodb/vault-plugin-secrets-mongodbatlas` ). 117 | 118 | Then you can download any of the required tools to bootstrap your environment: 119 | 120 | ```sh 121 | $ make bootstrap 122 | ``` 123 | 124 | To compile a development version of these plugins, run `make` or `make dev`. 125 | This will put the plugin binaries in the `bin` and `$GOPATH/bin` folders. `dev` 126 | mode will only generate binaries for your platform and is faster: 127 | 128 | ```sh 129 | $ make 130 | $ make dev 131 | ``` 132 | 133 | ### Install Plugin in Vault 134 | 135 | Put the plugin binaries into a location of your choice. This directory 136 | will be specified as the [`plugin_directory`](https://www.vaultproject.io/docs/configuration/index.html#plugin_directory) 137 | in the Vault config used to start the server. 138 | 139 | ```hcl 140 | 141 | plugin_directory = "path/to/plugin/directory" 142 | 143 | ``` 144 | 145 | Start a Vault server with this config file: 146 | ```sh 147 | $ vault server -config=path/to/config.json ... 148 | ``` 149 | 150 | Once the server is started, register the plugins in the Vault server's [plugin catalog](https://www.vaultproject.io/docs/internals/plugins.html#plugin-catalog): 151 | 152 | #### MongoDB Atlas Secrets Engine 153 | 154 | To register the MongoDB Atlas Secrets Engine run the following: 155 | 156 | ```sh 157 | $ vault write sys/plugins/catalog/vault-plugin-secrets-mongodbatlas \ 158 | sha_256="$(shasum -a 256 path/to/plugin/directory/vault-plugin-secrets-mongodbatlas | cut -d " " -f1)" \ 159 | command="vault-plugin-secrets-mongodbatlas" 160 | ``` 161 | 162 | Any name can be substituted for the plugin name "vault-plugin-secrets-mongodbatlas". This 163 | name will be referenced in the next step, where we enable the secrets 164 | plugin backend using the MongoDB Atlas Secrets Engine: 165 | 166 | ```sh 167 | $ vault secrets enable --plugin-name='vault-plugin-secrets-mongodbatlas' --path="vault-plugin-secrets-mongodbatlas" plugin 168 | 169 | ``` 170 | 171 | 172 | ### Tests 173 | 174 | This plugin has both integration tests, and acceptance tests. 175 | 176 | The integration tests are run by `$ make test` and rather than firing real 177 | API calls, they fire API calls at a local test server that returns expected 178 | responses. 179 | 180 | The acceptance tests fire real API calls, and are located in `acceptance_test.go`. 181 | These should be run once as a final step before placing a PR. Please see `acceptance_test.go` 182 | to learn the environment variables that will need to be set. 183 | 184 | **Warning:** The acceptance tests create/destroy/modify *real resources*, 185 | which may incur real costs in some cases. In the presence of a bug, 186 | it is technically possible that broken backends could leave dangling 187 | data behind. Therefore, please run the acceptance tests at your own risk. 188 | At the very least, we recommend running them in their own private 189 | account for whatever backend you're testing. 190 | 191 | Before running the acceptance tests export the following environment variables: 192 | 193 | - VAULT_ACC - Set to `1` to run the acceptance tests 194 | - ATLAS_ORGANIZATION_ID - Your Organization ID 195 | - ATLAS_PUBLIC_KEY and ATLAS_PRIVATE_KEY - Your Public and Private key with the correct permissions to run the tests 196 | - ATLAS_PROJECT_ID - Your Project ID 197 | 198 | To run the acceptance tests, after exporting the necessary environment variables, 199 | from the home directory run `VAULT_ACC=1 make test`: 200 | 201 | ```sh 202 | $ VAULT_ACC=1 make test 203 | ``` 204 | 205 | ## Other Docs 206 | 207 | **The following will be accurate after review and approval by Hashicorp, which is in progress. Until then read the docs within this repo for more information.** 208 | 209 | See up-to-date **MongoDB Atlas Secrets Engine** [docs](https://www.vaultproject.io/docs/secrets/mongodbatlas/index.html), 210 | **Database Secrets Engine for MongoDB Atlas plugin** [docs](https://www.vaultproject.io/docs/secrets/databases/mongodbatlas.html) 211 | and general [API docs](https://www.vaultproject.io/api/secret/mongodbatlas/index.html). 212 | -------------------------------------------------------------------------------- /test_env.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/hashicorp/vault/sdk/logical" 9 | ) 10 | 11 | type testEnv struct { 12 | PublicKey string 13 | PrivateKey string 14 | ProjectID string 15 | OrganizationID string 16 | 17 | Backend logical.Backend 18 | Context context.Context 19 | Storage logical.Storage 20 | 21 | MostRecentSecret *logical.Secret 22 | } 23 | 24 | func (e *testEnv) AddConfig(t *testing.T) { 25 | req := &logical.Request{ 26 | Operation: logical.UpdateOperation, 27 | Path: "config", 28 | Storage: e.Storage, 29 | Data: map[string]interface{}{ 30 | "public_key": e.PublicKey, 31 | "private_key": e.PrivateKey, 32 | }, 33 | } 34 | resp, err := e.Backend.HandleRequest(e.Context, req) 35 | if err != nil || (resp != nil && resp.IsError()) { 36 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 37 | } 38 | if resp != nil { 39 | t.Fatal("expected nil response to represent a 204") 40 | } 41 | } 42 | 43 | func (e *testEnv) AddLeaseConfig(t *testing.T) { 44 | req := &logical.Request{ 45 | Operation: logical.UpdateOperation, 46 | Path: "config/lease", 47 | Storage: e.Storage, 48 | Data: map[string]interface{}{ 49 | "ttl": "80s", 50 | "max_ttl": "160s", 51 | }, 52 | } 53 | resp, err := e.Backend.HandleRequest(e.Context, req) 54 | if err != nil || (resp != nil && resp.IsError()) { 55 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 56 | } 57 | if resp != nil { 58 | t.Fatal("expected nil response to represent a 204") 59 | } 60 | } 61 | 62 | func (e *testEnv) AddProgrammaticAPIKeyRole(t *testing.T) { 63 | roles := []string{"ORG_MEMBER"} 64 | req := &logical.Request{ 65 | Operation: logical.UpdateOperation, 66 | Path: "roles/test-programmatic-key", 67 | Storage: e.Storage, 68 | Data: map[string]interface{}{ 69 | "organization_id": e.OrganizationID, 70 | "roles": roles, 71 | }, 72 | } 73 | resp, err := e.Backend.HandleRequest(e.Context, req) 74 | if err != nil || (resp != nil && resp.IsError()) { 75 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 76 | } 77 | } 78 | 79 | func (e *testEnv) AddProgrammaticAPIKeyRoleWithProjectIDAndOrgID(t *testing.T) { 80 | roles := []string{"ORG_MEMBER"} 81 | projectRoles := []string{"GROUP_READ_ONLY"} 82 | req := &logical.Request{ 83 | Operation: logical.UpdateOperation, 84 | Path: "roles/test-programmatic-key", 85 | Storage: e.Storage, 86 | Data: map[string]interface{}{ 87 | "organization_id": e.OrganizationID, 88 | "project_id": e.ProjectID, 89 | "roles": roles, 90 | "project_roles": projectRoles, 91 | }, 92 | } 93 | resp, err := e.Backend.HandleRequest(e.Context, req) 94 | if err != nil || (resp != nil && resp.IsError()) { 95 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 96 | } 97 | } 98 | 99 | func (e *testEnv) AddProgrammaticAPIKeyRoleWithTTL(t *testing.T) { 100 | roles := []string{"ORG_MEMBER"} 101 | req := &logical.Request{ 102 | Operation: logical.UpdateOperation, 103 | Path: "roles/test-programmatic-key", 104 | Storage: e.Storage, 105 | Data: map[string]interface{}{ 106 | "organization_id": e.OrganizationID, 107 | "roles": roles, 108 | "ttl": "20s", 109 | "max_ttl": "60s", 110 | }, 111 | } 112 | resp, err := e.Backend.HandleRequest(e.Context, req) 113 | if err != nil || (resp != nil && resp.IsError()) { 114 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 115 | } 116 | } 117 | 118 | func (e *testEnv) AddProgrammaticAPIKeyRoleWithIP(t *testing.T) { 119 | roles := []string{"ORG_MEMBER"} 120 | ips := []string{"192.168.1.1", "192.168.1.2"} 121 | req := &logical.Request{ 122 | Operation: logical.UpdateOperation, 123 | Path: "roles/test-programmatic-key", 124 | Storage: e.Storage, 125 | Data: map[string]interface{}{ 126 | "organization_id": e.OrganizationID, 127 | "roles": roles, 128 | "ip_addresses": ips, 129 | }, 130 | } 131 | resp, err := e.Backend.HandleRequest(e.Context, req) 132 | if err != nil || (resp != nil && resp.IsError()) { 133 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 134 | } 135 | } 136 | 137 | func (e *testEnv) AddProgrammaticAPIKeyRoleProjectWithIP(t *testing.T) { 138 | roles := []string{"ORG_MEMBER"} 139 | ips := []string{"192.168.1.1", "192.168.1.2"} 140 | req := &logical.Request{ 141 | Operation: logical.UpdateOperation, 142 | Path: "roles/test-programmatic-key", 143 | Storage: e.Storage, 144 | Data: map[string]interface{}{ 145 | "project_id": e.ProjectID, 146 | "roles": roles, 147 | "ip_addresses": ips, 148 | }, 149 | } 150 | resp, err := e.Backend.HandleRequest(e.Context, req) 151 | if err != nil || (resp != nil && resp.IsError()) { 152 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 153 | } 154 | } 155 | 156 | func (e *testEnv) AddProgrammaticAPIKeyRoleWithCIDR(t *testing.T) { 157 | roles := []string{"ORG_MEMBER"} 158 | cidrBlocks := []string{"179.154.224.2/32"} 159 | req := &logical.Request{ 160 | Operation: logical.UpdateOperation, 161 | Path: "roles/test-programmatic-key", 162 | Storage: e.Storage, 163 | Data: map[string]interface{}{ 164 | "organization_id": e.OrganizationID, 165 | "roles": roles, 166 | "cidr_blocks": cidrBlocks, 167 | }, 168 | } 169 | resp, err := e.Backend.HandleRequest(e.Context, req) 170 | if err != nil || (resp != nil && resp.IsError()) { 171 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 172 | } 173 | } 174 | 175 | func (e *testEnv) AddProgrammaticAPIKeyRoleWithCIDRAndIP(t *testing.T) { 176 | roles := []string{"ORG_MEMBER"} 177 | cidrBlocks := []string{"179.154.224.2/32"} 178 | ips := []string{"192.168.1.1", "192.168.1.2"} 179 | req := &logical.Request{ 180 | Operation: logical.UpdateOperation, 181 | Path: "roles/test-programmatic-key", 182 | Storage: e.Storage, 183 | Data: map[string]interface{}{ 184 | "organization_id": e.OrganizationID, 185 | "roles": roles, 186 | "cidr_blocks": cidrBlocks, 187 | "ip_addresses": ips, 188 | }, 189 | } 190 | resp, err := e.Backend.HandleRequest(e.Context, req) 191 | if err != nil || (resp != nil && resp.IsError()) { 192 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 193 | } 194 | } 195 | 196 | func (e *testEnv) AddProgrammaticAPIKeyRoleWithProjectID(t *testing.T) { 197 | roles := []string{"ORG_MEMBER"} 198 | req := &logical.Request{ 199 | Operation: logical.UpdateOperation, 200 | Path: "roles/test-programmatic-key", 201 | Storage: e.Storage, 202 | Data: map[string]interface{}{ 203 | "roles": roles, 204 | "project_id": e.ProjectID, 205 | }, 206 | } 207 | resp, err := e.Backend.HandleRequest(e.Context, req) 208 | if err != nil || (resp != nil && resp.IsError()) { 209 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 210 | } 211 | } 212 | 213 | func (e *testEnv) ReadProgrammaticAPIKeyRule(t *testing.T) { 214 | req := &logical.Request{ 215 | Operation: logical.ReadOperation, 216 | Path: "creds/test-programmatic-key", 217 | Storage: e.Storage, 218 | } 219 | resp, err := e.Backend.HandleRequest(e.Context, req) 220 | if err != nil || (resp != nil && resp.IsError()) { 221 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 222 | } 223 | if resp == nil { 224 | t.Fatal("expected a response") 225 | } 226 | 227 | if resp.Data["public_key"] == "" { 228 | t.Fatal("failed to receive access_key") 229 | } 230 | if resp.Data["private_key"] == "" { 231 | t.Fatal("failed to receive secret_key") 232 | } 233 | e.MostRecentSecret = resp.Secret 234 | } 235 | 236 | func (e *testEnv) CheckLease(t *testing.T) { 237 | ttl := int(e.MostRecentSecret.TTL.Seconds()) 238 | wantedTTL := 20 239 | maxTTL := int(e.MostRecentSecret.MaxTTL.Seconds()) 240 | wantedMaxTTL := 60 241 | 242 | if ttl != wantedTTL { 243 | t.Fatal(fmt.Sprintf("ttl=%d, wanted=%d", ttl, wantedTTL)) 244 | } 245 | if maxTTL != wantedMaxTTL { 246 | t.Fatal(fmt.Sprintf("maxTTL=%d, wanted=%d", ttl, wantedMaxTTL)) 247 | } 248 | } 249 | 250 | func (e *testEnv) RenewProgrammaticAPIKeys(t *testing.T) { 251 | req := &logical.Request{ 252 | Operation: logical.RenewOperation, 253 | Storage: e.Storage, 254 | Secret: e.MostRecentSecret, 255 | Data: map[string]interface{}{ 256 | "lease_id": "foo", 257 | }, 258 | } 259 | resp, err := e.Backend.HandleRequest(e.Context, req) 260 | if err != nil || (resp != nil && resp.IsError()) { 261 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 262 | } 263 | if resp == nil { 264 | t.Fatal("expected a response") 265 | } 266 | if resp.Secret != e.MostRecentSecret { 267 | t.Fatalf("expected %+v but got %+v", e.MostRecentSecret, resp.Secret) 268 | } 269 | } 270 | 271 | func (e *testEnv) RevokeProgrammaticAPIKeys(t *testing.T) { 272 | req := &logical.Request{ 273 | Operation: logical.RevokeOperation, 274 | Storage: e.Storage, 275 | Secret: e.MostRecentSecret, 276 | Data: map[string]interface{}{ 277 | "lease_id": "foo", 278 | }, 279 | } 280 | resp, err := e.Backend.HandleRequest(e.Context, req) 281 | if err != nil || (resp != nil && resp.IsError()) { 282 | t.Fatalf("bad: resp: %#v\nerr:%v", resp, err) 283 | } 284 | if resp != nil { 285 | t.Fatal("expected nil response to represent a 204") 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /website/source/docs/secrets/mongodbatlas/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "docs" 3 | page_title: "MongoDB Atlas - Secrets Engines" 4 | sidebar_title: "MongoDB Atlas" 5 | sidebar_current: "docs-secrets-atlasmongodb" 6 | description: |- 7 | The MongoDB Atlas Secrets Engine for Vault generates MongoDB Atlas Programmatic API Keys dynamically. 8 | --- 9 | 10 | # MongoDB Atlas Secrets Engine 11 | 12 | The MongoDB Atlas Secrets Engine generates Programmatic API keys. This allows one to manage the 13 | lifecycle of these MongoDB Atlas secrets programmatically. The created MongoDB Atlas secrets are 14 | time-based and are automatically revoked when the Vault lease expires, unless renewed. 15 | 16 | This MongoDB Atlas Secrets Engine supports the creation of Programmatic API keys. Vault will create 17 | a Programmatic API key for each lease that provide appropriate access to the defined MongoDB Atlas 18 | project or organization with appropriate role(s) . The MongoDB Atlas Programmatic API Key Public and 19 | Private Key is returned to the caller. To learn more about Programmatic API Keys visit the 20 | [Programmatic API Keys Doc](https://docs.atlas.mongodb.com/reference/api/apiKeys/). 21 | 22 | ## Setup 23 | 24 | Most Secrets Engines must be configured in advance before they can perform their functions. These 25 | steps are usually completed by an operator or configuration management tool. 26 | 27 | ~> **Notice:** The following will be accurate after review and approval by Hashicorp, which is in 28 | progress. Until then follow the instructions in the [README developing section](./../../../../../README.md). 29 | 30 | 31 | 1. Enable the MongoDB Atlas Secrets Engine: 32 | 33 | ```bash 34 | $ vault secrets enable mongodbatlas 35 | Success! Enabled the mongodbatlas Secrets Engine at: mongodbatlas/ 36 | ``` 37 | 38 | By default, the Secrets Engine will mount at the name of the engine. To 39 | enable the Secrets Engine at a different path, use the `-path` argument. 40 | 41 | 2. It's necessary to generate and configure a MongoDB Atlas Programmatic API Key for your organization 42 | or project that has sufficient permissions to allow Vault to create other Programmatic API Keys. 43 | 44 | In order to grant Vault programmatic access to an organization or project using only the 45 | [API](https://docs.atlas.mongodb.com/api/) you need to create a MongoDB Atlas Programmatic API 46 | Key with the appropriate roles if you have not already done so. A Programmatic API Key consists 47 | of a public and private key so ensure you have both. Regarding roles, the Organization Owner and 48 | Project Owner roles should be sufficient for most needs, however be sure to check what each roles 49 | grants in the [MongoDB Atlas Programmatic API Key User Roles documentation](https://docs.atlas.mongodb.com/reference/user-roles/). 50 | Also ensure you set an IP Whitelist when creating the key. 51 | 52 | For more detailed instructions on how to create a Programmatic API Key in the Atlas UI, including 53 | available roles, visit the [Programmatic API Key documenation](https://docs.atlas.mongodb.com/configure-api-access/#programmatic-api-keys). 54 | 55 | 3. Once you have a MongoDB Atlas Programmatic Key pair, as created in the previous step, Vault can now 56 | be configured to use it with MongoDB Atlas: 57 | 58 | ```bash 59 | $ vault write mongodbatlas/config \ 60 | public_key=yhltsvan \ 61 | private_key=2c130c23-e6b6-4da8-a93f-a8bf33218830 62 | ``` 63 | 64 | Internally, Vault will connect to MongoDB Atlas using these credentials. As such, 65 | these credentials must be a superset of any policies which might be granted 66 | on API Keys. Since Vault uses the official [MongoDB Atlas Client](https://github.com/mongodb/go-client-mongodb-atlas), 67 | it will use the specified credentials. 68 | 69 | 71 | 72 | 73 | ## Programmatic API Keys 74 | 75 | Programmatic API Key credential types create a Vault role to generate a Programmatic API Key at 76 | either the MongoDB Atlas Organization or Project level with the designated role(s) for programmatic access. 77 | 78 | Programmatic API Keys: 79 | - Has two parts, a public key and a private key 80 | - Cannot be used to log into Atlas through the user interface 81 | - Must be granted appropriate roles to complete required tasks 82 | - Must belong to one organization, but may be granted access to any number of 83 | projects in that organization. 84 | - May have an IP whitelist configured and some capabilities may require a 85 | whitelist to be configured (these are noted in the MongoDB Atlas API 86 | documentation). 87 | 88 | 89 | 1. Create a Vault role for a MongoDB Atlas Programmatic API Key by mapping appropriate arguments to the 90 | organization or project designated. 91 | 92 | - Organization API Key: Set `organization_id` argument with the appropriate 93 | [Organization Level Roles](https://docs.atlas.mongodb.com/reference/user-roles/#organization-roles). 94 | - Project API Key: Set `project_id` with the appropriate [Project Level Roles](https://docs.atlas.mongodb.com/reference/user-roles/#project-roles). 95 | - Create a Organization Key and Assign to a project: creates an Organization 96 | key and [Assigns]() it to a project, for this `project_id` and `organization_id` 97 | must be set, with the appropiate [Project Level Roles](https://docs.atlas.mongodb.com/reference/user-roles/#project-roles). 98 | 99 | ~> **Notice:** Programmatic API keys can belong to only one Organization but can belong to one or more Projects. 100 | 101 | Examples: 102 | 103 | ```bash 104 | $ vault write mongodbatlas/roles/test \ 105 | organization_id=5b23ff2f96e82130d0aaec13 \ 106 | roles=ORG_MEMBER 107 | ``` 108 | ```bash 109 | $ vault write mongodbatlas/roles/test \ 110 | project_id=5cf5a45a9ccf6400e60981b6 \ 111 | roles=GROUP_DATA_ACCESS_READ_ONLY 112 | ``` 113 | 114 | In both of these examples, after performing a read on `mongodbatlas/creds/test`, for an example of a `read` action 115 | refer to [## Programmatic API Key Whitelist](#programmatic-api-key-whitelist) section. 116 | 117 | ## Programmatic API Key Whitelist 118 | 119 | Programmatic API Key access can and should be limited with a IP Whitelist. In the following example both a CIDR 120 | block and IP address are added to the IP whitelist for Keys generated with this Vault role: 121 | 122 | ```bash 123 | $ vault write atlas/roles/test \ 124 | project_id=5cf5a45a9ccf6400e60981b6 \ 125 | roles=GROUP_CLUSTER_MANAGER \ 126 | cidr_blocks=192.168.1.3/32 \ 127 | ip_addresses=192.168.1.3 128 | ``` 129 | 130 | Verify the created Programmatic API Key Vault role has the added CIDR block and IP address by running: 131 | 132 | ```bash 133 | $ vault read atlas/roles/test 134 | 135 | Key Value 136 | --- ----- 137 | cidr_blocks [192.168.1.3/32] 138 | ip_addresses [192.168.1.3] 139 | max_ttl 0s 140 | organization_id n/a 141 | roles [GROUP_CLUSTER_MANAGER] 142 | project_id 5cf5a45a9ccf6400e60981b6 143 | roles n/a 144 | ttl 0s 145 | ``` 146 | 147 | ```bash 148 | $ vault read mongodbatlas/creds/test 149 | 150 | Key Value 151 | --- ----- 152 | lease_id mongodbatlas/creds/test/0fLBv1c2YDzPlJB1PwsRRKHR 153 | lease_duration 20s 154 | lease_renewable true 155 | description vault-test-1563980947-1318 156 | private_key 905ae89e-6ee8-40rd-ab12-613t8e3fe836 157 | public_key klpruxce 158 | ``` 159 | 160 | ## TTL and Max TTL 161 | 162 | Programmatic API Keys Vault role can also have a time-to-live (TTL) and maximum time-to-live (Max TTL). 163 | When a credential expires and it's not renewed, it's automatically revoked. You can set the TTL and Max TTL for each role 164 | or globally using config/lease. 165 | 166 | The following creates a Vault role "test" for a Project level Programmatic API key with a 2 hours time-to-live and a 167 | max time-to-live of 5 hours. 168 | 169 | ```bash 170 | $ vault write mongodbatlas/roles/test \ 171 | project_id=5cf5a45a9ccf6400e60981b6 \ 172 | roles=GROUP_DATA_ACCESS_READ_ONLY \ 173 | ttl=2h \ 174 | max_ttl=5h 175 | ``` 176 | 177 | This then creates a credential with the lease time-to-live values: 178 | 179 | ```bash 180 | $ vault read mongodbatlas/creds/test 181 | 182 | Key Value 183 | --- ----- 184 | lease_id mongodbatlas/creds/test/0fLBv1c2YDzPlJB1PwsRRKHR 185 | lease_duration 2h 186 | lease_renewable true 187 | description vault-test-1563980947-1318 188 | private_key 905ae89e-6ee8-40rd-ab12-613t8e3fe836 189 | public_key klpruxce 190 | ``` 191 | 192 | You can verify the role that you have created with: 193 | 194 | ```bash 195 | $ vault read mongodbatlas/roles/test 196 | 197 | Key Value 198 | --- ----- 199 | max_ttl 5h0m0s 200 | organization_id 5b71ff2f96e82120d0aaec14 201 | roles [GROUP_DATA_ACCESS_READ_ONLY] 202 | project_id 5cf5a45a9ccf6400e60981b6 203 | roles n/a 204 | ttl 2h0m0s 205 | ``` 206 | 207 | ~> **Notice:** If you don't set the TTL and Max TTL when you are creating a role the default lease will be used if it 208 | was previously configured in the `mongodbatlas/config/lease` path. If a default was not created for MongoDB Atlas then 209 | Vault's default will be used. 210 | -------------------------------------------------------------------------------- /path_roles.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | ) 11 | 12 | func (b *Backend) pathRoles() *framework.Path { 13 | return &framework.Path{ 14 | Pattern: "roles/" + framework.GenericNameRegex("name"), 15 | Fields: map[string]*framework.FieldSchema{ 16 | "name": { 17 | Type: framework.TypeLowerCaseString, 18 | Description: "Name of the Roles", 19 | Required: true, 20 | }, 21 | "project_id": { 22 | Type: framework.TypeString, 23 | Description: fmt.Sprintf("Project ID the %s API key belongs to.", projectProgrammaticAPIKey), 24 | }, 25 | "roles": { 26 | Type: framework.TypeCommaStringSlice, 27 | Description: fmt.Sprintf("List of roles that the API Key should be granted. A minimum of one role must be provided. Any roles provided must be valid for the assigned Project, required for %s and %s keys.", orgProgrammaticAPIKey, projectProgrammaticAPIKey), 28 | Required: true, 29 | }, 30 | "organization_id": { 31 | Type: framework.TypeString, 32 | Description: fmt.Sprintf("Organization ID required for an %s API key", orgProgrammaticAPIKey), 33 | }, 34 | "ip_addresses": { 35 | Type: framework.TypeCommaStringSlice, 36 | Description: fmt.Sprintf("IP address to be added to the whitelist for the API key. Optional for %s and %s keys.", orgProgrammaticAPIKey, projectProgrammaticAPIKey), 37 | }, 38 | "cidr_blocks": { 39 | Type: framework.TypeCommaStringSlice, 40 | Description: fmt.Sprintf("Whitelist entry in CIDR notation to be added for the API key. Optional for %s and %s keys.", orgProgrammaticAPIKey, projectProgrammaticAPIKey), 41 | }, 42 | "project_roles": { 43 | Type: framework.TypeCommaStringSlice, 44 | Description: fmt.Sprintf("Roles assigned when an %s API Key is assigned to a %s API key", orgProgrammaticAPIKey, projectProgrammaticAPIKey), 45 | }, 46 | "ttl": { 47 | Type: framework.TypeDurationSecond, 48 | Description: `Duration in seconds after which the issued credential should expire. Defaults to 0, in which case the value will fallback to the system/mount defaults.`, 49 | }, 50 | "max_ttl": { 51 | Type: framework.TypeDurationSecond, 52 | Description: "The maximum allowed lifetime of credentials issued using this role.", 53 | }, 54 | }, 55 | 56 | Callbacks: map[logical.Operation]framework.OperationFunc{ 57 | logical.DeleteOperation: b.pathRolesDelete, 58 | logical.ReadOperation: b.pathRolesRead, 59 | logical.UpdateOperation: b.pathRolesWrite, 60 | }, 61 | 62 | HelpSynopsis: pathRolesHelpSyn, 63 | HelpDescription: pathRolesHelpDesc, 64 | } 65 | } 66 | 67 | func (b *Backend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 68 | err := req.Storage.Delete(ctx, "roles/"+d.Get("name").(string)) 69 | return nil, err 70 | } 71 | 72 | func (b *Backend) pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 73 | entry, err := b.credentialRead(ctx, req.Storage, d.Get("name").(string)) 74 | if err != nil { 75 | return nil, err 76 | } 77 | if entry == nil { 78 | return nil, nil 79 | } 80 | return &logical.Response{ 81 | Data: entry.toResponseData(), 82 | }, nil 83 | } 84 | 85 | func (b *Backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 86 | var resp logical.Response 87 | 88 | credentialName := d.Get("name").(string) 89 | if credentialName == "" { 90 | return logical.ErrorResponse("missing role name"), nil 91 | } 92 | 93 | b.credentialMutex.Lock() 94 | defer b.credentialMutex.Unlock() 95 | credentialEntry, err := b.credentialRead(ctx, req.Storage, credentialName) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | if credentialEntry == nil { 101 | credentialEntry = &atlasCredentialEntry{} 102 | } 103 | 104 | if organizationIDRaw, ok := d.GetOk("organization_id"); ok { 105 | credentialEntry.OrganizationID = organizationIDRaw.(string) 106 | } 107 | 108 | getAPIWhitelistArgs(credentialEntry, d) 109 | 110 | if projectIDRaw, ok := d.GetOk("project_id"); ok { 111 | projectID := projectIDRaw.(string) 112 | credentialEntry.ProjectID = projectID 113 | } 114 | 115 | if len(credentialEntry.OrganizationID) == 0 && len(credentialEntry.ProjectID) == 0 { 116 | return logical.ErrorResponse("organization_id or project_id are required"), nil 117 | } 118 | 119 | if programmaticKeyRolesRaw, ok := d.GetOk("roles"); ok { 120 | credentialEntry.Roles = programmaticKeyRolesRaw.([]string) 121 | } else { 122 | return logical.ErrorResponse("%s is required for %s and %s keys", "roles", orgProgrammaticAPIKey, projectProgrammaticAPIKey), nil 123 | } 124 | 125 | if projectRolesRaw, ok := d.GetOk("project_roles"); ok { 126 | credentialEntry.ProjectRoles = projectRolesRaw.([]string) 127 | } else { 128 | if isAssignedToProject(credentialEntry.OrganizationID, credentialEntry.ProjectID) { 129 | return logical.ErrorResponse("%s is required if both %s and %s are supplied", "roles", "organization_id", "project_id"), nil 130 | } 131 | } 132 | 133 | if ttlRaw, ok := d.GetOk("ttl"); ok { 134 | credentialEntry.TTL = time.Duration(ttlRaw.(int)) * time.Second 135 | } 136 | 137 | if maxttlRaw, ok := d.GetOk("max_ttl"); ok { 138 | credentialEntry.MaxTTL = time.Duration(maxttlRaw.(int)) * time.Second 139 | } 140 | 141 | if credentialEntry.MaxTTL > 0 && credentialEntry.TTL > credentialEntry.MaxTTL { 142 | return logical.ErrorResponse("ttl exceeds max_ttl"), nil 143 | } 144 | 145 | if err := setAtlasCredential(ctx, req.Storage, credentialName, credentialEntry); err != nil { 146 | return nil, err 147 | } 148 | 149 | return &resp, nil 150 | } 151 | 152 | func getAPIWhitelistArgs(credentialEntry *atlasCredentialEntry, d *framework.FieldData) { 153 | 154 | if cidrBlocks, ok := d.GetOk("cidr_blocks"); ok { 155 | credentialEntry.CIDRBlocks = cidrBlocks.([]string) 156 | } 157 | if addresses, ok := d.GetOk("ip_addresses"); ok { 158 | credentialEntry.IPAddresses = addresses.([]string) 159 | } 160 | } 161 | 162 | func setAtlasCredential(ctx context.Context, s logical.Storage, credentialName string, credentialEntry *atlasCredentialEntry) error { 163 | if credentialName == "" { 164 | return fmt.Errorf("empty role name") 165 | } 166 | if credentialEntry == nil { 167 | return fmt.Errorf("emtpy credentialEntry") 168 | } 169 | entry, err := logical.StorageEntryJSON("roles/"+credentialName, credentialEntry) 170 | if err != nil { 171 | return err 172 | } 173 | if entry == nil { 174 | return fmt.Errorf("nil result when writing to storage") 175 | } 176 | if err := s.Put(ctx, entry); err != nil { 177 | return err 178 | } 179 | return nil 180 | 181 | } 182 | 183 | func (b *Backend) credentialRead(ctx context.Context, s logical.Storage, credentialName string) (*atlasCredentialEntry, error) { 184 | if credentialName == "" { 185 | return nil, fmt.Errorf("missing credential name") 186 | } 187 | 188 | entry, err := s.Get(ctx, "roles/"+credentialName) 189 | if err != nil { 190 | return nil, err 191 | } 192 | var credentialEntry atlasCredentialEntry 193 | if entry != nil { 194 | if err := entry.DecodeJSON(&credentialEntry); err != nil { 195 | return nil, err 196 | } 197 | return &credentialEntry, nil 198 | } 199 | // Return nil here because all callers expect that if an entry 200 | // is nil, the method will return nil, nil. 201 | return nil, nil 202 | } 203 | 204 | type atlasCredentialEntry struct { 205 | ProjectID string `json:"project_id"` 206 | DatabaseName string `json:"database_name"` 207 | Roles []string `json:"roles"` 208 | OrganizationID string `json:"organization_id"` 209 | CIDRBlocks []string `json:"cidr_blocks"` 210 | IPAddresses []string `json:"ip_addresses"` 211 | ProjectRoles []string `json:"project_roles"` 212 | TTL time.Duration `json:"ttl"` 213 | MaxTTL time.Duration `json:"max_ttl"` 214 | } 215 | 216 | func (r atlasCredentialEntry) toResponseData() map[string]interface{} { 217 | respData := map[string]interface{}{ 218 | "project_id": r.ProjectID, 219 | "database_name": r.DatabaseName, 220 | "roles": r.Roles, 221 | "organization_id": r.OrganizationID, 222 | "cidr_blocks": r.CIDRBlocks, 223 | "ip_addresses": r.IPAddresses, 224 | "project_roles": r.ProjectRoles, 225 | "ttl": r.TTL.Seconds(), 226 | "max_ttl": r.MaxTTL.Seconds(), 227 | } 228 | return respData 229 | } 230 | 231 | const pathRolesHelpSyn = ` 232 | Manage the roles used to generate MongoDB Atlas Programmatic API Keys. 233 | 234 | ` 235 | const pathRolesHelpDesc = ` 236 | This path lets you manage the roles used to generate MongoDB Atlas Programmatic API Keys 237 | 238 | The "project_id" parameter specifies a project where the Programmatic API Key will be 239 | created. 240 | 241 | "organization_id" parameter specifies in which Organization the key will be created. 242 | 243 | If both are specified, the key will be created with the "organization_id" and then 244 | assigned to the Project with the provided "project_id". 245 | 246 | The "roles" parameter specifies the MongoDB Atlas Programmatic Key roles that should be assigned 247 | to the Programmatic API keys created for a given role. At least one role should be provided 248 | and must be valid for key level (project or org). 249 | 250 | "ip_addresses" and "cidr_blocks" are used to add whitelist entries for the API key. 251 | 252 | "project_roles" is used when both "organization_id" and "project_id" are supplied. 253 | And it's a list of roles that the API Key should be granted. A minimum of one role 254 | must be provided. Any roles provided must be valid for the assigned Project 255 | 256 | To validate the keys, attempt to read an access key after writing the policy. 257 | ` 258 | const orgProgrammaticAPIKey = `organization` 259 | const projectProgrammaticAPIKey = `project` 260 | const programmaticAPIKey = `programmatic_api_key` 261 | -------------------------------------------------------------------------------- /secret_programmatic_api_keys.go: -------------------------------------------------------------------------------- 1 | package mongodbatlas 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/hashicorp/errwrap" 11 | "github.com/hashicorp/vault/sdk/framework" 12 | "github.com/hashicorp/vault/sdk/logical" 13 | "github.com/mitchellh/mapstructure" 14 | "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas" 15 | ) 16 | 17 | func (b *Backend) programmaticAPIKeys() *framework.Secret { 18 | return &framework.Secret{ 19 | Type: programmaticAPIKey, 20 | Fields: map[string]*framework.FieldSchema{ 21 | "public_key": { 22 | Type: framework.TypeString, 23 | Description: "Programmatic API Key Public Key", 24 | }, 25 | 26 | "private_key": { 27 | Type: framework.TypeString, 28 | Description: "Programmatic API Key Private Key", 29 | }, 30 | }, 31 | Renew: b.programmaticAPIKeysRenew, 32 | Revoke: b.programmaticAPIKeyRevoke, 33 | } 34 | } 35 | 36 | func (b *Backend) programmaticAPIKeyCreate(ctx context.Context, s logical.Storage, displayName string, cred *atlasCredentialEntry) (*logical.Response, error) { 37 | 38 | apiKeyDescription, err := genUsername(displayName) 39 | if err != nil { 40 | return nil, errwrap.Wrapf("error generating username: {{err}}", err) 41 | } 42 | client, err := b.clientMongo(ctx, s) 43 | if err != nil { 44 | return logical.ErrorResponse(err.Error()), nil 45 | } 46 | walID, err := framework.PutWAL(ctx, s, programmaticAPIKey, &walEntry{ 47 | UserName: apiKeyDescription, 48 | }) 49 | if err != nil { 50 | return nil, errwrap.Wrapf("error writing WAL entry: {{err}}", err) 51 | } 52 | 53 | var key *mongodbatlas.APIKey 54 | 55 | switch { 56 | case isOrgKey(cred.OrganizationID, cred.ProjectID): 57 | key, err = createOrgKey(ctx, client, apiKeyDescription, cred) 58 | case isProjectKey(cred.OrganizationID, cred.ProjectID): 59 | key, err = createProjectAPIKey(ctx, client, apiKeyDescription, cred) 60 | case isAssignedToProject(cred.OrganizationID, cred.ProjectID): 61 | key, err = createAndAssignKey(ctx, client, apiKeyDescription, cred) 62 | } 63 | 64 | if err != nil { 65 | if walErr := framework.DeleteWAL(ctx, s, walID); walErr != nil { 66 | dbUserErr := errwrap.Wrapf("error creating programmaticAPIKey: {{err}}", err) 67 | return nil, errwrap.Wrap(errwrap.Wrapf("failed to delete WAL entry: {{err}}", walErr), dbUserErr) 68 | } 69 | return logical.ErrorResponse("Error creating programmatic api key: %s", err), err 70 | } 71 | 72 | if key == nil { 73 | return nil, errors.New("error creating credential") 74 | } 75 | 76 | if err := framework.DeleteWAL(ctx, s, walID); err != nil { 77 | return nil, errwrap.Wrapf("failed to commit WAL entry: {{err}}", err) 78 | } 79 | 80 | resp := b.Secret(programmaticAPIKey).Response(map[string]interface{}{ 81 | "public_key": key.PublicKey, 82 | "private_key": key.PrivateKey, 83 | "description": apiKeyDescription, 84 | }, map[string]interface{}{ 85 | "programmatic_api_key_id": key.ID, 86 | "project_id": cred.ProjectID, 87 | "organization_id": cred.OrganizationID, 88 | }) 89 | 90 | defaultLease, maxLease := b.getDefaultAndMaxLease() 91 | 92 | // If defined, credential TTL overrides default lease configuration 93 | if cred.TTL > 0 { 94 | defaultLease = cred.TTL 95 | } 96 | 97 | if cred.MaxTTL > 0 { 98 | maxLease = cred.MaxTTL 99 | } 100 | 101 | resp.Secret.TTL = defaultLease 102 | resp.Secret.MaxTTL = maxLease 103 | 104 | return resp, nil 105 | } 106 | 107 | func createOrgKey(ctx context.Context, client *mongodbatlas.Client, apiKeyDescription string, credentialEntry *atlasCredentialEntry) (*mongodbatlas.APIKey, error) { 108 | key, _, err := client.APIKeys.Create(ctx, credentialEntry.OrganizationID, 109 | &mongodbatlas.APIKeyInput{ 110 | Desc: apiKeyDescription, 111 | Roles: credentialEntry.Roles, 112 | }) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | if err := addWhitelistEntry(ctx, client, credentialEntry.OrganizationID, key.ID, credentialEntry); err != nil { 118 | return nil, err 119 | } 120 | 121 | return key, nil 122 | } 123 | 124 | func createProjectAPIKey(ctx context.Context, client *mongodbatlas.Client, apiKeyDescription string, credentialEntry *atlasCredentialEntry) (*mongodbatlas.APIKey, error) { 125 | key, _, err := client.ProjectAPIKeys.Create(ctx, credentialEntry.ProjectID, 126 | &mongodbatlas.APIKeyInput{ 127 | Desc: apiKeyDescription, 128 | Roles: credentialEntry.Roles, 129 | }) 130 | return key, err 131 | } 132 | 133 | func createAndAssignKey(ctx context.Context, client *mongodbatlas.Client, apiKeyDescription string, credentialEntry *atlasCredentialEntry) (*mongodbatlas.APIKey, error) { 134 | key, err := createOrgKey(ctx, client, apiKeyDescription, credentialEntry) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | if _, err := client.ProjectAPIKeys.Assign(ctx, credentialEntry.ProjectID, key.ID, &mongodbatlas.AssignAPIKey{ 140 | Roles: credentialEntry.ProjectRoles, 141 | }); err != nil { 142 | return nil, err 143 | } 144 | 145 | return key, nil 146 | } 147 | 148 | func addWhitelistEntry(ctx context.Context, client *mongodbatlas.Client, orgID string, keyID string, cred *atlasCredentialEntry) error { 149 | var entries []*mongodbatlas.WhitelistAPIKeysReq 150 | for _, cidrBlock := range cred.CIDRBlocks { 151 | cidr := &mongodbatlas.WhitelistAPIKeysReq{ 152 | CidrBlock: cidrBlock, 153 | } 154 | entries = append(entries, cidr) 155 | } 156 | 157 | for _, ipAddress := range cred.IPAddresses { 158 | ip := &mongodbatlas.WhitelistAPIKeysReq{ 159 | IPAddress: ipAddress, 160 | } 161 | entries = append(entries, ip) 162 | 163 | } 164 | 165 | if entries != nil { 166 | _, _, err := client.WhitelistAPIKeys.Create(ctx, orgID, keyID, entries) 167 | return err 168 | 169 | } 170 | 171 | return nil 172 | } 173 | 174 | func (b *Backend) programmaticAPIKeyRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 175 | 176 | programmaticAPIKeyIDRaw, ok := req.Secret.InternalData["programmatic_api_key_id"] 177 | if !ok { 178 | return nil, fmt.Errorf("secret is missing programmatic api key id internal data") 179 | } 180 | 181 | programmaticAPIKeyID, ok := programmaticAPIKeyIDRaw.(string) 182 | if !ok { 183 | return nil, fmt.Errorf("secret is missing programmatic api key id internal data") 184 | } 185 | 186 | organizationID := "" 187 | organizationIDRaw, ok := req.Secret.InternalData["organization_id"] 188 | if ok { 189 | organizationID, ok = organizationIDRaw.(string) 190 | if !ok { 191 | return nil, fmt.Errorf("secret is missing organization id internal data") 192 | } 193 | } 194 | 195 | projectID := "" 196 | projectIDRaw, ok := req.Secret.InternalData["project_id"] 197 | if ok { 198 | projectID, ok = projectIDRaw.(string) 199 | if !ok { 200 | return nil, fmt.Errorf("secret is missing project_id internal data") 201 | } 202 | } 203 | 204 | var data = map[string]interface{}{ 205 | "organization_id": organizationID, 206 | "programmatic_api_key_id": programmaticAPIKeyID, 207 | "project_id": projectID, 208 | } 209 | 210 | // Use the user rollback mechanism to delete this database_user 211 | if err := b.pathProgrammaticAPIKeyRollback(ctx, req, programmaticAPIKey, data); err != nil { 212 | return nil, err 213 | } 214 | return nil, nil 215 | } 216 | 217 | func (b *Backend) pathProgrammaticAPIKeyRollback(ctx context.Context, req *logical.Request, _kind string, data interface{}) error { 218 | 219 | var entry walEntry 220 | if err := mapstructure.Decode(data, &entry); err != nil { 221 | return err 222 | } 223 | 224 | // Get the client 225 | client, err := b.clientMongo(ctx, req.Storage) 226 | if err != nil { 227 | return nil 228 | } 229 | 230 | switch { 231 | case isOrgKey(entry.OrganizationID, entry.ProjectID): 232 | // check if the user exists or not 233 | _, res, err := client.APIKeys.Get(ctx, entry.OrganizationID, entry.ProgrammaticAPIKeyID) 234 | // if the user is gone, move along 235 | if err != nil { 236 | if res != nil && res.StatusCode == http.StatusNotFound { 237 | return nil 238 | } 239 | return err 240 | } 241 | 242 | // now, delete the api key 243 | res, err = client.APIKeys.Delete(ctx, entry.OrganizationID, entry.ProgrammaticAPIKeyID) 244 | if err != nil { 245 | if res != nil && res.StatusCode == http.StatusNotFound { 246 | return nil 247 | } 248 | return err 249 | } 250 | case isProjectKey(entry.OrganizationID, entry.ProjectID): 251 | // now, delete the user 252 | res, err := client.ProjectAPIKeys.Unassign(ctx, entry.ProjectID, entry.ProgrammaticAPIKeyID) 253 | if err != nil { 254 | if res != nil && res.StatusCode == http.StatusNotFound { 255 | return nil 256 | } 257 | return err 258 | } 259 | case isAssignedToProject(entry.OrganizationID, entry.ProjectID): 260 | // check if the user exists or not 261 | _, res, err := client.APIKeys.Get(ctx, entry.OrganizationID, entry.ProgrammaticAPIKeyID) 262 | // if the user is gone, move along 263 | if err != nil { 264 | if res != nil && res.StatusCode == http.StatusNotFound { 265 | return nil 266 | } 267 | return err 268 | } 269 | 270 | // now, delete the api key 271 | res, err = client.APIKeys.Delete(ctx, entry.OrganizationID, entry.ProgrammaticAPIKeyID) 272 | if err != nil { 273 | if res != nil && res.StatusCode == http.StatusNotFound { 274 | return nil 275 | } 276 | return err 277 | } 278 | 279 | } 280 | 281 | return nil 282 | } 283 | 284 | func (b *Backend) programmaticAPIKeysRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 285 | // Get the lease (if any) 286 | 287 | defaultLease, maxLease := b.getDefaultAndMaxLease() 288 | 289 | resp := &logical.Response{Secret: req.Secret} 290 | resp.Secret.TTL = defaultLease 291 | resp.Secret.MaxTTL = maxLease 292 | return resp, nil 293 | } 294 | 295 | func (b *Backend) getDefaultAndMaxLease() (time.Duration, time.Duration) { 296 | maxLease := b.system.MaxLeaseTTL() 297 | defaultLease := b.system.DefaultLeaseTTL() 298 | 299 | if defaultLease > maxLease { 300 | maxLease = defaultLease 301 | } 302 | return defaultLease, maxLease 303 | 304 | } 305 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a h1:KFHLI4QGttB0i7M3qOkAo8Zn/GSsxwwCnInFqBaYtkM= 4 | github.com/Sectorbob/mlab-ns2 v0.0.0-20171030222938-d3aa0c295a8a/go.mod h1:D73UAuEPckrDorYZdtlCu2ySOLuPB5W4rhIkmmc/XbI= 5 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= 6 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 7 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= 8 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 9 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 10 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 11 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 12 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 17 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 18 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 19 | github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= 20 | github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 21 | github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31 h1:28FVBuwkwowZMjbA7M0wXsI6t3PYulRTMio3SO+eKCM= 22 | github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 23 | github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= 24 | github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 25 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 26 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 27 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 30 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 31 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 32 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 33 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 34 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 35 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 36 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 37 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 38 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 39 | github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= 40 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 41 | github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= 42 | github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 43 | github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 44 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 45 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 46 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 47 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 48 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 49 | github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE= 50 | github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= 51 | github.com/hashicorp/go-retryablehttp v0.5.4 h1:1BZvpawXoJCWX6pNtow9+rpEj+3itIlutiqnntI6jOE= 52 | github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 53 | github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8= 54 | github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 55 | github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= 56 | github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= 57 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 58 | github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= 59 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 60 | github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= 61 | github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 62 | github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= 63 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 64 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 65 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 66 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 67 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 68 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 69 | github.com/hashicorp/vault/api v1.0.5-0.20190805220215-b4347d553834 h1:NXwkgTBtJQW7mrfr0C39u42MSqV1VeUmGazEMfSTP9Y= 70 | github.com/hashicorp/vault/api v1.0.5-0.20190805220215-b4347d553834/go.mod h1:/esx16KZFuF4o5HlRO/dfXWknTeEOW8jmjHmn0Qny3Y= 71 | github.com/hashicorp/vault/sdk v0.1.14-0.20190805214312-16112a336457 h1:kyUyet0dI5DBY/UDa5s/XFiGmdtXOqo4KHpRSwnVD9o= 72 | github.com/hashicorp/vault/sdk v0.1.14-0.20190805214312-16112a336457/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= 73 | github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 74 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= 75 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 76 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 77 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 78 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 79 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 80 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 81 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 82 | github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 83 | github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= 84 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 85 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 86 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 87 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 88 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 89 | github.com/mongodb/go-client-mongodb-atlas v0.1.1 h1:L7eaGSvw1raCQF69N/tlYWAyyJRIJpdyqK6d90BtQ74= 90 | github.com/mongodb/go-client-mongodb-atlas v0.1.1/go.mod h1:LS8O0YLkA+sbtOb3fZLF10yY3tJM+1xATXMJ3oU35LU= 91 | github.com/mwielbut/pointy v1.1.0 h1:U5/YEfoIkaGCHv0St3CgjduqXID4FNRoyZgLM1kY9vg= 92 | github.com/mwielbut/pointy v1.1.0/go.mod h1:MvvO+uMFj9T5DMda33HlvogsFBX7pWWKAkFIn4teYwY= 93 | github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= 94 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 95 | github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= 96 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 97 | github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= 98 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 99 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 100 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 101 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 102 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 103 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 104 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 105 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 106 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 107 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 108 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 109 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 110 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 111 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= 112 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 113 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 114 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 115 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 116 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 117 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 118 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 119 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 120 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 121 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 122 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 123 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 124 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 125 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 126 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 131 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 132 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 133 | golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 134 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 135 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= 136 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5 h1:sM3evRHxE/1RuMe1FYAL3j7C7fUfIjkbE+NiDAYUF8U= 139 | golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 141 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= 142 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 143 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 144 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 145 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 146 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 147 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 148 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 149 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 150 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 151 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 152 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 153 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 154 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 155 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 h1:xtNn7qFlagY2mQNFHMSRPjT2RkOV4OXM7P5TVy9xATo= 156 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 157 | google.golang.org/genproto v0.0.0-20190513181449-d00d292a067c h1:m9avZ3wyOWBR0fLC+qWbMBulk+Jiiqelngssgp8jfIs= 158 | google.golang.org/genproto v0.0.0-20190513181449-d00d292a067c/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 159 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 160 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 161 | google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= 162 | google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 163 | gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= 164 | gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= 165 | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 166 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 167 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | http://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. 363 | --------------------------------------------------------------------------------