diff --git a/provx/azure/azure.go b/provx/azure/azure.go new file mode 100644 index 0000000..1cd4752 --- /dev/null +++ b/provx/azure/azure.go @@ -0,0 +1,567 @@ +package azure + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "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" + msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" + graphapps "github.com/microsoftgraph/msgraph-sdk-go/applications" + graphmodels "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" + graphsps "github.com/microsoftgraph/msgraph-sdk-go/serviceprincipals" + "github.com/platform-engineering-labs/oox/provx" +) + +const ( + // resourceName is the well-known display name used for both the app + // registration and the federated identity credential. + resourceName = "formae-ai" + + oidcIssuer = "https://" + provx.Endpoint + tokenAudience = "api://AzureADTokenExchange" + + // contributorRoleDefinitionID is the built-in Contributor role. This GUID + // is stable across every Azure cloud and tenant. + contributorRoleDefinitionID = "b24988ac-6180-42a0-ab88-20f7382dd24c" + + // spPropagationTimeout bounds how long we wait for a freshly created + // service principal to become visible to ARM. + spPropagationTimeout = 3 * time.Minute +) + +// graphScope is the OAuth scope required for Microsoft Graph calls. +var graphScope = []string{"https://graph.microsoft.com/.default"} + +// roleAssignmentNamespace seeds the deterministic UUID used as the role +// assignment resource name, so repeated Create calls address the same object. +var roleAssignmentNamespace = uuid.MustParse("2b8f5a1c-3e4d-4a6b-9c0e-7f1d2a3b4c5d") + +// Azure manages the formae-ai identity resources in a single subscription. +type Azure struct { + logger *slog.Logger + + subscriptionId string + azTenantId string + + tenantId string + installationId string + + graph *msgraphsdk.GraphServiceClient + roleAssignments *armauthorization.RoleAssignmentsClient +} + +// New builds an Azure manager. +// +// The ambient credential (env vars, workload identity, managed identity, or +// `az login`) needs: +// - Microsoft Graph: Application.ReadWrite.All, or the "Application +// Administrator" directory role, to create and delete app registrations. +// - ARM: Microsoft.Authorization/roleAssignments/write and /delete at the +// subscription scope — "User Access Administrator" or "Owner". +func New(logger *slog.Logger, subscriptionId, azTenantId, tenantId, installationId string) (*Azure, error) { + if logger == nil { + logger = slog.Default() + } + + var missing []string + if subscriptionId == "" { + missing = append(missing, "subscriptionId") + } + if azTenantId == "" { + missing = append(missing, "azTenantId") + } + if tenantId == "" { + missing = append(missing, "tenantId") + } + if installationId == "" { + missing = append(missing, "installationId") + } + if len(missing) > 0 { + return nil, fmt.Errorf("azure: missing required argument(s): %s", strings.Join(missing, ", ")) + } + + cred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ + TenantID: azTenantId, + }) + if err != nil { + return nil, fmt.Errorf("azure: building credential: %w", err) + } + + graph, err := msgraphsdk.NewGraphServiceClientWithCredentials(cred, graphScope) + if err != nil { + return nil, fmt.Errorf("azure: building graph client: %w", err) + } + + factory, err := armauthorization.NewClientFactory(subscriptionId, cred, nil) + if err != nil { + return nil, fmt.Errorf("azure: building authorization client: %w", err) + } + + return &Azure{ + logger: logger.With("component", "azure", "subscription", subscriptionId), + subscriptionId: subscriptionId, + azTenantId: azTenantId, + tenantId: tenantId, + installationId: installationId, + graph: graph, + roleAssignments: factory.NewRoleAssignmentsClient(), + }, nil +} + +// scope is the ARM scope the Contributor grant is applied at. +func (az *Azure) scope() string { + return "/subscriptions/" + az.subscriptionId +} + +// roleDefinitionID is the fully qualified Contributor role definition. +func (az *Azure) roleDefinitionID() string { + return fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s", + az.subscriptionId, contributorRoleDefinitionID) +} + +// subject is the "sub" claim Formae-issued tokens will carry. +func (az *Azure) subject() string { + return fmt.Sprintf("fai:%s/%s", az.tenantId, az.installationId) +} + +// Create provisions the app registration, service principal, federated +// identity credential, and Contributor role assignment. Existing objects are +// adopted and reconciled rather than duplicated. +func (az *Azure) Create(ctx context.Context) error { + appObjectID, clientID, err := az.ensureApplication(ctx) + if err != nil { + return fmt.Errorf("azure: ensuring application %q: %w", resourceName, err) + } + + spObjectID, err := az.ensureServicePrincipal(ctx, clientID) + if err != nil { + return fmt.Errorf("azure: ensuring service principal for appId %s: %w", clientID, err) + } + + if err := az.ensureFederatedCredential(ctx, appObjectID); err != nil { + return fmt.Errorf("azure: ensuring federated credential %q: %w", resourceName, err) + } + + if err := az.ensureRoleAssignment(ctx, spObjectID); err != nil { + return fmt.Errorf("azure: ensuring Contributor assignment at %s: %w", az.scope(), err) + } + + az.logger.Info("azure connect resources ready", + "clientId", clientID, + "appObjectId", appObjectID, + "servicePrincipalId", spObjectID) + return nil +} + +// Delete removes the role assignment, service principal, and app registration. +// Missing objects are treated as already deleted. +func (az *Azure) Delete(ctx context.Context) error { + app, err := az.findApplication(ctx) + if err != nil { + return fmt.Errorf("azure: looking up application %q: %w", resourceName, err) + } + if app == nil { + az.logger.Info("no application to delete", "displayName", resourceName) + return nil + } + + appObjectID := deref(app.GetId()) + clientID := deref(app.GetAppId()) + + spObjectID, err := az.findServicePrincipal(ctx, clientID) + if err != nil { + return fmt.Errorf("azure: looking up service principal for appId %s: %w", clientID, err) + } + + // Role assignments first — an orphaned assignment pointing at a deleted + // principal is awkward to clean up afterwards. + if spObjectID != "" { + if err := az.deleteRoleAssignments(ctx, spObjectID); err != nil { + return fmt.Errorf("azure: deleting role assignments for %s: %w", spObjectID, err) + } + + if err := az.graph.ServicePrincipals(). + ByServicePrincipalId(spObjectID). + Delete(ctx, nil); err != nil && !isGraphNotFound(err) { + return fmt.Errorf("azure: deleting service principal %s: %w", spObjectID, graphErr(err)) + } + az.logger.Info("deleted service principal", "servicePrincipalId", spObjectID) + } + + // Deleting the application also removes its federated identity credentials. + if err := az.graph.Applications(). + ByApplicationId(appObjectID). + Delete(ctx, nil); err != nil && !isGraphNotFound(err) { + return fmt.Errorf("azure: deleting application %s: %w", appObjectID, graphErr(err)) + } + az.logger.Info("deleted application", "appObjectId", appObjectID, "clientId", clientID) + + return nil +} + +// --------------------------------------------------------------------------- +// application registration +// --------------------------------------------------------------------------- + +func (az *Azure) ensureApplication(ctx context.Context) (appObjectID, clientID string, err error) { + existing, err := az.findApplication(ctx) + if err != nil { + return "", "", err + } + if existing != nil { + az.logger.Debug("application already exists", + "appObjectId", deref(existing.GetId()), "clientId", deref(existing.GetAppId())) + return deref(existing.GetId()), deref(existing.GetAppId()), nil + } + + body := graphmodels.NewApplication() + body.SetDisplayName(to.Ptr(resourceName)) + body.SetSignInAudience(to.Ptr("AzureADMyOrg")) + + app, err := az.graph.Applications().Post(ctx, body, nil) + if err != nil { + return "", "", graphErr(err) + } + + appObjectID, clientID = deref(app.GetId()), deref(app.GetAppId()) + if appObjectID == "" || clientID == "" { + return "", "", errors.New("application response missing id or appId") + } + az.logger.Info("created application", "appObjectId", appObjectID, "clientId", clientID) + return appObjectID, clientID, nil +} + +func (az *Azure) findApplication(ctx context.Context) (graphmodels.Applicationable, error) { + cfg := &graphapps.ApplicationsRequestBuilderGetRequestConfiguration{ + QueryParameters: &graphapps.ApplicationsRequestBuilderGetQueryParameters{ + Filter: to.Ptr(fmt.Sprintf("displayName eq '%s'", odataEscape(resourceName))), + Select: []string{"id", "appId", "displayName"}, + Top: to.Ptr(int32(2)), + }, + } + + page, err := az.graph.Applications().Get(ctx, cfg) + if err != nil { + return nil, graphErr(err) + } + + apps := page.GetValue() + switch len(apps) { + case 0: + return nil, nil + case 1: + return apps[0], nil + default: + return nil, fmt.Errorf("found %d applications named %q; "+ + "refusing to guess which one to manage", len(apps), resourceName) + } +} + +// --------------------------------------------------------------------------- +// service principal +// --------------------------------------------------------------------------- + +func (az *Azure) ensureServicePrincipal(ctx context.Context, clientID string) (string, error) { + spObjectID, err := az.findServicePrincipal(ctx, clientID) + if err != nil { + return "", err + } + if spObjectID != "" { + az.logger.Debug("service principal already exists", "servicePrincipalId", spObjectID) + return spObjectID, nil + } + + body := graphmodels.NewServicePrincipal() + body.SetAppId(to.Ptr(clientID)) + + sp, err := az.graph.ServicePrincipals().Post(ctx, body, nil) + if err != nil { + // A concurrent run may have won the race. + if isGraphConflict(err) { + if id, lookupErr := az.findServicePrincipal(ctx, clientID); lookupErr == nil && id != "" { + return id, nil + } + } + return "", graphErr(err) + } + + spObjectID = deref(sp.GetId()) + if spObjectID == "" { + return "", errors.New("service principal response missing id") + } + az.logger.Info("created service principal", "servicePrincipalId", spObjectID) + return spObjectID, nil +} + +func (az *Azure) findServicePrincipal(ctx context.Context, clientID string) (string, error) { + if clientID == "" { + return "", nil + } + + cfg := &graphsps.ServicePrincipalsRequestBuilderGetRequestConfiguration{ + QueryParameters: &graphsps.ServicePrincipalsRequestBuilderGetQueryParameters{ + Filter: to.Ptr(fmt.Sprintf("appId eq '%s'", odataEscape(clientID))), + Select: []string{"id", "appId"}, + Top: to.Ptr(int32(1)), + }, + } + + page, err := az.graph.ServicePrincipals().Get(ctx, cfg) + if err != nil { + return "", graphErr(err) + } + if sps := page.GetValue(); len(sps) > 0 { + return deref(sps[0].GetId()), nil + } + return "", nil +} + +// --------------------------------------------------------------------------- +// federated identity credential +// --------------------------------------------------------------------------- + +func (az *Azure) ensureFederatedCredential(ctx context.Context, appObjectID string) error { + creds, err := az.graph.Applications(). + ByApplicationId(appObjectID). + FederatedIdentityCredentials(). + Get(ctx, nil) + if err != nil { + return graphErr(err) + } + + audiences := []string{tokenAudience} + subject := az.subject() + + for _, existing := range creds.GetValue() { + if deref(existing.GetName()) != resourceName { + continue + } + + // The name matches but issuer/subject/audiences are mutable, so + // reconcile instead of leaving a stale credential in place. This is + // what makes Create safe to re-run after an installationId change. + if deref(existing.GetIssuer()) == oidcIssuer && + deref(existing.GetSubject()) == subject && + sameStrings(existing.GetAudiences(), audiences) { + az.logger.Debug("federated credential already current", "name", resourceName) + return nil + } + + patch := graphmodels.NewFederatedIdentityCredential() + patch.SetIssuer(to.Ptr(oidcIssuer)) + patch.SetSubject(to.Ptr(subject)) + patch.SetAudiences(audiences) + + if _, err := az.graph.Applications(). + ByApplicationId(appObjectID). + FederatedIdentityCredentials(). + ByFederatedIdentityCredentialId(deref(existing.GetId())). + Patch(ctx, patch, nil); err != nil { + return graphErr(err) + } + az.logger.Info("updated federated credential", "name", resourceName, "subject", subject) + return nil + } + + body := graphmodels.NewFederatedIdentityCredential() + body.SetName(to.Ptr(resourceName)) + body.SetIssuer(to.Ptr(oidcIssuer)) + body.SetSubject(to.Ptr(subject)) + body.SetAudiences(audiences) + + if _, err := az.graph.Applications(). + ByApplicationId(appObjectID). + FederatedIdentityCredentials(). + Post(ctx, body, nil); err != nil { + return graphErr(err) + } + az.logger.Info("created federated credential", "name", resourceName, "subject", subject) + return nil +} + +// --------------------------------------------------------------------------- +// role assignment +// --------------------------------------------------------------------------- + +func (az *Azure) ensureRoleAssignment(ctx context.Context, spObjectID string) error { + scope := az.scope() + roleDefID := az.roleDefinitionID() + + // Deterministic name so re-runs address the same assignment resource. + seed := strings.Join([]string{scope, spObjectID, roleDefID}, "|") + name := uuid.NewSHA1(roleAssignmentNamespace, []byte(seed)).String() + + params := armauthorization.RoleAssignmentCreateParameters{ + Properties: &armauthorization.RoleAssignmentProperties{ + PrincipalID: to.Ptr(spObjectID), + RoleDefinitionID: to.Ptr(roleDefID), + PrincipalType: to.Ptr(armauthorization.PrincipalTypeServicePrincipal), + }, + } + + deadline := time.Now().Add(spPropagationTimeout) + backoff := 2 * time.Second + + for { + resp, err := az.roleAssignments.Create(ctx, scope, name, params, nil) + if err == nil { + az.logger.Info("created role assignment", "role", "Contributor", "id", deref(resp.ID)) + return nil + } + + switch armErrorCode(err) { + case "RoleAssignmentExists": + az.logger.Debug("role assignment already exists", "role", "Contributor") + return nil + + case "PrincipalNotFound", "PrincipalTypeNotSupported": + // Entra ID replication lag: ARM cannot see the new SP yet. + if time.Now().After(deadline) { + return fmt.Errorf("service principal %s did not propagate within %s: %w", + spObjectID, spPropagationTimeout, err) + } + az.logger.Debug("waiting for service principal to propagate", "backoff", backoff) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoff): + } + if backoff < 15*time.Second { + backoff *= 2 + } + + default: + return err + } + } +} + +// deleteRoleAssignments removes every Contributor grant this package could have +// created for the principal at the subscription scope. Grants for other roles, +// or at other scopes, are left alone. +func (az *Azure) deleteRoleAssignments(ctx context.Context, spObjectID string) error { + pager := az.roleAssignments.NewListForScopePager(az.scope(), + &armauthorization.RoleAssignmentsClientListForScopeOptions{ + Filter: to.Ptr(fmt.Sprintf("principalId eq '%s'", spObjectID)), + }) + + wantRole := strings.ToLower(az.roleDefinitionID()) + + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return err + } + for _, ra := range page.Value { + if ra == nil || ra.ID == nil || ra.Properties == nil { + continue + } + if !strings.EqualFold(deref(ra.Properties.RoleDefinitionID), wantRole) { + continue + } + if _, err := az.roleAssignments.DeleteByID(ctx, *ra.ID, nil); err != nil { + if armErrorCode(err) == "RoleAssignmentNotFound" || armStatusCode(err) == 404 { + continue + } + return err + } + az.logger.Info("deleted role assignment", "id", *ra.ID) + } + } + return nil +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +func deref(s *string) string { + if s == nil { + return "" + } + return *s +} + +func sameStrings(have, want []string) bool { + if len(have) != len(want) { + return false + } + seen := make(map[string]struct{}, len(want)) + for _, w := range want { + seen[w] = struct{}{} + } + for _, h := range have { + if _, ok := seen[h]; !ok { + return false + } + } + return true +} + +// odataEscape escapes single quotes for OData $filter string literals. +func odataEscape(s string) string { + return strings.ReplaceAll(s, "'", "''") +} + +// graphErr unwraps a Microsoft Graph ODataError, whose default Error() string +// is an unhelpful "error status code received from the API". +func graphErr(err error) error { + var odErr *odataerrors.ODataError + if !errors.As(err, &odErr) { + return err + } + detail := odErr.GetErrorEscaped() + if detail == nil { + return err + } + return fmt.Errorf("graph %s: %s", deref(detail.GetCode()), deref(detail.GetMessage())) +} + +func isGraphNotFound(err error) bool { + var odErr *odataerrors.ODataError + if errors.As(err, &odErr) { + return odErr.ResponseStatusCode == 404 + } + return false +} + +func isGraphConflict(err error) bool { + var odErr *odataerrors.ODataError + if !errors.As(err, &odErr) { + return false + } + if odErr.ResponseStatusCode == 409 { + return true + } + if detail := odErr.GetErrorEscaped(); detail != nil { + code := deref(detail.GetCode()) + return code == "Request_MultipleObjectsWithSameKeyValue" || + strings.Contains(code, "AlreadyExists") + } + return false +} + +func armErrorCode(err error) string { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) { + return respErr.ErrorCode + } + return "" +} + +func armStatusCode(err error) int { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) { + return respErr.StatusCode + } + return 0 +} diff --git a/provx/go.mod b/provx/go.mod index adf83a6..9abd2cf 100644 --- a/provx/go.mod +++ b/provx/go.mod @@ -3,10 +3,15 @@ module github.com/platform-engineering-labs/oox/provx go 1.26.0 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0 github.com/aws/aws-sdk-go-v2 v1.43.7 github.com/aws/aws-sdk-go-v2/config v1.32.38 github.com/aws/aws-sdk-go-v2/service/iam v1.59.2 github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 + github.com/google/uuid v1.6.0 + github.com/microsoftgraph/msgraph-sdk-go v1.101.0 google.golang.org/api v0.293.0 ) @@ -14,6 +19,8 @@ require ( cloud.google.com/go/auth v0.23.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.37 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 // indirect @@ -29,10 +36,21 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect github.com/googleapis/gax-go/v2 v2.23.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/microsoft/kiota-abstractions-go v1.9.4 // indirect + github.com/microsoft/kiota-authentication-azure-go v1.3.1 // indirect + github.com/microsoft/kiota-http-go v1.5.6 // indirect + github.com/microsoft/kiota-serialization-form-go v1.1.3 // indirect + github.com/microsoft/kiota-serialization-json-go v1.1.2 // indirect + github.com/microsoft/kiota-serialization-multipart-go v1.1.2 // indirect + github.com/microsoft/kiota-serialization-text-go v1.1.3 // indirect + github.com/microsoftgraph/msgraph-sdk-go-core v1.4.1 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect diff --git a/provx/go.sum b/provx/go.sum index 6eb1c75..ba9f98e 100644 --- a/provx/go.sum +++ b/provx/go.sum @@ -4,6 +4,20 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0 h1:4gRPBpN1f6xt88yi4WR26m7XaD9OlWtVT6bWPdGUIok= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.0/go.mod h1:G7QVLxw1j1JVyrO1MA95S8m8HStaaleDZYTcfGgjB2o= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +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/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/aws/aws-sdk-go-v2 v1.43.7 h1:msCzvkeYJA9ehbV8mRRmkZLo/zJg/+yDVLNtflg83hQ= github.com/aws/aws-sdk-go-v2 v1.43.7/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00= github.com/aws/aws-sdk-go-v2/config v1.32.38 h1:n4yPHBjtQ3BrIIUyk0/LAqf/BL2iv0Tw6XZcMRzM0ps= @@ -45,6 +59,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -57,8 +73,34 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.20 h1:t/xL64VUoN69MuMRQu github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/microsoft/kiota-abstractions-go v1.9.4 h1:VI3UVzSCQHHhRswe3jyaAQHUQWIFhUMp0z5mtZbTbcs= +github.com/microsoft/kiota-abstractions-go v1.9.4/go.mod h1:f06pl3qSyvUHEfVNkiRpXPkafx7khZqQEb71hN/pmuU= +github.com/microsoft/kiota-authentication-azure-go v1.3.1 h1:AGta92S6IL1E6ZMDb8YYB7NVNTIFUakbtLKUdY5RTuw= +github.com/microsoft/kiota-authentication-azure-go v1.3.1/go.mod h1:26zylt2/KfKwEWZSnwHaMxaArpbyN/CuzkbotdYXF0g= +github.com/microsoft/kiota-http-go v1.5.6 h1:KBdk7sxWYXZnRRExLjIcNt4I7LoOfh/XQJWWid4zBKE= +github.com/microsoft/kiota-http-go v1.5.6/go.mod h1:bpJkXfBAcnmiXRg03GXdnb/vF3Sqk3+EgLvXXjmzzQM= +github.com/microsoft/kiota-serialization-form-go v1.1.3 h1:eUY8eHXPFe4ma8cAdx0ya3g4NPlZgbPT+GlFC3xcgGY= +github.com/microsoft/kiota-serialization-form-go v1.1.3/go.mod h1:RMO99zyik+NvZjdVcIeyu6ikyfuKhQtzq2RK0fWJJio= +github.com/microsoft/kiota-serialization-json-go v1.1.2 h1:eJrPWeQ665nbjO0gsHWJ0Bw6V/ZHHU1OfFPaYfRG39k= +github.com/microsoft/kiota-serialization-json-go v1.1.2/go.mod h1:deaGt7fjZarywyp7TOTiRsjfYiyWxwJJPQZytXwYQn8= +github.com/microsoft/kiota-serialization-multipart-go v1.1.2 h1:1pUyA1QgIeKslQwbk7/ox1TehjlCUUT3r1f8cNlkvn4= +github.com/microsoft/kiota-serialization-multipart-go v1.1.2/go.mod h1:j2K7ZyYErloDu7Kuuk993DsvfoP7LPWvAo7rfDpdPio= +github.com/microsoft/kiota-serialization-text-go v1.1.3 h1:8z7Cebn0YAAr++xswVgfdxZjnAZ4GOB9O7XP4+r5r/M= +github.com/microsoft/kiota-serialization-text-go v1.1.3/go.mod h1:NDSvz4A3QalGMjNboKKQI9wR+8k+ih8UuagNmzIRgTQ= +github.com/microsoftgraph/msgraph-sdk-go v1.101.0 h1:9Ox6mlDTm9BroNpj9i4B241OIcnyv0kjwJHHNaeXkoY= +github.com/microsoftgraph/msgraph-sdk-go v1.101.0/go.mod h1:qxzY5SaoPigY6/Dpyfg4uigQjNDvL+sZl6fzD6EpWeQ= +github.com/microsoftgraph/msgraph-sdk-go-core v1.4.1 h1:k3YIaJm57ufoEX0KdsEY4l1X9BAMxEqrwr4a7WMRDzY= +github.com/microsoftgraph/msgraph-sdk-go-core v1.4.1/go.mod h1:yNqPNhXee2w9cZzkJW5mL1utVMSInsQSo/TyEB5sup8= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 h1:7hth9376EoQEd1hH4lAp3vnaLP2UMyxuMMghLKzDHyU= +github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3/go.mod h1:Z5KcoM0YLC7INlNhEezeIZ0TZNYf7WSNO0Lvah4DSeQ= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -83,6 +125,7 @@ golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=