diff --git a/.github/workflows/contract-testing.yaml b/.github/workflows/contract-testing.yaml index 42518cc96..76d4088e5 100644 --- a/.github/workflows/contract-testing.yaml +++ b/.github/workflows/contract-testing.yaml @@ -31,6 +31,7 @@ jobs: private-endpoint-service: ${{ steps.filter.outputs.private-endpoint-service }} privatelink-endpoint-service-data-federation-online-archive: ${{ steps.filter.outputs.privatelink-endpoint-service-data-federation-online-archive }} project: ${{ steps.filter.outputs.project }} + project-service-account-secret: ${{ steps.filter.outputs.project-service-account-secret }} resource-policy: ${{ steps.filter.outputs.resource-policy }} search-deployment: ${{ steps.filter.outputs.search-deployment }} search-index: ${{ steps.filter.outputs.search-index }} @@ -87,6 +88,8 @@ jobs: - 'cfn-resources/privatelink-endpoint-service-data-federation-online-archive/**' project: - 'cfn-resources/project/**' + project-service-account-secret: + - 'cfn-resources/project-service-account-secret/**' resource-policy: - 'cfn-resources/resource-policy/**' search-deployment: @@ -927,6 +930,46 @@ jobs: make run-contract-testing make delete-test-resources + project-service-account-secret: + needs: change-detection + if: ${{ needs.change-detection.outputs.project-service-account-secret == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 + with: + go-version-file: 'cfn-resources/go.mod' + - name: setup Atlas CLI + uses: mongodb/atlas-github-action@e3c9e0204659bafbb3b65e1eb1ee745cca0e9f3b + - uses: aws-actions/setup-sam@d78e1a4a9656d3b223e59b80676a797f20093133 + with: + use-installer: true + - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_TEST_ENV }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_TEST_ENV }} + aws-region: eu-west-1 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: '3.9' + cache: 'pip' # caching pip dependencies + - run: pip install cloudformation-cli cloudformation-cli-go-plugin + - name: Run the Contract test + shell: bash + env: + MONGODB_ATLAS_PUBLIC_API_KEY: ${{ secrets.CLOUD_DEV_PUBLIC_KEY }} + MONGODB_ATLAS_PRIVATE_API_KEY: ${{ secrets.CLOUD_DEV_PRIVATE_KEY }} + MONGODB_ATLAS_ORG_ID: ${{ secrets.CLOUD_DEV_ORG_ID }} + MONGODB_ATLAS_OPS_MANAGER_URL: ${{ vars.MONGODB_ATLAS_BASE_URL }} + MONGODB_ATLAS_PROFILE: cfn-cloud-dev-github-action + run: | + cd cfn-resources/project-service-account-secret + make create-test-resources + + cat inputs/* + + make run-contract-testing + make delete-test-resources resource-policy: needs: change-detection if: ${{ needs.change-detection.outputs.resource-policy == 'true' }} diff --git a/cfn-resources/project-service-account-secret/.rpdk-config b/cfn-resources/project-service-account-secret/.rpdk-config new file mode 100644 index 000000000..e75dafe7c --- /dev/null +++ b/cfn-resources/project-service-account-secret/.rpdk-config @@ -0,0 +1,12 @@ +{ + "typeName": "MongoDB::Atlas::ProjectServiceAccountSecret", + "language": "go", + "runtime": "provided.al2", + "entrypoint": "bootstrap", + "testEntrypoint": "bootstrap", + "settings": { + "import_path": "github.com/mongodb/mongodbatlas-cloudformation-resources/project-service-account-secret", + "protocolVersion": "2.0.0", + "pluginVersion": "2.0.4" + } +} diff --git a/cfn-resources/project-service-account-secret/Makefile b/cfn-resources/project-service-account-secret/Makefile new file mode 100644 index 000000000..4f3d93ebd --- /dev/null +++ b/cfn-resources/project-service-account-secret/Makefile @@ -0,0 +1,37 @@ +.PHONY: build test clean +tags=logging callback metrics scheduler +cgo=0 +goos=linux +goarch=amd64 +CFNREP_GIT_SHA?=$(shell git rev-parse HEAD) +ldXflags=-s -w -X github.com/mongodb/mongodbatlas-cloudformation-resources/util.defaultLogLevel=info -X github.com/mongodb/mongodbatlas-cloudformation-resources/version.Version=${CFNREP_GIT_SHA} +ldXflagsD=-X github.com/mongodb/mongodbatlas-cloudformation-resources/util.defaultLogLevel=debug -X github.com/mongodb/mongodbatlas-cloudformation-resources/version.Version=${CFNREP_GIT_SHA} + +build: + cfn generate + env GOOS=$(goos) CGO_ENABLED=$(cgo) GOARCH=$(goarch) go build -ldflags="$(ldXflags)" -tags="$(tags)" -o bin/bootstrap cmd/main.go + +debug: + cfn generate + env GOOS=$(goos) CGO_ENABLED=$(cgo) GOARCH=$(goarch) go build -ldflags="$(ldXflagsD)" -tags="$(tags)" -o bin/debug cmd/main.go + +clean: + rm -rf bin + +submit: clean build # submit to private registry must use release build not debug build + @echo "==> Submitting to private registry for testing" + cfn submit --set-default --region us-east-1 + +create-test-resources: + @echo "==> Creating test files and resources for contract testing" + ./test/contract-testing/cfn-test-create.sh + +delete-test-resources: + @echo "==> Delete test resources used for contract testing" + ./test/contract-testing/cfn-test-delete.sh + +run-contract-testing: + @echo "==> Run contract testing" + make build + sam local start-lambda & + cfn test --function-name TestEntrypoint --verbose diff --git a/cfn-resources/project-service-account-secret/README.md b/cfn-resources/project-service-account-secret/README.md new file mode 100644 index 000000000..d157e447b --- /dev/null +++ b/cfn-resources/project-service-account-secret/README.md @@ -0,0 +1,29 @@ +# MongoDB::Atlas::ProjectServiceAccountSecret + +## Description + +The Project Service Account Secret resource provides a secret for a Service Account at the project level. This resource lets you create and delete secrets for Project Service Accounts. For more information, see [Create One Project Service Account Secret](https://www.mongodb.com/docs/atlas/reference/api-resources-spec/v2/#tag/Service-Accounts/operation/createGroupServiceAccountSecret) in the MongoDB Atlas API documentation. + +~> **IMPORTANT WARNING:** Managing Service Account Secrets with CloudFormation **exposes sensitive organizational secrets** in CloudFormation's outputs and logs. We suggest following [AWS Secrets Manager best practices](https://docs.aws.amazon.com/secretsmanager/latest/userguide/best-practices.html) for handling sensitive data. + +-> **NOTE:** This resource does not support updates. Any property change will trigger a replacement (delete + create). To rotate secrets, simply replace the resource. + +## Requirements + +To securely give CloudFormation access to your Atlas credentials, you must +set up an [AWS Profile](/README.md#mongodb-atlas-api-keys-credential-management). + +## Attributes and Parameters + +See the [resource docs](docs/README.md). + +## Cloudformation Examples + +See the examples [CFN Template](/examples/project-service-account-secret/README.md) for example resource. + +## Important Notes + +- The `Secret` property contains the actual secret value and is only returned once during creation +- This resource does not support updates - any change will trigger a replacement +- Secrets have an expiration time controlled by `SecretExpiresAfterHours` +- The minimum and maximum expiration times are controlled by your organization's settings diff --git a/cfn-resources/project-service-account-secret/cmd/main.go b/cfn-resources/project-service-account-secret/cmd/main.go new file mode 100644 index 000000000..1e77b2e7b --- /dev/null +++ b/cfn-resources/project-service-account-secret/cmd/main.go @@ -0,0 +1,85 @@ +// Code generated by 'cfn generate', changes will be undone by the next invocation. DO NOT EDIT. +package main + +import ( + "errors" + "fmt" + "log" + + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn" + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + "github.com/mongodb/mongodbatlas-cloudformation-resources/project-service-account-secret/cmd/resource" +) + +// Handler is a container for the CRUDL actions exported by resources +type Handler struct{} + +// Create wraps the related Create function exposed by the resource code +func (r *Handler) Create(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.Create) +} + +// Read wraps the related Read function exposed by the resource code +func (r *Handler) Read(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.Read) +} + +// Update wraps the related Update function exposed by the resource code +func (r *Handler) Update(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.Update) +} + +// Delete wraps the related Delete function exposed by the resource code +func (r *Handler) Delete(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.Delete) +} + +// List wraps the related List function exposed by the resource code +func (r *Handler) List(req handler.Request) handler.ProgressEvent { + return wrap(req, resource.List) +} + +// main is the entry point of the application. +func main() { + cfn.Start(&Handler{}) +} + +type handlerFunc func(handler.Request, *resource.Model, *resource.Model) (handler.ProgressEvent, error) + +func wrap(req handler.Request, f handlerFunc) (response handler.ProgressEvent) { + defer func() { + // Catch any panics and return a failed ProgressEvent + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = errors.New(fmt.Sprint(r)) + } + + log.Printf("Trapped error in handler: %v", err) + + response = handler.NewFailedEvent(err) + } + }() + + // Populate the previous model + prevModel := &resource.Model{} + if err := req.UnmarshalPrevious(prevModel); err != nil { + log.Printf("Error unmarshaling prev model: %v", err) + return handler.NewFailedEvent(err) + } + + // Populate the current model + currentModel := &resource.Model{} + if err := req.Unmarshal(currentModel); err != nil { + log.Printf("Error unmarshaling model: %v", err) + return handler.NewFailedEvent(err) + } + + response, err := f(req, prevModel, currentModel) + if err != nil { + log.Printf("Error returned from handler function: %v", err) + return handler.NewFailedEvent(err) + } + + return response +} diff --git a/cfn-resources/project-service-account-secret/cmd/resource/config.go b/cfn-resources/project-service-account-secret/cmd/resource/config.go new file mode 100644 index 000000000..4d9eb7831 --- /dev/null +++ b/cfn-resources/project-service-account-secret/cmd/resource/config.go @@ -0,0 +1,19 @@ +// Code generated by 'cfn generate', changes will be undone by the next invocation. DO NOT EDIT. +// Updates to this type are made my editing the schema file and executing the 'generate' command. +package resource + +import "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + +// TypeConfiguration is autogenerated from the json schema +type TypeConfiguration struct { +} + +// Configuration returns a resource's configuration. +func Configuration(req handler.Request) (*TypeConfiguration, error) { + // Populate the type configuration + typeConfig := &TypeConfiguration{} + if err := req.UnmarshalTypeConfig(typeConfig); err != nil { + return typeConfig, err + } + return typeConfig, nil +} diff --git a/cfn-resources/project-service-account-secret/cmd/resource/handlers.go b/cfn-resources/project-service-account-secret/cmd/resource/handlers.go new file mode 100644 index 000000000..8b95a885e --- /dev/null +++ b/cfn-resources/project-service-account-secret/cmd/resource/handlers.go @@ -0,0 +1,151 @@ +// Copyright 2026 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resource + +import ( + "context" + "fmt" + "net/http" + + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants" + progress_events "github.com/mongodb/mongodbatlas-cloudformation-resources/util/progressevent" + "go.mongodb.org/atlas-sdk/v20250312013/admin" +) + +func handleCreate(client *util.MongoDBClient, model *Model) handler.ProgressEvent { + ctx := context.Background() + projectID := *model.ProjectId + clientID := *model.ClientId + + createReq := &admin.ServiceAccountSecretRequest{} + if model.SecretExpiresAfterHours != nil { + createReq.SecretExpiresAfterHours = *model.SecretExpiresAfterHours + } + + secretResp, apiResp, err := client.AtlasSDK.ServiceAccountsApi.CreateGroupSecret(ctx, projectID, clientID, createReq).Execute() + if err != nil { + return handleError(apiResp, constants.CREATE, err) + } + + UpdateModelFromSecret(model, secretResp) + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + ResourceModel: model, + } +} + +func handleRead(client *util.MongoDBClient, model *Model) handler.ProgressEvent { + ctx := context.Background() + projectID := *model.ProjectId + clientID := *model.ClientId + secretID := *model.SecretId + + serviceAccountResp, apiResp, err := client.AtlasSDK.ServiceAccountsApi.GetGroupServiceAccount(ctx, projectID, clientID).Execute() + if err != nil { + return handleError(apiResp, constants.READ, err) + } + + var foundSecret *admin.ServiceAccountSecret + if serviceAccountResp.Secrets != nil { + for i := range *serviceAccountResp.Secrets { + secret := &(*serviceAccountResp.Secrets)[i] + if secret.Id == secretID { + foundSecret = secret + break + } + } + } + + if foundSecret == nil { + return handler.ProgressEvent{ + OperationStatus: handler.Failed, + Message: fmt.Sprintf("Secret with ID %s not found in service account %s", secretID, clientID), + HandlerErrorCode: string(types.HandlerErrorCodeNotFound), + } + } + + UpdateModelFromSecret(model, foundSecret) + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.ReadComplete, + ResourceModel: model, + } +} + +func handleDelete(client *util.MongoDBClient, model *Model) handler.ProgressEvent { + ctx := context.Background() + projectID := *model.ProjectId + clientID := *model.ClientId + secretID := *model.SecretId + + apiResp, err := client.AtlasSDK.ServiceAccountsApi.DeleteGroupSecret(ctx, clientID, secretID, projectID).Execute() + if err != nil { + if util.StatusNotFound(apiResp) { + return handler.ProgressEvent{ + OperationStatus: handler.Failed, + Message: "Resource not found", + HandlerErrorCode: string(types.HandlerErrorCodeNotFound), + } + } + return handleError(apiResp, constants.DELETE, err) + } + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + } +} + +func handleList(client *util.MongoDBClient, model *Model) handler.ProgressEvent { + ctx := context.Background() + projectID := *model.ProjectId + clientID := *model.ClientId + + serviceAccountResp, apiResp, err := client.AtlasSDK.ServiceAccountsApi.GetGroupServiceAccount(ctx, projectID, clientID).Execute() + if err != nil { + return handleError(apiResp, constants.LIST, err) + } + + var secretModels []any + if serviceAccountResp.Secrets != nil { + for i := range *serviceAccountResp.Secrets { + secret := &(*serviceAccountResp.Secrets)[i] + secretModel := &Model{ + ProjectId: model.ProjectId, + ClientId: model.ClientId, + Profile: model.Profile, + } + UpdateModelFromSecret(secretModel, secret) + secretModels = append(secretModels, secretModel) + } + } + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: constants.Complete, + ResourceModels: secretModels, + } +} + +func handleError(response *http.Response, method constants.CfnFunctions, err error) handler.ProgressEvent { + errMsg := fmt.Sprintf("%s error: %s", method, err.Error()) + return progress_events.GetFailedEventByResponse(errMsg, response) +} diff --git a/cfn-resources/project-service-account-secret/cmd/resource/mappings.go b/cfn-resources/project-service-account-secret/cmd/resource/mappings.go new file mode 100644 index 000000000..353f85d15 --- /dev/null +++ b/cfn-resources/project-service-account-secret/cmd/resource/mappings.go @@ -0,0 +1,32 @@ +// Copyright 2026 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resource + +import ( + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "go.mongodb.org/atlas-sdk/v20250312013/admin" +) + +func UpdateModelFromSecret(model *Model, secret *admin.ServiceAccountSecret) { + if secret == nil { + return + } + model.SecretId = &secret.Id + model.Secret = secret.Secret + model.MaskedSecretValue = secret.MaskedSecretValue + model.CreatedAt = util.TimePtrToStringPtr(&secret.CreatedAt) + model.ExpiresAt = util.TimePtrToStringPtr(&secret.ExpiresAt) + model.LastUsedAt = util.TimePtrToStringPtr(secret.LastUsedAt) +} diff --git a/cfn-resources/project-service-account-secret/cmd/resource/mappings_test.go b/cfn-resources/project-service-account-secret/cmd/resource/mappings_test.go new file mode 100644 index 000000000..713d0dfa4 --- /dev/null +++ b/cfn-resources/project-service-account-secret/cmd/resource/mappings_test.go @@ -0,0 +1,59 @@ +// Copyright 2026 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resource_test + +import ( + "testing" + "time" + + "github.com/aws/smithy-go/ptr" + "github.com/mongodb/mongodbatlas-cloudformation-resources/project-service-account-secret/cmd/resource" + "github.com/stretchr/testify/assert" + "go.mongodb.org/atlas-sdk/v20250312013/admin" +) + +func TestUpdateModelFromSecret(t *testing.T) { + now := time.Now() + secretID := "698eac1419a6b89540c8e7b5" + secretValue := "mdb_sa_sk_test123" //nolint:gosec // test data + maskedValue := "****7b5" + projectID := "695feecd6de62c462a0f09cb" + clientID := "mdb_sa_id_698eac1419a6b89540c8e7b6" + + model := &resource.Model{ + ProjectId: ptr.String(projectID), + ClientId: ptr.String(clientID), + } + + secret := &admin.ServiceAccountSecret{ + Id: secretID, + Secret: ptr.String(secretValue), + MaskedSecretValue: ptr.String(maskedValue), + CreatedAt: now, + ExpiresAt: now.Add(720 * time.Hour), + LastUsedAt: ptr.Time(now.Add(-1 * time.Hour)), + } + + resource.UpdateModelFromSecret(model, secret) + + assert.Equal(t, secretID, *model.SecretId) + assert.Equal(t, secretValue, *model.Secret) + assert.Equal(t, maskedValue, *model.MaskedSecretValue) + assert.NotNil(t, model.CreatedAt) + assert.NotNil(t, model.ExpiresAt) + assert.NotNil(t, model.LastUsedAt) + assert.Equal(t, projectID, *model.ProjectId) + assert.Equal(t, clientID, *model.ClientId) +} diff --git a/cfn-resources/project-service-account-secret/cmd/resource/model.go b/cfn-resources/project-service-account-secret/cmd/resource/model.go new file mode 100644 index 000000000..f42fe8919 --- /dev/null +++ b/cfn-resources/project-service-account-secret/cmd/resource/model.go @@ -0,0 +1,17 @@ +// Code generated by 'cfn generate', changes will be undone by the next invocation. DO NOT EDIT. +// Updates to this type are made my editing the schema file and executing the 'generate' command. +package resource + +// Model is autogenerated from the json schema +type Model struct { + ProjectId *string `json:",omitempty"` + ClientId *string `json:",omitempty"` + SecretExpiresAfterHours *int `json:",omitempty"` + SecretId *string `json:",omitempty"` + Secret *string `json:",omitempty"` + MaskedSecretValue *string `json:",omitempty"` + CreatedAt *string `json:",omitempty"` + ExpiresAt *string `json:",omitempty"` + LastUsedAt *string `json:",omitempty"` + Profile *string `json:",omitempty"` +} diff --git a/cfn-resources/project-service-account-secret/cmd/resource/resource.go b/cfn-resources/project-service-account-secret/cmd/resource/resource.go new file mode 100644 index 000000000..91a6c8d33 --- /dev/null +++ b/cfn-resources/project-service-account-secret/cmd/resource/resource.go @@ -0,0 +1,84 @@ +// Copyright 2026 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resource + +import ( + "errors" + + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/validator" +) + +var ( + CreateRequiredFields = []string{constants.ProjectID, constants.ClientID} + ReadRequiredFields = []string{constants.ProjectID, constants.ClientID, constants.SecretID} + DeleteRequiredFields = []string{constants.ProjectID, constants.ClientID, constants.SecretID} + ListRequiredFields = []string{constants.ProjectID, constants.ClientID} +) + +func setupRequest(req handler.Request, model *Model, requiredFields []string) (*util.MongoDBClient, *handler.ProgressEvent) { + util.SetupLogger("mongodb-atlas-project-service-account-secret") + if modelValidation := validator.ValidateModel(requiredFields, model); modelValidation != nil { + return nil, modelValidation + } + util.SetDefaultProfileIfNotDefined(&model.Profile) + client, peErr := util.NewAtlasClient(&req, model.Profile) + if peErr != nil { + return nil, peErr + } + return client, nil +} + +// Create handles the Create event from the Cloudformation service. +func Create(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, CreateRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return handleCreate(client, model), nil +} + +// Read handles the Read event from the Cloudformation service. +func Read(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, ReadRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return handleRead(client, model), nil +} + +func Update(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + return handler.ProgressEvent{}, errors.New("not implemented: Update") +} + +// Delete handles the Delete event from the Cloudformation service. +func Delete(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, DeleteRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return handleDelete(client, model), nil +} + +// List handles the List event from the Cloudformation service. +func List(req handler.Request, prevModel *Model, model *Model) (handler.ProgressEvent, error) { + client, setupErr := setupRequest(req, model, ListRequiredFields) + if setupErr != nil { + return *setupErr, nil + } + return handleList(client, model), nil +} diff --git a/cfn-resources/project-service-account-secret/docs/README.md b/cfn-resources/project-service-account-secret/docs/README.md new file mode 100644 index 000000000..6443b88db --- /dev/null +++ b/cfn-resources/project-service-account-secret/docs/README.md @@ -0,0 +1,109 @@ +# MongoDB::Atlas::ProjectServiceAccountSecret + +Creates a secret for the specified Service Account at the project level. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+ "Type" : "MongoDB::Atlas::ProjectServiceAccountSecret",
+ "Properties" : {
+ "ProjectId" : String,
+ "ClientId" : String,
+ "SecretExpiresAfterHours" : Integer,
+ "Profile" : String
+ }
+}
+
+
+### YAML
+
++Type: MongoDB::Atlas::ProjectServiceAccountSecret +Properties: + ProjectId: String + ClientId: String + SecretExpiresAfterHours: Integer + Profile: String ++ +## Properties + +#### ProjectId + +Unique 24-hexadecimal digit string that identifies your project. + +_Required_: Yes + +_Type_: String + +_Pattern_:
^([a-f0-9]{24})$
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### ClientId
+
+The Client ID of the Service Account.
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### SecretExpiresAfterHours
+
+The expiration time of the new Service Account secret, provided in hours. The minimum and maximum allowed expiration times are subject to change and are controlled by the organization's settings.
+
+_Required_: No
+
+_Type_: Integer
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+#### Profile
+
+Profile used to provide credentials information, (a secret with the cfn/atlas/profile/{Profile}, is required), if not provided default is used.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
+
+## Return Values
+
+### Fn::GetAtt
+
+The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.
+
+For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).
+
+#### SecretId
+
+Unique 24-hexadecimal digit string that identifies the secret.
+
+#### Secret
+
+The secret value for the Service Account. It will be returned only the first time after creation.
+
+#### MaskedSecretValue
+
+The masked Service Account secret.
+
+#### CreatedAt
+
+The date that the secret was created on. This parameter expresses its value in the ISO 8601 timestamp format in UTC.
+
+#### ExpiresAt
+
+The date for the expiration of the secret. This parameter expresses its value in the ISO 8601 timestamp format in UTC.
+
+#### LastUsedAt
+
+The last time the secret was used. This parameter expresses its value in the ISO 8601 timestamp format in UTC.
+
diff --git a/cfn-resources/project-service-account-secret/mongodb-atlas-projectserviceaccountsecret.json b/cfn-resources/project-service-account-secret/mongodb-atlas-projectserviceaccountsecret.json
new file mode 100644
index 000000000..bb1f78cce
--- /dev/null
+++ b/cfn-resources/project-service-account-secret/mongodb-atlas-projectserviceaccountsecret.json
@@ -0,0 +1,91 @@
+{
+ "typeName": "MongoDB::Atlas::ProjectServiceAccountSecret",
+ "description": "Creates a secret for the specified Service Account at the project level.",
+ "sourceUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/tree/master/cfn-resources/project-service-account-secret",
+ "definitions": {},
+ "properties": {
+ "ProjectId": {
+ "description": "Unique 24-hexadecimal digit string that identifies your project.",
+ "type": "string",
+ "pattern": "^([a-f0-9]{24})$"
+ },
+ "ClientId": {
+ "description": "The Client ID of the Service Account.",
+ "type": "string"
+ },
+ "SecretExpiresAfterHours": {
+ "description": "The expiration time of the new Service Account secret, provided in hours. The minimum and maximum allowed expiration times are subject to change and are controlled by the organization's settings.",
+ "type": "integer",
+ "minimum": 1
+ },
+ "SecretId": {
+ "description": "Unique 24-hexadecimal digit string that identifies the secret.",
+ "type": "string",
+ "pattern": "^([a-f0-9]{24})$"
+ },
+ "Secret": {
+ "description": "The secret value for the Service Account. It will be returned only the first time after creation.",
+ "type": "string"
+ },
+ "MaskedSecretValue": {
+ "description": "The masked Service Account secret.",
+ "type": "string"
+ },
+ "CreatedAt": {
+ "description": "The date that the secret was created on. This parameter expresses its value in the ISO 8601 timestamp format in UTC.",
+ "type": "string"
+ },
+ "ExpiresAt": {
+ "description": "The date for the expiration of the secret. This parameter expresses its value in the ISO 8601 timestamp format in UTC.",
+ "type": "string"
+ },
+ "LastUsedAt": {
+ "description": "The last time the secret was used. This parameter expresses its value in the ISO 8601 timestamp format in UTC.",
+ "type": "string"
+ },
+ "Profile": {
+ "description": "Profile used to provide credentials information, (a secret with the cfn/atlas/profile/{Profile}, is required), if not provided default is used.",
+ "type": "string",
+ "default": "default"
+ }
+ },
+ "additionalProperties": false,
+ "required": ["ProjectId", "ClientId"],
+ "createOnlyProperties": [
+ "/properties/ProjectId",
+ "/properties/ClientId",
+ "/properties/SecretExpiresAfterHours",
+ "/properties/Profile"
+ ],
+ "readOnlyProperties": [
+ "/properties/SecretId",
+ "/properties/Secret",
+ "/properties/MaskedSecretValue",
+ "/properties/CreatedAt",
+ "/properties/ExpiresAt",
+ "/properties/LastUsedAt"
+ ],
+ "primaryIdentifier": [
+ "/properties/ProjectId",
+ "/properties/ClientId",
+ "/properties/SecretId"
+ ],
+ "handlers": {
+ "create": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ },
+ "read": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ },
+ "delete": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ },
+ "list": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ }
+ },
+ "documentationUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/blob/master/cfn-resources/project-service-account-secret/README.md",
+ "tagging": {
+ "taggable": false
+ }
+}
diff --git a/cfn-resources/project-service-account-secret/resource-role.yaml b/cfn-resources/project-service-account-secret/resource-role.yaml
new file mode 100644
index 000000000..ec8ef0b1f
--- /dev/null
+++ b/cfn-resources/project-service-account-secret/resource-role.yaml
@@ -0,0 +1,38 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Description: >
+ This CloudFormation template creates a role assumed by CloudFormation
+ during CRUDL operations to mutate resources on behalf of the customer.
+
+Resources:
+ ExecutionRole:
+ Type: AWS::IAM::Role
+ Properties:
+ MaxSessionDuration: 8400
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: resources.cloudformation.amazonaws.com
+ Action: sts:AssumeRole
+ Condition:
+ StringEquals:
+ aws:SourceAccount:
+ Ref: AWS::AccountId
+ StringLike:
+ aws:SourceArn:
+ Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/resource/MongoDB-Atlas-ProjectServiceAccountSecret/*
+ Path: "/"
+ Policies:
+ - PolicyName: ResourceTypePolicy
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - "secretsmanager:GetSecretValue"
+ Resource: "*"
+Outputs:
+ ExecutionRoleArn:
+ Value:
+ Fn::GetAtt: ExecutionRole.Arn
diff --git a/cfn-resources/project-service-account-secret/template.yml b/cfn-resources/project-service-account-secret/template.yml
new file mode 100644
index 000000000..b141baffd
--- /dev/null
+++ b/cfn-resources/project-service-account-secret/template.yml
@@ -0,0 +1,26 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Transform: AWS::Serverless-2016-10-31
+Description: AWS SAM template for the MongoDB::Atlas::ProjectServiceAccountSecret resource type
+
+Globals:
+ Function:
+ Timeout: 60
+
+Resources:
+ TypeFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: bootstrap
+ Runtime: provided.al2
+ CodeUri: bin/
+
+ TestEntrypoint:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: bootstrap
+ Runtime: provided.al2
+ CodeUri: bin/
+ Environment:
+ Variables:
+ MODE: Test
+ LOG_LEVEL: debug
diff --git a/cfn-resources/project-service-account-secret/test/cfn-test-create-inputs.sh b/cfn-resources/project-service-account-secret/test/cfn-test-create-inputs.sh
new file mode 100755
index 000000000..ed6794577
--- /dev/null
+++ b/cfn-resources/project-service-account-secret/test/cfn-test-create-inputs.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+# cfn-test-create-inputs.sh
+#
+# This tool generates json files in the inputs/ for `cfn test`.
+#
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+function usage {
+ echo "usage:$0