diff --git a/lib/auth/integration/integrationv1/service_test.go b/lib/auth/integration/integrationv1/service_test.go index 0fd6355ebb8f5..3316cf239d0e8 100644 --- a/lib/auth/integration/integrationv1/service_test.go +++ b/lib/auth/integration/integrationv1/service_test.go @@ -280,6 +280,29 @@ func TestIntegrationCRUD(t *testing.T) { }, ErrAssertion: trace.IsBadParameter, }, + { + Name: "can't delete integration referenced by AWS Identity Center plugin", + Role: types.RoleSpecV6{ + Allow: types.RoleConditions{Rules: []types.Rule{{ + Resources: []string{types.KindIntegration}, + Verbs: []string{types.VerbDelete}, + }}}, + }, + Setup: func(t *testing.T, igName string) { + _, err := localClient.CreateIntegration(ctx, sampleIntegrationFn(t, igName)) + require.NoError(t, err) + require.NoError(t, localClient.CreatePlugin(ctx, newPlugin(t, igName))) + }, + Test: func(ctx context.Context, resourceSvc *Service, igName string) error { + _, err := resourceSvc.DeleteIntegration(ctx, &integrationpb.DeleteIntegrationRequest{Name: igName}) + return err + }, + Cleanup: func(t *testing.T, igName string) { + err := localClient.DeletePlugin(ctx, newPlugin(t, igName).GetName()) + require.NoError(t, err) + }, + ErrAssertion: trace.IsBadParameter, + }, { Name: "access to delete integration", Role: types.RoleSpecV6{ @@ -369,6 +392,8 @@ type localClient interface { DeleteDraftExternalAuditStorage(ctx context.Context) error PromoteToClusterExternalAuditStorage(ctx context.Context) error DisableClusterExternalAuditStorage(ctx context.Context) error + CreatePlugin(ctx context.Context, plugin types.Plugin) error + DeletePlugin(ctx context.Context, name string) error } type testClient struct { @@ -390,6 +415,7 @@ func initSvc(t *testing.T, ca types.CertAuthority, clusterName string, proxyPubl userSvc, err := local.NewTestIdentityService(backend) require.NoError(t, err) easSvc := local.NewExternalAuditStorageService(backend) + pluginSvc := local.NewPluginsService(backend) _, err = clusterConfigSvc.UpsertAuthPreference(ctx, types.DefaultAuthPreference()) require.NoError(t, err) @@ -455,11 +481,13 @@ func initSvc(t *testing.T, ca types.CertAuthority, clusterName string, proxyPubl *local.IdentityService *local.ExternalAuditStorageService *local.IntegrationsService + *local.PluginsService }{ AccessService: roleSvc, IdentityService: userSvc, ExternalAuditStorageService: easSvc, IntegrationsService: localResourceService, + PluginsService: pluginSvc, }, resourceSvc } @@ -525,3 +553,28 @@ func newCertAuthority(t *testing.T, caType types.CertAuthType, domain string) ty return ca } + +func newPlugin(t *testing.T, integrationName string) *types.PluginV1 { + t.Helper() + return &types.PluginV1{ + Metadata: types.Metadata{ + Name: types.PluginTypeAWSIdentityCenter, + Labels: map[string]string{ + types.HostedPluginLabel: "true", + }, + }, + Spec: types.PluginSpecV1{ + Settings: &types.PluginSpecV1_AwsIc{ + AwsIc: &types.PluginAWSICSettings{ + IntegrationName: integrationName, + Region: "test-region", + Arn: "test-arn", + AccessListDefaultOwners: []string{"user1", "user2"}, + ProvisioningSpec: &types.AWSICProvisioningSpec{ + BaseUrl: "https://example.com", + }, + }, + }, + }, + } +} diff --git a/lib/services/local/integrations.go b/lib/services/local/integrations.go index b22baecaa0be6..3b9842ee79690 100644 --- a/lib/services/local/integrations.go +++ b/lib/services/local/integrations.go @@ -127,22 +127,44 @@ func (s *IntegrationsService) DeleteIntegration(ctx context.Context, name string return trace.Wrap(err) } - conditionalActions, err := notReferencedByEAS(ctx, s.backend, name) + deleteConditions, err := integrationDeletionConditions(ctx, s.backend, name) if err != nil { return trace.Wrap(err) } - conditionalActions = append(conditionalActions, backend.ConditionalAction{ + deleteConditions = append(deleteConditions, backend.ConditionalAction{ Key: s.svc.MakeKey(backend.NewKey(name)), Condition: backend.Exists(), Action: backend.Delete(), }) - _, err = s.backend.AtomicWrite(ctx, conditionalActions) + _, err = s.backend.AtomicWrite(ctx, deleteConditions) return trace.Wrap(err) } -// notReferencedByEAS returns a slice of ConditionalActions to use with a backend.AtomicWrite to ensure that +// integrationDeletionConditions returns a BadParameter error if the integration is referenced by another +// Teleport service. If it does not find any direct reference, the backend.ConditionalAction is returned +// with the current state of reference, which should be added to AtomicWrite to ensure that the current +// reference state remains unchanged until the integration is completely deleted. +// Service may have zero or multiple ConditionalActions returned. +func integrationDeletionConditions(ctx context.Context, bk backend.Backend, name string) ([]backend.ConditionalAction, error) { + var deleteConditionalActions []backend.ConditionalAction + easDeleteConditions, err := integrationReferencedByEAS(ctx, bk, name) + if err != nil { + return nil, trace.Wrap(err) + } + deleteConditionalActions = append(deleteConditionalActions, easDeleteConditions...) + + awsIcDeleteCondition, err := integrationReferencedByAWSICPlugin(ctx, bk, name) + if err != nil { + return nil, trace.Wrap(err) + } + deleteConditionalActions = append(deleteConditionalActions, awsIcDeleteCondition...) + + return deleteConditionalActions, nil +} + +// integrationReferencedByEAS returns a slice of ConditionalActions to use with a backend.AtomicWrite to ensure that // integration [name] is not referenced by any EAS (External Audit Storage) integration. -func notReferencedByEAS(ctx context.Context, bk backend.Backend, name string) ([]backend.ConditionalAction, error) { +func integrationReferencedByEAS(ctx context.Context, bk backend.Backend, name string) ([]backend.ConditionalAction, error) { var conditionalActions []backend.ConditionalAction for _, key := range []backend.Key{draftExternalAuditStorageBackendKey, clusterExternalAuditStorageBackendKey} { condition := backend.ConditionalAction{ @@ -173,6 +195,42 @@ func notReferencedByEAS(ctx context.Context, bk backend.Backend, name string) ([ return conditionalActions, nil } +// integrationReferencedByAWSICPlugin returns an error if the integration name is referenced +// by an existing AWS Identity Center plugin. In case the AWS Identity Center plugin exists +// but does not reference this integration, a conditional action is returned with a revision +// of the plugin to ensure that plugin hasn't changed during deletion of the AWS OIDC integration. +func integrationReferencedByAWSICPlugin(ctx context.Context, bk backend.Backend, name string) ([]backend.ConditionalAction, error) { + var conditionalActions []backend.ConditionalAction + pluginService := NewPluginsService(bk) + plugins, err := pluginService.GetPlugins(ctx, false) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, p := range plugins { + pluginV1, ok := p.(*types.PluginV1) + if !ok { + continue + } + + if pluginV1.GetType() == types.PluginType(types.PluginTypeAWSIdentityCenter) { + switch pluginV1.Spec.GetAwsIc().IntegrationName { + case name: + return nil, trace.BadParameter("cannot delete AWS OIDC integration currently referenced by AWS Identity Center integration %q", pluginV1.GetName()) + default: + conditionalActions = append(conditionalActions, backend.ConditionalAction{ + Key: backend.NewKey(pluginsPrefix, name), + Action: backend.Nop(), + Condition: backend.Revision(pluginV1.GetRevision()), + }) + return conditionalActions, nil + } + } + } + + return conditionalActions, nil +} + // DeleteAllIntegrations removes all Integration resources. This should only be used in a cache. func (s *IntegrationsService) DeleteAllIntegrations(ctx context.Context) error { if !s.cacheMode {