diff --git a/lib/config/configuration.go b/lib/config/configuration.go
index 45d3544012cfe..ee377cd41f8bb 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..91731ca98bd39
--- /dev/null
+++ b/lib/integrations/azureoidc/accessgraph_sync.go
@@ -0,0 +1,276 @@
+/*
+ * 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/azcore"
+ armpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+ "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"
+
+// azureUserAgent defines the user agent for the Azure SDK to better identify misbehaving clients
+const azureUserAgent = "teleport"
+
+// 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) {
+ telemetryOpts := policy.TelemetryOptions{
+ ApplicationID: azureUserAgent,
+ }
+ opts := &armpolicy.ClientOptions{
+ ClientOptions: policy.ClientOptions{
+ Telemetry: telemetryOpts,
+ },
+ }
+ cred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{
+ ClientOptions: azcore.ClientOptions{
+ Telemetry: telemetryOpts,
+ },
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ roleDefCli, err := armauthorization.NewRoleDefinitionsClient(cred, opts)
+ if err != nil {
+ return nil, trace.BadParameter("failed to create role definitions client: %v", err)
+ }
+ roleAssignCli, err := armauthorization.NewRoleAssignmentsClient(subscriptionID, cred, opts)
+ 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():