From 1a791755fded8474f1664a226b61b809654330e6 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Fri, 10 Jan 2025 09:21:47 -0600 Subject: [PATCH] Azure integration command (#47541) * Initial command to create the managed identity and role * Adding permissions and applying command params * Adding graph permissions to the MSI * Updating parameters * Adding some details and cleaning up comments * Fixing go.sum * Linting * License * PR feedback * Decoupling sync config with an interface for testing * Tweaks to test mocking * PR feedback * Rebase adjustments * PR feedback * Switch to empty struct maps instead of bool maps for set representation * Godocs --- go.mod | 1 + go.sum | 2 + integrations/terraform/go.sum | 2 + lib/config/configuration.go | 17 ++ ..._graph_aws_sync.go => accessgraph_sync.go} | 0 ..._sync_test.go => accessgraph_sync_test.go} | 0 .../azureoidc/accessgraph_sync.go | 258 ++++++++++++++++++ .../azureoidc/accessgraph_sync_test.go | 142 ++++++++++ lib/kube/proxy/resource_filters_test.go | 40 +-- lib/msgraph/client.go | 13 +- lib/msgraph/models.go | 12 +- tool/teleport/common/integration_configure.go | 16 ++ tool/teleport/common/teleport.go | 18 +- 13 files changed, 492 insertions(+), 29 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 create mode 100644 lib/integrations/azureoidc/accessgraph_sync_test.go diff --git a/go.mod b/go.mod index 3c35132910093..168e6f92a6c0c 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( connectrpc.com/connect v1.18.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.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.2.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 5665c4f7280c7..4363b60320fe7 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/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.2.0 h1:JAebRMoc3vL+Nd97GBprHYHucO4+wlW+tNbBIumqJlk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.2.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= diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 106e4e41c759b..d1d69898fab8c 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/v2 v2.2.0 h1:Hp+EScFOu9HeCbeW8WU2yQPJd4gGwhMgKxWe+G6jNzw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.2.0 h1:JAebRMoc3vL+Nd97GBprHYHucO4+wlW+tNbBIumqJlk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.2.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= diff --git a/lib/config/configuration.go b/lib/config/configuration.go index dda2ac6859cf4..83ad9f3971a5f 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -242,6 +242,10 @@ 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 // `teleport integration configure azure-oidc` command IntegrationConfAzureOIDCArguments IntegrationConfAzureOIDC @@ -274,6 +278,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 + // 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 +} + // 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..c5b6db92f0723 --- /dev/null +++ b/lib/integrations/azureoidc/accessgraph_sync.go @@ -0,0 +1,258 @@ +/* + * Teleport + * 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 + * 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 ( + "context" + "io" + "maps" + "slices" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2" + "github.com/google/uuid" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/cloud/provisioning" + "github.com/gravitational/teleport/lib/msgraph" + libslices "github.com/gravitational/teleport/lib/utils/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" + +// requiredGraphRoleNames is the list of Graph API roles required for the managed identity to fetch resources from Azure +var requiredGraphRoleNames = map[string]struct{}{ + "User.ReadBasic.All": {}, + "Group.Read.All": {}, + "Directory.Read.All": {}, + "User.Read.All": {}, + "Policy.Read.All": {}, +} + +// AccessGraphAzureConfigureClient provides an interface for granting the managed identity the necessary permissions +// to fetch Azure resources +type AccessGraphAzureConfigureClient interface { + // CreateRoleDefinition creates an Azure role definition + CreateRoleDefinition(ctx context.Context, scope string, roleDefinition armauthorization.RoleDefinition) (string, error) + // CreateRoleAssignment assigns a role to an Azure principal + CreateRoleAssignment(ctx context.Context, scope string, roleAssignment armauthorization.RoleAssignmentCreateParameters) error + // GetServicePrincipalByAppID returns a service principal based on its application ID + GetServicePrincipalByAppID(ctx context.Context, appID string) (*msgraph.ServicePrincipal, error) + // GrantAppRoleToServicePrincipal grants a specific type of application role to a service principal + GrantAppRoleToServicePrincipal(ctx context.Context, roleAssignment msgraph.AppRoleAssignment) error +} + +// azureConfigClient wraps the role definition, role assignments, and Graph API clients +type azureConfigClient struct { + roleDefCli *armauthorization.RoleDefinitionsClient + roleAssignCli *armauthorization.RoleAssignmentsClient + graphCli *msgraph.Client +} + +// NewAzureConfigClient returns a new config client for granting the managed identity the necessary permissions +// to fetch Azure resources +func NewAzureConfigClient(subscriptionID 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: %v", err) + } + roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subscriptionID, cred, nil) + if err != nil { + 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: %v", err) + } + return &azureConfigClient{ + roleDefCli: roleDefCli, + roleAssignCli: roleAssignCli, + graphCli: graphCli, + }, nil +} + +// CreateRoleDefinition creates an Azure role definition +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 +} + +// CreateRoleAssignment assigns a role to an Azure principal +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 +} + +// GetServicePrincipalByAppID returns a service principal based on its application ID +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: %v", err) + } + return graphPrincipal, nil +} + +// GrantAppRoleToServicePrincipal grants a specific type of application role to a service principal +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 +} + +// roleAssignmentAction assigns both the Azure role and Graph API roles to the managed identity +func roleAssignmentAction(clt AccessGraphAzureConfigureClient, subscriptionID string, managedID string, roleName string) (*provisioning.Action, error) { + customRole := "CustomRole" + scope := "/subscriptions/" + subscriptionID + runnerFn := func(ctx context.Context) error { + // Create the role + roleDefinition := armauthorization.RoleDefinition{ + Name: &roleName, + Properties: &armauthorization.RoleDefinitionProperties{ + RoleName: &roleName, + RoleType: &customRole, + Permissions: []*armauthorization.Permission{ + { + Actions: libslices.ToPointers([]string{ + "Microsoft.Compute/virtualMachines/read", + "Microsoft.Compute/virtualMachineScaleSets/virtualMachines/read", + "Microsoft.Authorization/roleDefinitions/read", + "Microsoft.Authorization/roleAssignments/read", + }), + }, + }, + AssignableScopes: []*string{&scope}, // Scope must be provided + }, + } + roleID, err := clt.CreateRoleDefinition(ctx, scope, roleDefinition) + if err != nil { + return trace.Errorf("failed to create custom role: %v", err) + } + + // Assign the new role to the managed identity + roleAssignParams := armauthorization.RoleAssignmentCreateParameters{ + Properties: &armauthorization.RoleAssignmentProperties{ + PrincipalID: &managedID, + RoleDefinitionID: &roleID, + }, + } + if err = clt.CreateRoleAssignment(ctx, scope, roleAssignParams); err != nil { + return trace.Errorf("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.Errorf("could not get the graph API service principal: %v", err) + } + rolesNotAssigned := make(map[string]struct{}) + for k, v := range requiredGraphRoleNames { + rolesNotAssigned[k] = v + } + for _, appRole := range graphPrincipal.AppRoles { + 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.Errorf("failed to assign graph API role to %s: %v", managedID, err) + } + delete(rolesNotAssigned, *appRole.Value) + } + } + if len(rolesNotAssigned) > 0 { + return trace.Errorf("could not assign all required roles: %v", slices.Collect(maps.Keys(rolesNotAssigned))) + } + return nil + } + cfg := provisioning.ActionConfig{ + Name: "AssignRole", + 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:\n\n", + "\t1) Azure resource permissions in order to fetch virtual machines, role definitions, etc, and\n", + "\t2) Graph API permissions to fetch users, groups, and service principals.\n\n", + "The command assigns both Azure resource permissions as well as Graph API permissions to the specified ", + "managed identity.", + }, ""), + RunnerFn: runnerFn, + } + return provisioning.NewAction(cfg) +} + +// ConfigureAccessGraphSyncAzure sets up the managed identity and role required for Teleport to be able to pull +// Azure resources into Teleport. +func ConfigureAccessGraphSyncAzure(ctx context.Context, clt AccessGraphAzureConfigureClient, req AccessGraphAzureConfigureRequest) error { + managedIDAction, err := roleAssignmentAction(clt, req.SubscriptionID, req.ManagedIdentity, req.RoleName) + if err != nil { + return trace.Wrap(err) + } + opCfg := provisioning.OperationConfig{ + Name: "access-graph-azure-sync", + Actions: []provisioning.Action{ + *managedIDAction, + }, + 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..91020aab1c2e4 --- /dev/null +++ b/lib/integrations/azureoidc/accessgraph_sync_test.go @@ -0,0 +1,142 @@ +/* + * Teleport + * 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 + * 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 ( + "bytes" + "context" + "fmt" + "maps" + "slices" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2" + "github.com/google/uuid" + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/lib/msgraph" +) + +type mockClientConfig struct { + createRoleErr bool + assignRoleErr bool + fetchPrincipalErr bool + grantAppRoleErr bool +} + +type mockAzureConfigClient struct { + cfg mockClientConfig +} + +func (c *mockAzureConfigClient) CreateRoleDefinition(ctx context.Context, scope string, roleDefinition armauthorization.RoleDefinition) (string, error) { + if c.cfg.createRoleErr { + return "", trace.Errorf("failed to create role definition") + } + return "foo", nil +} + +func (c *mockAzureConfigClient) CreateRoleAssignment(ctx context.Context, scope string, roleAssignment armauthorization.RoleAssignmentCreateParameters) error { + if c.cfg.assignRoleErr { + return trace.Errorf("failed to assign role") + } + return nil +} + +func (c *mockAzureConfigClient) GetServicePrincipalByAppID(ctx context.Context, appID string) (*msgraph.ServicePrincipal, error) { + if c.cfg.fetchPrincipalErr { + return nil, trace.Errorf("failed to fetch principal") + } + spID := uuid.NewString() + appRoleValues := slices.Collect(maps.Keys(requiredGraphRoleNames)) + var roles []*msgraph.AppRole + for _, rv := range appRoleValues { + roleID := uuid.NewString() + roles = append(roles, &msgraph.AppRole{ + ID: &roleID, + Value: &rv, + }) + } + return &msgraph.ServicePrincipal{ + DirectoryObject: msgraph.DirectoryObject{ + ID: &spID, + }, + AppRoles: roles, + }, nil +} + +func (c *mockAzureConfigClient) GrantAppRoleToServicePrincipal(ctx context.Context, roleAssignment msgraph.AppRoleAssignment) error { + if c.cfg.grantAppRoleErr { + return fmt.Errorf("failed to grant app role") + } + return nil +} + +func TestAccessGraphAzureConfigOutput(t *testing.T) { + ctx := context.Background() + for _, tt := range []struct { + clientCfg mockClientConfig + hasError bool + }{ + { + clientCfg: mockClientConfig{}, + hasError: false, + }, + { + clientCfg: mockClientConfig{ + createRoleErr: true, + }, + hasError: true, + }, + { + clientCfg: mockClientConfig{ + assignRoleErr: true, + }, + hasError: true, + }, + { + clientCfg: mockClientConfig{ + fetchPrincipalErr: true, + }, + hasError: true, + }, + { + clientCfg: mockClientConfig{ + grantAppRoleErr: true, + }, + hasError: true, + }, + } { + var buf bytes.Buffer + req := AccessGraphAzureConfigureRequest{ + ManagedIdentity: "foo", + RoleName: "bar", + SubscriptionID: "1234567890", + AutoConfirm: true, + stdout: &buf, + } + clt := &mockAzureConfigClient{ + cfg: tt.clientCfg, + } + err := ConfigureAccessGraphSyncAzure(ctx, clt, req) + if !tt.hasError { + require.NoError(t, err) + } + } +} 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 26ea34e1d45c2..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,17 @@ 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) + 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"` diff --git a/tool/teleport/common/integration_configure.go b/tool/teleport/common/integration_configure.go index 97f531910e45e..26f8d93896853 100644 --- a/tool/teleport/common/integration_configure.go +++ b/tool/teleport/common/integration_configure.go @@ -241,6 +241,22 @@ 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) + 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 { // 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 15e96fc949346..02d45ed828632 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 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", "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) + 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. @@ -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():