From 1d508cb7e8953eec40c91cfe9141e39254a05ab3 Mon Sep 17 00:00:00 2001 From: Matt Brock Date: Fri, 3 Jan 2025 15:48:53 -0600 Subject: [PATCH] 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"`