diff --git a/.github/workflows/contract-testing.yaml b/.github/workflows/contract-testing.yaml index ab660c710..553bbcf31 100644 --- a/.github/workflows/contract-testing.yaml +++ b/.github/workflows/contract-testing.yaml @@ -20,6 +20,7 @@ jobs: cluster-outage-simulation: ${{ steps.filter.outputs.cluster-outage-simulation }} federated-database-instance: ${{ steps.filter.outputs.federated-database-instance }} federated-query-limit: ${{ steps.filter.outputs.federated-query-limit }} + federated-settings-identity-provider: ${{ steps.filter.outputs.federated-settings-identity-provider }} flex-cluster: ${{ steps.filter.outputs.flex-cluster }} online-archive: ${{ steps.filter.outputs.online-archive }} organization: ${{ steps.filter.outputs.organization }} @@ -59,6 +60,8 @@ jobs: - 'cfn-resources/federated-database-instance/**' federated-query-limit: - 'cfn-resources/federated-query-limit/**' + federated-settings-identity-provider: + - 'cfn-resources/federated-settings-identity-provider/**' flex-cluster: - 'cfn-resources/flex-cluster/**' online-archive: @@ -451,7 +454,50 @@ jobs: cat inputs/inputs_1_create.json cat inputs/inputs_1_update.json - + + make run-contract-testing + make delete-test-resources + + federated-settings-identity-provider: + needs: change-detection + if: ${{ needs.change-detection.outputs.federated-settings-identity-provider == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + - 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@c2a20b1822cc4a6bc594ff7f1dbb658758e383c3 + with: + use-installer: true + - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 + 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@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 + 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 + MONGODB_ATLAS_FEDERATION_SETTINGS_ID: ${{ vars.MONGODB_ATLAS_FEDERATION_SETTINGS_ID }} + run: | + cd cfn-resources/federated-settings-identity-provider + make create-test-resources + + cat inputs/inputs_1_create.json + cat inputs/inputs_1_update.json + make run-contract-testing make delete-test-resources diff --git a/cfn-resources/federated-settings-identity-provider/.rpdk-config b/cfn-resources/federated-settings-identity-provider/.rpdk-config new file mode 100644 index 000000000..7824f8cda --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/.rpdk-config @@ -0,0 +1,12 @@ +{ + "typeName": "MongoDB::Atlas::FederatedSettingsIdentityProvider", + "language": "go", + "runtime": "provided.al2", + "entrypoint": "bootstrap", + "testEntrypoint": "bootstrap", + "settings": { + "import_path": "github.com/mongodb/mongodbatlas-cloudformation-resources/federated-settings-identity-provider", + "protocolVersion": "2.0.0", + "pluginVersion": "2.0.4" + } +} diff --git a/cfn-resources/federated-settings-identity-provider/Makefile b/cfn-resources/federated-settings-identity-provider/Makefile new file mode 100644 index 000000000..4f3d93ebd --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/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/federated-settings-identity-provider/README.md b/cfn-resources/federated-settings-identity-provider/README.md new file mode 100644 index 000000000..81534d636 --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/README.md @@ -0,0 +1,31 @@ +# MongoDB::Atlas::FederatedSettingsIdentityProvider + +## Description + +The federated settings identity provider resource provides access to your Atlas +federated authentication identity providers (SAML and OIDC). It lets you +create, edit, and delete identity providers within an Atlas federation. + +## 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 + +Examples for this resource will be added in `/examples/`. + +## Contract Testing + +Contract testing requires a valid Federation Settings ID +export MONGODB_ATLAS_FEDERATION_SETTINGS_ID="your-federation-settings-id" + +# Run contract tests + +make create-test-resources +cfn test -- -k contract_create_delete +make delete-test-resources diff --git a/cfn-resources/federated-settings-identity-provider/cmd/main.go b/cfn-resources/federated-settings-identity-provider/cmd/main.go new file mode 100644 index 000000000..be60b7a80 --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/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/federated-settings-identity-provider/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/federated-settings-identity-provider/cmd/resource/config.go b/cfn-resources/federated-settings-identity-provider/cmd/resource/config.go new file mode 100644 index 000000000..4d9eb7831 --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/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/federated-settings-identity-provider/cmd/resource/handlers.go b/cfn-resources/federated-settings-identity-provider/cmd/resource/handlers.go new file mode 100644 index 000000000..4d34aeef1 --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/cmd/resource/handlers.go @@ -0,0 +1,169 @@ +// 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" + + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "go.mongodb.org/atlas-sdk/v20250312012/admin" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/progressevent" +) + +func HandleCreate(client *util.MongoDBClient, currentModel *Model) handler.ProgressEvent { + if currentModel.Protocol == nil || *currentModel.Protocol != ProtocolOIDC { + return progressevent.GetFailedEventByCode( + fmt.Sprintf("create is only supported by %s, %s must be imported", ProtocolOIDC, ProtocolSAML), + string(types.HandlerErrorCodeInvalidRequest), + ) + } + + federationSettingsID := util.SafeString(currentModel.FederationSettingsId) + + createRequest := ExpandOIDCCreateRequest(currentModel) + created, res, err := client.AtlasSDK.FederatedAuthenticationApi.CreateIdentityProvider(context.Background(), federationSettingsID, createRequest).Execute() + if err != nil { + return progressevent.GetFailedEventByResponse( + fmt.Sprintf("error creating federation settings identity provider (%s): %s", federationSettingsID, err.Error()), + res, + ) + } + + currentModel.IdpId = util.Pointer(created.GetId()) + + return HandleRead(client, currentModel) +} + +func HandleRead(client *util.MongoDBClient, currentModel *Model) handler.ProgressEvent { + federationSettingsID := util.SafeString(currentModel.FederationSettingsId) + idpID := util.SafeString(currentModel.IdpId) + + idp, res, err := client.AtlasSDK.FederatedAuthenticationApi.GetIdentityProvider(context.Background(), federationSettingsID, idpID).Execute() + if err != nil { + if util.StatusNotFound(res) { + return progressevent.GetFailedEventByCode("Resource not found", string(types.HandlerErrorCodeNotFound)) + } + return progressevent.GetFailedEventByResponse( + fmt.Sprintf("error getting federation settings identity provider (%s/%s): %s", federationSettingsID, idpID, err.Error()), + res, + ) + } + + model := GetFederatedSettingsIdentityProviderModel(idp, currentModel) + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Read Complete", + ResourceModel: model, + } +} + +func HandleUpdate(client *util.MongoDBClient, prevModel *Model, currentModel *Model) handler.ProgressEvent { + federationSettingsID := util.SafeString(currentModel.FederationSettingsId) + idpID := util.SafeString(currentModel.IdpId) + + associatedDomains := getStringSliceOrEmpty(currentModel.AssociatedDomains) + requestedScopes := getStringSliceOrEmpty(currentModel.RequestedScopes) + + updateReq := &admin.FederationIdentityProviderUpdate{ + AssociatedDomains: &associatedDomains, + Audience: currentModel.Audience, + AuthorizationType: currentModel.AuthorizationType, + ClientId: currentModel.ClientId, + Description: currentModel.Description, + DisplayName: currentModel.Name, + GroupsClaim: currentModel.GroupsClaim, + IdpType: currentModel.IdpType, + IssuerUri: currentModel.IssuerUri, + Protocol: currentModel.Protocol, + PemFileInfo: nil, + RequestBinding: currentModel.RequestBinding, + RequestedScopes: &requestedScopes, + ResponseSignatureAlgorithm: currentModel.ResponseSignatureAlgorithm, + SsoDebugEnabled: currentModel.SsoDebugEnabled, + SsoUrl: currentModel.SsoUrl, + Status: currentModel.Status, + UserClaim: currentModel.UserClaim, + } + + updated, updRes, err := client.AtlasSDK.FederatedAuthenticationApi.UpdateIdentityProvider(context.Background(), federationSettingsID, idpID, updateReq).Execute() + if err != nil { + return progressevent.GetFailedEventByResponse( + fmt.Sprintf("error updating federation settings identity provider (%s): %s", federationSettingsID, err.Error()), + updRes, + ) + } + + model := GetFederatedSettingsIdentityProviderModel(updated, currentModel) + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Update Complete", + ResourceModel: model, + } +} + +func HandleDelete(client *util.MongoDBClient, currentModel *Model) handler.ProgressEvent { + federationSettingsID := util.SafeString(currentModel.FederationSettingsId) + idpID := util.SafeString(currentModel.IdpId) + + res, err := client.AtlasSDK.FederatedAuthenticationApi.DeleteIdentityProvider(context.Background(), federationSettingsID, idpID).Execute() + if err != nil { + return progressevent.GetFailedEventByResponse( + fmt.Sprintf("error deleting federation settings identity provider (%s): %s, error: %s", federationSettingsID, idpID, err.Error()), + res, + ) + } + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "Delete Complete", + } +} + +func HandleList(client *util.MongoDBClient, currentModel *Model) handler.ProgressEvent { + federationSettingsID := util.SafeString(currentModel.FederationSettingsId) + + params := &admin.ListIdentityProvidersApiParams{ + FederationSettingsId: federationSettingsID, + Protocol: &allProtocols, + IdpType: &allIdpTypes, + } + providers, res, err := client.AtlasSDK.FederatedAuthenticationApi.ListIdentityProvidersWithParams(context.Background(), params).Execute() + if err != nil { + return progressevent.GetFailedEventByResponse( + fmt.Sprintf("error listing federation settings identity providers (%s): %s", federationSettingsID, err.Error()), + res, + ) + } + + results := providers.GetResults() + models := make([]any, 0, len(results)) + for i := range results { + m := &Model{ + Profile: currentModel.Profile, + FederationSettingsId: currentModel.FederationSettingsId, + } + models = append(models, GetFederatedSettingsIdentityProviderModel(&results[i], m)) + } + + return handler.ProgressEvent{ + OperationStatus: handler.Success, + Message: "List Complete", + ResourceModels: models, + } +} diff --git a/cfn-resources/federated-settings-identity-provider/cmd/resource/mappings.go b/cfn-resources/federated-settings-identity-provider/cmd/resource/mappings.go new file mode 100644 index 000000000..05b907439 --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/cmd/resource/mappings.go @@ -0,0 +1,124 @@ +// 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 ( + "go.mongodb.org/atlas-sdk/v20250312012/admin" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/util" +) + +const ( + ProtocolSAML = "SAML" + ProtocolOIDC = "OIDC" + + IdpTypeWorkforce = "WORKFORCE" + IdpTypeWorkload = "WORKLOAD" +) + +var ( + allProtocols = []string{ProtocolSAML, ProtocolOIDC} + allIdpTypes = []string{IdpTypeWorkforce, IdpTypeWorkload} +) + +func getStringSliceOrEmpty(slice []string) []string { + if slice != nil { + return slice + } + return []string{} +} + +func GetFederatedSettingsIdentityProviderModel(api *admin.FederationIdentityProvider, currentModel *Model) *Model { + var model *Model + if currentModel != nil { + model = currentModel + } else { + model = &Model{} + } + + if api == nil { + return model + } + + if oktaID, ok := api.GetOktaIdpIdOk(); ok && *oktaID != "" { + model.OktaIdpId = oktaID + } + + model.IdpId = util.Pointer(api.GetId()) + model.Name = util.Pointer(api.GetDisplayName()) + model.IssuerUri = util.Pointer(api.GetIssuerUri()) + model.Protocol = util.Pointer(api.GetProtocol()) + model.Description = util.Pointer(api.GetDescription()) + model.AuthorizationType = util.Pointer(api.GetAuthorizationType()) + model.IdpType = util.Pointer(api.GetIdpType()) + + protocol := api.GetProtocol() + switch protocol { + case ProtocolSAML: + model.RequestBinding = util.Pointer(api.GetRequestBinding()) + model.ResponseSignatureAlgorithm = util.Pointer(api.GetResponseSignatureAlgorithm()) + model.SsoDebugEnabled = api.SsoDebugEnabled + model.SsoUrl = util.Pointer(api.GetSsoUrl()) + model.Status = util.Pointer(api.GetStatus()) + + associatedDomains := api.GetAssociatedDomains() + if len(associatedDomains) == 0 && currentModel != nil && len(currentModel.AssociatedDomains) > 0 { + associatedDomains = currentModel.AssociatedDomains + } + model.AssociatedDomains = associatedDomains + case ProtocolOIDC: + model.Audience = util.Pointer(api.GetAudience()) + model.ClientId = util.Pointer(api.GetClientId()) + model.GroupsClaim = util.Pointer(api.GetGroupsClaim()) + + requestedScopes := api.GetRequestedScopes() + if len(requestedScopes) == 0 && currentModel != nil && len(currentModel.RequestedScopes) > 0 { + requestedScopes = currentModel.RequestedScopes + } + model.RequestedScopes = requestedScopes + + model.UserClaim = util.Pointer(api.GetUserClaim()) + + associatedDomains := api.GetAssociatedDomains() + if len(associatedDomains) == 0 && currentModel != nil && len(currentModel.AssociatedDomains) > 0 { + associatedDomains = currentModel.AssociatedDomains + } + model.AssociatedDomains = associatedDomains + default: + return model + } + + return model +} + +func ExpandOIDCCreateRequest(model *Model) *admin.FederationOidcIdentityProviderUpdate { + associatedDomains := getStringSliceOrEmpty(model.AssociatedDomains) + requestedScopes := getStringSliceOrEmpty(model.RequestedScopes) + + return &admin.FederationOidcIdentityProviderUpdate{ + Audience: util.Pointer(util.SafeString(model.Audience)), + AssociatedDomains: &associatedDomains, + AuthorizationType: util.Pointer(util.SafeString(model.AuthorizationType)), + ClientId: util.Pointer(util.SafeString(model.ClientId)), + Description: util.Pointer(util.SafeString(model.Description)), + DisplayName: util.Pointer(util.SafeString(model.Name)), + GroupsClaim: util.Pointer(util.SafeString(model.GroupsClaim)), + IdpType: util.Pointer(util.SafeString(model.IdpType)), + IssuerUri: util.Pointer(util.SafeString(model.IssuerUri)), + Protocol: util.Pointer(util.SafeString(model.Protocol)), + RequestedScopes: &requestedScopes, + UserClaim: util.Pointer(util.SafeString(model.UserClaim)), + } +} diff --git a/cfn-resources/federated-settings-identity-provider/cmd/resource/mappings_test.go b/cfn-resources/federated-settings-identity-provider/cmd/resource/mappings_test.go new file mode 100644 index 000000000..105ae08d3 --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/cmd/resource/mappings_test.go @@ -0,0 +1,119 @@ +// Copyright 2025 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" + + "github.com/mongodb/mongodbatlas-cloudformation-resources/federated-settings-identity-provider/cmd/resource" + "github.com/stretchr/testify/assert" + "go.mongodb.org/atlas-sdk/v20250312012/admin" +) + +const ( + testIdpID = "test-idp-id" + testOktaID = "test-okta-id" +) + +func TestGetFederatedSettingsIdentityProviderModel_SAML(t *testing.T) { + protocol := "SAML" + displayName := "saml-name" + issuerURI := "https://issuer.example.com" + api := &admin.FederationIdentityProvider{ + Id: testIdpID, + OktaIdpId: testOktaID, + Protocol: &protocol, + DisplayName: &displayName, + IssuerUri: &issuerURI, + RequestBinding: func() *string { s := "HTTP-POST"; return &s }(), + ResponseSignatureAlgorithm: func() *string { s := "RSA-SHA256"; return &s }(), + SsoDebugEnabled: func() *bool { b := true; return &b }(), + SsoUrl: func() *string { s := "https://sso.example.com"; return &s }(), + Status: func() *string { s := "ACTIVE"; return &s }(), + AssociatedDomains: func() *[]string { s := []string{"example.com"}; return &s }(), + } + + model := resource.GetFederatedSettingsIdentityProviderModel(api, &resource.Model{}) + + assert.Equal(t, testIdpID, *model.IdpId) + assert.Equal(t, testOktaID, *model.OktaIdpId) + assert.Equal(t, "SAML", *model.Protocol) + assert.Equal(t, "saml-name", *model.Name) + assert.Equal(t, "https://issuer.example.com", *model.IssuerUri) + assert.Equal(t, "HTTP-POST", *model.RequestBinding) + assert.Equal(t, "RSA-SHA256", *model.ResponseSignatureAlgorithm) + assert.True(t, *model.SsoDebugEnabled) + assert.Equal(t, "https://sso.example.com", *model.SsoUrl) + assert.Equal(t, "ACTIVE", *model.Status) + assert.Equal(t, []string{"example.com"}, model.AssociatedDomains) + // OIDC-only fields should not be set by the SAML branch + assert.Nil(t, model.ClientId) + assert.Nil(t, model.UserClaim) +} + +func TestGetFederatedSettingsIdentityProviderModel_OIDC(t *testing.T) { + protocol := "OIDC" + displayName := "oidc-name" + issuerURI := "https://issuer.oidc.example.com" + api := &admin.FederationIdentityProvider{ + Id: testIdpID, + OktaIdpId: testOktaID, + Protocol: &protocol, + DisplayName: &displayName, + IssuerUri: &issuerURI, + Audience: func() *string { s := "aud"; return &s }(), + ClientId: func() *string { s := "client"; return &s }(), + GroupsClaim: func() *string { s := "groups"; return &s }(), + RequestedScopes: func() *[]string { s := []string{"openid", "profile"}; return &s }(), + UserClaim: func() *string { s := "sub"; return &s }(), + AssociatedDomains: func() *[]string { s := []string{"oidc.example.com"}; return &s }(), + } + + model := resource.GetFederatedSettingsIdentityProviderModel(api, &resource.Model{}) + + assert.Equal(t, testIdpID, *model.IdpId) + assert.Equal(t, testOktaID, *model.OktaIdpId) + assert.Equal(t, "OIDC", *model.Protocol) + assert.Equal(t, "oidc-name", *model.Name) + assert.Equal(t, "https://issuer.oidc.example.com", *model.IssuerUri) + assert.Equal(t, "aud", *model.Audience) + assert.Equal(t, "client", *model.ClientId) + assert.Equal(t, "groups", *model.GroupsClaim) + assert.Equal(t, []string{"openid", "profile"}, model.RequestedScopes) + assert.Equal(t, "sub", *model.UserClaim) + assert.Equal(t, []string{"oidc.example.com"}, model.AssociatedDomains) + // SAML-only fields should not be set by the OIDC branch + assert.Nil(t, model.RequestBinding) + assert.Nil(t, model.SsoUrl) +} + +func TestExpandOIDCCreateRequest_DefaultSlices(t *testing.T) { + protocol := resource.ProtocolOIDC + name := "n" + issuer := "i" + m := &resource.Model{ + Protocol: &protocol, + Name: &name, + IssuerUri: &issuer, + // AssociatedDomains and RequestedScopes intentionally nil to exercise defaults + } + + req := resource.ExpandOIDCCreateRequest(m) + assert.NotNil(t, req) + assert.NotNil(t, req.AssociatedDomains) + assert.NotNil(t, req.RequestedScopes) + assert.Empty(t, *req.AssociatedDomains) + assert.Empty(t, *req.RequestedScopes) +} diff --git a/cfn-resources/federated-settings-identity-provider/cmd/resource/model.go b/cfn-resources/federated-settings-identity-provider/cmd/resource/model.go new file mode 100644 index 000000000..055fea9cc --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/cmd/resource/model.go @@ -0,0 +1,28 @@ +// 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 { + Profile *string `json:",omitempty"` + FederationSettingsId *string `json:",omitempty"` + Name *string `json:",omitempty"` + IssuerUri *string `json:",omitempty"` + RequestBinding *string `json:",omitempty"` + ResponseSignatureAlgorithm *string `json:",omitempty"` + AssociatedDomains []string `json:",omitempty"` + SsoDebugEnabled *bool `json:",omitempty"` + SsoUrl *string `json:",omitempty"` + Status *string `json:",omitempty"` + OktaIdpId *string `json:",omitempty"` + IdpId *string `json:",omitempty"` + Protocol *string `json:",omitempty"` + Audience *string `json:",omitempty"` + ClientId *string `json:",omitempty"` + GroupsClaim *string `json:",omitempty"` + RequestedScopes []string `json:",omitempty"` + UserClaim *string `json:",omitempty"` + Description *string `json:",omitempty"` + AuthorizationType *string `json:",omitempty"` + IdpType *string `json:",omitempty"` +} diff --git a/cfn-resources/federated-settings-identity-provider/cmd/resource/resource.go b/cfn-resources/federated-settings-identity-provider/cmd/resource/resource.go new file mode 100644 index 000000000..816bd6946 --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/cmd/resource/resource.go @@ -0,0 +1,86 @@ +// 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" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/constants" + "github.com/mongodb/mongodbatlas-cloudformation-resources/util/validator" + + "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" +) + +var ( + createRequiredFields = []string{constants.FederationSettingsID, constants.Name, constants.IssuerURI} + readRequiredFields = []string{constants.FederationSettingsID, constants.IdpID} + updateRequiredFields = []string{constants.FederationSettingsID, constants.IdpID} + deleteRequiredFields = []string{constants.FederationSettingsID, constants.IdpID} + listRequiredFields = []string{constants.FederationSettingsID} +) + +func setupRequest(req handler.Request, currentModel *Model, requiredFields []string) (*util.MongoDBClient, *handler.ProgressEvent) { + util.SetupLogger("mongodb-atlas-federated-settings-identity-provider") + util.SetDefaultProfileIfNotDefined(¤tModel.Profile) + + if errEvent := validator.ValidateModel(requiredFields, currentModel); errEvent != nil { + return nil, errEvent + } + + client, pe := util.NewAtlasClient(&req, currentModel.Profile) + if pe != nil { + return nil, pe + } + return client, nil +} + +func Create(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + client, peErr := setupRequest(req, currentModel, createRequiredFields) + if peErr != nil { + return *peErr, nil + } + return HandleCreate(client, currentModel), nil +} + +func Read(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + client, peErr := setupRequest(req, currentModel, readRequiredFields) + if peErr != nil { + return *peErr, nil + } + return HandleRead(client, currentModel), nil +} + +func Update(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + client, peErr := setupRequest(req, currentModel, updateRequiredFields) + if peErr != nil { + return *peErr, nil + } + return HandleUpdate(client, prevModel, currentModel), nil +} + +func Delete(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + client, peErr := setupRequest(req, currentModel, deleteRequiredFields) + if peErr != nil { + return *peErr, nil + } + return HandleDelete(client, currentModel), nil +} + +func List(req handler.Request, prevModel *Model, currentModel *Model) (handler.ProgressEvent, error) { + client, peErr := setupRequest(req, currentModel, listRequiredFields) + if peErr != nil { + return *peErr, nil + } + return HandleList(client, currentModel), nil +} diff --git a/cfn-resources/federated-settings-identity-provider/docs/README.md b/cfn-resources/federated-settings-identity-provider/docs/README.md new file mode 100644 index 000000000..ecc553898 --- /dev/null +++ b/cfn-resources/federated-settings-identity-provider/docs/README.md @@ -0,0 +1,281 @@ +# MongoDB::Atlas::FederatedSettingsIdentityProvider + +Resource for managing MongoDB Atlas Federated Settings Identity Providers (SAML and OIDC) within an Atlas federation. + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+ "Type" : "MongoDB::Atlas::FederatedSettingsIdentityProvider",
+ "Properties" : {
+ "Profile" : String,
+ "FederationSettingsId" : String,
+ "Name" : String,
+ "IssuerUri" : String,
+ "RequestBinding" : String,
+ "ResponseSignatureAlgorithm" : String,
+ "AssociatedDomains" : [ String, ... ],
+ "SsoDebugEnabled" : Boolean,
+ "SsoUrl" : String,
+ "Status" : String,
+ "Protocol" : String,
+ "Audience" : String,
+ "ClientId" : String,
+ "GroupsClaim" : String,
+ "RequestedScopes" : [ String, ... ],
+ "UserClaim" : String,
+ "Description" : String,
+ "AuthorizationType" : String,
+ "IdpType" : String
+ }
+}
+
+
+### YAML
+
++Type: MongoDB::Atlas::FederatedSettingsIdentityProvider +Properties: + Profile: String + FederationSettingsId: String + Name: String + IssuerUri: String + RequestBinding: String + ResponseSignatureAlgorithm: String + AssociatedDomains: + - String + SsoDebugEnabled: Boolean + SsoUrl: String + Status: String + Protocol: String + Audience: String + ClientId: String + GroupsClaim: String + RequestedScopes: + - String + UserClaim: String + Description: String + AuthorizationType: String + IdpType: String ++ +## Properties + +#### Profile + +The profile is defined in AWS Secrets Manager. See [Secret Manager Profile setup](../../../examples/profile-secret.yaml). + +_Required_: No + +_Type_: String + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +#### FederationSettingsId + +Unique 24-hexadecimal digit string that identifies your federation. + +_Required_: Yes + +_Type_: String + +_Minimum Length_:
24
+
+_Maximum Length_: 24
+
+_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)
+
+#### Name
+
+Human-readable name (display name) of the identity provider.
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### IssuerUri
+
+Issuer URI of the identity provider.
+
+_Required_: Yes
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### RequestBinding
+
+SAML request binding.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ResponseSignatureAlgorithm
+
+SAML response signature algorithm.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### AssociatedDomains
+
+List of associated domains for this identity provider.
+
+_Required_: No
+
+_Type_: List of String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### SsoDebugEnabled
+
+Flag that indicates whether to enable SSO debug.
+
+_Required_: No
+
+_Type_: Boolean
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### SsoUrl
+
+SSO URL.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Status
+
+Identity provider status.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Protocol
+
+Identity provider protocol.
+
+_Required_: No
+
+_Type_: String
+
+_Allowed Values_: SAML | OIDC
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Audience
+
+OIDC audience.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### ClientId
+
+OIDC client ID.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### GroupsClaim
+
+OIDC groups claim.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### RequestedScopes
+
+OIDC requested scopes.
+
+_Required_: No
+
+_Type_: List of String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### UserClaim
+
+OIDC user claim.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### Description
+
+Description of the identity provider.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### AuthorizationType
+
+OIDC authorization type.
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+#### IdpType
+
+Identity provider type (for OIDC).
+
+_Required_: No
+
+_Type_: String
+
+_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
+
+## 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).
+
+#### OktaIdpId
+
+Legacy identity provider identifier (Okta IdP ID). Returned by the API.
+
+#### IdpId
+
+Unique identifier of the identity provider. Returned by the API.
+
diff --git a/cfn-resources/federated-settings-identity-provider/mongodb-atlas-federatedsettingsidentityprovider.json b/cfn-resources/federated-settings-identity-provider/mongodb-atlas-federatedsettingsidentityprovider.json
new file mode 100644
index 000000000..31b94196f
--- /dev/null
+++ b/cfn-resources/federated-settings-identity-provider/mongodb-atlas-federatedsettingsidentityprovider.json
@@ -0,0 +1,137 @@
+{
+ "typeName": "MongoDB::Atlas::FederatedSettingsIdentityProvider",
+ "description": "Resource for managing MongoDB Atlas Federated Settings Identity Providers (SAML and OIDC) within an Atlas federation.",
+ "sourceUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/tree/master/cfn-resources/federated-settings-identity-provider",
+ "properties": {
+ "Profile": {
+ "type": "string",
+ "description": "The profile is defined in AWS Secrets Manager. See [Secret Manager Profile setup](../../../examples/profile-secret.yaml).",
+ "default": "default"
+ },
+ "FederationSettingsId": {
+ "type": "string",
+ "description": "Unique 24-hexadecimal digit string that identifies your federation.",
+ "minLength": 24,
+ "maxLength": 24,
+ "pattern": "^([a-f0-9]{24})$"
+ },
+ "Name": {
+ "type": "string",
+ "description": "Human-readable name (display name) of the identity provider."
+ },
+ "IssuerUri": {
+ "type": "string",
+ "description": "Issuer URI of the identity provider."
+ },
+ "RequestBinding": {
+ "type": "string",
+ "description": "SAML request binding."
+ },
+ "ResponseSignatureAlgorithm": {
+ "type": "string",
+ "description": "SAML response signature algorithm."
+ },
+ "AssociatedDomains": {
+ "type": "array",
+ "description": "List of associated domains for this identity provider.",
+ "insertionOrder": false,
+ "items": {
+ "type": "string"
+ }
+ },
+ "SsoDebugEnabled": {
+ "type": "boolean",
+ "description": "Flag that indicates whether to enable SSO debug."
+ },
+ "SsoUrl": {
+ "type": "string",
+ "description": "SSO URL."
+ },
+ "Status": {
+ "type": "string",
+ "description": "Identity provider status."
+ },
+ "OktaIdpId": {
+ "type": "string",
+ "description": "Legacy identity provider identifier (Okta IdP ID). Returned by the API."
+ },
+ "IdpId": {
+ "type": "string",
+ "description": "Unique identifier of the identity provider. Returned by the API."
+ },
+ "Protocol": {
+ "type": "string",
+ "description": "Identity provider protocol.",
+ "enum": ["SAML", "OIDC"]
+ },
+ "Audience": {
+ "type": "string",
+ "description": "OIDC audience."
+ },
+ "ClientId": {
+ "type": "string",
+ "description": "OIDC client ID."
+ },
+ "GroupsClaim": {
+ "type": "string",
+ "description": "OIDC groups claim."
+ },
+ "RequestedScopes": {
+ "type": "array",
+ "description": "OIDC requested scopes.",
+ "insertionOrder": false,
+ "items": {
+ "type": "string"
+ }
+ },
+ "UserClaim": {
+ "type": "string",
+ "description": "OIDC user claim."
+ },
+ "Description": {
+ "type": "string",
+ "description": "Description of the identity provider."
+ },
+ "AuthorizationType": {
+ "type": "string",
+ "description": "OIDC authorization type."
+ },
+ "IdpType": {
+ "type": "string",
+ "description": "Identity provider type (for OIDC)."
+ }
+ },
+ "additionalProperties": false,
+ "required": ["FederationSettingsId", "Name", "IssuerUri"],
+ "readOnlyProperties": ["/properties/OktaIdpId", "/properties/IdpId"],
+ "createOnlyProperties": [
+ "/properties/FederationSettingsId",
+ "/properties/Profile"
+ ],
+ "primaryIdentifier": [
+ "/properties/FederationSettingsId",
+ "/properties/IdpId",
+ "/properties/Profile"
+ ],
+ "handlers": {
+ "create": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ },
+ "read": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ },
+ "update": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ },
+ "delete": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ },
+ "list": {
+ "permissions": ["secretsmanager:GetSecretValue"]
+ }
+ },
+ "documentationUrl": "https://github.com/mongodb/mongodbatlas-cloudformation-resources/blob/master/cfn-resources/federated-settings-identity-provider/README.md",
+ "tagging": {
+ "taggable": false
+ }
+}
diff --git a/cfn-resources/federated-settings-identity-provider/resource-role.yaml b/cfn-resources/federated-settings-identity-provider/resource-role.yaml
new file mode 100644
index 000000000..8cc4ee238
--- /dev/null
+++ b/cfn-resources/federated-settings-identity-provider/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-FederatedSettingsIdentityProvider/*
+ 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/federated-settings-identity-provider/template.yml b/cfn-resources/federated-settings-identity-provider/template.yml
new file mode 100644
index 000000000..9f4c8dd9a
--- /dev/null
+++ b/cfn-resources/federated-settings-identity-provider/template.yml
@@ -0,0 +1,26 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Transform: AWS::Serverless-2016-10-31
+Description: AWS SAM template for the MongoDB::Atlas::FederatedSettingsIdentityProvider 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/federated-settings-identity-provider/test/cfn-test-create-inputs.sh b/cfn-resources/federated-settings-identity-provider/test/cfn-test-create-inputs.sh
new file mode 100755
index 000000000..b90bb0c64
--- /dev/null
+++ b/cfn-resources/federated-settings-identity-provider/test/cfn-test-create-inputs.sh
@@ -0,0 +1,69 @@
+#!/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 [federation_settings_id]"
+ echo "Generates test input files for federated settings identity provider"
+ exit 0
+}
+
+if [[ "${1:-}" == "help" ]]; then usage; fi
+
+rm -rf inputs
+mkdir inputs
+
+profile="default"
+if [ ${MONGODB_ATLAS_PROFILE+x} ]; then
+ echo "profile set to ${MONGODB_ATLAS_PROFILE}"
+ profile=${MONGODB_ATLAS_PROFILE}
+fi
+
+if [ -n "${MONGODB_ATLAS_FEDERATION_SETTINGS_ID:-}" ]; then
+ federationSettingsId="${MONGODB_ATLAS_FEDERATION_SETTINGS_ID}"
+ echo "Using federation settings ID from environment variable: ${federationSettingsId}"
+elif [[ "${1:-}" =~ ^[a-f0-9]{24}$ ]]; then
+ federationSettingsId="${1}"
+ echo "Using federation settings ID from argument: ${federationSettingsId}"
+else
+ echo "ERROR: MONGODB_ATLAS_FEDERATION_SETTINGS_ID must be set or a valid 24-char hex ID must be provided as argument"
+ exit 1
+fi
+idpName="cfn-test-idp-$(date +%s)-$RANDOM"
+updatedName="${idpName}-updated"
+uniqueAudience="cfn-test-audience-$(date +%s)-$RANDOM"
+
+idpId=""
+if [ ${MONGODB_ATLAS_IDP_ID+x} ]; then
+ echo "idp id set to ${MONGODB_ATLAS_IDP_ID}"
+ idpId=${MONGODB_ATLAS_IDP_ID}
+fi
+
+WORDTOREMOVE="template."
+cd "$(dirname "$0")" || exit
+for inputFile in inputs_*; do
+ outputFile=${inputFile//$WORDTOREMOVE/}
+ nameValue="$idpName"
+ if [[ "$inputFile" == *"update.template.json" ]]; then
+ nameValue="$updatedName"
+ fi
+ jq --arg profile "$profile" \
+ --arg federationSettingsId "$federationSettingsId" \
+ --arg name "$nameValue" \
+ --arg audience "$uniqueAudience" \
+ --arg idpId "$idpId" \
+ '.Profile?|=$profile
+ | .FederationSettingsId?|=$federationSettingsId
+ | .Name?|=$name
+ | .Audience?|=$audience
+ | (if $idpId != "" then .IdpId?=$idpId else . end)' \
+ "$inputFile" >"../inputs/$outputFile"
+done
+cd ..
+ls -l inputs
diff --git a/cfn-resources/federated-settings-identity-provider/test/cfn-test-delete-inputs.sh b/cfn-resources/federated-settings-identity-provider/test/cfn-test-delete-inputs.sh
new file mode 100755
index 000000000..e53aeb41d
--- /dev/null
+++ b/cfn-resources/federated-settings-identity-provider/test/cfn-test-delete-inputs.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+# cfn-test-delete-inputs.sh
+#
+# Needs to exist to be called in Publish, but no cleanup is needed.
diff --git a/cfn-resources/federated-settings-identity-provider/test/contract-testing/cfn-test-create.sh b/cfn-resources/federated-settings-identity-provider/test/contract-testing/cfn-test-create.sh
new file mode 100755
index 000000000..957f8b53d
--- /dev/null
+++ b/cfn-resources/federated-settings-identity-provider/test/contract-testing/cfn-test-create.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+# This tool generates the resources and json files in the inputs/ for `cfn test`.
+set -o errexit
+set -o nounset
+set -o pipefail
+
+./test/cfn-test-create-inputs.sh
diff --git a/cfn-resources/federated-settings-identity-provider/test/contract-testing/cfn-test-delete.sh b/cfn-resources/federated-settings-identity-provider/test/contract-testing/cfn-test-delete.sh
new file mode 100755
index 000000000..411daf374
--- /dev/null
+++ b/cfn-resources/federated-settings-identity-provider/test/contract-testing/cfn-test-delete.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+# This tool deletes the mongodb resources used for `cfn test` as inputs.
+set -o errexit
+set -o nounset
+set -o pipefail
+
+echo "No cleanup required for federated settings identity provider."
diff --git a/cfn-resources/federated-settings-identity-provider/test/federated-settings-identity-provider.sample-cfn-request.json b/cfn-resources/federated-settings-identity-provider/test/federated-settings-identity-provider.sample-cfn-request.json
new file mode 100644
index 000000000..3a8bcecb2
--- /dev/null
+++ b/cfn-resources/federated-settings-identity-provider/test/federated-settings-identity-provider.sample-cfn-request.json
@@ -0,0 +1,18 @@
+{
+ "desiredResourceState": {
+ "Profile": "default",
+ "FederationSettingsId": "