diff --git a/api/types/constants.go b/api/types/constants.go
index 85dbe7c140388..36e2571891067 100644
--- a/api/types/constants.go
+++ b/api/types/constants.go
@@ -564,6 +564,20 @@ const (
// KindStaticHostUser is a host user to be created on matching SSH nodes.
KindStaticHostUser = "static_host_user"
+ // KindIdentityCenterAccount describes an Identity-Center managed AWS Account
+ KindIdentityCenterAccount = "aws_ic_account"
+
+ // KindIdentityCenterPermissionSet describes an AWS Identity Center Permission Set
+ KindIdentityCenterPermissionSet = "aws_ic_permission_set"
+
+ // KindIdentityCenterPermissionSet describes an AWS Principal Assignment, representing
+ // a collection Account Assignments assigned to a Teleport User or AccessList
+ KindIdentityCenterPrincipalAssignment = "aws_ic_principal_assignment"
+
+ // KindIdentityCenterAccountAssignment describes an AWS Account and Permission Set
+ // pair that can be requested by a Teleport User.
+ KindIdentityCenterAccountAssignment = "aws_ic_account_assignment"
+
// MetaNameAccessGraphSettings is the exact name of the singleton resource holding
// access graph settings.
MetaNameAccessGraphSettings = "access-graph-settings"
diff --git a/lib/auth/accesspoint/accesspoint.go b/lib/auth/accesspoint/accesspoint.go
index 158243d126550..3cf7c1d2e86aa 100644
--- a/lib/auth/accesspoint/accesspoint.go
+++ b/lib/auth/accesspoint/accesspoint.go
@@ -106,6 +106,7 @@ type Config struct {
WindowsDesktops services.WindowsDesktops
AutoUpdateService services.AutoUpdateServiceGetter
ProvisioningStates services.ProvisioningStates
+ IdentityCenter services.IdentityCenter
}
func (c *Config) CheckAndSetDefaults() error {
diff --git a/lib/auth/auth.go b/lib/auth/auth.go
index ffffea18d4e72..232c7587f9c6c 100644
--- a/lib/auth/auth.go
+++ b/lib/auth/auth.go
@@ -347,7 +347,14 @@ func NewServer(cfg *InitConfig, opts ...ServerOption) (*Server, error) {
if cfg.ProvisioningStates == nil {
cfg.ProvisioningStates, err = local.NewProvisioningStateService(cfg.Backend)
if err != nil {
- return nil, trace.Wrap(err, "Creating provisioning state service")
+ return nil, trace.Wrap(err)
+ }
+ }
+ if cfg.IdentityCenter == nil {
+ svcCfg := local.IdentityCenterServiceConfig{Backend: cfg.Backend}
+ cfg.IdentityCenter, err = local.NewIdentityCenterService(svcCfg)
+ if err != nil {
+ return nil, trace.Wrap(err)
}
}
if cfg.CloudClients == nil {
@@ -676,6 +683,7 @@ type Services struct {
services.StaticHostUser
services.AutoUpdateService
services.ProvisioningStates
+ services.IdentityCenter
}
// GetWebSession returns existing web session described by req.
diff --git a/lib/auth/helpers.go b/lib/auth/helpers.go
index 227d9568699de..851cca043ad92 100644
--- a/lib/auth/helpers.go
+++ b/lib/auth/helpers.go
@@ -342,6 +342,7 @@ func NewTestAuthServer(cfg TestAuthServerConfig) (*TestAuthServer, error) {
DiscoveryConfigs: svces.DiscoveryConfigs,
DynamicAccess: svces.DynamicAccessExt,
Events: svces.Events,
+ IdentityCenter: svces.IdentityCenter,
Integrations: svces.Integrations,
KubeWaitingContainers: svces.KubeWaitingContainer,
Kubernetes: svces.Kubernetes,
diff --git a/lib/auth/init.go b/lib/auth/init.go
index 9ac7abc541db5..28a94a6e39546 100644
--- a/lib/auth/init.go
+++ b/lib/auth/init.go
@@ -328,6 +328,10 @@ type InitConfig struct {
// Logger is the logger instance for the auth service to use.
Logger *slog.Logger
+
+ // IdentityCenter is the Identity Center state storage service to use in
+ // this node.
+ IdentityCenter services.IdentityCenter
}
// Init instantiates and configures an instance of AuthServer
diff --git a/lib/services/identitycenter.go b/lib/services/identitycenter.go
new file mode 100644
index 0000000000000..d3fbf6aca9756
--- /dev/null
+++ b/lib/services/identitycenter.go
@@ -0,0 +1,220 @@
+// Teleport
+// Copyright (C) 2024 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package services
+
+import (
+ "context"
+
+ "google.golang.org/protobuf/proto"
+
+ identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
+ "github.com/gravitational/teleport/lib/utils/pagination"
+)
+
+// IdentityCenterAccount wraps a raw identity center record in a new type to
+// allow it to implement the interfaces required for use with the Unified
+// Resource listing.
+//
+// IdentityCenterAccount simply wraps a pointer to the underlying
+// identitycenterv1.Account record, and can be treated as a reference-like type.
+// Copies of an IdentityCenterAccount will point to the same record.
+type IdentityCenterAccount struct {
+ // This wrapper needs to:
+ // - implement the interfaces required for use with the Unified Resource
+ // service.
+ // - expose the existing interfaces & methods on the underlying
+ // identitycenterv1.Account
+ // - avoid copying the underlying identitycenterv1.Account due to embedded
+ // mutexes in the protobuf-generated code
+ //
+ // Given those requirements, storing an embedded pointer seems to be the
+ // least-bad approach.
+
+ *identitycenterv1.Account
+}
+
+// CloneResource creates a deep copy of the underlying account resource
+func (a IdentityCenterAccount) CloneResource() IdentityCenterAccount {
+ return IdentityCenterAccount{
+ Account: proto.Clone(a.Account).(*identitycenterv1.Account),
+ }
+}
+
+// IdentityCenterAccountID is a strongly-typed Identity Center account ID.
+type IdentityCenterAccountID string
+
+// IdentityCenterAccountGetter provides read-only access to Identity Center
+// Account records
+type IdentityCenterAccountGetter interface {
+ // ListIdentityCenterAccounts provides a paged list of all known identity
+ // center accounts
+ ListIdentityCenterAccounts(context.Context, int, *pagination.PageRequestToken) ([]IdentityCenterAccount, pagination.NextPageToken, error)
+
+ // GetIdentityCenterAccount fetches a specific Identity Center Account
+ GetIdentityCenterAccount(context.Context, IdentityCenterAccountID) (IdentityCenterAccount, error)
+}
+
+// IdentityCenterAccounts defines read/write access to Identity Center account
+// resources
+type IdentityCenterAccounts interface {
+ IdentityCenterAccountGetter
+
+ // CreateIdentityCenterAccount creates a new Identity Center Account record
+ CreateIdentityCenterAccount(context.Context, IdentityCenterAccount) (IdentityCenterAccount, error)
+
+ // UpdateIdentityCenterAccount performs a conditional update on an Identity
+ // Center Account record, returning the updated record on success.
+ UpdateIdentityCenterAccount(context.Context, IdentityCenterAccount) (IdentityCenterAccount, error)
+
+ // UpsertIdentityCenterAccount performs an *unconditional* upsert on an
+ // Identity Center Account record, returning the updated record on success.
+ // Be careful when mixing UpsertIdentityCenterAccount() with resources
+ // protected by optimistic locking
+ UpsertIdentityCenterAccount(context.Context, IdentityCenterAccount) (IdentityCenterAccount, error)
+
+ // DeleteIdentityCenterAccount deletes an Identity Center Account record
+ DeleteIdentityCenterAccount(context.Context, IdentityCenterAccountID) error
+
+ // DeleteAllIdentityCenterAccounts deletes all Identity Center Account records
+ DeleteAllIdentityCenterAccounts(context.Context) error
+}
+
+// PrincipalAssignmentID is a strongly-typed ID for Identity Center Principal
+// Assignments
+type PrincipalAssignmentID string
+
+// IdentityCenterPrincipalAssignments defines operations on an Identity Center
+// principal assignment database
+type IdentityCenterPrincipalAssignments interface {
+ // ListPrincipalAssignments lists all PrincipalAssignment records in the
+ // service
+ ListPrincipalAssignments(context.Context, int, *pagination.PageRequestToken) ([]*identitycenterv1.PrincipalAssignment, pagination.NextPageToken, error)
+
+ // CreatePrincipalAssignment creates a new Principal Assignment record in
+ // the service from the supplied in-memory representation. Returns the
+ // created record on success.
+ CreatePrincipalAssignment(context.Context, *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error)
+
+ // GetPrincipalAssignment fetches a specific Principal Assignment record.
+ GetPrincipalAssignment(context.Context, PrincipalAssignmentID) (*identitycenterv1.PrincipalAssignment, error)
+
+ // UpdatePrincipalAssignment performs a conditional update on a Principal
+ // Assignment record
+ UpdatePrincipalAssignment(context.Context, *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error)
+
+ // UpsertPrincipalAssignment performs an unconditional update on a Principal
+ // Assignment record
+ UpsertPrincipalAssignment(context.Context, *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error)
+
+ // DeletePrincipalAssignment deletes a specific principal assignment record
+ DeletePrincipalAssignment(context.Context, PrincipalAssignmentID) error
+
+ // DeleteAllPrincipalAssignments deletes all assignment record
+ DeleteAllPrincipalAssignments(context.Context) error
+}
+
+// PermissionSetID is a strongly typed ID for an identitycenterv1.PermissionSet
+type PermissionSetID string
+
+// IdentityCenterPermissionSets defines the operations to create and maintain
+// identitycenterv1.PermissionSet records in the service.
+type IdentityCenterPermissionSets interface {
+ // ListPermissionSets list the known Permission Sets
+ ListPermissionSets(context.Context, int, *pagination.PageRequestToken) ([]*identitycenterv1.PermissionSet, pagination.NextPageToken, error)
+
+ // CreatePermissionSet creates a new PermissionSet record based on the
+ // supplied in-memory representation, returning the created record on
+ // success
+ CreatePermissionSet(context.Context, *identitycenterv1.PermissionSet) (*identitycenterv1.PermissionSet, error)
+
+ // GetPermissionSet fetches a specific PermissionSet record
+ GetPermissionSet(context.Context, PermissionSetID) (*identitycenterv1.PermissionSet, error)
+
+ // UpdatePermissionSet performs a conditional update on the supplied Identity
+ // Center Permission Set
+ UpdatePermissionSet(context.Context, *identitycenterv1.PermissionSet) (*identitycenterv1.PermissionSet, error)
+
+ // DeletePermissionSet deletes a specific Identity Center PermissionSet
+ DeletePermissionSet(context.Context, PermissionSetID) error
+}
+
+// IdentityCenterAccountAssignment wraps a raw identitycenterv1.AccountAssignment
+// record in a new type to allow it to implement the interfaces required for use
+// with the Unified Resource listing. IdentityCenterAccountAssignment simply
+// wraps a pointer to the underlying account record, and can be treated as a
+// reference-like type.
+//
+// Copies of an IdentityCenterAccountAssignment will point to the same record.
+type IdentityCenterAccountAssignment struct {
+ // This wrapper needs to:
+ // - implement the interfaces required for use with the Unified Resource
+ // service.
+ // - expose the existing interfaces & methods on the underlying
+ // identitycenterv1.AccountAssignment
+ // - avoid copying the underlying identitycenterv1.AccountAssignment due to
+ // embedded mutexes in the protobuf-generated code
+ //
+ // Given those requirements, storing an embedded pointer seems to be the
+ // least-bad approach.
+
+ *identitycenterv1.AccountAssignment
+}
+
+// CloneResource creates a deep copy of the underlying account resource
+func (a IdentityCenterAccountAssignment) CloneResource() IdentityCenterAccountAssignment {
+ return IdentityCenterAccountAssignment{
+ AccountAssignment: proto.Clone(a.AccountAssignment).(*identitycenterv1.AccountAssignment),
+ }
+}
+
+// IdentityCenterAccountAssignmentID is a strongly typed ID for an
+// IdentityCenterAccountAssignment
+type IdentityCenterAccountAssignmentID string
+
+// IdentityCenterAccountAssignments defines the operations to create and maintain
+// Identity Center account assignment records in the service.
+type IdentityCenterAccountAssignments interface {
+ // ListAccountAssignments lists all IdentityCenterAccountAssignment record
+ // known to the service
+ ListAccountAssignments(context.Context, int, *pagination.PageRequestToken) ([]IdentityCenterAccountAssignment, pagination.NextPageToken, error)
+
+ // CreateAccountAssignment creates a new Account Assignment record in
+ // the service from the supplied in-memory representation. Returns the
+ // created record on success.
+ CreateAccountAssignment(context.Context, IdentityCenterAccountAssignment) (IdentityCenterAccountAssignment, error)
+
+ // GetAccountAssignment fetches a specific Account Assignment record.
+ GetAccountAssignment(context.Context, IdentityCenterAccountAssignmentID) (IdentityCenterAccountAssignment, error)
+
+ // UpdateAccountAssignment performs a conditional update on the supplied
+ // Account Assignment, returning the updated record on success.
+ UpdateAccountAssignment(context.Context, IdentityCenterAccountAssignment) (IdentityCenterAccountAssignment, error)
+
+ // DeleteAccountAssignment deletes a specific account assignment
+ DeleteAccountAssignment(context.Context, IdentityCenterAccountAssignmentID) error
+
+ // DeleteAllAccountAssignments deletes all known account assignments
+ DeleteAllAccountAssignments(context.Context) error
+}
+
+// IdentityCenter combines all the resource managers used by the Identity Center plugin
+type IdentityCenter interface {
+ IdentityCenterAccounts
+ IdentityCenterPermissionSets
+ IdentityCenterPrincipalAssignments
+ IdentityCenterAccountAssignments
+}
diff --git a/lib/services/identitycenter_test.go b/lib/services/identitycenter_test.go
new file mode 100644
index 0000000000000..5cbc87493feed
--- /dev/null
+++ b/lib/services/identitycenter_test.go
@@ -0,0 +1,97 @@
+// Teleport
+// Copyright (C) 2024 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package services
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
+ identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
+ "github.com/gravitational/teleport/api/types"
+)
+
+func TestIdentityCenterAccountClone(t *testing.T) {
+ // GIVEN an Account Record
+ src := IdentityCenterAccount{
+ Account: &identitycenterv1.Account{
+ Kind: types.KindIdentityCenterAccount,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{Name: "some-account"},
+ Spec: &identitycenterv1.AccountSpec{
+ Id: "aws-account-id",
+ Arn: "arn:aws:sso::account-id:",
+ Description: "Test account",
+ PermissionSetInfo: []*identitycenterv1.PermissionSetInfo{
+ {
+ Name: "original value",
+ Arn: "arn:aws:sso:::permissionSet/ic-instance/ps-instance",
+ },
+ },
+ },
+ },
+ }
+
+ // WHEN I clone the resource
+ dst := src.CloneResource()
+
+ // EXPECT that the resulting clone compares equally
+ require.Equal(t, src, dst)
+
+ // WHEN I modify the source object in a way that would be shared with a
+ // shallow copy
+ src.Spec.PermissionSetInfo[0].Name = "some new value"
+
+ // EXPECT that the cloned object DOES NOT inherit the update
+ require.NotEqual(t, src, dst)
+ require.Equal(t, "original value", dst.Spec.PermissionSetInfo[0].Name)
+}
+
+func TestIdentityCenterAccountAssignmentClone(t *testing.T) {
+ // GIVEN an Account Assignment Record
+ src := IdentityCenterAccountAssignment{
+ AccountAssignment: &identitycenterv1.AccountAssignment{
+ Kind: types.KindIdentityCenterAccountAssignment,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{Name: "u-test@example.com"},
+ Spec: &identitycenterv1.AccountAssignmentSpec{
+ Display: "Some-Permission-set on Some-AWS-account",
+ PermissionSet: &identitycenterv1.PermissionSetInfo{
+ Arn: "arn:aws:sso:::permissionSet/ic-instance/ps-instance",
+ Name: "original name",
+ },
+ AccountName: "Some Account Name",
+ AccountId: "some account id",
+ },
+ },
+ }
+
+ // WHEN I clone the resource
+ dst := src.CloneResource()
+
+ // EXPECT that the resulting clone compares equally
+ require.Equal(t, src, dst)
+
+ // WHEN I modify the source object in a way that would be shared with a
+ // shallow copy
+ src.Spec.PermissionSet.Name = "some new name"
+
+ // EXPECT that the cloned object DOES NOT inherit the update
+ require.NotEqual(t, src, dst)
+ require.Equal(t, "original name", dst.Spec.PermissionSet.Name)
+}
diff --git a/lib/services/local/events_test.go b/lib/services/local/events_test.go
index a57f07aadf2de..c4d7c91ed4e26 100644
--- a/lib/services/local/events_test.go
+++ b/lib/services/local/events_test.go
@@ -49,7 +49,8 @@ func fetchEvent(t *testing.T, w types.Watcher, timeout time.Duration) types.Even
return ev
}
-func testContext(t *testing.T) context.Context {
+func newTestContext(t *testing.T) context.Context {
+ t.Helper()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
return ctx
@@ -209,7 +210,7 @@ func TestWatchers(t *testing.T) {
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
- ctx := testContext(t)
+ ctx := newTestContext(t)
// GIVEN an empty back-end
clock := clockwork.NewFakeClock()
diff --git a/lib/services/local/identitycenter.go b/lib/services/local/identitycenter.go
new file mode 100644
index 0000000000000..a24e433ef3b84
--- /dev/null
+++ b/lib/services/local/identitycenter.go
@@ -0,0 +1,399 @@
+// Teleport
+// Copyright (C) 2024 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package local
+
+import (
+ "context"
+ "log/slog"
+
+ "github.com/gravitational/trace"
+
+ "github.com/gravitational/teleport"
+ identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/backend"
+ "github.com/gravitational/teleport/lib/services"
+ "github.com/gravitational/teleport/lib/services/local/generic"
+ "github.com/gravitational/teleport/lib/utils/pagination"
+)
+
+const (
+ identityCenterPageSize = 100
+)
+
+const (
+ awsResourcePrefix = "identity_center"
+ awsAccountPrefix = "accounts"
+ awsPermissionSetPrefix = "permission_sets"
+ awsPrincipalAssignmentPrefix = "principal_assignments"
+ awsAccountAssignmentPrefix = "account_assignments"
+)
+
+// IdentityCenterServiceConfig provides configuration parameters for an
+// IdentityCenterService
+type IdentityCenterServiceConfig struct {
+ // Backend is the storage backend to use for the service
+ Backend backend.Backend
+
+ // Logger is the logger for the service to use. A default will be supplied
+ // if not specified.
+ Logger *slog.Logger
+}
+
+// CheckAndSetDefaults validates the cfg and supplies defaults where
+// appropriate.
+func (cfg *IdentityCenterServiceConfig) CheckAndSetDefaults() error {
+ if cfg.Backend == nil {
+ return trace.BadParameter("must supply backend")
+ }
+
+ if cfg.Logger == nil {
+ cfg.Logger = slog.Default().With(teleport.ComponentKey, "AWS-IC-LOCAL")
+ }
+
+ return nil
+}
+
+// IdentityCenterService handles low-level CRUD operations for the identity-
+// center related resources
+type IdentityCenterService struct {
+ accounts *generic.ServiceWrapper[*identitycenterv1.Account]
+ permissionSets *generic.ServiceWrapper[*identitycenterv1.PermissionSet]
+ principalAssignments *generic.ServiceWrapper[*identitycenterv1.PrincipalAssignment]
+ accountAssignments *generic.ServiceWrapper[*identitycenterv1.AccountAssignment]
+}
+
+// compile-time assertion that the IdentityCenterService implements the
+// services.IdentityCenter interface
+var _ services.IdentityCenter = (*IdentityCenterService)(nil)
+
+// NewIdentityCenterService creates a new service for managing identity-center
+// related resources
+func NewIdentityCenterService(cfg IdentityCenterServiceConfig) (*IdentityCenterService, error) {
+ if err := cfg.CheckAndSetDefaults(); err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ accountsSvc, err := generic.NewServiceWrapper(generic.ServiceWrapperConfig[*identitycenterv1.Account]{
+ Backend: cfg.Backend,
+ ResourceKind: types.KindIdentityCenterAccount,
+ BackendPrefix: backend.NewKey(awsResourcePrefix, awsAccountPrefix),
+ MarshalFunc: services.MarshalProtoResource[*identitycenterv1.Account],
+ UnmarshalFunc: services.UnmarshalProtoResource[*identitycenterv1.Account],
+ })
+ if err != nil {
+ return nil, trace.Wrap(err, "creating accounts service")
+ }
+
+ permissionSetSvc, err := generic.NewServiceWrapper(generic.ServiceWrapperConfig[*identitycenterv1.PermissionSet]{
+ Backend: cfg.Backend,
+ ResourceKind: types.KindIdentityCenterPermissionSet,
+ BackendPrefix: backend.NewKey(awsResourcePrefix, awsPermissionSetPrefix),
+ MarshalFunc: services.MarshalProtoResource[*identitycenterv1.PermissionSet],
+ UnmarshalFunc: services.UnmarshalProtoResource[*identitycenterv1.PermissionSet],
+ })
+ if err != nil {
+ return nil, trace.Wrap(err, "creating permission sets service")
+ }
+
+ principalsSvc, err := generic.NewServiceWrapper(generic.ServiceWrapperConfig[*identitycenterv1.PrincipalAssignment]{
+ Backend: cfg.Backend,
+ ResourceKind: types.KindIdentityCenterPrincipalAssignment,
+ BackendPrefix: backend.NewKey(awsResourcePrefix, awsPrincipalAssignmentPrefix),
+ MarshalFunc: services.MarshalProtoResource[*identitycenterv1.PrincipalAssignment],
+ UnmarshalFunc: services.UnmarshalProtoResource[*identitycenterv1.PrincipalAssignment],
+ })
+ if err != nil {
+ return nil, trace.Wrap(err, "creating principal assignments service")
+ }
+
+ accountAssignmentsSvc, err := generic.NewServiceWrapper(generic.ServiceWrapperConfig[*identitycenterv1.AccountAssignment]{
+ Backend: cfg.Backend,
+ ResourceKind: types.KindIdentityCenterAccountAssignment,
+ BackendPrefix: backend.NewKey(awsResourcePrefix, awsAccountAssignmentPrefix),
+ MarshalFunc: services.MarshalProtoResource[*identitycenterv1.AccountAssignment],
+ UnmarshalFunc: services.UnmarshalProtoResource[*identitycenterv1.AccountAssignment],
+ })
+ if err != nil {
+ return nil, trace.Wrap(err, "creating account assignments service")
+ }
+
+ svc := &IdentityCenterService{
+ accounts: accountsSvc,
+ permissionSets: permissionSetSvc,
+ principalAssignments: principalsSvc,
+ accountAssignments: accountAssignmentsSvc,
+ }
+
+ return svc, nil
+}
+
+// ListIdentityCenterAccounts provides a paged list of all AWS accounts known
+// to the Identity Center integration
+func (svc *IdentityCenterService) ListIdentityCenterAccounts(ctx context.Context, pageSize int, page *pagination.PageRequestToken) ([]services.IdentityCenterAccount, pagination.NextPageToken, error) {
+ if pageSize == 0 {
+ pageSize = identityCenterPageSize
+ }
+
+ pageToken, err := page.Consume()
+ if err != nil {
+ return nil, "", trace.Wrap(err, "listing identity center assignment records")
+ }
+
+ accounts, nextPage, err := svc.accounts.ListResources(ctx, pageSize, pageToken)
+ if err != nil {
+ return nil, "", trace.Wrap(err, "listing identity center assignment records")
+ }
+
+ result := make([]services.IdentityCenterAccount, len(accounts))
+ for i, acct := range accounts {
+ result[i] = services.IdentityCenterAccount{Account: acct}
+ }
+
+ return result, pagination.NextPageToken(nextPage), nil
+}
+
+// CreateIdentityCenterAccount creates a new Identity Center Account record
+func (svc *IdentityCenterService) CreateIdentityCenterAccount(ctx context.Context, acct services.IdentityCenterAccount) (services.IdentityCenterAccount, error) {
+ created, err := svc.accounts.CreateResource(ctx, acct.Account)
+ if err != nil {
+ return services.IdentityCenterAccount{}, trace.Wrap(err, "creating identity center account")
+ }
+ return services.IdentityCenterAccount{Account: created}, nil
+}
+
+// GetIdentityCenterAccount fetches a specific Identity Center Account
+func (svc *IdentityCenterService) GetIdentityCenterAccount(ctx context.Context, name services.IdentityCenterAccountID) (services.IdentityCenterAccount, error) {
+ acct, err := svc.accounts.GetResource(ctx, string(name))
+ if err != nil {
+ return services.IdentityCenterAccount{}, trace.Wrap(err, "fetching identity center account")
+ }
+ return services.IdentityCenterAccount{Account: acct}, nil
+}
+
+// UpdateIdentityCenterAccount performs a conditional update on an Identity
+// Center Account record, returning the updated record on success.
+func (svc *IdentityCenterService) UpdateIdentityCenterAccount(ctx context.Context, acct services.IdentityCenterAccount) (services.IdentityCenterAccount, error) {
+ updated, err := svc.accounts.ConditionalUpdateResource(ctx, acct.Account)
+ if err != nil {
+ return services.IdentityCenterAccount{}, trace.Wrap(err, "updating identity center account record")
+ }
+ return services.IdentityCenterAccount{Account: updated}, nil
+}
+
+// UpsertIdentityCenterAccount performs an *unconditional* upsert on an
+// Identity Center Account record, returning the updated record on success.
+// Be careful when mixing UpsertIdentityCenterAccount() with resources
+// protected by optimistic locking
+func (svc *IdentityCenterService) UpsertIdentityCenterAccount(ctx context.Context, acct services.IdentityCenterAccount) (services.IdentityCenterAccount, error) {
+ updated, err := svc.accounts.UpsertResource(ctx, acct.Account)
+ if err != nil {
+ return services.IdentityCenterAccount{}, trace.Wrap(err, "upserting identity center account record")
+ }
+ return services.IdentityCenterAccount{Account: updated}, nil
+}
+
+// DeleteIdentityCenterAccount deletes an Identity Center Account record
+func (svc *IdentityCenterService) DeleteIdentityCenterAccount(ctx context.Context, name services.IdentityCenterAccountID) error {
+ return trace.Wrap(svc.accounts.DeleteResource(ctx, string(name)))
+}
+
+// DeleteAllIdentityCenterAccounts deletes all Identity Center Account records
+func (svc *IdentityCenterService) DeleteAllIdentityCenterAccounts(ctx context.Context) error {
+ return trace.Wrap(svc.accounts.DeleteAllResources(ctx))
+}
+
+// ListPrincipalAssignments lists all PrincipalAssignment records in the service
+func (svc *IdentityCenterService) ListPrincipalAssignments(ctx context.Context, pageSize int, page *pagination.PageRequestToken) ([]*identitycenterv1.PrincipalAssignment, pagination.NextPageToken, error) {
+ if pageSize == 0 {
+ pageSize = identityCenterPageSize
+ }
+
+ pageToken, err := page.Consume()
+ if err != nil {
+ return nil, "", trace.Wrap(err, "extracting page token")
+ }
+
+ resp, nextPage, err := svc.principalAssignments.ListResources(ctx, pageSize, pageToken)
+ if err != nil {
+ return nil, "", trace.Wrap(err, "listing identity center assignment records")
+ }
+ return resp, pagination.NextPageToken(nextPage), nil
+}
+
+// CreatePrincipalAssignment creates a new Principal Assignment record in the
+// service from the supplied in-memory representation. Returns the created
+// record on success.
+func (svc *IdentityCenterService) CreatePrincipalAssignment(ctx context.Context, asmt *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) {
+ created, err := svc.principalAssignments.CreateResource(ctx, asmt)
+ if err != nil {
+ return nil, trace.Wrap(err, "creating principal assignment")
+ }
+ return created, nil
+}
+
+// GetPrincipalAssignment fetches a specific Principal Assignment record.
+func (svc *IdentityCenterService) GetPrincipalAssignment(ctx context.Context, name services.PrincipalAssignmentID) (*identitycenterv1.PrincipalAssignment, error) {
+ state, err := svc.principalAssignments.GetResource(ctx, string(name))
+ if err != nil {
+ return nil, trace.Wrap(err, "fetching principal assignment")
+ }
+ return state, nil
+}
+
+// UpdatePrincipalAssignment performs a conditional update on a Principal
+// Assignment record
+func (svc *IdentityCenterService) UpdatePrincipalAssignment(ctx context.Context, asmt *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) {
+ updated, err := svc.principalAssignments.ConditionalUpdateResource(ctx, asmt)
+ if err != nil {
+ return nil, trace.Wrap(err, "updating principal assignment record")
+ }
+ return updated, nil
+}
+
+// UpsertPrincipalAssignment performs an unconditional update on a Principal
+// Assignment record
+func (svc *IdentityCenterService) UpsertPrincipalAssignment(ctx context.Context, asmt *identitycenterv1.PrincipalAssignment) (*identitycenterv1.PrincipalAssignment, error) {
+ updated, err := svc.principalAssignments.UpsertResource(ctx, asmt)
+ if err != nil {
+ return nil, trace.Wrap(err, "upserting principal assignment record")
+ }
+ return updated, nil
+}
+
+// DeletePrincipalAssignment deletes a specific principal assignment record
+func (svc *IdentityCenterService) DeletePrincipalAssignment(ctx context.Context, name services.PrincipalAssignmentID) error {
+ return trace.Wrap(svc.principalAssignments.DeleteResource(ctx, string(name)))
+}
+
+// DeleteAllPrincipalAssignments deletes all assignment record
+func (svc *IdentityCenterService) DeleteAllPrincipalAssignments(ctx context.Context) error {
+ return trace.Wrap(svc.principalAssignments.DeleteAllResources(ctx))
+}
+
+// ListPermissionSets list the known Permission Sets in the managed Identity Center
+func (svc *IdentityCenterService) ListPermissionSets(ctx context.Context, pageSize int, page *pagination.PageRequestToken) ([]*identitycenterv1.PermissionSet, pagination.NextPageToken, error) {
+ if pageSize == 0 {
+ pageSize = identityCenterPageSize
+ }
+ pageToken, err := page.Consume()
+ if err != nil {
+ return nil, "", trace.Wrap(err, "extracting page token")
+ }
+ resp, nextPage, err := svc.permissionSets.ListResources(ctx, pageSize, pageToken)
+ if err != nil {
+ return nil, "", trace.Wrap(err, "listing identity center permission set records")
+ }
+ return resp, pagination.NextPageToken(nextPage), nil
+}
+
+// CreatePermissionSet creates a new PermissionSet record based on the supplied
+// in-memory representation, returning the created record on success.
+func (svc *IdentityCenterService) CreatePermissionSet(ctx context.Context, asmt *identitycenterv1.PermissionSet) (*identitycenterv1.PermissionSet, error) {
+ created, err := svc.permissionSets.CreateResource(ctx, asmt)
+ if err != nil {
+ return nil, trace.Wrap(err, "creating identity center permission set")
+ }
+ return created, nil
+}
+
+// GetPermissionSet fetches a specific PermissionSet record
+func (svc *IdentityCenterService) GetPermissionSet(ctx context.Context, name services.PermissionSetID) (*identitycenterv1.PermissionSet, error) {
+ state, err := svc.permissionSets.GetResource(ctx, string(name))
+ if err != nil {
+ return nil, trace.Wrap(err, "fetching permission set")
+ }
+ return state, nil
+}
+
+// UpdatePermissionSet performs a conditional update on the supplied Identity
+// Center Permission Set
+func (svc *IdentityCenterService) UpdatePermissionSet(ctx context.Context, asmt *identitycenterv1.PermissionSet) (*identitycenterv1.PermissionSet, error) {
+ updated, err := svc.permissionSets.ConditionalUpdateResource(ctx, asmt)
+ if err != nil {
+ return nil, trace.Wrap(err, "updating permission set record")
+ }
+ return updated, nil
+}
+
+// DeletePermissionSet deletes a specific Identity Center PermissionSet
+func (svc *IdentityCenterService) DeletePermissionSet(ctx context.Context, name services.PermissionSetID) error {
+ return trace.Wrap(svc.permissionSets.DeleteResource(ctx, string(name)))
+}
+
+// ListAccountAssignments lists all IdentityCenterAccountAssignment record
+// known to the service
+func (svc *IdentityCenterService) ListAccountAssignments(ctx context.Context, pageSize int, page *pagination.PageRequestToken) ([]services.IdentityCenterAccountAssignment, pagination.NextPageToken, error) {
+ if pageSize == 0 {
+ pageSize = identityCenterPageSize
+ }
+ pageToken, err := page.Consume()
+ if err != nil {
+ return nil, "", trace.Wrap(err, "extracting page token")
+ }
+ assignments, nextPage, err := svc.accountAssignments.ListResources(ctx, pageSize, pageToken)
+ if err != nil {
+ return nil, "", trace.Wrap(err, "listing identity center assignment records")
+ }
+
+ result := make([]services.IdentityCenterAccountAssignment, len(assignments))
+ for i, asmt := range assignments {
+ result[i] = services.IdentityCenterAccountAssignment{AccountAssignment: asmt}
+ }
+
+ return result, pagination.NextPageToken(nextPage), nil
+}
+
+// CreateAccountAssignment creates a new Account Assignment record in
+// the service from the supplied in-memory representation. Returns the
+// created record on success.
+func (svc *IdentityCenterService) CreateAccountAssignment(ctx context.Context, asmt services.IdentityCenterAccountAssignment) (services.IdentityCenterAccountAssignment, error) {
+ created, err := svc.accountAssignments.CreateResource(ctx, asmt.AccountAssignment)
+ if err != nil {
+ return services.IdentityCenterAccountAssignment{}, trace.Wrap(err, "creating principal assignment")
+ }
+ return services.IdentityCenterAccountAssignment{AccountAssignment: created}, nil
+}
+
+// GetAccountAssignment fetches a specific Account Assignment record.
+func (svc *IdentityCenterService) GetAccountAssignment(ctx context.Context, name services.IdentityCenterAccountAssignmentID) (services.IdentityCenterAccountAssignment, error) {
+ asmt, err := svc.accountAssignments.GetResource(ctx, string(name))
+ if err != nil {
+ return services.IdentityCenterAccountAssignment{}, trace.Wrap(err, "fetching principal assignment")
+ }
+ return services.IdentityCenterAccountAssignment{AccountAssignment: asmt}, nil
+}
+
+// UpdateAccountAssignment performs a conditional update on the supplied
+// Account Assignment, returning the updated record on success.
+func (svc *IdentityCenterService) UpdateAccountAssignment(ctx context.Context, asmt services.IdentityCenterAccountAssignment) (services.IdentityCenterAccountAssignment, error) {
+ updated, err := svc.accountAssignments.ConditionalUpdateResource(ctx, asmt.AccountAssignment)
+ if err != nil {
+ return services.IdentityCenterAccountAssignment{}, trace.Wrap(err, "updating principal assignment record")
+ }
+ return services.IdentityCenterAccountAssignment{AccountAssignment: updated}, nil
+}
+
+// DeleteAccountAssignment deletes a specific account assignment
+func (svc *IdentityCenterService) DeleteAccountAssignment(ctx context.Context, name services.IdentityCenterAccountAssignmentID) error {
+ return trace.Wrap(svc.accountAssignments.DeleteResource(ctx, string(name)))
+}
+
+// DeleteAllAccountAssignments deletes all known account assignments
+func (svc *IdentityCenterService) DeleteAllAccountAssignments(ctx context.Context) error {
+ return trace.Wrap(svc.accountAssignments.DeleteAllResources(ctx))
+}
diff --git a/lib/services/local/identitycenter_test.go b/lib/services/local/identitycenter_test.go
new file mode 100644
index 0000000000000..a15c2a51f323b
--- /dev/null
+++ b/lib/services/local/identitycenter_test.go
@@ -0,0 +1,298 @@
+// Teleport
+// Copyright (C) 2024 Gravitational, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package local
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/gravitational/trace"
+ "github.com/jonboulle/clockwork"
+ "github.com/stretchr/testify/require"
+
+ headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
+ identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/backend"
+ "github.com/gravitational/teleport/lib/backend/lite"
+ "github.com/gravitational/teleport/lib/services"
+)
+
+func newTestBackend(t *testing.T, ctx context.Context, clock clockwork.Clock) backend.Backend {
+ t.Helper()
+ sqliteBackend, err := lite.NewWithConfig(ctx, lite.Config{
+ Path: t.TempDir(),
+ Clock: clock,
+ })
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, sqliteBackend.Close())
+ })
+ return sqliteBackend
+}
+
+func TestIdentityCenterResourceCRUD(t *testing.T) {
+ t.Parallel()
+
+ const resourceID = "alpha"
+
+ testCases := []struct {
+ name string
+ createResource func(*testing.T, context.Context, services.IdentityCenter, string) types.Resource153
+ getResource func(context.Context, services.IdentityCenter, string) (types.Resource153, error)
+ updateResource func(context.Context, services.IdentityCenter, types.Resource153) (types.Resource153, error)
+ upsertResource func(context.Context, services.IdentityCenter, types.Resource153) (types.Resource153, error)
+ }{
+ {
+ name: "Account",
+ createResource: func(subtestT *testing.T, subtestCtx context.Context, svc services.IdentityCenter, id string) types.Resource153 {
+ return makeTestIdentityCenterAccount(subtestT, subtestCtx, svc, id)
+ },
+ getResource: func(subtestCtx context.Context, svc services.IdentityCenter, id string) (types.Resource153, error) {
+ return svc.GetIdentityCenterAccount(subtestCtx, services.IdentityCenterAccountID(id))
+ },
+ updateResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) {
+ acct := r.(services.IdentityCenterAccount)
+ return svc.UpdateIdentityCenterAccount(subtestCtx, acct)
+ },
+ upsertResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) {
+ acct := r.(services.IdentityCenterAccount)
+ return svc.UpsertIdentityCenterAccount(subtestCtx, acct)
+ },
+ },
+ {
+ name: "PermissionSet",
+ createResource: func(subtestT *testing.T, subtestCtx context.Context, svc services.IdentityCenter, id string) types.Resource153 {
+ return makeTestIdentityCenterPermissionSet(subtestT, subtestCtx, svc, id)
+ },
+ getResource: func(subtestCtx context.Context, svc services.IdentityCenter, id string) (types.Resource153, error) {
+ return svc.GetPermissionSet(subtestCtx, services.PermissionSetID(id))
+ },
+ updateResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) {
+ ps := r.(*identitycenterv1.PermissionSet)
+ return svc.UpdatePermissionSet(subtestCtx, ps)
+ },
+ },
+ {
+ name: "AccountAssignment",
+ createResource: func(subtestT *testing.T, subtestCtx context.Context, svc services.IdentityCenter, id string) types.Resource153 {
+ return makeTestIdentityCenterAccountAssignment(subtestT, subtestCtx, svc, id)
+ },
+ getResource: func(subtestCtx context.Context, svc services.IdentityCenter, id string) (types.Resource153, error) {
+ return svc.GetAccountAssignment(subtestCtx, services.IdentityCenterAccountAssignmentID(id))
+ },
+ updateResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) {
+ asmt := r.(services.IdentityCenterAccountAssignment)
+ return svc.UpdateAccountAssignment(subtestCtx, asmt)
+ },
+ },
+ {
+ name: "PrincipalAssignment",
+ createResource: func(subtestT *testing.T, subtestCtx context.Context, svc services.IdentityCenter, id string) types.Resource153 {
+ return makeTestIdentityCenterPrincipalAssignment(subtestT, subtestCtx, svc, id)
+ },
+ getResource: func(subtestCtx context.Context, svc services.IdentityCenter, id string) (types.Resource153, error) {
+ return svc.GetPrincipalAssignment(subtestCtx, services.PrincipalAssignmentID(id))
+ },
+ updateResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) {
+ asmt := r.(*identitycenterv1.PrincipalAssignment)
+ return svc.UpdatePrincipalAssignment(subtestCtx, asmt)
+ },
+ upsertResource: func(subtestCtx context.Context, svc services.IdentityCenter, r types.Resource153) (types.Resource153, error) {
+ asmt := r.(*identitycenterv1.PrincipalAssignment)
+ return svc.UpsertPrincipalAssignment(subtestCtx, asmt)
+ },
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.name, func(t *testing.T) {
+ t.Run("OptimisticLocking", func(t *testing.T) {
+ const resourceID = "alpha"
+
+ ctx := newTestContext(t)
+ clock := clockwork.NewFakeClock()
+ backend := newTestBackend(t, ctx, clock)
+
+ // GIVEN an IdentityCenter service populated with a resource
+ uut, err := NewIdentityCenterService(IdentityCenterServiceConfig{Backend: backend})
+ require.NoError(t, err)
+ createdResource := test.createResource(t, ctx, uut, resourceID)
+
+ // WHEN I modify the backend record for that resource...
+ tmpResource, err := test.getResource(ctx, uut, resourceID)
+ require.NoError(t, err)
+ tmpResource.GetMetadata().Labels = map[string]string{"update": "1"}
+ updatedResource, err := test.updateResource(ctx, uut, tmpResource)
+ require.NoError(t, err)
+
+ // EXPECT that any attempt to update backend via the original in-memory
+ // version of the resource fails with a comparison error
+ createdResource.GetMetadata().Labels = map[string]string{"update": "2"}
+ _, err = test.updateResource(ctx, uut, createdResource)
+ require.True(t, trace.IsCompareFailed(err), "expected a compare failed error, got %T (%s)", err, err)
+
+ // EXPECT that the backend still reflects the first updated revision,
+ // rather than failed update
+ r, err := test.getResource(ctx, uut, resourceID)
+ require.NoError(t, err)
+ require.Equal(t, "1", r.GetMetadata().Labels["update"])
+
+ // WHEN I attempt update the backend via the latest revision of the
+ // record...
+ updatedResource.GetMetadata().Labels["update"] = "3"
+ _, err = test.updateResource(ctx, uut, updatedResource)
+
+ // EXPECT the update to succeed, and the backend record to have been
+ // updated
+ require.NoError(t, err)
+ r, err = test.getResource(ctx, uut, resourceID)
+ require.NoError(t, err)
+ require.Equal(t, "3", r.GetMetadata().Labels["update"])
+ })
+
+ t.Run("UnconditionalUpsert", func(t *testing.T) {
+ t.Parallel()
+
+ if test.upsertResource == nil {
+ t.Skip(test.name + " does not support unconditional upsert")
+ }
+
+ ctx := newTestContext(t)
+ clock := clockwork.NewFakeClock()
+ backend := newTestBackend(t, ctx, clock)
+
+ // GIVEN an IdentityCenter service populated with a resource
+ uut, err := NewIdentityCenterService(IdentityCenterServiceConfig{Backend: backend})
+ require.NoError(t, err)
+ originalResource := test.createResource(t, ctx, uut, resourceID)
+
+ // GIVEN that the backend record for that resource has been changed
+ // between us looking up the original resource and us committing
+ // any changes to it...
+ tmpResource, err := test.getResource(ctx, uut, resourceID)
+ require.NoError(t, err)
+ tmpResource.GetMetadata().Labels = map[string]string{"update": "1"}
+ _, err = test.updateResource(ctx, uut, tmpResource)
+ require.NoError(t, err)
+
+ // WHEN I attempt to Update the modified original resource
+ _, err = test.updateResource(ctx, uut, originalResource)
+
+ // EXPECT the Update to fail due to the changed underlying record
+ require.True(t, trace.IsCompareFailed(err), "expected a compare failed error, got %T (%s)", err, err)
+
+ // WHEN I attempt to Upsert the modified original resource
+ originalResource.GetMetadata().Labels = map[string]string{"update": "2"}
+ _, err = test.upsertResource(ctx, uut, originalResource)
+
+ // EXPECT that an upsert will succeed, even though the underlying
+ // record has changed
+ require.NoError(t, err)
+
+ // EXPECT that the backend reflects the updated values from the
+ // upsert
+ r, err := test.getResource(ctx, uut, resourceID)
+ require.NoError(t, err)
+ require.Equal(t, "2", r.GetMetadata().Labels["update"])
+ })
+ })
+ }
+}
+
+func makeTestIdentityCenterAccount(t *testing.T, ctx context.Context, svc services.IdentityCenter, id string) services.IdentityCenterAccount {
+ t.Helper()
+ created, err := svc.CreateIdentityCenterAccount(ctx, services.IdentityCenterAccount{
+ Account: &identitycenterv1.Account{
+ Kind: types.KindIdentityCenterAccount,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{Name: id},
+ Spec: &identitycenterv1.AccountSpec{
+ Id: "aws-account-id-" + id,
+ Arn: fmt.Sprintf("arn:aws:sso::%s:", id),
+ Description: "Test account " + id,
+ PermissionSetInfo: []*identitycenterv1.PermissionSetInfo{
+ {
+ Name: "dummy",
+ Arn: "arn:aws:sso:::permissionSet/ic-instance/ps-instance",
+ },
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+ return created
+}
+
+func makeTestIdentityCenterPermissionSet(t *testing.T, ctx context.Context, svc services.IdentityCenter, id string) *identitycenterv1.PermissionSet {
+ t.Helper()
+ created, err := svc.CreatePermissionSet(ctx, &identitycenterv1.PermissionSet{
+ Kind: types.KindIdentityCenterPermissionSet,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{Name: id},
+ Spec: &identitycenterv1.PermissionSetSpec{
+ Arn: fmt.Sprintf("arn:aws:sso:::permissionSet/ic-instance/%s", id),
+ Name: "aws-permission-set-" + id,
+ Description: "Test permission set " + id,
+ },
+ })
+ require.NoError(t, err)
+ return created
+}
+
+func makeTestIdentityCenterAccountAssignment(t *testing.T, ctx context.Context, svc services.IdentityCenter, id string) services.IdentityCenterAccountAssignment {
+ t.Helper()
+ created, err := svc.CreateAccountAssignment(ctx, services.IdentityCenterAccountAssignment{
+ AccountAssignment: &identitycenterv1.AccountAssignment{
+ Kind: types.KindIdentityCenterAccountAssignment,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{Name: id},
+ Spec: &identitycenterv1.AccountAssignmentSpec{
+ Display: "Some-Permission-set on Some-AWS-account",
+ PermissionSet: &identitycenterv1.PermissionSetInfo{
+ Arn: "arn:aws:sso:::permissionSet/ic-instance/ps-instance",
+ Name: "some permission set",
+ },
+ AccountName: "Some Account Name",
+ AccountId: "some account id",
+ },
+ },
+ })
+ require.NoError(t, err)
+ return created
+}
+
+func makeTestIdentityCenterPrincipalAssignment(t *testing.T, ctx context.Context, svc services.IdentityCenterPrincipalAssignments, id string) *identitycenterv1.PrincipalAssignment {
+ t.Helper()
+ created, err := svc.CreatePrincipalAssignment(ctx, &identitycenterv1.PrincipalAssignment{
+ Kind: types.KindIdentityCenterPrincipalAssignment,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{Name: id},
+ Spec: &identitycenterv1.PrincipalAssignmentSpec{
+ PrincipalType: identitycenterv1.PrincipalType_PRINCIPAL_TYPE_USER,
+ PrincipalId: id,
+ ExternalIdSource: "scim",
+ ExternalId: "some external id",
+ },
+ Status: &identitycenterv1.PrincipalAssignmentStatus{
+ ProvisioningState: identitycenterv1.ProvisioningState_PROVISIONING_STATE_STALE,
+ },
+ })
+ require.NoError(t, err)
+ return created
+}