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/event-handler/go.mod b/integrations/event-handler/go.mod
index 19d919b359e39..2a4ec93e2f6ac 100644
--- a/integrations/event-handler/go.mod
+++ b/integrations/event-handler/go.mod
@@ -37,6 +37,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0 // indirect
diff --git a/integrations/event-handler/go.sum b/integrations/event-handler/go.sum
index 1f0435df0d184..fbd5df9b4923f 100644
--- a/integrations/event-handler/go.sum
+++ b/integrations/event-handler/go.sum
@@ -631,6 +631,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/integrations/terraform/go.mod b/integrations/terraform/go.mod
index 5222dc914a105..3f0a69be92443 100644
--- a/integrations/terraform/go.mod
+++ b/integrations/terraform/go.mod
@@ -43,6 +43,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6 v6.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.2.0 // indirect
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/cloud/azure/roleassignments.go b/lib/cloud/azure/roleassignments.go
new file mode 100644
index 0000000000000..114bceef88b96
--- /dev/null
+++ b/lib/cloud/azure/roleassignments.go
@@ -0,0 +1,57 @@
+/*
+ * 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 azure
+
+import (
+ "context"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
+ "github.com/gravitational/trace"
+)
+
+// RoleAssignmentsClient wraps the Azure API to provide a high level subset of functionality
+type RoleAssignmentsClient struct {
+ cli *armauthorization.RoleAssignmentsClient
+}
+
+// NewRoleAssignmentsClient creates a new client for a given subscription and credentials
+func NewRoleAssignmentsClient(subscription string, cred azcore.TokenCredential, options *arm.ClientOptions) (*RoleAssignmentsClient, error) {
+ clientFactory, err := armauthorization.NewClientFactory(subscription, cred, options)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ roleDefCli := clientFactory.NewRoleAssignmentsClient()
+ return &RoleAssignmentsClient{cli: roleDefCli}, nil
+}
+
+// ListRoleAssignments returns role assignments for a given scope
+func (c *RoleAssignmentsClient) ListRoleAssignments(ctx context.Context, scope string) ([]*armauthorization.RoleAssignment, error) {
+ pager := c.cli.NewListForScopePager(scope, nil)
+ var roleDefs []*armauthorization.RoleAssignment
+ for pager.More() {
+ page, err := pager.NextPage(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ roleDefs = append(roleDefs, page.Value...)
+ }
+ return roleDefs, nil
+}
diff --git a/lib/cloud/azure/roledefinitions.go b/lib/cloud/azure/roledefinitions.go
new file mode 100644
index 0000000000000..cdc46196aa530
--- /dev/null
+++ b/lib/cloud/azure/roledefinitions.go
@@ -0,0 +1,57 @@
+/*
+ * 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 azure
+
+import (
+ "context"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
+ "github.com/gravitational/trace"
+)
+
+// RoleDefinitionsClient wraps the Azure API to provide a high level subset of functionality
+type RoleDefinitionsClient struct {
+ cli *armauthorization.RoleDefinitionsClient
+}
+
+// NewRoleDefinitionsClient creates a new client for a given subscription and credentials
+func NewRoleDefinitionsClient(subscription string, cred azcore.TokenCredential, options *arm.ClientOptions) (*RoleDefinitionsClient, error) {
+ clientFactory, err := armauthorization.NewClientFactory(subscription, cred, options)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ roleDefCli := clientFactory.NewRoleDefinitionsClient()
+ return &RoleDefinitionsClient{cli: roleDefCli}, nil
+}
+
+// ListRoleDefinitions returns role definitions for a given scope
+func (c *RoleDefinitionsClient) ListRoleDefinitions(ctx context.Context, scope string) ([]*armauthorization.RoleDefinition, error) {
+ pager := c.cli.NewListPager(scope, nil)
+ var roleDefs []*armauthorization.RoleDefinition
+ for pager.More() {
+ page, err := pager.NextPage(ctx)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ roleDefs = append(roleDefs, page.Value...)
+ }
+ return roleDefs, nil
+}
diff --git a/lib/cloud/clients.go b/lib/cloud/clients.go
index 99c2deb4001f0..ee4c228ddbb31 100644
--- a/lib/cloud/clients.go
+++ b/lib/cloud/clients.go
@@ -352,6 +352,10 @@ type azureClients struct {
azurePostgresFlexServersClients azure.ClientMap[azure.PostgresFlexServersClient]
// azureRunCommandClients contains the cached Azure Run Command clients.
azureRunCommandClients azure.ClientMap[azure.RunCommandClient]
+ // azureRoleDefinitionsClients contains the cached Azure Role Definitions clients.
+ azureRoleDefinitionsClients azure.ClientMap[azure.RoleDefinitionsClient]
+ // azureRoleAssignmentsClients contains the cached Azure Role Assignments clients.
+ azureRoleAssignmentsClients azure.ClientMap[azure.RoleAssignmentsClient]
}
// credentialsSource defines where the credentials must come from.
@@ -743,6 +747,16 @@ func (c *cloudClients) GetAzureRunCommandClient(subscription string) (azure.RunC
return c.azureRunCommandClients.Get(subscription, c.GetAzureCredential)
}
+// GetAzureRoleDefinitionsClient returns an Azure Role Definitions client
+func (c *cloudClients) GetAzureRoleDefinitionsClient(subscription string) (azure.RoleDefinitionsClient, error) {
+ return c.azureRoleDefinitionsClients.Get(subscription, c.GetAzureCredential)
+}
+
+// GetAzureRoleAssignmentsClient returns an Azure Role Assignments client
+func (c *cloudClients) GetAzureRoleAssignmentsClient(subscription string) (azure.RoleAssignmentsClient, error) {
+ return c.azureRoleAssignmentsClients.Get(subscription, c.GetAzureCredential)
+}
+
// Close closes all initialized clients.
func (c *cloudClients) Close() (err error) {
c.mtx.Lock()
@@ -1050,6 +1064,8 @@ type TestCloudClients struct {
AzureMySQLFlex azure.MySQLFlexServersClient
AzurePostgresFlex azure.PostgresFlexServersClient
AzureRunCommand azure.RunCommandClient
+ AzureRoleDefinitions azure.RoleDefinitionsClient
+ AzureRoleAssignments azure.RoleAssignmentsClient
}
// GetAWSSession returns AWS session for the specified region, optionally
@@ -1294,11 +1310,21 @@ func (c *TestCloudClients) GetAzurePostgresFlexServersClient(subscription string
return c.AzurePostgresFlex, nil
}
-// GetAzureRunCommand returns an Azure Run Command client for the given subscription.
+// GetAzureRunCommandClient returns an Azure Run Command client for the given subscription.
func (c *TestCloudClients) GetAzureRunCommandClient(subscription string) (azure.RunCommandClient, error) {
return c.AzureRunCommand, nil
}
+// GetAzureRoleDefinitionsClient returns an Azure Role Definitions client for the given subscription.
+func (c *TestCloudClients) GetAzureRoleDefinitionsClient(subscription string) (azure.RoleDefinitionsClient, error) {
+ return c.AzureRoleDefinitions, nil
+}
+
+// GetAzureRoleAssignmentsClient returns an Azure Role Assignments client for the given subscription.
+func (c *TestCloudClients) GetAzureRoleAssignmentsClient(subscription string) (azure.RoleAssignmentsClient, error) {
+ return c.AzureRoleAssignments, nil
+}
+
// Close closes all initialized clients.
func (c *TestCloudClients) Close() error {
return nil
diff --git a/lib/msgraph/paginated.go b/lib/msgraph/paginated.go
index 51c587f19d074..a0b9488af9d70 100644
--- a/lib/msgraph/paginated.go
+++ b/lib/msgraph/paginated.go
@@ -101,6 +101,14 @@ func (c *Client) IterateUsers(ctx context.Context, f func(*User) bool) error {
return iterateSimple(c, ctx, "users", f)
}
+// IterateServicePrincipals lists all service principals in the Entra ID directory using pagination.
+// `f` will be called for each object in the result set.
+// if `f` returns `false`, the iteration is stopped (equivalent to `break` in a normal loop).
+// Ref: [https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list].
+func (c *Client) IterateServicePrincipals(ctx context.Context, f func(principal *ServicePrincipal) bool) error {
+ return iterateSimple(c, ctx, "servicePrincipals", f)
+}
+
// IterateGroupMembers lists all members for the given Entra ID group using pagination.
// `f` will be called for each object in the result set.
// if `f` returns `false`, the iteration is stopped (equivalent to `break` in a normal loop).
diff --git a/lib/srv/discovery/fetchers/azure-sync/memberships.go b/lib/srv/discovery/fetchers/azure-sync/memberships.go
new file mode 100644
index 0000000000000..f05be8f72567c
--- /dev/null
+++ b/lib/srv/discovery/fetchers/azure-sync/memberships.go
@@ -0,0 +1,65 @@
+/*
+ * 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 azuresync
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+ "golang.org/x/sync/errgroup"
+
+ accessgraphv1alpha "github.com/gravitational/teleport/gen/proto/go/accessgraph/v1alpha"
+ "github.com/gravitational/teleport/lib/msgraph"
+)
+
+const parallelism = 10 //nolint:unused // invoked in a dependent PR
+
+// expandMemberships adds membership data to AzurePrincipal objects by querying the Graph API for group memberships
+func expandMemberships(ctx context.Context, cli *msgraph.Client, principals []*accessgraphv1alpha.AzurePrincipal) ([]*accessgraphv1alpha.AzurePrincipal, error) { //nolint:unused // invoked in a dependent PR
+ // Map principals by ID
+ var principalsMap = make(map[string]*accessgraphv1alpha.AzurePrincipal)
+ for _, principal := range principals {
+ principalsMap[principal.Id] = principal
+ }
+ // Iterate through the Azure groups and add the group ID as a membership for its corresponding principal
+ eg, _ := errgroup.WithContext(ctx)
+ eg.SetLimit(parallelism)
+ errCh := make(chan error, len(principals))
+ for _, principal := range principals {
+ if principal.ObjectType != "group" {
+ continue
+ }
+ group := principal
+ eg.Go(func() error {
+ err := cli.IterateGroupMembers(ctx, group.Id, func(member msgraph.GroupMember) bool {
+ if memberPrincipal, ok := principalsMap[*member.GetID()]; ok {
+ memberPrincipal.MemberOf = append(memberPrincipal.MemberOf, group.Id)
+ }
+ return true
+ })
+ if err != nil {
+ errCh <- err
+ }
+ return nil
+ })
+ }
+ _ = eg.Wait()
+ close(errCh)
+ return principals, trace.NewAggregateFromChannel(errCh, ctx)
+}
diff --git a/lib/srv/discovery/fetchers/azure-sync/principals.go b/lib/srv/discovery/fetchers/azure-sync/principals.go
new file mode 100644
index 0000000000000..073d6c4713e0c
--- /dev/null
+++ b/lib/srv/discovery/fetchers/azure-sync/principals.go
@@ -0,0 +1,87 @@
+/*
+ * 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 azuresync
+
+import (
+ "context"
+
+ "github.com/gravitational/trace"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ accessgraphv1alpha "github.com/gravitational/teleport/gen/proto/go/accessgraph/v1alpha"
+ "github.com/gravitational/teleport/lib/msgraph"
+)
+
+type dirObjMetadata struct { //nolint:unused // invoked in a dependent PR
+ objectType string
+}
+
+type queryResult struct { //nolint:unused // invoked in a dependent PR
+ metadata dirObjMetadata
+ dirObj msgraph.DirectoryObject
+}
+
+// fetchPrincipals fetches the Azure principals (users, groups, and service principals) using the Graph API
+func fetchPrincipals(ctx context.Context, subscriptionID string, cli *msgraph.Client) ([]*accessgraphv1alpha.AzurePrincipal, error) { //nolint: unused // invoked in a dependent PR
+ // Fetch the users, groups, and service principals as directory objects
+ var queryResults []queryResult
+ err := cli.IterateUsers(ctx, func(user *msgraph.User) bool {
+ res := queryResult{metadata: dirObjMetadata{objectType: "user"}, dirObj: user.DirectoryObject}
+ queryResults = append(queryResults, res)
+ return true
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ err = cli.IterateGroups(ctx, func(group *msgraph.Group) bool {
+ res := queryResult{metadata: dirObjMetadata{objectType: "group"}, dirObj: group.DirectoryObject}
+ queryResults = append(queryResults, res)
+ return true
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ err = cli.IterateServicePrincipals(ctx, func(servicePrincipal *msgraph.ServicePrincipal) bool {
+ res := queryResult{metadata: dirObjMetadata{objectType: "servicePrincipal"}, dirObj: servicePrincipal.DirectoryObject}
+ queryResults = append(queryResults, res)
+ return true
+ })
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // Return the users, groups, and service principals as protobuf messages
+ var fetchErrs []error
+ var pbPrincipals []*accessgraphv1alpha.AzurePrincipal
+ for _, res := range queryResults {
+ if res.dirObj.ID == nil || res.dirObj.DisplayName == nil {
+ fetchErrs = append(fetchErrs,
+ trace.BadParameter("nil values on msgraph directory object: %v", res.dirObj))
+ continue
+ }
+ pbPrincipals = append(pbPrincipals, &accessgraphv1alpha.AzurePrincipal{
+ Id: *res.dirObj.ID,
+ SubscriptionId: subscriptionID,
+ LastSyncTime: timestamppb.Now(),
+ DisplayName: *res.dirObj.DisplayName,
+ ObjectType: res.metadata.objectType,
+ })
+ }
+ return pbPrincipals, trace.NewAggregate(fetchErrs...)
+}
diff --git a/lib/srv/discovery/fetchers/azure-sync/roleassignments.go b/lib/srv/discovery/fetchers/azure-sync/roleassignments.go
new file mode 100644
index 0000000000000..a97fe69727ef8
--- /dev/null
+++ b/lib/srv/discovery/fetchers/azure-sync/roleassignments.go
@@ -0,0 +1,68 @@
+/*
+ * 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 azuresync
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
+ "github.com/gravitational/trace"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ accessgraphv1alpha "github.com/gravitational/teleport/gen/proto/go/accessgraph/v1alpha"
+)
+
+// RoleAssignmentsClient specifies the methods used to fetch role assignments from Azure
+type RoleAssignmentsClient interface {
+ ListRoleAssignments(ctx context.Context, scope string) ([]*armauthorization.RoleAssignment, error)
+}
+
+// fetchRoleAssignments fetches Azure role assignments using the Azure role assignments API
+func fetchRoleAssignments(ctx context.Context, subscriptionID string, cli RoleAssignmentsClient) ([]*accessgraphv1alpha.AzureRoleAssignment, error) { //nolint:unused // invoked in a dependent PR
+ // List the role definitions
+ roleAssigns, err := cli.ListRoleAssignments(ctx, fmt.Sprintf("/subscriptions/%s", subscriptionID))
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // Convert to protobuf format
+ pbRoleAssigns := make([]*accessgraphv1alpha.AzureRoleAssignment, 0, len(roleAssigns))
+ var fetchErrs []error
+ for _, roleAssign := range roleAssigns {
+ if roleAssign.ID == nil ||
+ roleAssign.Properties == nil ||
+ roleAssign.Properties.PrincipalID == nil ||
+ roleAssign.Properties.Scope == nil {
+ fetchErrs = append(fetchErrs,
+ trace.BadParameter("nil values on AzureRoleAssignment object: %v", roleAssign))
+ continue
+ }
+ pbRoleAssign := &accessgraphv1alpha.AzureRoleAssignment{
+ Id: *roleAssign.ID,
+ SubscriptionId: subscriptionID,
+ LastSyncTime: timestamppb.Now(),
+ PrincipalId: *roleAssign.Properties.PrincipalID,
+ RoleDefinitionId: *roleAssign.Properties.RoleDefinitionID,
+ Scope: *roleAssign.Properties.Scope,
+ }
+ pbRoleAssigns = append(pbRoleAssigns, pbRoleAssign)
+ }
+ return pbRoleAssigns, trace.NewAggregate(fetchErrs...)
+}
diff --git a/lib/srv/discovery/fetchers/azure-sync/roledefinitions.go b/lib/srv/discovery/fetchers/azure-sync/roledefinitions.go
new file mode 100644
index 0000000000000..485117f898b81
--- /dev/null
+++ b/lib/srv/discovery/fetchers/azure-sync/roledefinitions.go
@@ -0,0 +1,78 @@
+/*
+ * 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 azuresync
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
+ "github.com/gravitational/trace"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ accessgraphv1alpha "github.com/gravitational/teleport/gen/proto/go/accessgraph/v1alpha"
+ "github.com/gravitational/teleport/lib/utils/slices"
+)
+
+// RoleDefinitionsClient specifies the methods used to fetch roles from Azure
+type RoleDefinitionsClient interface {
+ ListRoleDefinitions(ctx context.Context, scope string) ([]*armauthorization.RoleDefinition, error)
+}
+
+func fetchRoleDefinitions(ctx context.Context, subscriptionID string, cli RoleDefinitionsClient) ([]*accessgraphv1alpha.AzureRoleDefinition, error) { //nolint:unused // used in a dependent PR
+ // List the role definitions
+ roleDefs, err := cli.ListRoleDefinitions(ctx, fmt.Sprintf("/subscriptions/%s", subscriptionID))
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // Convert to protobuf format
+ pbRoleDefs := make([]*accessgraphv1alpha.AzureRoleDefinition, 0, len(roleDefs))
+ var fetchErrs []error
+ for _, roleDef := range roleDefs {
+ if roleDef.ID == nil ||
+ roleDef.Properties == nil ||
+ roleDef.Properties.Permissions == nil ||
+ roleDef.Properties.RoleName == nil {
+ fetchErrs = append(fetchErrs, trace.BadParameter("nil values on AzureRoleDefinition object: %v", roleDef))
+ continue
+ }
+ pbPerms := make([]*accessgraphv1alpha.AzureRBACPermission, 0, len(roleDef.Properties.Permissions))
+ for _, perm := range roleDef.Properties.Permissions {
+ if perm.Actions == nil && perm.NotActions == nil {
+ fetchErrs = append(fetchErrs, trace.BadParameter("nil values on Permission object: %v", perm))
+ continue
+ }
+ pbPerm := accessgraphv1alpha.AzureRBACPermission{
+ Actions: slices.FromPointers(perm.Actions),
+ NotActions: slices.FromPointers(perm.NotActions),
+ }
+ pbPerms = append(pbPerms, &pbPerm)
+ }
+ pbRoleDef := &accessgraphv1alpha.AzureRoleDefinition{
+ Id: *roleDef.ID,
+ Name: *roleDef.Properties.RoleName,
+ SubscriptionId: subscriptionID,
+ LastSyncTime: timestamppb.Now(),
+ Permissions: pbPerms,
+ }
+ pbRoleDefs = append(pbRoleDefs, pbRoleDef)
+ }
+ return pbRoleDefs, trace.NewAggregate(fetchErrs...)
+}
diff --git a/lib/srv/discovery/fetchers/azure-sync/virtualmachines.go b/lib/srv/discovery/fetchers/azure-sync/virtualmachines.go
new file mode 100644
index 0000000000000..cf0d068db7b0c
--- /dev/null
+++ b/lib/srv/discovery/fetchers/azure-sync/virtualmachines.go
@@ -0,0 +1,61 @@
+/*
+ * 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 azuresync
+
+import (
+ "context"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v6"
+ "github.com/gravitational/trace"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ accessgraphv1alpha "github.com/gravitational/teleport/gen/proto/go/accessgraph/v1alpha"
+)
+
+const allResourceGroups = "*" //nolint:unused // invoked in a dependent PR
+
+// VirtualMachinesClient specifies the methods used to fetch virtual machines from Azure
+type VirtualMachinesClient interface {
+ ListVirtualMachines(ctx context.Context, resourceGroup string) ([]*armcompute.VirtualMachine, error)
+}
+
+func fetchVirtualMachines(ctx context.Context, subscriptionID string, cli VirtualMachinesClient) ([]*accessgraphv1alpha.AzureVirtualMachine, error) { //nolint:unused // invoked in a dependent PR
+ vms, err := cli.ListVirtualMachines(ctx, allResourceGroups)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ // Return the VMs as protobuf messages
+ pbVms := make([]*accessgraphv1alpha.AzureVirtualMachine, 0, len(vms))
+ var fetchErrs []error
+ for _, vm := range vms {
+ if vm.ID == nil || vm.Name == nil {
+ fetchErrs = append(fetchErrs, trace.BadParameter("nil values on AzureVirtualMachine object: %v", vm))
+ continue
+ }
+ pbVm := accessgraphv1alpha.AzureVirtualMachine{
+ Id: *vm.ID,
+ SubscriptionId: subscriptionID,
+ LastSyncTime: timestamppb.Now(),
+ Name: *vm.Name,
+ }
+ pbVms = append(pbVms, &pbVm)
+ }
+ return pbVms, trace.NewAggregate(fetchErrs...)
+}