diff --git a/cmd/idpscim/cmd/root.go b/cmd/idpscim/cmd/root.go index 246bd005..0ccfea96 100644 --- a/cmd/idpscim/cmd/root.go +++ b/cmd/idpscim/cmd/root.go @@ -104,6 +104,8 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfg.SyncMethod, "sync-method", "m", config.DefaultSyncMethod, "Sync method to use [groups]") rootCmd.PersistentFlags().BoolVarP(&cfg.UseSecretsManager, "use-secrets-manager", "g", config.DefaultUseSecretsManager, "use AWS Secrets Manager content or not (default false)") + rootCmd.PersistentFlags().BoolVar(&cfg.PreventGroupDeletion, "prevent-group-deletion", config.DefaultPreventGroupDeletion, "determines are we delete groups from AWS Identity Store or not") + rootCmd.PersistentFlags().BoolVar(&cfg.PreventUserDeletion, "prevent-user-deletion", config.DefaultPreventUserDeletion, "determines are we delete users from AWS Identity Store or not") } // initConfig reads in config file and ENV variables if set. @@ -126,6 +128,8 @@ func initConfig() { "aws_scim_endpoint", "aws_scim_endpoint_secret_name", "use_secrets_manager", + "prevent_group_deletion", + "prevent_user_deletion", } for _, e := range envVars { if err := viper.BindEnv(e); err != nil { @@ -211,18 +215,8 @@ func getSecrets() { log.Fatalf(errors.Wrap(err, "cannot create aws secrets manager service").Error()) } - log.WithField("name", cfg.GWSUserEmailSecretName).Debug("reading secret") - unwrap, err := secrets.GetSecretValue(context.Background(), cfg.GWSUserEmailSecretName) - if err != nil { - log.Fatalf(errors.Wrap(err, "cannot get secretmanager value").Error()) - } - cfg.GWSUserEmail = unwrap - log.WithFields( - log.Fields{"secretARN": cfg.GWSUserEmailSecretName}, - ).Debug("read secret") - log.WithField("name", cfg.GWSServiceAccountFileSecretName).Debug("reading secret") - unwrap, err = secrets.GetSecretValue(context.Background(), cfg.GWSServiceAccountFileSecretName) + unwrap, err := secrets.GetSecretValue(context.Background(), cfg.GWSServiceAccountFileSecretName) if err != nil { log.Fatalf(errors.Wrap(err, "cannot get secretmanager value").Error()) } @@ -231,6 +225,18 @@ func getSecrets() { log.Fields{"secretARN": cfg.GWSServiceAccountFileSecretName}, ).Debug("read secret") + if cfg.GWSUserEmailSecretName != "" { + log.WithField("name", cfg.GWSUserEmailSecretName).Debug("reading secret") + unwrap, err = secrets.GetSecretValue(context.Background(), cfg.GWSUserEmailSecretName) + if err != nil { + log.Fatalf(errors.Wrap(err, "cannot get secretmanager value").Error()) + } + cfg.GWSUserEmail = unwrap + log.WithFields( + log.Fields{"secretARN": cfg.GWSUserEmailSecretName}, + ).Debug("read secret") + } + log.WithField("name", cfg.AWSSCIMAccessTokenSecretName).Debug("reading secret") unwrap, err = secrets.GetSecretValue(context.Background(), cfg.AWSSCIMAccessTokenSecretName) if err != nil { @@ -241,15 +247,17 @@ func getSecrets() { log.Fields{"secretARN": cfg.AWSSCIMAccessTokenSecretName}, ).Debug("read secret") - log.WithField("name", cfg.AWSSCIMEndpointSecretName).Debug("reading secret") - unwrap, err = secrets.GetSecretValue(context.Background(), cfg.AWSSCIMEndpointSecretName) - if err != nil { - log.Fatalf(errors.Wrap(err, "cannot get secretmanager value").Error()) + if cfg.AWSSCIMEndpointSecretName != "" { + log.WithField("name", cfg.AWSSCIMEndpointSecretName).Debug("reading secret") + unwrap, err = secrets.GetSecretValue(context.Background(), cfg.AWSSCIMEndpointSecretName) + if err != nil { + log.Fatalf(errors.Wrap(err, "cannot get secretmanager value").Error()) + } + cfg.AWSSCIMEndpoint = unwrap + log.WithFields( + log.Fields{"secretARN": cfg.AWSSCIMEndpointSecretName}, + ).Debug("read secret") } - cfg.AWSSCIMEndpoint = unwrap - log.WithFields( - log.Fields{"secretARN": cfg.AWSSCIMEndpointSecretName}, - ).Debug("read secret") } func sync() error { @@ -340,7 +348,7 @@ func syncGroups() error { log.Fatalf(errors.Wrap(err, "cannot create s3 repository").Error()) } - ss, err := core.NewSyncService(idpService, scimService, repo, core.WithIdentityProviderGroupsFilter(cfg.GWSGroupsFilter)) + ss, err := core.NewSyncService(idpService, scimService, repo, core.WithIdentityProviderGroupsFilter(cfg.GWSGroupsFilter), core.WithSCIMGroupDeleteionPrevention(cfg.PreventGroupDeletion), core.WithSCIMUserDeleteionPrevention(cfg.PreventUserDeletion)) if err != nil { return errors.Wrap(err, "cannot create sync service") } diff --git a/cmd/idpscimcli/cmd/root.go b/cmd/idpscimcli/cmd/root.go index 4f0cdaaa..13e03c6c 100644 --- a/cmd/idpscimcli/cmd/root.go +++ b/cmd/idpscimcli/cmd/root.go @@ -66,6 +66,7 @@ func initConfig() { "gws_users_filter", "aws_scim_access_token", "aws_scim_endpoint", + "prevent_group_deletion", } for _, e := range envVars { if err := viper.BindEnv(e); err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 07d4375c..d9a60399 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,16 +31,22 @@ const ( DefaultGWSServiceAccountFileSecretName = "IDPSCIM_GWSServiceAccountFile" // DefaultGWSUserEmailSecretName is the name of the secret containing the user email. - DefaultGWSUserEmailSecretName = "IDPSCIM_GWSUserEmail" + DefaultGWSUserEmailSecretName = "" // DefaultAWSSCIMEndpointSecretName is the name of the secret containing the SCIM endpoint. - DefaultAWSSCIMEndpointSecretName = "IDPSCIM_SCIMEndpoint" + DefaultAWSSCIMEndpointSecretName = "" // DefaultAWSSCIMAccessTokenSecretName is the name of the secret containing the SCIM access token. DefaultAWSSCIMAccessTokenSecretName = "IDPSCIM_SCIMAccessToken" // DefaultUseSecretsManager determines if we will use the AWS Secrets Manager secrets or program parameter values DefaultUseSecretsManager = false + + // DefaultPreventGroupDeletion determines are we delete groups from AWS Identity Store or not + DefaultPreventGroupDeletion = false + + // DefaultPreventUserDeletion determines are we delete users from AWS Identity Store or not + DefaultPreventUserDeletion = false ) // Config represents the configuration of the application. @@ -72,6 +78,12 @@ type Config struct { // UseSecretsManager determines if we will use the AWS Secrets Manager secrets or program parameter values UseSecretsManager bool `mapstructure:"use_secrets_manager" json:"use_secrets_manager" yaml:"use_secrets_manager"` + + // PreventGroupDeletion determines are we delete groups from AWS Identity Store or not + PreventGroupDeletion bool `mapstructure:"prevent_group_deletion" json:"prevent_group_deletion", yaml:"prevent_group_deletion"` + + // PreventGroupDeletion determines are we delete users from AWS Identity Store or not + PreventUserDeletion bool `mapstructure:"prevent_user_deletion" json:"prevent_user_deletion", yaml:"prevent_user_deletion"` } // New returns a new Config @@ -90,5 +102,7 @@ func New() Config { AWSSCIMEndpointSecretName: DefaultAWSSCIMEndpointSecretName, AWSSCIMAccessTokenSecretName: DefaultAWSSCIMAccessTokenSecretName, UseSecretsManager: DefaultUseSecretsManager, + PreventGroupDeletion: DefaultPreventGroupDeletion, + PreventUserDeletion: DefaultPreventUserDeletion, } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8e20099a..27b1784c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -24,4 +24,5 @@ func TestNew(t *testing.T) { assert.Equal(cfg.AWSSCIMEndpointSecretName, DefaultAWSSCIMEndpointSecretName) assert.Equal(cfg.AWSSCIMAccessTokenSecretName, DefaultAWSSCIMAccessTokenSecretName) assert.Equal(cfg.UseSecretsManager, DefaultUseSecretsManager) + assert.Equal(cfg.PreventGroupDeletion, DefaultUseSecretsManager) } diff --git a/internal/core/actions.go b/internal/core/actions.go index d68c35e9..c2b46bf8 100644 --- a/internal/core/actions.go +++ b/internal/core/actions.go @@ -17,6 +17,8 @@ func scimSync( idpGroupsResult *model.GroupsResult, idpUsersResult *model.UsersResult, idpGroupsMembersResult *model.GroupsMembersResult, + preventUserDeletion bool, + preventGroupDeletion bool, ) (*model.GroupsResult, *model.UsersResult, *model.GroupsMembersResult, error) { log.Warn("reconciling the SCIM data with the Identity Provider data") @@ -39,6 +41,10 @@ func scimSync( return nil, nil, nil, fmt.Errorf("error reconciling groups: %w", err) } + if preventGroupDeletion { + groupsDelete = &model.GroupsResult{} + } + groupsCreated, groupsUpdated, err := reconcilingGroups(ctx, scim, groupsCreate, groupsUpdate, groupsDelete) if err != nil { return nil, nil, nil, fmt.Errorf("error reconciling groups: %w", err) @@ -62,6 +68,10 @@ func scimSync( return nil, nil, nil, fmt.Errorf("error operating with users: %w", err) } + if preventUserDeletion { + usersDelete = &model.UsersResult{} + } + usersCreated, usersUpdated, err := reconcilingUsers(ctx, scim, usersCreate, usersUpdate, usersDelete) if err != nil { return nil, nil, nil, fmt.Errorf("error reconciling users: %w", err) @@ -108,6 +118,8 @@ func stateSync( idpGroupsResult *model.GroupsResult, idpUsersResult *model.UsersResult, idpGroupsMembersResult *model.GroupsMembersResult, + preventUserDeletion bool, + preventGroupDeletion bool, ) (*model.GroupsResult, *model.UsersResult, *model.GroupsMembersResult, error) { var totalGroupsResult *model.GroupsResult var totalUsersResult *model.UsersResult @@ -143,6 +155,10 @@ func stateSync( return nil, nil, nil, fmt.Errorf("error reconciling groups: %w", err) } + if preventGroupDeletion { + groupsDelete = &model.GroupsResult{} + } + groupsCreated, groupsUpdated, err := reconcilingGroups(ctx, scim, groupsCreate, groupsUpdate, groupsDelete) if err != nil { return nil, nil, nil, fmt.Errorf("error reconciling groups: %w", err) @@ -168,6 +184,10 @@ func stateSync( return nil, nil, nil, fmt.Errorf("error operating with users: %w", err) } + if preventUserDeletion { + usersDelete = &model.UsersResult{} + } + usersCreated, usersUpdated, err := reconcilingUsers(ctx, scim, usersCreate, usersUpdate, usersDelete) if err != nil { return nil, nil, nil, fmt.Errorf("error reconciling users: %w", err) diff --git a/internal/core/options.go b/internal/core/options.go index ea4f68ea..62002ac2 100644 --- a/internal/core/options.go +++ b/internal/core/options.go @@ -19,3 +19,15 @@ func WithIdentityProviderUsersFilter(filter []string) SyncServiceOption { ss.provUsersFilter = filter } } + +func WithSCIMGroupDeleteionPrevention(preventGroupDeletion bool) SyncServiceOption { + return func(ss *SyncService) { + ss.scimPreventGroupDeletion = preventGroupDeletion + } +} + +func WithSCIMUserDeleteionPrevention(preventUserDeletion bool) SyncServiceOption { + return func(ss *SyncService) { + ss.scimPreventUserDeletion = preventUserDeletion + } +} diff --git a/internal/core/sync.go b/internal/core/sync.go index d98973b4..64b2d80e 100644 --- a/internal/core/sync.go +++ b/internal/core/sync.go @@ -26,11 +26,13 @@ var ( // SyncService represent the sync service and the core of the sync process type SyncService struct { - provGroupsFilter []string - provUsersFilter []string - prov IdentityProviderService - scim SCIMService - repo StateRepository + provGroupsFilter []string + provUsersFilter []string + scimPreventGroupDeletion bool + scimPreventUserDeletion bool + prov IdentityProviderService + scim SCIMService + repo StateRepository } // NewSyncService creates a new sync service. @@ -46,11 +48,13 @@ func NewSyncService(prov IdentityProviderService, scim SCIMService, repo StateRe } ss := &SyncService{ - prov: prov, - provGroupsFilter: []string{}, // fill in with the opts - provUsersFilter: []string{}, // fill in with the opts - scim: scim, - repo: repo, + prov: prov, + provGroupsFilter: []string{}, // fill in with the opts + provUsersFilter: []string{}, // fill in with the opts + scimPreventUserDeletion: false, + scimPreventGroupDeletion: false, + scim: scim, + repo: repo, } for _, opt := range opts { @@ -139,6 +143,8 @@ func (ss *SyncService) SyncGroupsAndTheirMembers(ctx context.Context) error { idpGroupsResult, idpUsersResult, idpGroupsMembersResult, + ss.scimPreventUserDeletion, + ss.scimPreventGroupDeletion, ) if err != nil { return fmt.Errorf("error doing the first sync: %w", err) @@ -152,6 +158,8 @@ func (ss *SyncService) SyncGroupsAndTheirMembers(ctx context.Context) error { idpGroupsResult, idpUsersResult, idpGroupsMembersResult, + ss.scimPreventUserDeletion, + ss.scimPreventGroupDeletion, ) if err != nil { return fmt.Errorf("error syncing state: %w", err) diff --git a/template.yaml b/template.yaml index 0c388e10..f1d587be 100644 --- a/template.yaml +++ b/template.yaml @@ -22,6 +22,8 @@ Metadata: - MemorySize - Timeout - LogGroupRetentionDays + - PreventGroupDeletion + - PreventUserDeletion - Label: default: "State File - Configuration" Parameters: @@ -30,17 +32,13 @@ Metadata: - Label: default: "Google Workspace - Credentials" Parameters: - - GWSServiceAccountFile - - GWSServiceAccountFileSecretName - GWSUserEmail - - GWSUserEmailSecretName + - GWSServiceAccountFileSecretARN - Label: default: "AWS Single Sign-On SCIM - Credentials" Parameters: - SCIMEndpoint - - SCIMEndpointSecretName - - SCIMAccessToken - - SCIMAccessTokenSecretName + - SCIMAccessTokenSecretARN AWS::ServerlessRepo::Application: Name: idp-scim-sync @@ -109,55 +107,27 @@ Parameters: The key "file" where the state data will be stored Default: data/state.json - GWSServiceAccountFile: - Type: String - Description: | - The Google Workspace credentials file content (content of credentials.json after creates the service account: https://cloud.google.com/iam/docs/creating-managing-service-account-keys) - NoEcho: true - - GWSServiceAccountFileSecretName: - Type: String - Description: | - The Google Workspace credentials file secret name - Default: IDPSCIM_GWSServiceAccountFile GWSUserEmail: Type: String Description: | The Google Workspace user email authorized on the creation creation of the service account - NoEcho: true - - GWSUserEmailSecretName: - Type: String - Description: | - The Google Workspace user email secret name - Default: IDPSCIM_GWSUserEmail SCIMEndpoint: Type: String Description: | The AWS SSO SCIM Endpoint Url Reference: https://docs.aws.amazon.com/singlesignon/latest/userguide/provision-automatically.html - NoEcho: true - SCIMEndpointSecretName: + GWSServiceAccountFileSecretARN: Type: String Description: | - The AWS SSO SCIM Endpoint Url secret name - Default: IDPSCIM_SCIMEndpoint + The Google Workspace credentials file secret ARN - SCIMAccessToken: - Type: String - Description: | - The AWS SSO SCIM AccessToken - Reference: https://docs.aws.amazon.com/singlesignon/latest/userguide/provision-automatically.html - NoEcho: true - - SCIMAccessTokenSecretName: + SCIMAccessTokenSecretARN: Type: String Description: | The AWS SSO SCIM AccessToken secret name - Default: IDPSCIM_SCIMAccessToken GWSGroupsFilter: Type: String @@ -173,6 +143,24 @@ Parameters: AllowedValues: - groups + PreventGroupDeletion: + Type: String + Description: | + Do not delete groups from AWS + Default: "true" + AllowedValues: + - "true" + - "false" + + PreventUserDeletion: + Type: String + Description: | + Do not delete users from AWS + Default: "true" + AllowedValues: + - "true" + - "false" + MemorySize: Type: Number Description: | @@ -239,7 +227,6 @@ Parameters: Type: String Description: | The Lambda function handler - Reference: https://docs.aws.amazon.com/lambda/latest/dg/configuration-console.html Default: bootstrap @@ -266,13 +253,15 @@ Resources: IDPSCIM_LOG_LEVEL: !Ref LogLevel IDPSCIM_LOG_FORMAT: !Ref LogFormat IDPSCIM_SYNC_METHOD: !Ref SyncMethod + IDPSCIM_PREVENT_GROUP_DELETION: !Ref PreventGroupDeletion + IDPSCIM_PREVENT_USER_DELETION: !Ref PreventUserDeletion IDPSCIM_AWS_S3_BUCKET_NAME: !Sub "${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}" IDPSCIM_AWS_S3_BUCKET_KEY: !Ref BucketKey IDPSCIM_GWS_GROUPS_FILTER: !Ref GWSGroupsFilter - IDPSCIM_GWS_USER_EMAIL_SECRET_NAME: !Ref AWSGWSUserEmailSecret - IDPSCIM_GWS_SERVICE_ACCOUNT_FILE_SECRET_NAME: !Ref AWSGWSServiceAccountFileSecret - IDPSCIM_AWS_SCIM_ENDPOINT_SECRET_NAME: !Ref AWSSCIMEndpointSecret - IDPSCIM_AWS_SCIM_ACCESS_TOKEN_SECRET_NAME: !Ref AWSSCIMAccessTokenSecret + IDPSCIM_GWS_USER_EMAIL: !Ref GWSUserEmail + IDPSCIM_GWS_SERVICE_ACCOUNT_FILE_SECRET_NAME: !Ref GWSServiceAccountFileSecretARN + IDPSCIM_AWS_SCIM_ENDPOINT: !Ref SCIMEndpoint + IDPSCIM_AWS_SCIM_ACCESS_TOKEN_SECRET_NAME: !Ref SCIMAccessTokenSecretARN Role: !GetAtt LambdaFunctionRole.Arn Events: SyncScheduledEvent: @@ -297,8 +286,8 @@ Resources: Principal: Service: "lambda.amazonaws.com" ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess + - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSXrayWriteOnlyAccess" Policies: - PolicyName: CustomLambdaPolicy PolicyDocument: @@ -310,10 +299,8 @@ Resources: - secretsmanager:GetResourcePolicy - secretsmanager:GetSecretValue Resource: - - !Ref AWSGWSServiceAccountFileSecret - - !Ref AWSGWSUserEmailSecret - - !Ref AWSSCIMEndpointSecret - - !Ref AWSSCIMAccessTokenSecret + - !Ref GWSServiceAccountFileSecretARN + - !Ref SCIMAccessTokenSecretARN - Sid: S3Policy Effect: Allow Action: @@ -324,8 +311,8 @@ Resources: - s3:PutObjectAcl - s3:ListBucket Resource: - - !Sub "arn:aws:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}" - - !Sub "arn:aws:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}/*" + - !Sub "arn:${AWS::Partition}:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}" + - !Sub "arn:${AWS::Partition}:s3:::${BucketNamePrefix}-${AWS::AccountId}-${AWS::Region}/*" - Sid: KMSGetDataPolicy Effect: Allow Action: @@ -341,30 +328,6 @@ Resources: Resource: - !GetAtt KMSKey.Arn - AWSGWSServiceAccountFileSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Ref GWSServiceAccountFileSecretName - SecretString: !Ref GWSServiceAccountFile - - AWSGWSUserEmailSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Ref GWSUserEmailSecretName - SecretString: !Ref GWSUserEmail - - AWSSCIMEndpointSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Ref SCIMEndpointSecretName - SecretString: !Ref SCIMEndpoint - - AWSSCIMAccessTokenSecret: - Type: AWS::SecretsManager::Secret - Properties: - Name: !Ref SCIMAccessTokenSecretName - SecretString: !Ref SCIMAccessToken - KMSKey: Type: AWS::KMS::Key Properties: @@ -376,7 +339,7 @@ Resources: - Sid: AllowIAMThisAccount Effect: Allow Principal: - AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" + AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" Action: "kms:*" Resource: "*" - Sid: AllowAWSLambdaToRetrieveKMSKey @@ -384,7 +347,7 @@ Resources: Principal: Service: "lambda.amazonaws.com" #AWS: !GetAtt LambdaFunctionRole.Arn # Fails because circular reference - #AWS: !Sub "arn:aws:iam::${AWS::AccountId}:role/serverless-idp-scim-sync-${AWS::AccountId}-${AWS::Region}" # Fails in runtime because the roles is not created yet + #AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/serverless-idp-scim-sync-${AWS::AccountId}-${AWS::Region}" # Fails in runtime because the roles is not created yet Action: - kms:Encrypt - kms:Decrypt @@ -417,7 +380,7 @@ Resources: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: - KMSMasterKeyID: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:${KMSKeyAlias}" + KMSMasterKeyID: !Sub "arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:${KMSKeyAlias}" SSEAlgorithm: "aws:kms" BucketKeyEnabled: true # https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-key.html @@ -431,7 +394,7 @@ Resources: - Sid: AllowAWSLambdaFunction Principal: AWS: - - !Sub "arn:aws:iam::${AWS::AccountId}:role/serverless-idp-scim-sync-${AWS::AccountId}-${AWS::Region}" + - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/serverless-idp-scim-sync-${AWS::AccountId}-${AWS::Region}" Effect: Allow Action: - s3:GetObject