From 1510ad54fb7b3edc1a6978f5cdb809e6ebb6f3e0 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Sat, 12 Oct 2024 19:19:31 -0500 Subject: [PATCH 01/12] Initial command to create the managed identity and role --- go.mod | 1 + lib/config/configuration.go | 15 +++ ..._graph_aws_sync.go => accessgraph_sync.go} | 0 ..._sync_test.go => accessgraph_sync_test.go} | 0 .../azureoidc/accessgraph_sync.go | 107 ++++++++++++++++++ tool/teleport/common/integration_configure.go | 6 + tool/teleport/common/teleport.go | 18 ++- 7 files changed, 142 insertions(+), 5 deletions(-) rename lib/integrations/awsoidc/{access_graph_aws_sync.go => accessgraph_sync.go} (100%) rename lib/integrations/awsoidc/{access_graph_aws_sync_test.go => accessgraph_sync_test.go} (100%) create mode 100644 lib/integrations/azureoidc/accessgraph_sync.go diff --git a/go.mod b/go.mod index 8c464ff082e65..42637613208f9 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( connectrpc.com/connect v1.17.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0 diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 5d42d9f4aa8a4..5725b80ef82ba 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -251,6 +251,8 @@ type CommandLineFlags struct { // `teleport integration configure access-graph aws-iam` command IntegrationConfAccessGraphAWSSyncArguments IntegrationConfAccessGraphAWSSync + IntegrationConfAccessGraphAzureSyncArguments IntegrationConfAccessGraphAzureSync + // IntegrationConfAzureOIDCArguments contains the arguments of // `teleport integration configure azure-oidc` command IntegrationConfAzureOIDCArguments IntegrationConfAzureOIDC @@ -283,6 +285,19 @@ type IntegrationConfAccessGraphAWSSync struct { AutoConfirm bool } +// IntegrationConfAccessGraphAzureSync contains the arguments of +// `teleport integration configure access-graph azure` command. +type IntegrationConfAccessGraphAzureSync struct { + // ManagedIdentity is the principal performing the discovery + ManagedIdentity string + // Role is the Azure Role associated with the integration + Role string + // SubscriptionID is the Azure subscription containing resources for sync + SubscriptionID string + // AutoConfirm skips user confirmation of the operation plan if true. + AutoConfirm bool +} + // IntegrationConfAzureOIDC contains the arguments of // `teleport integration configure azure-oidc` command type IntegrationConfAzureOIDC struct { diff --git a/lib/integrations/awsoidc/access_graph_aws_sync.go b/lib/integrations/awsoidc/accessgraph_sync.go similarity index 100% rename from lib/integrations/awsoidc/access_graph_aws_sync.go rename to lib/integrations/awsoidc/accessgraph_sync.go diff --git a/lib/integrations/awsoidc/access_graph_aws_sync_test.go b/lib/integrations/awsoidc/accessgraph_sync_test.go similarity index 100% rename from lib/integrations/awsoidc/access_graph_aws_sync_test.go rename to lib/integrations/awsoidc/accessgraph_sync_test.go diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go new file mode 100644 index 0000000000000..e4411710ecafc --- /dev/null +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -0,0 +1,107 @@ +package azureoidc + +import ( + "context" + "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" + "github.com/google/uuid" + "github.com/gravitational/teleport/lib/cloud/provisioning" + "github.com/gravitational/teleport/lib/config" + "github.com/gravitational/trace" + "log/slog" + "os" +) + +func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, name string) (*provisioning.Action, error) { + runnerFn := func(ctx context.Context) error { + // Create the managed identity + userIdCli, err := armmsi.NewUserAssignedIdentitiesClient(subId, cred, nil) + if err != nil { + return trace.Wrap(fmt.Errorf("could not create managed identity client: %v", err)) + } + id := armmsi.Identity{} + mgdIdRes, err := userIdCli.CreateOrUpdate(ctx, "", name, id, nil) + if err != nil { + return trace.Wrap(fmt.Errorf("could not create managed identity: %v", err)) + } + slog.InfoContext(ctx, fmt.Sprintf( + "Managed identity created, Name: %s, ID: %s", *mgdIdRes.Name, *mgdIdRes.ID)) + + // Create the role + roleDefCli, err := armauthorization.NewRoleDefinitionsClient(cred, nil) + if err != nil { + return trace.Wrap(fmt.Errorf("failed to create role definitions client: %v", err)) + } + roleDefId := uuid.New().String() + customRole := "CustomRole" + // TODO(mbrock): Determine scope + scope := "" + roleDefinition := armauthorization.RoleDefinition{ + Name: &roleDefId, + Properties: &armauthorization.RoleDefinitionProperties{ + RoleName: &name, + RoleType: &customRole, + Permissions: []*armauthorization.Permission{ + // TODO(mbrock): Add permissions + }, + AssignableScopes: []*string{&scope}, // Scope must be provided + }, + } + roleRes, err := roleDefCli.CreateOrUpdate(ctx, scope, roleDefId, roleDefinition, nil) + if err != nil { + return trace.Wrap(fmt.Errorf("failed to create custom role: %v", err)) + } + + // Assign the role to the managed identity + roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subId, cred, nil) + if err != nil { + return fmt.Errorf("failed to create role assignments client: %v", err) + } + assignName := uuid.New().String() + if err != nil { + return trace.Wrap(fmt.Errorf("failed to create role assignments client: %v", err)) + } + roleAssignParams := armauthorization.RoleAssignmentCreateParameters{ + Properties: &armauthorization.RoleAssignmentProperties{ + PrincipalID: mgdIdRes.ID, + RoleDefinitionID: roleRes.ID, + }, + } + _, err = roleAssignCli.Create(ctx, scope, assignName, roleAssignParams, nil) + if err != nil { + return fmt.Errorf("failed to create role assignment: %v", err) + } + + return nil + } + cfg := provisioning.ActionConfig{ + Name: "NewSyncManagedId", + Summary: "Creates a new Azure managed ID for the discovery service to use", + RunnerFn: runnerFn, + } + return provisioning.NewAction(cfg) +} + +// ConfigureAccessGraphSyncAzure sets up the managed identity and role required for Teleport to be able to pull +// AWS resources into Teleport. +func ConfigureAccessGraphSyncAzure(ctx context.Context, params config.IntegrationConfAccessGraphAzureSync) error { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return trace.Wrap(err) + } + managedIdAction, err := newManagedIdAction(cred, params.SubscriptionID, params.ManagedIdentity) + if err != nil { + return trace.Wrap(err) + } + opCfg := provisioning.OperationConfig{ + Name: "access-graph-azure-sync", + Actions: []provisioning.Action{ + *managedIdAction, + }, + AutoConfirm: params.AutoConfirm, + Output: os.Stdout, + } + return trace.Wrap(provisioning.Run(ctx, opCfg)) +} diff --git a/tool/teleport/common/integration_configure.go b/tool/teleport/common/integration_configure.go index 97f531910e45e..13c9e32980c7e 100644 --- a/tool/teleport/common/integration_configure.go +++ b/tool/teleport/common/integration_configure.go @@ -241,6 +241,12 @@ func onIntegrationConfAccessGraphAWSSync(ctx context.Context, params config.Inte return trace.Wrap(awsoidc.ConfigureAccessGraphSyncIAM(ctx, clt, confReq)) } +func onIntegrationConfAccessGraphAzureSync(ctx context.Context, params config.IntegrationConfAccessGraphAzureSync) error { + // Ensure we print output to the user. LogLevel at this point was set to Error. + utils.InitLogger(utils.LoggingForDaemon, slog.LevelInfo) + return trace.Wrap(azureoidc.ConfigureAccessGraphSyncAzure(ctx, params)) +} + func onIntegrationConfAzureOIDCCmd(ctx context.Context, params config.IntegrationConfAzureOIDC) error { // Ensure we print output to the user. LogLevel at this point was set to Error. utils.InitLogger(utils.LoggingForDaemon, slog.LevelInfo) diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index 8318fcf68a45f..bf900ee78cc01 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -508,10 +508,16 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con integrationConfEKSCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfEKSIAMArguments.AutoConfirm) integrationConfAccessGraphCmd := integrationConfigureCmd.Command("access-graph", "Manages Access Graph configuration.") - integrationConfTAGSyncCmd := integrationConfAccessGraphCmd.Command("aws-iam", "Adds required IAM permissions for syncing data into Access Graph service.") - integrationConfTAGSyncCmd.Flag("role", "The AWS Role used by the AWS OIDC Integration.").Required().StringVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.Role) - integrationConfTAGSyncCmd.Flag("aws-account-id", "The AWS account ID.").StringVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.AccountID) - integrationConfTAGSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.AutoConfirm) + integrationConfAccessGraphAWSSyncCmd := integrationConfAccessGraphCmd.Command("aws-iam", "Adds required IAM permissions for syncing data into Access Graph service.") + integrationConfAccessGraphAWSSyncCmd.Flag("role", "The AWS Role used by the AWS OIDC Integration.").Required().StringVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.Role) + integrationConfAccessGraphAWSSyncCmd.Flag("aws-account-id", "The AWS account ID.").StringVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.AccountID) + integrationConfAccessGraphAWSSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.AutoConfirm) + + integrationConfAccessGraphAzureSyncCmd := integrationConfAccessGraphCmd.Command("azure", "Creates/updates permissions for syncing data into Access Graph service.") + integrationConfAccessGraphAzureSyncCmd.Flag("managed-identity", "The managed identity runs the discovery service.").Required() + integrationConfAccessGraphAzureSyncCmd.Flag("role", "The role attached to the managed identity with the discovery permissions.").Required() + integrationConfAccessGraphAzureSyncCmd.Flag("subscription-id", "The subscription ID in which to discovery resources.") + integrationConfAccessGraphAzureSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.") integrationConfAWSOIDCIdPCmd := integrationConfigureCmd.Command("awsoidc-idp", "Creates an IAM IdP (OIDC) in your AWS account to allow the AWS OIDC Integration to access AWS APIs.") integrationConfAWSOIDCIdPCmd.Flag("cluster", "Teleport Cluster name.").Required().StringVar(&ccf. @@ -721,8 +727,10 @@ Examples: err = onIntegrationConfListDatabasesIAM(ctx, ccf.IntegrationConfListDatabasesIAMArguments) case integrationConfExternalAuditCmd.FullCommand(): err = onIntegrationConfExternalAuditCmd(ctx, ccf.IntegrationConfExternalAuditStorageArguments) - case integrationConfTAGSyncCmd.FullCommand(): + case integrationConfAccessGraphAWSSyncCmd.FullCommand(): err = onIntegrationConfAccessGraphAWSSync(ctx, ccf.IntegrationConfAccessGraphAWSSyncArguments) + case integrationConfAccessGraphAzureSyncCmd.FullCommand(): + err = onIntegrationConfAccessGraphAzureSync(ctx, ccf.IntegrationConfAccessGraphAzureSyncArguments) case integrationConfAzureOIDCCmd.FullCommand(): err = onIntegrationConfAzureOIDCCmd(ctx, ccf.IntegrationConfAzureOIDCArguments) case integrationSAMLIdPGCPWorkforce.FullCommand(): From 260144fd67c1a82107cd28408975078f3b2765c1 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Mon, 23 Dec 2024 11:23:12 -0600 Subject: [PATCH 02/12] Go.mod conflicts --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 42637613208f9..4de1578b98ec8 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( connectrpc.com/connect v1.17.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0 diff --git a/go.sum b/go.sum index 354d9459e95f9..9de3414e778af 100644 --- a/go.sum +++ b/go.sum @@ -668,6 +668,8 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLC github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0 h1:qtRcg5Y7jNJ4jEzPq4GpWLfTspHdNe2ZK6LjwGcjgmU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0/go.mod h1:lPneRe3TwsoDRKY4O6YDLXHhEWrD+TIRa8XrV/3/fqw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.1.0 h1:zDeQI/PaWztI2tcrGO/9RIMey9NvqYbnyttf/0P3QWM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.1.0/go.mod h1:zflC9v4VfViJrSvcvplqws/yGXVbUEMZi/iHpZdSPWA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 h1:5n7dPVqsWfVKw+ZiEKSd3Kzu7gwBkbEBkeXb8rgaE9Q= From a39c6e81545c681ccd48dd3cfb8de5c33acaca1f Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Fri, 3 Jan 2025 10:03:56 -0600 Subject: [PATCH 03/12] Adding permissions and applying command params --- .../azureoidc/accessgraph_sync.go | 33 ++++++++++++++----- lib/msgraph/client.go | 4 +++ tool/teleport/common/teleport.go | 8 ++--- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index e4411710ecafc..7959b83f3547d 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -9,12 +9,14 @@ import ( "github.com/google/uuid" "github.com/gravitational/teleport/lib/cloud/provisioning" "github.com/gravitational/teleport/lib/config" + "github.com/gravitational/teleport/lib/msgraph" + "github.com/gravitational/teleport/lib/utils/slices" "github.com/gravitational/trace" "log/slog" "os" ) -func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, name string) (*provisioning.Action, error) { +func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, managedId string, roleName string) (*provisioning.Action, error) { runnerFn := func(ctx context.Context) error { // Create the managed identity userIdCli, err := armmsi.NewUserAssignedIdentitiesClient(subId, cred, nil) @@ -22,6 +24,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, n return trace.Wrap(fmt.Errorf("could not create managed identity client: %v", err)) } id := armmsi.Identity{} + userIdCli.Get(ctx) mgdIdRes, err := userIdCli.CreateOrUpdate(ctx, "", name, id, nil) if err != nil { return trace.Wrap(fmt.Errorf("could not create managed identity: %v", err)) @@ -36,15 +39,23 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, n } roleDefId := uuid.New().String() customRole := "CustomRole" - // TODO(mbrock): Determine scope - scope := "" + scope := fmt.Sprintf("/subscriptions/%s", subId) roleDefinition := armauthorization.RoleDefinition{ Name: &roleDefId, Properties: &armauthorization.RoleDefinitionProperties{ - RoleName: &name, - RoleType: &customRole, + RoleName: &roleName, + RoleType: &customRole, Permissions: []*armauthorization.Permission{ - // TODO(mbrock): Add permissions + { + Actions: slices.ToPointers([]string{ + "Microsoft.Compute/virtualMachines/read", + "Microsoft.Compute/virtualMachines/list", + "Microsoft.Compute/virtualMachineScaleSets/virtualMachines/read", + "Microsoft.Compute/virtualMachineScaleSets/virtualMachines/list", + "Microsoft.Authorization/roleDefinitions/read", + "Microsoft.Authorization/roleAssignments/read", + }), + }, }, AssignableScopes: []*string{&scope}, // Scope must be provided }, @@ -54,7 +65,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, n return trace.Wrap(fmt.Errorf("failed to create custom role: %v", err)) } - // Assign the role to the managed identity + // Assign the Azure role to the managed identity roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subId, cred, nil) if err != nil { return fmt.Errorf("failed to create role assignments client: %v", err) @@ -65,7 +76,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, n } roleAssignParams := armauthorization.RoleAssignmentCreateParameters{ Properties: &armauthorization.RoleAssignmentProperties{ - PrincipalID: mgdIdRes.ID, + PrincipalID: &managedId, RoleDefinitionID: roleRes.ID, }, } @@ -74,6 +85,12 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, n return fmt.Errorf("failed to create role assignment: %v", err) } + // Assign the Graph API permissions to the managed identity + graphCli, err := msgraph.NewClient(msgraph.Config{ + TokenProvider: cred, + }) + graphCli.GetServicePrincipalByAppId() + return nil } cfg := provisioning.ActionConfig{ diff --git a/lib/msgraph/client.go b/lib/msgraph/client.go index 26ea34e1d45c2..9fdffd7eb3bfa 100644 --- a/lib/msgraph/client.go +++ b/lib/msgraph/client.go @@ -45,6 +45,10 @@ const baseURL = "https://graph.microsoft.com/v1.0" // defaultPageSize is the page size used when [Config.PageSize] is not specified. const defaultPageSize = 500 +// graphAppId is the pre-defined application ID of the Graph API +// Ref: [https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications]. +const graphAppId = "00000003-0000-0000-c000-000000000000" + // scopes defines OAuth scopes the client authenticates for. var scopes = []string{"https://graph.microsoft.com/.default"} diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index bf900ee78cc01..af460e08ddd91 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -514,10 +514,10 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con integrationConfAccessGraphAWSSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.AutoConfirm) integrationConfAccessGraphAzureSyncCmd := integrationConfAccessGraphCmd.Command("azure", "Creates/updates permissions for syncing data into Access Graph service.") - integrationConfAccessGraphAzureSyncCmd.Flag("managed-identity", "The managed identity runs the discovery service.").Required() - integrationConfAccessGraphAzureSyncCmd.Flag("role", "The role attached to the managed identity with the discovery permissions.").Required() - integrationConfAccessGraphAzureSyncCmd.Flag("subscription-id", "The subscription ID in which to discovery resources.") - integrationConfAccessGraphAzureSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.") + integrationConfAccessGraphAzureSyncCmd.Flag("managed-identity", "The managed identity runs the Discovery service.").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.ManagedIdentity) + integrationConfAccessGraphAzureSyncCmd.Flag("role", "The role attached to the managed identity with the discovery permissions.").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.Role) + integrationConfAccessGraphAzureSyncCmd.Flag("subscription-id", "The subscription ID in which to discovery resources.").StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.SubscriptionID) + integrationConfAccessGraphAzureSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.AutoConfirm) integrationConfAWSOIDCIdPCmd := integrationConfigureCmd.Command("awsoidc-idp", "Creates an IAM IdP (OIDC) in your AWS account to allow the AWS OIDC Integration to access AWS APIs.") integrationConfAWSOIDCIdPCmd.Flag("cluster", "Teleport Cluster name.").Required().StringVar(&ccf. From 1d508cb7e8953eec40c91cfe9141e39254a05ab3 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Fri, 3 Jan 2025 15:48:53 -0600 Subject: [PATCH 04/12] Adding graph permissions to the MSI --- .../azureoidc/accessgraph_sync.go | 50 ++++++++++++------- lib/msgraph/client.go | 13 +++-- lib/msgraph/models.go | 12 +++-- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index 7959b83f3547d..3ec1047ba2118 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -10,28 +10,26 @@ import ( "github.com/gravitational/teleport/lib/cloud/provisioning" "github.com/gravitational/teleport/lib/config" "github.com/gravitational/teleport/lib/msgraph" - "github.com/gravitational/teleport/lib/utils/slices" + tslices "github.com/gravitational/teleport/lib/utils/slices" "github.com/gravitational/trace" - "log/slog" "os" + "slices" ) +// graphAppId is the pre-defined application ID of the Graph API +// Ref: [https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications]. +const graphAppId = "00000003-0000-0000-c000-000000000000" + +var requiredGraphRoleNames = []string{ + "User.ReadBasic.All", + "Group.Read.All", + "Directory.Read.All", + "User.Read.All", + "Policy.Read.All", +} + func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, managedId string, roleName string) (*provisioning.Action, error) { runnerFn := func(ctx context.Context) error { - // Create the managed identity - userIdCli, err := armmsi.NewUserAssignedIdentitiesClient(subId, cred, nil) - if err != nil { - return trace.Wrap(fmt.Errorf("could not create managed identity client: %v", err)) - } - id := armmsi.Identity{} - userIdCli.Get(ctx) - mgdIdRes, err := userIdCli.CreateOrUpdate(ctx, "", name, id, nil) - if err != nil { - return trace.Wrap(fmt.Errorf("could not create managed identity: %v", err)) - } - slog.InfoContext(ctx, fmt.Sprintf( - "Managed identity created, Name: %s, ID: %s", *mgdIdRes.Name, *mgdIdRes.ID)) - // Create the role roleDefCli, err := armauthorization.NewRoleDefinitionsClient(cred, nil) if err != nil { @@ -47,7 +45,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m RoleType: &customRole, Permissions: []*armauthorization.Permission{ { - Actions: slices.ToPointers([]string{ + Actions: tslices.ToPointers([]string{ "Microsoft.Compute/virtualMachines/read", "Microsoft.Compute/virtualMachines/list", "Microsoft.Compute/virtualMachineScaleSets/virtualMachines/read", @@ -89,7 +87,23 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m graphCli, err := msgraph.NewClient(msgraph.Config{ TokenProvider: cred, }) - graphCli.GetServicePrincipalByAppId() + graphPrincipal, err := graphCli.GetServicePrincipalByAppId(ctx, graphAppId) + var graphRoleIds []string + for _, appRole := range graphPrincipal.AppRoles { + if slices.Contains(requiredGraphRoleNames, *appRole.Value) { + graphRoleIds = append(graphRoleIds, *appRole.ID) + } + } + for _, graphRoleId := range graphRoleIds { + _, err := graphCli.GrantAppRoleToServicePrincipal(ctx, managedId, &msgraph.AppRoleAssignment{ + AppRoleID: &graphRoleId, + PrincipalID: &managedId, + ResourceID: graphPrincipal.ID, + }) + if err != nil { + return trace.Wrap(fmt.Errorf("failed to create role assignment: %v", err)) + } + } return nil } diff --git a/lib/msgraph/client.go b/lib/msgraph/client.go index 9fdffd7eb3bfa..846ee2cbd8800 100644 --- a/lib/msgraph/client.go +++ b/lib/msgraph/client.go @@ -45,10 +45,6 @@ const baseURL = "https://graph.microsoft.com/v1.0" // defaultPageSize is the page size used when [Config.PageSize] is not specified. const defaultPageSize = 500 -// graphAppId is the pre-defined application ID of the Graph API -// Ref: [https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications]. -const graphAppId = "00000003-0000-0000-c000-000000000000" - // scopes defines OAuth scopes the client authenticates for. var scopes = []string{"https://graph.microsoft.com/.default"} @@ -340,6 +336,15 @@ func (c *Client) GetServicePrincipalsByDisplayName(ctx context.Context, displayN return out.Value, nil } +func (c *Client) GetServicePrincipal(ctx context.Context, principalId string) (*ServicePrincipal, error) { + uri := c.endpointURI(fmt.Sprintf("servicePrincipals/%s", principalId)) + out, err := roundtrip[*ServicePrincipal](ctx, c, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, trace.Wrap(err) + } + return out, nil +} + // GrantAppRoleToServicePrincipal grants the given app role to the specified Service Principal. // Ref: [https://learn.microsoft.com/en-us/graph/api/serviceprincipal-post-approleassignedto] func (c *Client) GrantAppRoleToServicePrincipal(ctx context.Context, spID string, assignment *AppRoleAssignment) (*AppRoleAssignment, error) { diff --git a/lib/msgraph/models.go b/lib/msgraph/models.go index 52c3e97cfec7b..3984fee85ccdf 100644 --- a/lib/msgraph/models.go +++ b/lib/msgraph/models.go @@ -123,9 +123,10 @@ type WebApplication struct { type ServicePrincipal struct { DirectoryObject - AppRoleAssignmentRequired *bool `json:"appRoleAssignmentRequired,omitempty"` - PreferredSingleSignOnMode *string `json:"preferredSingleSignOnMode,omitempty"` - PreferredTokenSigningKeyThumbprint *string `json:"preferredTokenSigningKeyThumbprint,omitempty"` + AppRoleAssignmentRequired *bool `json:"appRoleAssignmentRequired,omitempty"` + PreferredSingleSignOnMode *string `json:"preferredSingleSignOnMode,omitempty"` + PreferredTokenSigningKeyThumbprint *string `json:"preferredTokenSigningKeyThumbprint,omitempty"` + AppRoles []*AppRole `json:"appRoles,omitempty"` } type ApplicationServicePrincipal struct { @@ -144,6 +145,11 @@ type SelfSignedCertificate struct { Thumbprint *string `json:"thumbprint,omitempty"` } +type AppRole struct { + ID *string `json:"id,omitempty"` + Value *string `json:"value,omitempty"` +} + type AppRoleAssignment struct { ID *string `json:"id,omitempty"` AppRoleID *string `json:"appRoleId,omitempty"` From c7b66b17cb15a6674ed4cb5f7260ea336af32c5a Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Fri, 3 Jan 2025 22:35:26 -0600 Subject: [PATCH 05/12] Updating parameters --- lib/config/configuration.go | 6 +++--- lib/integrations/azureoidc/accessgraph_sync.go | 6 +++--- tool/teleport/common/teleport.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 5725b80ef82ba..8916254716452 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -290,11 +290,11 @@ type IntegrationConfAccessGraphAWSSync struct { type IntegrationConfAccessGraphAzureSync struct { // ManagedIdentity is the principal performing the discovery ManagedIdentity string - // Role is the Azure Role associated with the integration - Role string + // RoleName is the name of the Azure Role to create and assign to the managed identity + RoleName string // SubscriptionID is the Azure subscription containing resources for sync SubscriptionID string - // AutoConfirm skips user confirmation of the operation plan if true. + // AutoConfirm skips user confirmation of the operation plan if true AutoConfirm bool } diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index 3ec1047ba2118..7147512cd64d7 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "github.com/google/uuid" "github.com/gravitational/teleport/lib/cloud/provisioning" "github.com/gravitational/teleport/lib/config" @@ -109,7 +108,8 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m } cfg := provisioning.ActionConfig{ Name: "NewSyncManagedId", - Summary: "Creates a new Azure managed ID for the discovery service to use", + Summary: "Creates a new Azure role and attaches it to a managed identity for the Discovery service", + Details: "Creates a new Azure role and attaches it to a managed identity for the Discovery service", RunnerFn: runnerFn, } return provisioning.NewAction(cfg) @@ -122,7 +122,7 @@ func ConfigureAccessGraphSyncAzure(ctx context.Context, params config.Integratio if err != nil { return trace.Wrap(err) } - managedIdAction, err := newManagedIdAction(cred, params.SubscriptionID, params.ManagedIdentity) + managedIdAction, err := newManagedIdAction(cred, params.SubscriptionID, params.ManagedIdentity, params.RoleName) if err != nil { return trace.Wrap(err) } diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index af460e08ddd91..912b01186a52a 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -514,8 +514,8 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con integrationConfAccessGraphAWSSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.AutoConfirm) integrationConfAccessGraphAzureSyncCmd := integrationConfAccessGraphCmd.Command("azure", "Creates/updates permissions for syncing data into Access Graph service.") - integrationConfAccessGraphAzureSyncCmd.Flag("managed-identity", "The managed identity runs the Discovery service.").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.ManagedIdentity) - integrationConfAccessGraphAzureSyncCmd.Flag("role", "The role attached to the managed identity with the discovery permissions.").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.Role) + integrationConfAccessGraphAzureSyncCmd.Flag("managed-identity", "The ID of the managed identity to run the Discovery service.").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.ManagedIdentity) + integrationConfAccessGraphAzureSyncCmd.Flag("role-name", "The name of the Azure Role to create and assign to the managed identity").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.RoleName) integrationConfAccessGraphAzureSyncCmd.Flag("subscription-id", "The subscription ID in which to discovery resources.").StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.SubscriptionID) integrationConfAccessGraphAzureSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.AutoConfirm) From 69fe4229f6b3a26583fee5cc0a95b7fff5aa76cf Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Sat, 4 Jan 2025 18:19:25 -0600 Subject: [PATCH 06/12] Adding some details and cleaning up comments --- .../azureoidc/accessgraph_sync.go | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index 7147512cd64d7..d8c830dd077a4 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -13,6 +13,7 @@ import ( "github.com/gravitational/trace" "os" "slices" + "strings" ) // graphAppId is the pre-defined application ID of the Graph API @@ -46,9 +47,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m { Actions: tslices.ToPointers([]string{ "Microsoft.Compute/virtualMachines/read", - "Microsoft.Compute/virtualMachines/list", "Microsoft.Compute/virtualMachineScaleSets/virtualMachines/read", - "Microsoft.Compute/virtualMachineScaleSets/virtualMachines/list", "Microsoft.Authorization/roleDefinitions/read", "Microsoft.Authorization/roleAssignments/read", }), @@ -62,7 +61,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m return trace.Wrap(fmt.Errorf("failed to create custom role: %v", err)) } - // Assign the Azure role to the managed identity + // Assign the new role to the managed identity roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subId, cred, nil) if err != nil { return fmt.Errorf("failed to create role assignments client: %v", err) @@ -100,16 +99,22 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m ResourceID: graphPrincipal.ID, }) if err != nil { - return trace.Wrap(fmt.Errorf("failed to create role assignment: %v", err)) + return trace.Wrap(fmt.Errorf("failed to create graph API role assignment: %v", err)) } } - return nil } cfg := provisioning.ActionConfig{ - Name: "NewSyncManagedId", - Summary: "Creates a new Azure role and attaches it to a managed identity for the Discovery service", - Details: "Creates a new Azure role and attaches it to a managed identity for the Discovery service", + Name: "NewSyncManagedId", + Summary: "Creates a new Azure role and attaches it to a managed identity for the Discovery service", + Details: strings.Join([]string{ + "The Discovery service needs to run as a credentialed Azure managed identity. This managed identity ", + "can be system assigned (i.e. tied to the lifecycle of a virtual machine running the Discovery service), ", + "or user-assigned (i.e. a persistent identity). The managed identity requires two types of permissions: ", + "1) Azure resource permissions in order to fetch virtual machines, role definitions, etc, and 2) Graph ", + "API permissions to fetch users, groups, and service principals. The command assigns both Azure resource ", + "permissions as well as Graph API permissions to the specified managed identity.", + }, ""), RunnerFn: runnerFn, } return provisioning.NewAction(cfg) From 25cedb2f4d29ba6cf0b102fcb0e02d57452835d6 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Sat, 4 Jan 2025 18:25:51 -0600 Subject: [PATCH 07/12] Fixing go.sum --- integrations/terraform/go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 860c97ee36879..28031d54a808c 100644 --- a/integrations/terraform/go.sum +++ b/integrations/terraform/go.sum @@ -644,6 +644,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvUL github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0 h1:qtRcg5Y7jNJ4jEzPq4GpWLfTspHdNe2ZK6LjwGcjgmU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization v1.0.0/go.mod h1:lPneRe3TwsoDRKY4O6YDLXHhEWrD+TIRa8XrV/3/fqw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.1.0 h1:zDeQI/PaWztI2tcrGO/9RIMey9NvqYbnyttf/0P3QWM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.1.0/go.mod h1:zflC9v4VfViJrSvcvplqws/yGXVbUEMZi/iHpZdSPWA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 h1:5n7dPVqsWfVKw+ZiEKSd3Kzu7gwBkbEBkeXb8rgaE9Q= From 664ea80c958a8ec65d6be5a29db51252ce9c544e Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Sat, 4 Jan 2025 18:39:38 -0600 Subject: [PATCH 08/12] Linting --- .../azureoidc/accessgraph_sync.go | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index d8c830dd077a4..2801d6de82606 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -3,17 +3,19 @@ package azureoidc import ( "context" "fmt" + "os" + "slices" + "strings" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization" "github.com/google/uuid" + "github.com/gravitational/trace" + "github.com/gravitational/teleport/lib/cloud/provisioning" "github.com/gravitational/teleport/lib/config" "github.com/gravitational/teleport/lib/msgraph" tslices "github.com/gravitational/teleport/lib/utils/slices" - "github.com/gravitational/trace" - "os" - "slices" - "strings" ) // graphAppId is the pre-defined application ID of the Graph API @@ -33,7 +35,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m // Create the role roleDefCli, err := armauthorization.NewRoleDefinitionsClient(cred, nil) if err != nil { - return trace.Wrap(fmt.Errorf("failed to create role definitions client: %v", err)) + return trace.Wrap(fmt.Errorf("failed to create role definitions client: %w", err)) } roleDefId := uuid.New().String() customRole := "CustomRole" @@ -58,17 +60,17 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m } roleRes, err := roleDefCli.CreateOrUpdate(ctx, scope, roleDefId, roleDefinition, nil) if err != nil { - return trace.Wrap(fmt.Errorf("failed to create custom role: %v", err)) + return trace.Wrap(fmt.Errorf("failed to create custom role: %w", err)) } // Assign the new role to the managed identity roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subId, cred, nil) if err != nil { - return fmt.Errorf("failed to create role assignments client: %v", err) + return fmt.Errorf("failed to create role assignments client: %w", err) } assignName := uuid.New().String() if err != nil { - return trace.Wrap(fmt.Errorf("failed to create role assignments client: %v", err)) + return trace.Wrap(fmt.Errorf("failed to create role assignments client: %w", err)) } roleAssignParams := armauthorization.RoleAssignmentCreateParameters{ Properties: &armauthorization.RoleAssignmentProperties{ @@ -78,14 +80,21 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m } _, err = roleAssignCli.Create(ctx, scope, assignName, roleAssignParams, nil) if err != nil { - return fmt.Errorf("failed to create role assignment: %v", err) + return fmt.Errorf( + "failed to assign role %s to principal %s: %w", roleName, managedId, err) } // Assign the Graph API permissions to the managed identity graphCli, err := msgraph.NewClient(msgraph.Config{ TokenProvider: cred, }) + if err != nil { + return trace.Wrap(fmt.Errorf("failed to create msgraph client: %w", err)) + } graphPrincipal, err := graphCli.GetServicePrincipalByAppId(ctx, graphAppId) + if err != nil { + return trace.Wrap(fmt.Errorf("failed to get the graph API service principal: %w", err)) + } var graphRoleIds []string for _, appRole := range graphPrincipal.AppRoles { if slices.Contains(requiredGraphRoleNames, *appRole.Value) { @@ -99,7 +108,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m ResourceID: graphPrincipal.ID, }) if err != nil { - return trace.Wrap(fmt.Errorf("failed to create graph API role assignment: %v", err)) + return trace.Wrap(fmt.Errorf("failed to assign graph API role to %s: %w", managedId, err)) } } return nil From d2f37cd6e3f02a07271ab97ee4572a0e0b9daa0d Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Sat, 4 Jan 2025 18:51:52 -0600 Subject: [PATCH 09/12] License --- lib/integrations/azureoidc/accessgraph_sync.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index 2801d6de82606..9aafe621eb1a7 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -1,3 +1,21 @@ +/* + * 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 azureoidc import ( From 89a35740039c747060bac71a28e3880cc6a92b16 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Tue, 7 Jan 2025 14:42:35 -0600 Subject: [PATCH 10/12] PR feedback --- lib/config/configuration.go | 2 + .../azureoidc/accessgraph_sync.go | 34 +++++++--------- lib/kube/proxy/resource_filters_test.go | 40 +++++++++---------- lib/msgraph/client.go | 4 +- tool/teleport/common/teleport.go | 4 +- 5 files changed, 41 insertions(+), 43 deletions(-) diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 8916254716452..d6c9b87fff808 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -251,6 +251,8 @@ type CommandLineFlags struct { // `teleport integration configure access-graph aws-iam` command IntegrationConfAccessGraphAWSSyncArguments IntegrationConfAccessGraphAWSSync + // IntegrationConfAccessGarphAzureSyncArguments contains the arguments of + // `teleport integration configure access-graph azure` command IntegrationConfAccessGraphAzureSyncArguments IntegrationConfAccessGraphAzureSync // IntegrationConfAzureOIDCArguments contains the arguments of diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index 9aafe621eb1a7..8fa22a9c43aa6 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -20,7 +20,6 @@ package azureoidc import ( "context" - "fmt" "os" "slices" "strings" @@ -33,7 +32,7 @@ import ( "github.com/gravitational/teleport/lib/cloud/provisioning" "github.com/gravitational/teleport/lib/config" "github.com/gravitational/teleport/lib/msgraph" - tslices "github.com/gravitational/teleport/lib/utils/slices" + libslices "github.com/gravitational/teleport/lib/utils/slices" ) // graphAppId is the pre-defined application ID of the Graph API @@ -49,15 +48,15 @@ var requiredGraphRoleNames = []string{ } func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, managedId string, roleName string) (*provisioning.Action, error) { + customRole := "CustomRole" + scope := "/subscriptions/" + subId runnerFn := func(ctx context.Context) error { // Create the role roleDefCli, err := armauthorization.NewRoleDefinitionsClient(cred, nil) if err != nil { - return trace.Wrap(fmt.Errorf("failed to create role definitions client: %w", err)) + return trace.BadParameter("failed to create role definitions client: %w", err) } roleDefId := uuid.New().String() - customRole := "CustomRole" - scope := fmt.Sprintf("/subscriptions/%s", subId) roleDefinition := armauthorization.RoleDefinition{ Name: &roleDefId, Properties: &armauthorization.RoleDefinitionProperties{ @@ -65,7 +64,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m RoleType: &customRole, Permissions: []*armauthorization.Permission{ { - Actions: tslices.ToPointers([]string{ + Actions: libslices.ToPointers([]string{ "Microsoft.Compute/virtualMachines/read", "Microsoft.Compute/virtualMachineScaleSets/virtualMachines/read", "Microsoft.Authorization/roleDefinitions/read", @@ -78,27 +77,23 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m } roleRes, err := roleDefCli.CreateOrUpdate(ctx, scope, roleDefId, roleDefinition, nil) if err != nil { - return trace.Wrap(fmt.Errorf("failed to create custom role: %w", err)) + return trace.BadParameter("failed to create custom role: %w", err) } // Assign the new role to the managed identity roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subId, cred, nil) if err != nil { - return fmt.Errorf("failed to create role assignments client: %w", err) + return trace.BadParameter("failed to create role assignments client: %w", err) } assignName := uuid.New().String() - if err != nil { - return trace.Wrap(fmt.Errorf("failed to create role assignments client: %w", err)) - } roleAssignParams := armauthorization.RoleAssignmentCreateParameters{ Properties: &armauthorization.RoleAssignmentProperties{ PrincipalID: &managedId, RoleDefinitionID: roleRes.ID, }, } - _, err = roleAssignCli.Create(ctx, scope, assignName, roleAssignParams, nil) - if err != nil { - return fmt.Errorf( + if _, err = roleAssignCli.Create(ctx, scope, assignName, roleAssignParams, nil); err != nil { + return trace.BadParameter( "failed to assign role %s to principal %s: %w", roleName, managedId, err) } @@ -107,11 +102,11 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m TokenProvider: cred, }) if err != nil { - return trace.Wrap(fmt.Errorf("failed to create msgraph client: %w", err)) + return trace.BadParameter("failed to create msgraph client: %w", err) } graphPrincipal, err := graphCli.GetServicePrincipalByAppId(ctx, graphAppId) if err != nil { - return trace.Wrap(fmt.Errorf("failed to get the graph API service principal: %w", err)) + return trace.BadParameter("failed to get the graph API service principal: %w", err) } var graphRoleIds []string for _, appRole := range graphPrincipal.AppRoles { @@ -120,13 +115,12 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m } } for _, graphRoleId := range graphRoleIds { - _, err := graphCli.GrantAppRoleToServicePrincipal(ctx, managedId, &msgraph.AppRoleAssignment{ + if _, err := graphCli.GrantAppRoleToServicePrincipal(ctx, managedId, &msgraph.AppRoleAssignment{ AppRoleID: &graphRoleId, PrincipalID: &managedId, ResourceID: graphPrincipal.ID, - }) - if err != nil { - return trace.Wrap(fmt.Errorf("failed to assign graph API role to %s: %w", managedId, err)) + }); err != nil { + return trace.BadParameter("failed to assign graph API role to %s: %w", managedId, err) } } return nil diff --git a/lib/kube/proxy/resource_filters_test.go b/lib/kube/proxy/resource_filters_test.go index d193513247345..e00779a2bf660 100644 --- a/lib/kube/proxy/resource_filters_test.go +++ b/lib/kube/proxy/resource_filters_test.go @@ -42,7 +42,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/kube/proxy/responsewriters" "github.com/gravitational/teleport/lib/utils" - tslices "github.com/gravitational/teleport/lib/utils/slices" + libslices "github.com/gravitational/teleport/lib/utils/slices" ) func Test_filterBuffer(t *testing.T) { @@ -188,43 +188,43 @@ func Test_filterBuffer(t *testing.T) { var resources []string switch o := obj.(type) { case *corev1.SecretList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *appsv1.DeploymentList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *appsv1.DaemonSetList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *appsv1.StatefulSetList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *authv1.RoleBindingList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *batchv1.CronJobList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *batchv1.JobList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *corev1.PodList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *corev1.ConfigMapList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *corev1.ServiceAccountList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *appsv1.ReplicaSetList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *corev1.ServiceList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *corev1.PersistentVolumeClaimList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *authv1.RoleList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *networkingv1.IngressList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *extensionsv1beta1.IngressList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *extensionsv1beta1.DaemonSetList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *extensionsv1beta1.ReplicaSetList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *extensionsv1beta1.DeploymentList: - resources = collectResourcesFromResponse(tslices.ToPointers(o.Items)) + resources = collectResourcesFromResponse(libslices.ToPointers(o.Items)) case *metav1.Table: for i := range o.Rows { row := &(o.Rows[i]) diff --git a/lib/msgraph/client.go b/lib/msgraph/client.go index 846ee2cbd8800..a622ffe673e77 100644 --- a/lib/msgraph/client.go +++ b/lib/msgraph/client.go @@ -1,5 +1,5 @@ // Teleport -// Copyright (C) 2024 Gravitational, Inc. +// Copyright (C) 2025 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 @@ -336,6 +336,8 @@ func (c *Client) GetServicePrincipalsByDisplayName(ctx context.Context, displayN return out.Value, nil } +// GetServicePrincipal returns the service principal for the given principal ID. +// Ref: [https://learn.microsoft.com/en-us/graph/api/serviceprincipal-get]. func (c *Client) GetServicePrincipal(ctx context.Context, principalId string) (*ServicePrincipal, error) { uri := c.endpointURI(fmt.Sprintf("servicePrincipals/%s", principalId)) out, err := roundtrip[*ServicePrincipal](ctx, c, http.MethodGet, uri.String(), nil) diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index 912b01186a52a..18a3981bca56a 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -508,12 +508,12 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con integrationConfEKSCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfEKSIAMArguments.AutoConfirm) integrationConfAccessGraphCmd := integrationConfigureCmd.Command("access-graph", "Manages Access Graph configuration.") - integrationConfAccessGraphAWSSyncCmd := integrationConfAccessGraphCmd.Command("aws-iam", "Adds required IAM permissions for syncing data into Access Graph service.") + integrationConfAccessGraphAWSSyncCmd := integrationConfAccessGraphCmd.Command("aws-iam", "Adds required AWS IAM permissions for syncing AWS resources into Access Graph service.") integrationConfAccessGraphAWSSyncCmd.Flag("role", "The AWS Role used by the AWS OIDC Integration.").Required().StringVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.Role) integrationConfAccessGraphAWSSyncCmd.Flag("aws-account-id", "The AWS account ID.").StringVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.AccountID) integrationConfAccessGraphAWSSyncCmd.Flag("confirm", "Apply changes without confirmation prompt.").BoolVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.AutoConfirm) - integrationConfAccessGraphAzureSyncCmd := integrationConfAccessGraphCmd.Command("azure", "Creates/updates permissions for syncing data into Access Graph service.") + integrationConfAccessGraphAzureSyncCmd := integrationConfAccessGraphCmd.Command("azure", "Adds required Azure permissions for syncing Azure resources into Access Graph service.") integrationConfAccessGraphAzureSyncCmd.Flag("managed-identity", "The ID of the managed identity to run the Discovery service.").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.ManagedIdentity) integrationConfAccessGraphAzureSyncCmd.Flag("role-name", "The name of the Azure Role to create and assign to the managed identity").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.RoleName) integrationConfAccessGraphAzureSyncCmd.Flag("subscription-id", "The subscription ID in which to discovery resources.").StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.SubscriptionID) From e44e2de1c91b402548f1bc260af38781a5dd7605 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Tue, 7 Jan 2025 17:46:34 -0600 Subject: [PATCH 11/12] Decoupling sync config with an interface for testing --- .../azureoidc/accessgraph_sync.go | 166 +++++++++++++----- .../azureoidc/accessgraph_sync_test.go | 23 +++ tool/teleport/common/integration_configure.go | 12 +- 3 files changed, 158 insertions(+), 43 deletions(-) create mode 100644 lib/integrations/azureoidc/accessgraph_sync_test.go diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index 8fa22a9c43aa6..268d5d11c18a2 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -20,8 +20,7 @@ package azureoidc import ( "context" - "os" - "slices" + "io" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" @@ -30,7 +29,6 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/lib/cloud/provisioning" - "github.com/gravitational/teleport/lib/config" "github.com/gravitational/teleport/lib/msgraph" libslices "github.com/gravitational/teleport/lib/utils/slices" ) @@ -39,24 +37,118 @@ import ( // Ref: [https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications]. const graphAppId = "00000003-0000-0000-c000-000000000000" -var requiredGraphRoleNames = []string{ - "User.ReadBasic.All", - "Group.Read.All", - "Directory.Read.All", - "User.Read.All", - "Policy.Read.All", +var requiredGraphRoleNames = map[string]bool{ + "User.ReadBasic.All": true, + "Group.Read.All": true, + "Directory.Read.All": true, + "User.Read.All": true, + "Policy.Read.All": true, } -func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, managedId string, roleName string) (*provisioning.Action, error) { +type AccessGraphAzureConfigureClient interface { + CreateRoleDefinition(ctx context.Context, scope string, roleDefinition armauthorization.RoleDefinition) (string, error) + CreateRoleAssignment(ctx context.Context, scope string, roleAssignment armauthorization.RoleAssignmentCreateParameters) error + GetServicePrincipalByAppId(ctx context.Context, appId string) (*msgraph.ServicePrincipal, error) + GrantAppRoleToServicePrincipal(ctx context.Context, roleAssignment msgraph.AppRoleAssignment) error +} + +type azureConfigClient struct { + roleDefCli *armauthorization.RoleDefinitionsClient + roleAssignCli *armauthorization.RoleAssignmentsClient + graphCli *msgraph.Client +} + +func NewAzureConfigClient(subId string) (AccessGraphAzureConfigureClient, error) { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, trace.Wrap(err) + } + roleDefCli, err := armauthorization.NewRoleDefinitionsClient(cred, nil) + if err != nil { + return nil, trace.BadParameter("failed to create role definitions client: %w", err) + } + roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subId, cred, nil) + if err != nil { + return nil, trace.BadParameter("failed to create role assignments client: %w", err) + } + graphCli, err := msgraph.NewClient(msgraph.Config{ + TokenProvider: cred, + }) + if err != nil { + return nil, trace.BadParameter("failed to create msgraph client: %w", err) + } + return &azureConfigClient{ + roleDefCli: roleDefCli, + roleAssignCli: roleAssignCli, + graphCli: graphCli, + }, nil +} + +func (c *azureConfigClient) CreateRoleDefinition(ctx context.Context, scope string, roleDefinition armauthorization.RoleDefinition) (string, error) { + newUuid, err := uuid.NewRandom() + if err != nil { + return "", trace.Wrap(err) + } + roleDefId := newUuid.String() + roleRes, err := c.roleDefCli.CreateOrUpdate(ctx, scope, roleDefId, roleDefinition, nil) + if err != nil { + return "", trace.Wrap(err) + } + return *roleRes.ID, err +} + +func (c *azureConfigClient) CreateRoleAssignment(ctx context.Context, scope string, roleAssignment armauthorization.RoleAssignmentCreateParameters) error { + newUuid, err := uuid.NewRandom() + if err != nil { + return trace.Wrap(err) + } + assignId := newUuid.String() + if _, err = c.roleAssignCli.Create(ctx, scope, assignId, roleAssignment, nil); err != nil { + return trace.Wrap(err) + } + return nil +} + +func (c *azureConfigClient) GetServicePrincipalByAppId(ctx context.Context, appId string) (*msgraph.ServicePrincipal, error) { + graphPrincipal, err := c.graphCli.GetServicePrincipalByAppId(ctx, appId) + if err != nil { + return nil, trace.BadParameter("failed to get the graph API service principal: %w", err) + } + return graphPrincipal, nil +} + +func (c *azureConfigClient) GrantAppRoleToServicePrincipal(ctx context.Context, roleAssignment msgraph.AppRoleAssignment) error { + _, err := c.graphCli.GrantAppRoleToServicePrincipal(ctx, *roleAssignment.PrincipalID, &roleAssignment) + if err != nil { + return trace.Wrap(err) + } + return nil +} + +// AccessGraphAzureConfigureRequest is a request to configure the required Policies to use the TAG AWS Sync. +type AccessGraphAzureConfigureRequest struct { + // ManagedIdentity is the principal performing the discovery + ManagedIdentity string + // RoleName is the name of the Azure Role to create and assign to the managed identity + RoleName string + // SubscriptionID is the Azure subscription containing resources for sync + SubscriptionID string + // AutoConfirm skips user confirmation of the operation plan if true + AutoConfirm bool + // stdout is used to override stdout output in tests. + stdout io.Writer +} + +func newManagedIdAction(cred *azidentity.DefaultAzureCredential, clt AccessGraphAzureConfigureClient, subId string, managedId string, roleName string) (*provisioning.Action, error) { customRole := "CustomRole" scope := "/subscriptions/" + subId runnerFn := func(ctx context.Context) error { // Create the role - roleDefCli, err := armauthorization.NewRoleDefinitionsClient(cred, nil) + newUuid, err := uuid.NewRandom() if err != nil { - return trace.BadParameter("failed to create role definitions client: %w", err) + return trace.Wrap(err) } - roleDefId := uuid.New().String() + roleDefId := newUuid.String() roleDefinition := armauthorization.RoleDefinition{ Name: &roleDefId, Properties: &armauthorization.RoleDefinitionProperties{ @@ -75,52 +167,42 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m AssignableScopes: []*string{&scope}, // Scope must be provided }, } - roleRes, err := roleDefCli.CreateOrUpdate(ctx, scope, roleDefId, roleDefinition, nil) + roleId, err := clt.CreateRoleDefinition(ctx, scope, roleDefinition) if err != nil { return trace.BadParameter("failed to create custom role: %w", err) } // Assign the new role to the managed identity - roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subId, cred, nil) + newUuid, err = uuid.NewRandom() if err != nil { - return trace.BadParameter("failed to create role assignments client: %w", err) + return trace.Wrap(err) } - assignName := uuid.New().String() roleAssignParams := armauthorization.RoleAssignmentCreateParameters{ Properties: &armauthorization.RoleAssignmentProperties{ PrincipalID: &managedId, - RoleDefinitionID: roleRes.ID, + RoleDefinitionID: &roleId, }, } - if _, err = roleAssignCli.Create(ctx, scope, assignName, roleAssignParams, nil); err != nil { + if err = clt.CreateRoleAssignment(ctx, scope, roleAssignParams); err != nil { return trace.BadParameter( "failed to assign role %s to principal %s: %w", roleName, managedId, err) } // Assign the Graph API permissions to the managed identity - graphCli, err := msgraph.NewClient(msgraph.Config{ - TokenProvider: cred, - }) - if err != nil { - return trace.BadParameter("failed to create msgraph client: %w", err) - } - graphPrincipal, err := graphCli.GetServicePrincipalByAppId(ctx, graphAppId) + graphPrincipal, err := clt.GetServicePrincipalByAppId(ctx, graphAppId) if err != nil { return trace.BadParameter("failed to get the graph API service principal: %w", err) } - var graphRoleIds []string for _, appRole := range graphPrincipal.AppRoles { - if slices.Contains(requiredGraphRoleNames, *appRole.Value) { - graphRoleIds = append(graphRoleIds, *appRole.ID) - } - } - for _, graphRoleId := range graphRoleIds { - if _, err := graphCli.GrantAppRoleToServicePrincipal(ctx, managedId, &msgraph.AppRoleAssignment{ - AppRoleID: &graphRoleId, - PrincipalID: &managedId, - ResourceID: graphPrincipal.ID, - }); err != nil { - return trace.BadParameter("failed to assign graph API role to %s: %w", managedId, err) + if _, ok := requiredGraphRoleNames[*appRole.Value]; ok { + roleAssignment := msgraph.AppRoleAssignment{ + AppRoleID: appRole.ID, + PrincipalID: &managedId, + ResourceID: graphPrincipal.ID, + } + if err = clt.GrantAppRoleToServicePrincipal(ctx, roleAssignment); err != nil { + return trace.BadParameter("failed to assign graph API role to %s: %w", managedId, err) + } } } return nil @@ -143,12 +225,12 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, subId string, m // ConfigureAccessGraphSyncAzure sets up the managed identity and role required for Teleport to be able to pull // AWS resources into Teleport. -func ConfigureAccessGraphSyncAzure(ctx context.Context, params config.IntegrationConfAccessGraphAzureSync) error { +func ConfigureAccessGraphSyncAzure(ctx context.Context, clt AccessGraphAzureConfigureClient, req AccessGraphAzureConfigureRequest) error { cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { return trace.Wrap(err) } - managedIdAction, err := newManagedIdAction(cred, params.SubscriptionID, params.ManagedIdentity, params.RoleName) + managedIdAction, err := newManagedIdAction(cred, clt, req.SubscriptionID, req.ManagedIdentity, req.RoleName) if err != nil { return trace.Wrap(err) } @@ -157,8 +239,8 @@ func ConfigureAccessGraphSyncAzure(ctx context.Context, params config.Integratio Actions: []provisioning.Action{ *managedIdAction, }, - AutoConfirm: params.AutoConfirm, - Output: os.Stdout, + AutoConfirm: req.AutoConfirm, + Output: req.stdout, } return trace.Wrap(provisioning.Run(ctx, opCfg)) } diff --git a/lib/integrations/azureoidc/accessgraph_sync_test.go b/lib/integrations/azureoidc/accessgraph_sync_test.go new file mode 100644 index 0000000000000..0c1875011186e --- /dev/null +++ b/lib/integrations/azureoidc/accessgraph_sync_test.go @@ -0,0 +1,23 @@ +package azureoidc + +import ( + "bytes" + "context" + "testing" +) + +func TestAccessGraphAzureConfigOutput(t *testing.T) { + ctx := context.Background() + var buf bytes.Buffer + req := AccessGraphAzureConfigureRequest{ + ManagedIdentity: "foo", + RoleName: "bar", + SubscriptionID: "1234567890", + AutoConfirm: true, + stdout: &buf, + } + err := ConfigureAccessGraphSyncAzure(ctx, req) + if err != nil { + return + } +} diff --git a/tool/teleport/common/integration_configure.go b/tool/teleport/common/integration_configure.go index 13c9e32980c7e..26f8d93896853 100644 --- a/tool/teleport/common/integration_configure.go +++ b/tool/teleport/common/integration_configure.go @@ -244,7 +244,17 @@ func onIntegrationConfAccessGraphAWSSync(ctx context.Context, params config.Inte func onIntegrationConfAccessGraphAzureSync(ctx context.Context, params config.IntegrationConfAccessGraphAzureSync) error { // Ensure we print output to the user. LogLevel at this point was set to Error. utils.InitLogger(utils.LoggingForDaemon, slog.LevelInfo) - return trace.Wrap(azureoidc.ConfigureAccessGraphSyncAzure(ctx, params)) + confReq := azureoidc.AccessGraphAzureConfigureRequest{ + ManagedIdentity: params.ManagedIdentity, + RoleName: params.RoleName, + SubscriptionID: params.SubscriptionID, + AutoConfirm: params.AutoConfirm, + } + clt, err := azureoidc.NewAzureConfigClient(params.SubscriptionID) + if err != nil { + return trace.Wrap(err) + } + return trace.Wrap(azureoidc.ConfigureAccessGraphSyncAzure(ctx, clt, confReq)) } func onIntegrationConfAzureOIDCCmd(ctx context.Context, params config.IntegrationConfAzureOIDC) error { From 7efdf3d118ba4699e464863046bf9466104b52dc Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Tue, 7 Jan 2025 18:30:24 -0600 Subject: [PATCH 12/12] Tweaks to test mocking --- .../azureoidc/accessgraph_sync.go | 27 +++++--------- .../azureoidc/accessgraph_sync_test.go | 37 ++++++++++++++++++- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/lib/integrations/azureoidc/accessgraph_sync.go b/lib/integrations/azureoidc/accessgraph_sync.go index 268d5d11c18a2..a6a2948887b81 100644 --- a/lib/integrations/azureoidc/accessgraph_sync.go +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -65,17 +65,17 @@ func NewAzureConfigClient(subId string) (AccessGraphAzureConfigureClient, error) } roleDefCli, err := armauthorization.NewRoleDefinitionsClient(cred, nil) if err != nil { - return nil, trace.BadParameter("failed to create role definitions client: %w", err) + return nil, trace.BadParameter("failed to create role definitions client: %v", err) } roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subId, cred, nil) if err != nil { - return nil, trace.BadParameter("failed to create role assignments client: %w", err) + return nil, trace.BadParameter("failed to create role assignments client: %v", err) } graphCli, err := msgraph.NewClient(msgraph.Config{ TokenProvider: cred, }) if err != nil { - return nil, trace.BadParameter("failed to create msgraph client: %w", err) + return nil, trace.BadParameter("failed to create msgraph client: %v", err) } return &azureConfigClient{ roleDefCli: roleDefCli, @@ -112,7 +112,7 @@ func (c *azureConfigClient) CreateRoleAssignment(ctx context.Context, scope stri func (c *azureConfigClient) GetServicePrincipalByAppId(ctx context.Context, appId string) (*msgraph.ServicePrincipal, error) { graphPrincipal, err := c.graphCli.GetServicePrincipalByAppId(ctx, appId) if err != nil { - return nil, trace.BadParameter("failed to get the graph API service principal: %w", err) + return nil, trace.BadParameter("failed to get the graph API service principal: %v", err) } return graphPrincipal, nil } @@ -144,13 +144,8 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, clt AccessGraph scope := "/subscriptions/" + subId runnerFn := func(ctx context.Context) error { // Create the role - newUuid, err := uuid.NewRandom() - if err != nil { - return trace.Wrap(err) - } - roleDefId := newUuid.String() roleDefinition := armauthorization.RoleDefinition{ - Name: &roleDefId, + Name: &roleName, Properties: &armauthorization.RoleDefinitionProperties{ RoleName: &roleName, RoleType: &customRole, @@ -169,14 +164,10 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, clt AccessGraph } roleId, err := clt.CreateRoleDefinition(ctx, scope, roleDefinition) if err != nil { - return trace.BadParameter("failed to create custom role: %w", err) + return trace.BadParameter("failed to create custom role: %v", err) } // Assign the new role to the managed identity - newUuid, err = uuid.NewRandom() - if err != nil { - return trace.Wrap(err) - } roleAssignParams := armauthorization.RoleAssignmentCreateParameters{ Properties: &armauthorization.RoleAssignmentProperties{ PrincipalID: &managedId, @@ -185,13 +176,13 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, clt AccessGraph } if err = clt.CreateRoleAssignment(ctx, scope, roleAssignParams); err != nil { return trace.BadParameter( - "failed to assign role %s to principal %s: %w", roleName, managedId, err) + "failed to assign role %s to principal %s: %v", roleName, managedId, err) } // Assign the Graph API permissions to the managed identity graphPrincipal, err := clt.GetServicePrincipalByAppId(ctx, graphAppId) if err != nil { - return trace.BadParameter("failed to get the graph API service principal: %w", err) + return trace.BadParameter("failed to get the graph API service principal: %v", err) } for _, appRole := range graphPrincipal.AppRoles { if _, ok := requiredGraphRoleNames[*appRole.Value]; ok { @@ -201,7 +192,7 @@ func newManagedIdAction(cred *azidentity.DefaultAzureCredential, clt AccessGraph ResourceID: graphPrincipal.ID, } if err = clt.GrantAppRoleToServicePrincipal(ctx, roleAssignment); err != nil { - return trace.BadParameter("failed to assign graph API role to %s: %w", managedId, err) + return trace.BadParameter("failed to assign graph API role to %s: %v", managedId, err) } } } diff --git a/lib/integrations/azureoidc/accessgraph_sync_test.go b/lib/integrations/azureoidc/accessgraph_sync_test.go index 0c1875011186e..db6de6b00edd0 100644 --- a/lib/integrations/azureoidc/accessgraph_sync_test.go +++ b/lib/integrations/azureoidc/accessgraph_sync_test.go @@ -3,9 +3,43 @@ package azureoidc import ( "bytes" "context" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization" + "github.com/gravitational/teleport/lib/msgraph" "testing" ) +type mockAzureConfigClient struct { +} + +func (c *mockAzureConfigClient) CreateRoleDefinition(ctx context.Context, scope string, roleDefinition armauthorization.RoleDefinition) (string, error) { + roleId := "foo" + return roleId, nil +} + +func (c *mockAzureConfigClient) CreateRoleAssignment(ctx context.Context, scope string, roleAssignment armauthorization.RoleAssignmentCreateParameters) error { + return nil +} + +func (c *mockAzureConfigClient) GetServicePrincipalByAppId(ctx context.Context, appId string) (*msgraph.ServicePrincipal, error) { + spId := "foo" + appRoleValue := "bar" + return &msgraph.ServicePrincipal{ + DirectoryObject: msgraph.DirectoryObject{ + ID: &spId, + }, + AppRoles: []*msgraph.AppRole{ + { + ID: &appId, + Value: &appRoleValue, + }, + }, + }, nil +} + +func (c *mockAzureConfigClient) GrantAppRoleToServicePrincipal(ctx context.Context, roleAssignment msgraph.AppRoleAssignment) error { + return nil +} + func TestAccessGraphAzureConfigOutput(t *testing.T) { ctx := context.Background() var buf bytes.Buffer @@ -16,7 +50,8 @@ func TestAccessGraphAzureConfigOutput(t *testing.T) { AutoConfirm: true, stdout: &buf, } - err := ConfigureAccessGraphSyncAzure(ctx, req) + clt := &mockAzureConfigClient{} + err := ConfigureAccessGraphSyncAzure(ctx, clt, req) if err != nil { return }