Adds CRUD service for Identity Center resources (#47609)

* Adds CRUD service for Identity Center resources

This is just the pure CRUD read/write. Caching & watching support coming as
necessary in subsequent PRs.

Co-authored-by: Pawel Kopiczko <kopiczko@gmail.com>
Co-authored-by: Sakshyam Shah <sshah@goteleport.com>

* Update identitycenter.go

* linter appeasement

---------

Co-authored-by: Pawel Kopiczko <kopiczko@gmail.com>
Co-authored-by: Sakshyam Shah <sshah@goteleport.com>
This commit is contained in:
Trent Clarke
2024-10-22 11:04:14 +00:00
committed by GitHub
co-authored by Pawel Kopiczko Sakshyam Shah
parent 28a68483e0
commit bc06d0f55c
10 changed files with 1046 additions and 3 deletions
+14
View File
@@ -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"
+1
View File
@@ -106,6 +106,7 @@ type Config struct {
WindowsDesktops services.WindowsDesktops
AutoUpdateService services.AutoUpdateServiceGetter
ProvisioningStates services.ProvisioningStates
IdentityCenter services.IdentityCenter
}
func (c *Config) CheckAndSetDefaults() error {
+9 -1
View File
@@ -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.
+1
View File
@@ -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,
+4
View File
@@ -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
+220
View File
@@ -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 <http://www.gnu.org/licenses/>.
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
}
+97
View File
@@ -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 <http://www.gnu.org/licenses/>.
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)
}
+3 -2
View File
@@ -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()
+399
View File
@@ -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 <http://www.gnu.org/licenses/>.
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))
}
+298
View File
@@ -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 <http://www.gnu.org/licenses/>.
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
}