From 8e3df595cdcaeee38d4c1018893f3aef47d76e0a Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Tue, 15 Sep 2026 22:45:46 +0000 Subject: [PATCH 1/5] feat(ext): expose current principal through the Account API Resolve resource-tenant object IDs with the host's principal lookup and share login-based principal type resolution with provisioning. Recognize system-assigned managed identities in login details. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c24d9e3-50ce-4517-960f-a5813a362002 --- .../docs/extensions/extension-framework.md | 34 +++ .../extensions/extension-sdk-reference.md | 6 + .../proto/azd/extensions/v1/account.proto | 21 ++ .../proto/azd/extensions/v1beta/account.proto | 21 ++ .../internal/grpcserver/account_service.go | 68 ++++- .../grpcserver/account_service_test.go | 252 +++++++++++++++++- .../versioned_services_generated.go | 23 ++ cli/azd/pkg/auth/manager.go | 7 +- cli/azd/pkg/auth/manager_test.go | 27 ++ cli/azd/pkg/auth/principal.go | 33 +++ cli/azd/pkg/azdext/contracts/v1/account.pb.go | 206 ++++++++++++-- .../azdext/contracts/v1/account_grpc.pb.go | 44 ++- .../pkg/azdext/contracts/v1beta/account.pb.go | 206 ++++++++++++-- .../contracts/v1beta/account_grpc.pb.go | 44 ++- cli/azd/pkg/azdext/contracts_v1_aliases.go | 9 + .../current_principal_id_provider.go | 18 +- docs/architecture/extension-framework.md | 2 +- docs/guides/creating-an-extension.md | 2 + 18 files changed, 958 insertions(+), 65 deletions(-) create mode 100644 cli/azd/pkg/auth/principal.go diff --git a/cli/azd/docs/extensions/extension-framework.md b/cli/azd/docs/extensions/extension-framework.md index c25b4736b78..d1b33538a91 100644 --- a/cli/azd/docs/extensions/extension-framework.md +++ b/cli/azd/docs/extensions/extension-framework.md @@ -3060,6 +3060,40 @@ func getSubscriptionDetails(ctx context.Context, azdClient *azdext.AzdClient, su - Validate subscription access before performing operations - Set up proper authentication context for Azure SDK calls +#### GetCurrentPrincipal + +Resolves the current identity for role assignments in a specified subscription. The host returns the object ID in the subscription's resource tenant, which can differ from a guest user's home-tenant object ID. Unlike `LookupTenant`, this method uses the resource tenant rather than the user access tenant. + +| Field | Description | +|---|---| +| Request `subscription_id` | Required subscription ID. No active environment or default subscription is used. | +| Response `object_id` | Object ID of the signed-in identity in the resource tenant, not an application client ID. | +| Response `principal_type` | `PRINCIPAL_TYPE_USER` or `PRINCIPAL_TYPE_SERVICE_PRINCIPAL`, determined from azd's login details. | + +The host reuses its principal lookup, including the ARM token `oid` claim and Graph fallback. Service-principal logins and both system-assigned and user-assigned managed identities return `PRINCIPAL_TYPE_SERVICE_PRINCIPAL`. Access tokens are neither accepted nor returned by this RPC. An empty subscription ID returns `InvalidArgument`; authentication, subscription, and principal lookup failures return errors rather than an empty identity. + +```go +principal, err := azdClient.Account().GetCurrentPrincipal(ctx, &azdext.GetCurrentPrincipalRequest{ + SubscriptionId: subscriptionId, +}) +if err != nil { + return fmt.Errorf("resolving current principal: %w", err) +} + +var principalType string +switch principal.PrincipalType { +case azdext.PrincipalType_PRINCIPAL_TYPE_USER: + principalType = "User" +case azdext.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL: + principalType = "ServicePrincipal" +default: + return fmt.Errorf("unsupported principal type: %v", principal.PrincipalType) +} +// Pass principal.ObjectId and principalType to the role assignment. +``` + +This method is available in both `v1` and `v1beta`. Older azd hosts return `Unimplemented`. Extensions must consume an SDK release containing the method and require a host release that supports it before removing their existing principal lookup. + --- ### Copilot Service diff --git a/cli/azd/docs/extensions/extension-sdk-reference.md b/cli/azd/docs/extensions/extension-sdk-reference.md index c073942e545..e346c4e18ff 100644 --- a/cli/azd/docs/extensions/extension-sdk-reference.md +++ b/cli/azd/docs/extensions/extension-sdk-reference.md @@ -530,6 +530,12 @@ Always call `defer client.Close()` after creation. request, response, and enum types. They are intentionally excluded from the stable `azdext` contract facade until those services graduate to `v1`. +#### AccountService + +`Account().GetCurrentPrincipal(ctx, &azdext.GetCurrentPrincipalRequest{SubscriptionId: subscriptionID})` returns the current identity's `ObjectId` in the subscription's resource tenant and its `PrincipalType` enum. Use both values for role assignments instead of decoding access tokens in the extension. The subscription ID is required, and no active environment is needed. + +See [GetCurrentPrincipal](extension-framework.md#getcurrentprincipal) for the enum mapping, guest-user behavior, and host compatibility requirements. + #### TelemetryService `Telemetry().ReportUsage(ctx, &v1beta.ReportUsageRequest{EventName, Attributes})` diff --git a/cli/azd/grpc/proto/azd/extensions/v1/account.proto b/cli/azd/grpc/proto/azd/extensions/v1/account.proto index 09fb5c04508..77f7b4f0d7c 100644 --- a/cli/azd/grpc/proto/azd/extensions/v1/account.proto +++ b/cli/azd/grpc/proto/azd/extensions/v1/account.proto @@ -14,6 +14,9 @@ service AccountService { // LookupTenant resolves the tenant ID required to access a specific subscription. rpc LookupTenant (LookupTenantRequest) returns (LookupTenantResponse); + + // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. + rpc GetCurrentPrincipal (GetCurrentPrincipalRequest) returns (GetCurrentPrincipalResponse); } message ListSubscriptionsRequest { @@ -34,3 +37,21 @@ message LookupTenantResponse { // The tenant ID required to access the subscription. string tenant_id = 1; } + +message GetCurrentPrincipalRequest { + // Required subscription ID. The active environment is not used as a default. + string subscription_id = 1; +} + +message GetCurrentPrincipalResponse { + // Object ID in the subscription's resource tenant, not the user's home tenant or an application client ID. + string object_id = 1; + // Principal type determined from the host's login details. + PrincipalType principal_type = 2; +} + +enum PrincipalType { + PRINCIPAL_TYPE_UNSPECIFIED = 0; + PRINCIPAL_TYPE_USER = 1; + PRINCIPAL_TYPE_SERVICE_PRINCIPAL = 2; +} diff --git a/cli/azd/grpc/proto/azd/extensions/v1beta/account.proto b/cli/azd/grpc/proto/azd/extensions/v1beta/account.proto index e51b207c5f8..7614a60f3ee 100644 --- a/cli/azd/grpc/proto/azd/extensions/v1beta/account.proto +++ b/cli/azd/grpc/proto/azd/extensions/v1beta/account.proto @@ -14,6 +14,9 @@ service AccountService { // LookupTenant resolves the tenant ID required to access a specific subscription. rpc LookupTenant (LookupTenantRequest) returns (LookupTenantResponse); + + // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. + rpc GetCurrentPrincipal (GetCurrentPrincipalRequest) returns (GetCurrentPrincipalResponse); } message ListSubscriptionsRequest { @@ -34,3 +37,21 @@ message LookupTenantResponse { // The tenant ID required to access the subscription. string tenant_id = 1; } + +message GetCurrentPrincipalRequest { + // Required subscription ID. The active environment is not used as a default. + string subscription_id = 1; +} + +message GetCurrentPrincipalResponse { + // Object ID in the subscription's resource tenant, not the user's home tenant or an application client ID. + string object_id = 1; + // Principal type determined from the host's login details. + PrincipalType principal_type = 2; +} + +enum PrincipalType { + PRINCIPAL_TYPE_UNSPECIFIED = 0; + PRINCIPAL_TYPE_USER = 1; + PRINCIPAL_TYPE_SERVICE_PRINCIPAL = 2; +} diff --git a/cli/azd/internal/grpcserver/account_service.go b/cli/azd/internal/grpcserver/account_service.go index 59cdfa2d9e0..f55afbfd1e9 100644 --- a/cli/azd/internal/grpcserver/account_service.go +++ b/cli/azd/internal/grpcserver/account_service.go @@ -5,22 +5,84 @@ package grpcserver import ( "context" + "fmt" + "strings" "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/auth" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/azureutil" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type accountService struct { azdext.UnimplementedAccountServiceServer - subscriptionsManager *account.SubscriptionsManager + subscriptionsManager interface { + account.SubscriptionResolver + GetSubscriptions(context.Context) ([]account.Subscription, error) + LookupTenant(context.Context, string) (string, error) + } + userProfileService *azapi.UserProfileService + principalTypeProvider interface { + CurrentPrincipalType(context.Context) (auth.PrincipalType, error) + } } -func NewAccountService(subscriptionsManager *account.SubscriptionsManager) azdext.AccountServiceServer { +func NewAccountService( + subscriptionsManager *account.SubscriptionsManager, + userProfileService *azapi.UserProfileService, + authManager *auth.Manager, +) azdext.AccountServiceServer { return &accountService{ - subscriptionsManager: subscriptionsManager, + subscriptionsManager: subscriptionsManager, + userProfileService: userProfileService, + principalTypeProvider: authManager, + } +} + +func (s *accountService) GetCurrentPrincipal( + ctx context.Context, + req *azdext.GetCurrentPrincipalRequest, +) (*azdext.GetCurrentPrincipalResponse, error) { + if strings.TrimSpace(req.GetSubscriptionId()) == "" { + return nil, status.Error(codes.InvalidArgument, "subscription id is required") + } + + principalType, err := s.principalTypeProvider.CurrentPrincipalType(ctx) + if err != nil { + return nil, err } + + var protoType azdext.PrincipalType + switch principalType { + case auth.UserPrincipalType: + protoType = azdext.PrincipalType_PRINCIPAL_TYPE_USER + case auth.ServicePrincipalType: + protoType = azdext.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL + default: + return nil, status.Error(codes.Internal, "unsupported current principal type") + } + + subscription, err := s.subscriptionsManager.GetSubscription(ctx, req.SubscriptionId) + if err != nil { + return nil, fmt.Errorf("getting subscription %s: %w", req.SubscriptionId, err) + } + + // Role assignments need the object ID in the resource tenant, even when access uses another tenant. + objectID, err := azureutil.GetCurrentPrincipalId(ctx, s.userProfileService, subscription.TenantId) + if err != nil { + return nil, fmt.Errorf("fetching current principal information: %w", err) + } + if objectID == "" { + return nil, status.Error(codes.Internal, "current principal object id is empty") + } + + return &azdext.GetCurrentPrincipalResponse{ + ObjectId: objectID, + PrincipalType: protoType, + }, nil } func (s *accountService) ListSubscriptions( diff --git a/cli/azd/internal/grpcserver/account_service_test.go b/cli/azd/internal/grpcserver/account_service_test.go index 8ba52f6f01c..1ada4cd73fc 100644 --- a/cli/azd/internal/grpcserver/account_service_test.go +++ b/cli/azd/internal/grpcserver/account_service_test.go @@ -4,23 +4,43 @@ package grpcserver import ( + "context" + "errors" + "net/http" + "sync/atomic" "testing" + "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + azcloud "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/auth" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + v1beta "github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta" + "github.com/azure/azure-dev/cli/azd/pkg/cloud" + "github.com/azure/azure-dev/cli/azd/pkg/extensions" + "github.com/azure/azure-dev/cli/azd/pkg/graphsdk" + "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" ) func TestNewAccountService(t *testing.T) { t.Parallel() - svc := NewAccountService(nil) + svc := NewAccountService(nil, nil, nil) require.NotNil(t, svc) } func TestAccountService_LookupTenant_EmptySubscriptionId(t *testing.T) { t.Parallel() - svc := NewAccountService(nil) + svc := NewAccountService(nil, nil, nil) _, err := svc.LookupTenant(t.Context(), &azdext.LookupTenantRequest{ SubscriptionId: "", }) @@ -30,3 +50,231 @@ func TestAccountService_LookupTenant_EmptySubscriptionId(t *testing.T) { require.Equal(t, codes.InvalidArgument, st.Code()) require.Contains(t, st.Message(), "subscription id is required") } + +type mockAccountSubscriptions struct { + *account.SubscriptionsManager + mock.Mock +} + +func (m *mockAccountSubscriptions) GetSubscription( + ctx context.Context, subscriptionID string, +) (*account.Subscription, error) { + args := m.Called(ctx, subscriptionID) + subscription, _ := args.Get(0).(*account.Subscription) + return subscription, args.Error(1) +} + +type mockAccountPrincipalType struct { + mock.Mock +} + +func (m *mockAccountPrincipalType) CurrentPrincipalType(ctx context.Context) (auth.PrincipalType, error) { + args := m.Called(ctx) + return args.Get(0).(auth.PrincipalType), args.Error(1) +} + +func TestAccountService_GetCurrentPrincipal(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + accessTenant string + principalType auth.PrincipalType + protoType azdext.PrincipalType + }{ + {"user", "resource-tenant", auth.UserPrincipalType, azdext.PrincipalType_PRINCIPAL_TYPE_USER}, + {"guest", "home-tenant", auth.UserPrincipalType, azdext.PrincipalType_PRINCIPAL_TYPE_USER}, + { + "service principal", "resource-tenant", auth.ServicePrincipalType, + azdext.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL, + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + subscriptions := &mockAccountSubscriptions{} + subscriptions.On("GetSubscription", mock.Anything, "sub-123").Return(&account.Subscription{ + Id: "sub-123", TenantId: "resource-tenant", UserAccessTenantId: tt.accessTenant, + }, nil).Twice() + principalTypes := &mockAccountPrincipalType{} + principalTypes.On("CurrentPrincipalType", mock.Anything).Return(tt.principalType, nil).Twice() + + mockContext := mocks.NewMockContext(ctx) + azureCloud := cloud.AzurePublic() + armScope := azureCloud.Configuration.Services[azcloud.ResourceManager].Audience + "/.default" + var tokenCalls atomic.Int32 + userProfile := azapi.NewUserProfileService( + &mocks.MockMultiTenantCredentialProvider{TokenMap: map[string]mocks.MockCredentials{ + "resource-tenant": { + GetTokenFn: func( + ctx context.Context, options policy.TokenRequestOptions, + ) (azcore.AccessToken, error) { + tokenCalls.Add(1) + assert.Equal(t, []string{armScope}, options.Scopes) + // No principal-type claims: the host must use login details, not token heuristics. + return azcore.AccessToken{ + Token: mocks.CreateJwtToken(t, map[string]string{"oid": "resource-object-id"}), + ExpiresOn: time.Now().Add(time.Hour), + }, nil + }, + }, + "home-tenant": { + GetTokenFn: func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) { + t.Error("principal lookup must not acquire a home-tenant token") + return azcore.AccessToken{}, errors.New("unexpected home tenant") + }, + }, + }}, + &azcore.ClientOptions{Transport: mockContext.HttpClient}, + azureCloud, + ) + svc := &accountService{ + subscriptionsManager: subscriptions, userProfileService: userProfile, principalTypeProvider: principalTypes, + } + + server := newServerWithContainerService(azdext.UnimplementedContainerServiceServer{}) + server.accountService = svc + info, err := server.Start() + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, server.Stop()) }) + token, err := GenerateExtensionToken(&extensions.Extension{Id: "azd.internal.test", Namespace: "test"}, info) + require.NoError(t, err) + ctx = azdext.WithAccessToken(ctx, token) + client, err := azdext.NewAzdClient(azdext.WithAddress(info.Address)) + require.NoError(t, err) + t.Cleanup(client.Close) + + response, err := client.Account().GetCurrentPrincipal(ctx, &azdext.GetCurrentPrincipalRequest{ + SubscriptionId: "sub-123", + }) + require.NoError(t, err) + require.Equal(t, "resource-object-id", response.ObjectId) + require.Equal(t, tt.protoType, response.PrincipalType) + + connection, err := grpc.NewClient(info.Address, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, connection.Close()) }) + betaResponse, err := v1beta.NewAccountServiceClient(connection).GetCurrentPrincipal( + ctx, &v1beta.GetCurrentPrincipalRequest{SubscriptionId: "sub-123"}, + ) + require.NoError(t, err) + require.Equal(t, response.ObjectId, betaResponse.ObjectId) + require.Equal(t, int32(response.PrincipalType), int32(betaResponse.PrincipalType)) + require.EqualValues(t, 2, tokenCalls.Load()) + subscriptions.AssertExpectations(t) + principalTypes.AssertExpectations(t) + }) + } +} + +func TestAccountService_GetCurrentPrincipal_InvalidRequest(t *testing.T) { + t.Parallel() + for _, request := range []*azdext.GetCurrentPrincipalRequest{ + nil, {}, {SubscriptionId: " \t"}, + } { + // No dependencies: validation must run before authentication or subscription lookup. + response, err := NewAccountService(nil, nil, nil).GetCurrentPrincipal(t.Context(), request) + require.Nil(t, response) + require.Equal(t, codes.InvalidArgument, status.Code(err)) + } +} + +func TestAccountService_GetCurrentPrincipal_LookupErrors(t *testing.T) { + t.Parallel() + subscriptionErr := errors.New("subscription unavailable") + for _, tt := range []struct { + name string + principalType auth.PrincipalType + loginErr error + subErr error + wantErr error + wantCode codes.Code + }{ + {name: "not logged in", loginErr: auth.ErrNoCurrentUser, wantErr: auth.ErrNoCurrentUser}, + {name: "login cancelled", loginErr: context.Canceled, wantErr: context.Canceled}, + {name: "unsupported type", principalType: "unknown", wantCode: codes.Internal}, + {name: "subscription", principalType: auth.UserPrincipalType, subErr: subscriptionErr, wantErr: subscriptionErr}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + subscriptions := &mockAccountSubscriptions{} + if tt.subErr != nil { + subscriptions.On("GetSubscription", t.Context(), "sub-123").Return(nil, tt.subErr).Once() + } + principalTypes := &mockAccountPrincipalType{} + principalTypes.On("CurrentPrincipalType", t.Context()).Return(tt.principalType, tt.loginErr).Once() + svc := &accountService{principalTypeProvider: principalTypes, subscriptionsManager: subscriptions} + response, err := svc.GetCurrentPrincipal(t.Context(), &azdext.GetCurrentPrincipalRequest{ + SubscriptionId: "sub-123", + }) + require.Nil(t, response) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + } else { + require.Equal(t, tt.wantCode, status.Code(err)) + } + subscriptions.AssertExpectations(t) + principalTypes.AssertExpectations(t) + }) + } +} + +func TestAccountService_GetCurrentPrincipal_GraphFallback(t *testing.T) { + t.Parallel() + for _, tt := range []struct { + name string + graphID string + statusCode int + wantError string + }{ + {name: "success", graphID: "graph-object-id", statusCode: http.StatusOK}, + {name: "empty object id", statusCode: http.StatusOK, wantError: "current principal object id is empty"}, + {name: "lookup failure", statusCode: http.StatusForbidden, wantError: "fetching current principal information"}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + subscriptions := &mockAccountSubscriptions{} + subscriptions.On("GetSubscription", t.Context(), "sub-123").Return(&account.Subscription{ + Id: "sub-123", TenantId: "resource-tenant", UserAccessTenantId: "home-tenant", + }, nil).Once() + principalTypes := &mockAccountPrincipalType{} + principalTypes.On("CurrentPrincipalType", t.Context()).Return(auth.UserPrincipalType, nil).Once() + mockContext := mocks.NewMockContext(t.Context()) + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodGet && request.URL.Host == "graph.microsoft.com" + }).RespondFn(func(request *http.Request) (*http.Response, error) { + return mocks.CreateHttpResponseWithBody(request, tt.statusCode, &graphsdk.UserProfile{Id: tt.graphID}) + }) + userProfile := azapi.NewUserProfileService( + &mocks.MockMultiTenantCredentialProvider{TokenMap: map[string]mocks.MockCredentials{ + "resource-tenant": { + GetTokenFn: func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) { + return azcore.AccessToken{ + Token: mocks.CreateJwtToken(t, map[string]string{}), ExpiresOn: time.Now().Add(time.Hour), + }, nil + }, + }, + }}, + &azcore.ClientOptions{Transport: mockContext.HttpClient}, + cloud.AzurePublic(), + ) + svc := &accountService{ + subscriptionsManager: subscriptions, principalTypeProvider: principalTypes, userProfileService: userProfile, + } + response, err := svc.GetCurrentPrincipal(t.Context(), &azdext.GetCurrentPrincipalRequest{ + SubscriptionId: "sub-123", + }) + if tt.wantError != "" { + require.ErrorContains(t, err, tt.wantError) + require.Nil(t, response) + } else { + require.NoError(t, err) + require.Equal(t, tt.graphID, response.ObjectId) + require.Equal(t, azdext.PrincipalType_PRINCIPAL_TYPE_USER, response.PrincipalType) + } + subscriptions.AssertExpectations(t) + principalTypes.AssertExpectations(t) + }) + } +} diff --git a/cli/azd/internal/grpcserver/versioned_services_generated.go b/cli/azd/internal/grpcserver/versioned_services_generated.go index 84c7d249961..ba9b4f8fc44 100644 --- a/cli/azd/internal/grpcserver/versioned_services_generated.go +++ b/cli/azd/internal/grpcserver/versioned_services_generated.go @@ -65,6 +65,11 @@ type BetaAccountServiceLookupTenantOverride interface { LookupTenant(context.Context, *v1beta.LookupTenantRequest) (*v1beta.LookupTenantResponse, error) } +// BetaAccountServiceGetCurrentPrincipalOverride overrides the beta AccountService.GetCurrentPrincipal method before stable adaptation. +type BetaAccountServiceGetCurrentPrincipalOverride interface { + GetCurrentPrincipal(context.Context, *v1beta.GetCurrentPrincipalRequest) (*v1beta.GetCurrentPrincipalResponse, error) +} + func validateBetaAccountServiceOverride(override any) error { return validateBetaServiceOverride( "AccountService", @@ -72,6 +77,7 @@ func validateBetaAccountServiceOverride(override any) error { reflect.TypeFor[v1beta.AccountServiceServer](), reflect.TypeFor[BetaAccountServiceListSubscriptionsOverride](), reflect.TypeFor[BetaAccountServiceLookupTenantOverride](), + reflect.TypeFor[BetaAccountServiceGetCurrentPrincipalOverride](), ) } @@ -847,6 +853,23 @@ func (a *betaAccountServiceAdapter) LookupTenant( ) } +func (a *betaAccountServiceAdapter) GetCurrentPrincipal( + ctx context.Context, + req *v1beta.GetCurrentPrincipalRequest, +) (*v1beta.GetCurrentPrincipalResponse, error) { + if override, ok := a.override.(BetaAccountServiceGetCurrentPrincipalOverride); ok { + return override.GetCurrentPrincipal(ctx, req) + } + return adaptBetaUnary( + ctx, + req, + new(v1.GetCurrentPrincipalRequest), + a.stable.GetCurrentPrincipal, + new(v1beta.GetCurrentPrincipalResponse), + "AccountService.GetCurrentPrincipal", + ) +} + type betaAiModelServiceAdapter struct { v1beta.UnimplementedAiModelServiceServer stable v1.AiModelServiceServer diff --git a/cli/azd/pkg/auth/manager.go b/cli/azd/pkg/auth/manager.go index 73ef83056f0..0fb9e71103f 100644 --- a/cli/azd/pkg/auth/manager.go +++ b/cli/azd/pkg/auth/manager.go @@ -1475,7 +1475,8 @@ type LogInDetails struct { // outbound HTTP call) and derive the account identifier from the token claims. // When running in Azure Cloud Shell and no azd-managed user is logged in, // it derives the account from the ambient Cloud Shell credential and reports -// an authenticated user. +// an authenticated user. System-assigned managed identities report a client ID-based +// login with an empty account identifier because no client ID was configured. func (m *Manager) LogInDetails(ctx context.Context) (*LogInDetails, error) { if m.UseExternalAuth() { claims, err := m.ClaimsForCurrentUser(ctx, nil) @@ -1567,6 +1568,10 @@ func (m *Manager) LogInDetails(ctx context.Context) (*LogInDetails, error) { LoginType: ClientIdLoginType, Account: *currentUser.ClientID, }, nil + } else if currentUser.ManagedIdentity { + return &LogInDetails{ + LoginType: ClientIdLoginType, + }, nil } return nil, ErrNoCurrentUser diff --git a/cli/azd/pkg/auth/manager_test.go b/cli/azd/pkg/auth/manager_test.go index dfedaa954bf..078d30609e2 100644 --- a/cli/azd/pkg/auth/manager_test.go +++ b/cli/azd/pkg/auth/manager_test.go @@ -1060,6 +1060,14 @@ func TestLoginWithManagedIdentity(t *testing.T) { cred2, err := m.CredentialForCurrentUser(t.Context(), nil) require.NoError(t, err) require.IsType(t, new(azidentity.ManagedIdentityCredential), cred2) + + details, err := m.LogInDetails(t.Context()) + require.NoError(t, err) + require.Equal(t, ClientIdLoginType, details.LoginType) + require.Empty(t, details.Account) + principalType, err := m.CurrentPrincipalType(t.Context()) + require.NoError(t, err) + require.Equal(t, ServicePrincipalType, principalType) }) t.Run("WithClientID", func(t *testing.T) { @@ -1073,6 +1081,14 @@ func TestLoginWithManagedIdentity(t *testing.T) { cred, err := m.LoginWithManagedIdentity(t.Context(), "my-client-id") require.NoError(t, err) require.IsType(t, new(azidentity.ManagedIdentityCredential), cred) + + details, err := m.LogInDetails(t.Context()) + require.NoError(t, err) + require.Equal(t, ClientIdLoginType, details.LoginType) + require.Equal(t, "my-client-id", details.Account) + principalType, err := m.CurrentPrincipalType(t.Context()) + require.NoError(t, err) + require.Equal(t, ServicePrincipalType, principalType) }) } @@ -1373,6 +1389,10 @@ func TestLogInDetails_ServicePrincipalNative(t *testing.T) { require.NoError(t, err) assert.Equal(t, ClientIdLoginType, details.LoginType) assert.Equal(t, "myClientId", details.Account) + + principalType, err := m.CurrentPrincipalType(t.Context()) + require.NoError(t, err) + assert.Equal(t, ServicePrincipalType, principalType) } func TestLogInDetails_InteractiveUser(t *testing.T) { @@ -1400,6 +1420,10 @@ func TestLogInDetails_InteractiveUser(t *testing.T) { require.NoError(t, err) assert.Equal(t, EmailLoginType, details.LoginType) assert.Equal(t, "user@example.com", details.Account) + + principalType, err := m.CurrentPrincipalType(t.Context()) + require.NoError(t, err) + assert.Equal(t, UserPrincipalType, principalType) } func TestLogInDetails_NotLoggedIn(t *testing.T) { @@ -1412,6 +1436,9 @@ func TestLogInDetails_NotLoggedIn(t *testing.T) { _, err := m.LogInDetails(t.Context()) require.Error(t, err) assert.ErrorIs(t, err, ErrNoCurrentUser) + principalType, err := m.CurrentPrincipalType(t.Context()) + require.ErrorIs(t, err, ErrNoCurrentUser) + assert.Empty(t, principalType) } func TestLogInDetails_HomeAccountNotFound(t *testing.T) { diff --git a/cli/azd/pkg/auth/principal.go b/cli/azd/pkg/auth/principal.go new file mode 100644 index 00000000000..1662902a272 --- /dev/null +++ b/cli/azd/pkg/auth/principal.go @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package auth + +import ( + "context" + "fmt" +) + +// PrincipalType is the identity type used for Azure role assignments. +type PrincipalType string + +const ( + // UserPrincipalType identifies an interactive user. + UserPrincipalType PrincipalType = "User" + // ServicePrincipalType identifies an application or managed identity. + ServicePrincipalType PrincipalType = "ServicePrincipal" +) + +// CurrentPrincipalType returns the principal type from the recorded login details. +func (m *Manager) CurrentPrincipalType(ctx context.Context) (PrincipalType, error) { + loginDetails, err := m.LogInDetails(ctx) + if err != nil { + return "", fmt.Errorf("fetching login details: %w", err) + } + + if loginDetails.LoginType == ClientIdLoginType { + return ServicePrincipalType, nil + } + + return UserPrincipalType, nil +} diff --git a/cli/azd/pkg/azdext/contracts/v1/account.pb.go b/cli/azd/pkg/azdext/contracts/v1/account.pb.go index 8c65d343fbf..8987c15ff35 100644 --- a/cli/azd/pkg/azdext/contracts/v1/account.pb.go +++ b/cli/azd/pkg/azdext/contracts/v1/account.pb.go @@ -24,6 +24,55 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type PrincipalType int32 + +const ( + PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED PrincipalType = 0 + PrincipalType_PRINCIPAL_TYPE_USER PrincipalType = 1 + PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL PrincipalType = 2 +) + +// Enum value maps for PrincipalType. +var ( + PrincipalType_name = map[int32]string{ + 0: "PRINCIPAL_TYPE_UNSPECIFIED", + 1: "PRINCIPAL_TYPE_USER", + 2: "PRINCIPAL_TYPE_SERVICE_PRINCIPAL", + } + PrincipalType_value = map[string]int32{ + "PRINCIPAL_TYPE_UNSPECIFIED": 0, + "PRINCIPAL_TYPE_USER": 1, + "PRINCIPAL_TYPE_SERVICE_PRINCIPAL": 2, + } +) + +func (x PrincipalType) Enum() *PrincipalType { + p := new(PrincipalType) + *p = x + return p +} + +func (x PrincipalType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PrincipalType) Descriptor() protoreflect.EnumDescriptor { + return file_azd_extensions_v1_account_proto_enumTypes[0].Descriptor() +} + +func (PrincipalType) Type() protoreflect.EnumType { + return &file_azd_extensions_v1_account_proto_enumTypes[0] +} + +func (x PrincipalType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PrincipalType.Descriptor instead. +func (PrincipalType) EnumDescriptor() ([]byte, []int) { + return file_azd_extensions_v1_account_proto_rawDescGZIP(), []int{0} +} + type ListSubscriptionsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Optional: filter subscriptions by tenant ID. @@ -203,6 +252,105 @@ func (x *LookupTenantResponse) GetTenantId() string { return "" } +type GetCurrentPrincipalRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required subscription ID. The active environment is not used as a default. + SubscriptionId string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId,proto3" json:"subscription_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentPrincipalRequest) Reset() { + *x = GetCurrentPrincipalRequest{} + mi := &file_azd_extensions_v1_account_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentPrincipalRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentPrincipalRequest) ProtoMessage() {} + +func (x *GetCurrentPrincipalRequest) ProtoReflect() protoreflect.Message { + mi := &file_azd_extensions_v1_account_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCurrentPrincipalRequest.ProtoReflect.Descriptor instead. +func (*GetCurrentPrincipalRequest) Descriptor() ([]byte, []int) { + return file_azd_extensions_v1_account_proto_rawDescGZIP(), []int{4} +} + +func (x *GetCurrentPrincipalRequest) GetSubscriptionId() string { + if x != nil { + return x.SubscriptionId + } + return "" +} + +type GetCurrentPrincipalResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Object ID in the subscription's resource tenant, not the user's home tenant or an application client ID. + ObjectId string `protobuf:"bytes,1,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"` + // Principal type determined from the host's login details. + PrincipalType PrincipalType `protobuf:"varint,2,opt,name=principal_type,json=principalType,proto3,enum=azd.extensions.v1.PrincipalType" json:"principal_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentPrincipalResponse) Reset() { + *x = GetCurrentPrincipalResponse{} + mi := &file_azd_extensions_v1_account_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentPrincipalResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentPrincipalResponse) ProtoMessage() {} + +func (x *GetCurrentPrincipalResponse) ProtoReflect() protoreflect.Message { + mi := &file_azd_extensions_v1_account_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCurrentPrincipalResponse.ProtoReflect.Descriptor instead. +func (*GetCurrentPrincipalResponse) Descriptor() ([]byte, []int) { + return file_azd_extensions_v1_account_proto_rawDescGZIP(), []int{5} +} + +func (x *GetCurrentPrincipalResponse) GetObjectId() string { + if x != nil { + return x.ObjectId + } + return "" +} + +func (x *GetCurrentPrincipalResponse) GetPrincipalType() PrincipalType { + if x != nil { + return x.PrincipalType + } + return PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED +} + var File_azd_extensions_v1_account_proto protoreflect.FileDescriptor const file_azd_extensions_v1_account_proto_rawDesc = "" + @@ -217,10 +365,20 @@ const file_azd_extensions_v1_account_proto_rawDesc = "" + "\x13LookupTenantRequest\x12'\n" + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\"3\n" + "\x14LookupTenantResponse\x12\x1b\n" + - "\ttenant_id\x18\x01 \x01(\tR\btenantId2\xe1\x01\n" + + "\ttenant_id\x18\x01 \x01(\tR\btenantId\"E\n" + + "\x1aGetCurrentPrincipalRequest\x12'\n" + + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\"\x83\x01\n" + + "\x1bGetCurrentPrincipalResponse\x12\x1b\n" + + "\tobject_id\x18\x01 \x01(\tR\bobjectId\x12G\n" + + "\x0eprincipal_type\x18\x02 \x01(\x0e2 .azd.extensions.v1.PrincipalTypeR\rprincipalType*n\n" + + "\rPrincipalType\x12\x1e\n" + + "\x1aPRINCIPAL_TYPE_UNSPECIFIED\x10\x00\x12\x17\n" + + "\x13PRINCIPAL_TYPE_USER\x10\x01\x12$\n" + + " PRINCIPAL_TYPE_SERVICE_PRINCIPAL\x10\x022\xd7\x02\n" + "\x0eAccountService\x12n\n" + "\x11ListSubscriptions\x12+.azd.extensions.v1.ListSubscriptionsRequest\x1a,.azd.extensions.v1.ListSubscriptionsResponse\x12_\n" + - "\fLookupTenant\x12&.azd.extensions.v1.LookupTenantRequest\x1a'.azd.extensions.v1.LookupTenantResponseB?Z=github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1;v1b\x06proto3" + "\fLookupTenant\x12&.azd.extensions.v1.LookupTenantRequest\x1a'.azd.extensions.v1.LookupTenantResponse\x12t\n" + + "\x13GetCurrentPrincipal\x12-.azd.extensions.v1.GetCurrentPrincipalRequest\x1a..azd.extensions.v1.GetCurrentPrincipalResponseB?Z=github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1;v1b\x06proto3" var ( file_azd_extensions_v1_account_proto_rawDescOnce sync.Once @@ -234,25 +392,32 @@ func file_azd_extensions_v1_account_proto_rawDescGZIP() []byte { return file_azd_extensions_v1_account_proto_rawDescData } -var file_azd_extensions_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_azd_extensions_v1_account_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_azd_extensions_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_azd_extensions_v1_account_proto_goTypes = []any{ - (*ListSubscriptionsRequest)(nil), // 0: azd.extensions.v1.ListSubscriptionsRequest - (*ListSubscriptionsResponse)(nil), // 1: azd.extensions.v1.ListSubscriptionsResponse - (*LookupTenantRequest)(nil), // 2: azd.extensions.v1.LookupTenantRequest - (*LookupTenantResponse)(nil), // 3: azd.extensions.v1.LookupTenantResponse - (*Subscription)(nil), // 4: azd.extensions.v1.Subscription + (PrincipalType)(0), // 0: azd.extensions.v1.PrincipalType + (*ListSubscriptionsRequest)(nil), // 1: azd.extensions.v1.ListSubscriptionsRequest + (*ListSubscriptionsResponse)(nil), // 2: azd.extensions.v1.ListSubscriptionsResponse + (*LookupTenantRequest)(nil), // 3: azd.extensions.v1.LookupTenantRequest + (*LookupTenantResponse)(nil), // 4: azd.extensions.v1.LookupTenantResponse + (*GetCurrentPrincipalRequest)(nil), // 5: azd.extensions.v1.GetCurrentPrincipalRequest + (*GetCurrentPrincipalResponse)(nil), // 6: azd.extensions.v1.GetCurrentPrincipalResponse + (*Subscription)(nil), // 7: azd.extensions.v1.Subscription } var file_azd_extensions_v1_account_proto_depIdxs = []int32{ - 4, // 0: azd.extensions.v1.ListSubscriptionsResponse.subscriptions:type_name -> azd.extensions.v1.Subscription - 0, // 1: azd.extensions.v1.AccountService.ListSubscriptions:input_type -> azd.extensions.v1.ListSubscriptionsRequest - 2, // 2: azd.extensions.v1.AccountService.LookupTenant:input_type -> azd.extensions.v1.LookupTenantRequest - 1, // 3: azd.extensions.v1.AccountService.ListSubscriptions:output_type -> azd.extensions.v1.ListSubscriptionsResponse - 3, // 4: azd.extensions.v1.AccountService.LookupTenant:output_type -> azd.extensions.v1.LookupTenantResponse - 3, // [3:5] is the sub-list for method output_type - 1, // [1:3] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 7, // 0: azd.extensions.v1.ListSubscriptionsResponse.subscriptions:type_name -> azd.extensions.v1.Subscription + 0, // 1: azd.extensions.v1.GetCurrentPrincipalResponse.principal_type:type_name -> azd.extensions.v1.PrincipalType + 1, // 2: azd.extensions.v1.AccountService.ListSubscriptions:input_type -> azd.extensions.v1.ListSubscriptionsRequest + 3, // 3: azd.extensions.v1.AccountService.LookupTenant:input_type -> azd.extensions.v1.LookupTenantRequest + 5, // 4: azd.extensions.v1.AccountService.GetCurrentPrincipal:input_type -> azd.extensions.v1.GetCurrentPrincipalRequest + 2, // 5: azd.extensions.v1.AccountService.ListSubscriptions:output_type -> azd.extensions.v1.ListSubscriptionsResponse + 4, // 6: azd.extensions.v1.AccountService.LookupTenant:output_type -> azd.extensions.v1.LookupTenantResponse + 6, // 7: azd.extensions.v1.AccountService.GetCurrentPrincipal:output_type -> azd.extensions.v1.GetCurrentPrincipalResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_azd_extensions_v1_account_proto_init() } @@ -267,13 +432,14 @@ func file_azd_extensions_v1_account_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_azd_extensions_v1_account_proto_rawDesc), len(file_azd_extensions_v1_account_proto_rawDesc)), - NumEnums: 0, - NumMessages: 4, + NumEnums: 1, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, GoTypes: file_azd_extensions_v1_account_proto_goTypes, DependencyIndexes: file_azd_extensions_v1_account_proto_depIdxs, + EnumInfos: file_azd_extensions_v1_account_proto_enumTypes, MessageInfos: file_azd_extensions_v1_account_proto_msgTypes, }.Build() File_azd_extensions_v1_account_proto = out.File diff --git a/cli/azd/pkg/azdext/contracts/v1/account_grpc.pb.go b/cli/azd/pkg/azdext/contracts/v1/account_grpc.pb.go index 24e6232409f..b1163aa334e 100644 --- a/cli/azd/pkg/azdext/contracts/v1/account_grpc.pb.go +++ b/cli/azd/pkg/azdext/contracts/v1/account_grpc.pb.go @@ -22,8 +22,9 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - AccountService_ListSubscriptions_FullMethodName = "/azd.extensions.v1.AccountService/ListSubscriptions" - AccountService_LookupTenant_FullMethodName = "/azd.extensions.v1.AccountService/LookupTenant" + AccountService_ListSubscriptions_FullMethodName = "/azd.extensions.v1.AccountService/ListSubscriptions" + AccountService_LookupTenant_FullMethodName = "/azd.extensions.v1.AccountService/LookupTenant" + AccountService_GetCurrentPrincipal_FullMethodName = "/azd.extensions.v1.AccountService/GetCurrentPrincipal" ) // AccountServiceClient is the client API for AccountService service. @@ -34,6 +35,8 @@ type AccountServiceClient interface { ListSubscriptions(ctx context.Context, in *ListSubscriptionsRequest, opts ...grpc.CallOption) (*ListSubscriptionsResponse, error) // LookupTenant resolves the tenant ID required to access a specific subscription. LookupTenant(ctx context.Context, in *LookupTenantRequest, opts ...grpc.CallOption) (*LookupTenantResponse, error) + // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. + GetCurrentPrincipal(ctx context.Context, in *GetCurrentPrincipalRequest, opts ...grpc.CallOption) (*GetCurrentPrincipalResponse, error) } type accountServiceClient struct { @@ -64,6 +67,16 @@ func (c *accountServiceClient) LookupTenant(ctx context.Context, in *LookupTenan return out, nil } +func (c *accountServiceClient) GetCurrentPrincipal(ctx context.Context, in *GetCurrentPrincipalRequest, opts ...grpc.CallOption) (*GetCurrentPrincipalResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetCurrentPrincipalResponse) + err := c.cc.Invoke(ctx, AccountService_GetCurrentPrincipal_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AccountServiceServer is the server API for AccountService service. // All implementations must embed UnimplementedAccountServiceServer // for forward compatibility. @@ -72,6 +85,8 @@ type AccountServiceServer interface { ListSubscriptions(context.Context, *ListSubscriptionsRequest) (*ListSubscriptionsResponse, error) // LookupTenant resolves the tenant ID required to access a specific subscription. LookupTenant(context.Context, *LookupTenantRequest) (*LookupTenantResponse, error) + // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. + GetCurrentPrincipal(context.Context, *GetCurrentPrincipalRequest) (*GetCurrentPrincipalResponse, error) mustEmbedUnimplementedAccountServiceServer() } @@ -88,6 +103,9 @@ func (UnimplementedAccountServiceServer) ListSubscriptions(context.Context, *Lis func (UnimplementedAccountServiceServer) LookupTenant(context.Context, *LookupTenantRequest) (*LookupTenantResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method LookupTenant not implemented") } +func (UnimplementedAccountServiceServer) GetCurrentPrincipal(context.Context, *GetCurrentPrincipalRequest) (*GetCurrentPrincipalResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCurrentPrincipal not implemented") +} func (UnimplementedAccountServiceServer) mustEmbedUnimplementedAccountServiceServer() {} func (UnimplementedAccountServiceServer) testEmbeddedByValue() {} @@ -145,6 +163,24 @@ func _AccountService_LookupTenant_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _AccountService_GetCurrentPrincipal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCurrentPrincipalRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServiceServer).GetCurrentPrincipal(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AccountService_GetCurrentPrincipal_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServiceServer).GetCurrentPrincipal(ctx, req.(*GetCurrentPrincipalRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AccountService_ServiceDesc is the grpc.ServiceDesc for AccountService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -160,6 +196,10 @@ var AccountService_ServiceDesc = grpc.ServiceDesc{ MethodName: "LookupTenant", Handler: _AccountService_LookupTenant_Handler, }, + { + MethodName: "GetCurrentPrincipal", + Handler: _AccountService_GetCurrentPrincipal_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "azd/extensions/v1/account.proto", diff --git a/cli/azd/pkg/azdext/contracts/v1beta/account.pb.go b/cli/azd/pkg/azdext/contracts/v1beta/account.pb.go index cd52e0a0899..2871975d065 100644 --- a/cli/azd/pkg/azdext/contracts/v1beta/account.pb.go +++ b/cli/azd/pkg/azdext/contracts/v1beta/account.pb.go @@ -24,6 +24,55 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type PrincipalType int32 + +const ( + PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED PrincipalType = 0 + PrincipalType_PRINCIPAL_TYPE_USER PrincipalType = 1 + PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL PrincipalType = 2 +) + +// Enum value maps for PrincipalType. +var ( + PrincipalType_name = map[int32]string{ + 0: "PRINCIPAL_TYPE_UNSPECIFIED", + 1: "PRINCIPAL_TYPE_USER", + 2: "PRINCIPAL_TYPE_SERVICE_PRINCIPAL", + } + PrincipalType_value = map[string]int32{ + "PRINCIPAL_TYPE_UNSPECIFIED": 0, + "PRINCIPAL_TYPE_USER": 1, + "PRINCIPAL_TYPE_SERVICE_PRINCIPAL": 2, + } +) + +func (x PrincipalType) Enum() *PrincipalType { + p := new(PrincipalType) + *p = x + return p +} + +func (x PrincipalType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PrincipalType) Descriptor() protoreflect.EnumDescriptor { + return file_azd_extensions_v1beta_account_proto_enumTypes[0].Descriptor() +} + +func (PrincipalType) Type() protoreflect.EnumType { + return &file_azd_extensions_v1beta_account_proto_enumTypes[0] +} + +func (x PrincipalType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PrincipalType.Descriptor instead. +func (PrincipalType) EnumDescriptor() ([]byte, []int) { + return file_azd_extensions_v1beta_account_proto_rawDescGZIP(), []int{0} +} + type ListSubscriptionsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Optional: filter subscriptions by tenant ID. @@ -203,6 +252,105 @@ func (x *LookupTenantResponse) GetTenantId() string { return "" } +type GetCurrentPrincipalRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required subscription ID. The active environment is not used as a default. + SubscriptionId string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId,proto3" json:"subscription_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentPrincipalRequest) Reset() { + *x = GetCurrentPrincipalRequest{} + mi := &file_azd_extensions_v1beta_account_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentPrincipalRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentPrincipalRequest) ProtoMessage() {} + +func (x *GetCurrentPrincipalRequest) ProtoReflect() protoreflect.Message { + mi := &file_azd_extensions_v1beta_account_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCurrentPrincipalRequest.ProtoReflect.Descriptor instead. +func (*GetCurrentPrincipalRequest) Descriptor() ([]byte, []int) { + return file_azd_extensions_v1beta_account_proto_rawDescGZIP(), []int{4} +} + +func (x *GetCurrentPrincipalRequest) GetSubscriptionId() string { + if x != nil { + return x.SubscriptionId + } + return "" +} + +type GetCurrentPrincipalResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Object ID in the subscription's resource tenant, not the user's home tenant or an application client ID. + ObjectId string `protobuf:"bytes,1,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"` + // Principal type determined from the host's login details. + PrincipalType PrincipalType `protobuf:"varint,2,opt,name=principal_type,json=principalType,proto3,enum=azd.extensions.v1beta.PrincipalType" json:"principal_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentPrincipalResponse) Reset() { + *x = GetCurrentPrincipalResponse{} + mi := &file_azd_extensions_v1beta_account_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentPrincipalResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentPrincipalResponse) ProtoMessage() {} + +func (x *GetCurrentPrincipalResponse) ProtoReflect() protoreflect.Message { + mi := &file_azd_extensions_v1beta_account_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCurrentPrincipalResponse.ProtoReflect.Descriptor instead. +func (*GetCurrentPrincipalResponse) Descriptor() ([]byte, []int) { + return file_azd_extensions_v1beta_account_proto_rawDescGZIP(), []int{5} +} + +func (x *GetCurrentPrincipalResponse) GetObjectId() string { + if x != nil { + return x.ObjectId + } + return "" +} + +func (x *GetCurrentPrincipalResponse) GetPrincipalType() PrincipalType { + if x != nil { + return x.PrincipalType + } + return PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED +} + var File_azd_extensions_v1beta_account_proto protoreflect.FileDescriptor const file_azd_extensions_v1beta_account_proto_rawDesc = "" + @@ -217,10 +365,20 @@ const file_azd_extensions_v1beta_account_proto_rawDesc = "" + "\x13LookupTenantRequest\x12'\n" + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\"3\n" + "\x14LookupTenantResponse\x12\x1b\n" + - "\ttenant_id\x18\x01 \x01(\tR\btenantId2\xf1\x01\n" + + "\ttenant_id\x18\x01 \x01(\tR\btenantId\"E\n" + + "\x1aGetCurrentPrincipalRequest\x12'\n" + + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\"\x87\x01\n" + + "\x1bGetCurrentPrincipalResponse\x12\x1b\n" + + "\tobject_id\x18\x01 \x01(\tR\bobjectId\x12K\n" + + "\x0eprincipal_type\x18\x02 \x01(\x0e2$.azd.extensions.v1beta.PrincipalTypeR\rprincipalType*n\n" + + "\rPrincipalType\x12\x1e\n" + + "\x1aPRINCIPAL_TYPE_UNSPECIFIED\x10\x00\x12\x17\n" + + "\x13PRINCIPAL_TYPE_USER\x10\x01\x12$\n" + + " PRINCIPAL_TYPE_SERVICE_PRINCIPAL\x10\x022\xef\x02\n" + "\x0eAccountService\x12v\n" + "\x11ListSubscriptions\x12/.azd.extensions.v1beta.ListSubscriptionsRequest\x1a0.azd.extensions.v1beta.ListSubscriptionsResponse\x12g\n" + - "\fLookupTenant\x12*.azd.extensions.v1beta.LookupTenantRequest\x1a+.azd.extensions.v1beta.LookupTenantResponseBGZEgithub.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta;v1betab\x06proto3" + "\fLookupTenant\x12*.azd.extensions.v1beta.LookupTenantRequest\x1a+.azd.extensions.v1beta.LookupTenantResponse\x12|\n" + + "\x13GetCurrentPrincipal\x121.azd.extensions.v1beta.GetCurrentPrincipalRequest\x1a2.azd.extensions.v1beta.GetCurrentPrincipalResponseBGZEgithub.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta;v1betab\x06proto3" var ( file_azd_extensions_v1beta_account_proto_rawDescOnce sync.Once @@ -234,25 +392,32 @@ func file_azd_extensions_v1beta_account_proto_rawDescGZIP() []byte { return file_azd_extensions_v1beta_account_proto_rawDescData } -var file_azd_extensions_v1beta_account_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_azd_extensions_v1beta_account_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_azd_extensions_v1beta_account_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_azd_extensions_v1beta_account_proto_goTypes = []any{ - (*ListSubscriptionsRequest)(nil), // 0: azd.extensions.v1beta.ListSubscriptionsRequest - (*ListSubscriptionsResponse)(nil), // 1: azd.extensions.v1beta.ListSubscriptionsResponse - (*LookupTenantRequest)(nil), // 2: azd.extensions.v1beta.LookupTenantRequest - (*LookupTenantResponse)(nil), // 3: azd.extensions.v1beta.LookupTenantResponse - (*Subscription)(nil), // 4: azd.extensions.v1beta.Subscription + (PrincipalType)(0), // 0: azd.extensions.v1beta.PrincipalType + (*ListSubscriptionsRequest)(nil), // 1: azd.extensions.v1beta.ListSubscriptionsRequest + (*ListSubscriptionsResponse)(nil), // 2: azd.extensions.v1beta.ListSubscriptionsResponse + (*LookupTenantRequest)(nil), // 3: azd.extensions.v1beta.LookupTenantRequest + (*LookupTenantResponse)(nil), // 4: azd.extensions.v1beta.LookupTenantResponse + (*GetCurrentPrincipalRequest)(nil), // 5: azd.extensions.v1beta.GetCurrentPrincipalRequest + (*GetCurrentPrincipalResponse)(nil), // 6: azd.extensions.v1beta.GetCurrentPrincipalResponse + (*Subscription)(nil), // 7: azd.extensions.v1beta.Subscription } var file_azd_extensions_v1beta_account_proto_depIdxs = []int32{ - 4, // 0: azd.extensions.v1beta.ListSubscriptionsResponse.subscriptions:type_name -> azd.extensions.v1beta.Subscription - 0, // 1: azd.extensions.v1beta.AccountService.ListSubscriptions:input_type -> azd.extensions.v1beta.ListSubscriptionsRequest - 2, // 2: azd.extensions.v1beta.AccountService.LookupTenant:input_type -> azd.extensions.v1beta.LookupTenantRequest - 1, // 3: azd.extensions.v1beta.AccountService.ListSubscriptions:output_type -> azd.extensions.v1beta.ListSubscriptionsResponse - 3, // 4: azd.extensions.v1beta.AccountService.LookupTenant:output_type -> azd.extensions.v1beta.LookupTenantResponse - 3, // [3:5] is the sub-list for method output_type - 1, // [1:3] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 7, // 0: azd.extensions.v1beta.ListSubscriptionsResponse.subscriptions:type_name -> azd.extensions.v1beta.Subscription + 0, // 1: azd.extensions.v1beta.GetCurrentPrincipalResponse.principal_type:type_name -> azd.extensions.v1beta.PrincipalType + 1, // 2: azd.extensions.v1beta.AccountService.ListSubscriptions:input_type -> azd.extensions.v1beta.ListSubscriptionsRequest + 3, // 3: azd.extensions.v1beta.AccountService.LookupTenant:input_type -> azd.extensions.v1beta.LookupTenantRequest + 5, // 4: azd.extensions.v1beta.AccountService.GetCurrentPrincipal:input_type -> azd.extensions.v1beta.GetCurrentPrincipalRequest + 2, // 5: azd.extensions.v1beta.AccountService.ListSubscriptions:output_type -> azd.extensions.v1beta.ListSubscriptionsResponse + 4, // 6: azd.extensions.v1beta.AccountService.LookupTenant:output_type -> azd.extensions.v1beta.LookupTenantResponse + 6, // 7: azd.extensions.v1beta.AccountService.GetCurrentPrincipal:output_type -> azd.extensions.v1beta.GetCurrentPrincipalResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_azd_extensions_v1beta_account_proto_init() } @@ -267,13 +432,14 @@ func file_azd_extensions_v1beta_account_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_azd_extensions_v1beta_account_proto_rawDesc), len(file_azd_extensions_v1beta_account_proto_rawDesc)), - NumEnums: 0, - NumMessages: 4, + NumEnums: 1, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, GoTypes: file_azd_extensions_v1beta_account_proto_goTypes, DependencyIndexes: file_azd_extensions_v1beta_account_proto_depIdxs, + EnumInfos: file_azd_extensions_v1beta_account_proto_enumTypes, MessageInfos: file_azd_extensions_v1beta_account_proto_msgTypes, }.Build() File_azd_extensions_v1beta_account_proto = out.File diff --git a/cli/azd/pkg/azdext/contracts/v1beta/account_grpc.pb.go b/cli/azd/pkg/azdext/contracts/v1beta/account_grpc.pb.go index 741bffb564c..ac20d9354c6 100644 --- a/cli/azd/pkg/azdext/contracts/v1beta/account_grpc.pb.go +++ b/cli/azd/pkg/azdext/contracts/v1beta/account_grpc.pb.go @@ -22,8 +22,9 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - AccountService_ListSubscriptions_FullMethodName = "/azd.extensions.v1beta.AccountService/ListSubscriptions" - AccountService_LookupTenant_FullMethodName = "/azd.extensions.v1beta.AccountService/LookupTenant" + AccountService_ListSubscriptions_FullMethodName = "/azd.extensions.v1beta.AccountService/ListSubscriptions" + AccountService_LookupTenant_FullMethodName = "/azd.extensions.v1beta.AccountService/LookupTenant" + AccountService_GetCurrentPrincipal_FullMethodName = "/azd.extensions.v1beta.AccountService/GetCurrentPrincipal" ) // AccountServiceClient is the client API for AccountService service. @@ -34,6 +35,8 @@ type AccountServiceClient interface { ListSubscriptions(ctx context.Context, in *ListSubscriptionsRequest, opts ...grpc.CallOption) (*ListSubscriptionsResponse, error) // LookupTenant resolves the tenant ID required to access a specific subscription. LookupTenant(ctx context.Context, in *LookupTenantRequest, opts ...grpc.CallOption) (*LookupTenantResponse, error) + // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. + GetCurrentPrincipal(ctx context.Context, in *GetCurrentPrincipalRequest, opts ...grpc.CallOption) (*GetCurrentPrincipalResponse, error) } type accountServiceClient struct { @@ -64,6 +67,16 @@ func (c *accountServiceClient) LookupTenant(ctx context.Context, in *LookupTenan return out, nil } +func (c *accountServiceClient) GetCurrentPrincipal(ctx context.Context, in *GetCurrentPrincipalRequest, opts ...grpc.CallOption) (*GetCurrentPrincipalResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetCurrentPrincipalResponse) + err := c.cc.Invoke(ctx, AccountService_GetCurrentPrincipal_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AccountServiceServer is the server API for AccountService service. // All implementations must embed UnimplementedAccountServiceServer // for forward compatibility. @@ -72,6 +85,8 @@ type AccountServiceServer interface { ListSubscriptions(context.Context, *ListSubscriptionsRequest) (*ListSubscriptionsResponse, error) // LookupTenant resolves the tenant ID required to access a specific subscription. LookupTenant(context.Context, *LookupTenantRequest) (*LookupTenantResponse, error) + // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. + GetCurrentPrincipal(context.Context, *GetCurrentPrincipalRequest) (*GetCurrentPrincipalResponse, error) mustEmbedUnimplementedAccountServiceServer() } @@ -88,6 +103,9 @@ func (UnimplementedAccountServiceServer) ListSubscriptions(context.Context, *Lis func (UnimplementedAccountServiceServer) LookupTenant(context.Context, *LookupTenantRequest) (*LookupTenantResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method LookupTenant not implemented") } +func (UnimplementedAccountServiceServer) GetCurrentPrincipal(context.Context, *GetCurrentPrincipalRequest) (*GetCurrentPrincipalResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCurrentPrincipal not implemented") +} func (UnimplementedAccountServiceServer) mustEmbedUnimplementedAccountServiceServer() {} func (UnimplementedAccountServiceServer) testEmbeddedByValue() {} @@ -145,6 +163,24 @@ func _AccountService_LookupTenant_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _AccountService_GetCurrentPrincipal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCurrentPrincipalRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServiceServer).GetCurrentPrincipal(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AccountService_GetCurrentPrincipal_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServiceServer).GetCurrentPrincipal(ctx, req.(*GetCurrentPrincipalRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AccountService_ServiceDesc is the grpc.ServiceDesc for AccountService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -160,6 +196,10 @@ var AccountService_ServiceDesc = grpc.ServiceDesc{ MethodName: "LookupTenant", Handler: _AccountService_LookupTenant_Handler, }, + { + MethodName: "GetCurrentPrincipal", + Handler: _AccountService_GetCurrentPrincipal_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "azd/extensions/v1beta/account.proto", diff --git a/cli/azd/pkg/azdext/contracts_v1_aliases.go b/cli/azd/pkg/azdext/contracts_v1_aliases.go index fd1be5c0e19..55b212bc50d 100644 --- a/cli/azd/pkg/azdext/contracts_v1_aliases.go +++ b/cli/azd/pkg/azdext/contracts_v1_aliases.go @@ -11,6 +11,7 @@ import ( ) const ( + AccountService_GetCurrentPrincipal_FullMethodName = v1.AccountService_GetCurrentPrincipal_FullMethodName AccountService_ListSubscriptions_FullMethodName = v1.AccountService_ListSubscriptions_FullMethodName AccountService_LookupTenant_FullMethodName = v1.AccountService_LookupTenant_FullMethodName AiModelService_ListLocationsWithQuota_FullMethodName = v1.AiModelService_ListLocationsWithQuota_FullMethodName @@ -54,6 +55,9 @@ const ( LocationKind_LOCATION_KIND_LOCAL = v1.LocationKind_LOCATION_KIND_LOCAL LocationKind_LOCATION_KIND_REMOTE = v1.LocationKind_LOCATION_KIND_REMOTE LocationKind_LOCATION_KIND_UNSPECIFIED = v1.LocationKind_LOCATION_KIND_UNSPECIFIED + PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL = v1.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL + PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED = v1.PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED + PrincipalType_PRINCIPAL_TYPE_USER = v1.PrincipalType_PRINCIPAL_TYPE_USER ProjectService_AddService_FullMethodName = v1.ProjectService_AddService_FullMethodName ProjectService_GetConfigSection_FullMethodName = v1.ProjectService_GetConfigSection_FullMethodName ProjectService_GetConfigValue_FullMethodName = v1.ProjectService_GetConfigValue_FullMethodName @@ -199,6 +203,8 @@ type ( GetConfigSectionResponse = v1.GetConfigSectionResponse GetConfigStringRequest = v1.GetConfigStringRequest GetConfigStringResponse = v1.GetConfigStringResponse + GetCurrentPrincipalRequest = v1.GetCurrentPrincipalRequest + GetCurrentPrincipalResponse = v1.GetCurrentPrincipalResponse GetDeploymentContextResponse = v1.GetDeploymentContextResponse GetDeploymentResponse = v1.GetDeploymentResponse GetEnvRequest = v1.GetEnvRequest @@ -253,6 +259,7 @@ type ( ParseGitHubUrlResponse = v1.ParseGitHubUrlResponse PrepareValidationContextChunk = v1.PrepareValidationContextChunk PrepareValidationContextResponse = v1.PrepareValidationContextResponse + PrincipalType = v1.PrincipalType ProjectConfig = v1.ProjectConfig ProjectHandlerStatus = v1.ProjectHandlerStatus ProjectMetadata = v1.ProjectMetadata @@ -515,6 +522,8 @@ var ( FrameworkService_ServiceDesc = v1.FrameworkService_ServiceDesc LocationKind_name = v1.LocationKind_name LocationKind_value = v1.LocationKind_value + PrincipalType_name = v1.PrincipalType_name + PrincipalType_value = v1.PrincipalType_value ProjectService_ServiceDesc = v1.ProjectService_ServiceDesc PromptService_ServiceDesc = v1.PromptService_ServiceDesc ProvisioningService_ServiceDesc = v1.ProvisioningService_ServiceDesc diff --git a/cli/azd/pkg/infra/provisioning/current_principal_id_provider.go b/cli/azd/pkg/infra/provisioning/current_principal_id_provider.go index 2b20c850581..bad641c7411 100644 --- a/cli/azd/pkg/infra/provisioning/current_principal_id_provider.go +++ b/cli/azd/pkg/infra/provisioning/current_principal_id_provider.go @@ -58,22 +58,12 @@ func (p *principalIDProvider) CurrentPrincipalId(ctx context.Context) (string, e } const ( - UserType PrincipalType = "User" - ServicePrincipalType PrincipalType = "ServicePrincipal" + UserType PrincipalType = auth.UserPrincipalType + ServicePrincipalType PrincipalType = auth.ServicePrincipalType ) -type PrincipalType string +type PrincipalType = auth.PrincipalType func (p *principalIDProvider) CurrentPrincipalType(ctx context.Context) (PrincipalType, error) { - loginDetails, err := p.authManager.LogInDetails(ctx) - if err != nil { - return "", fmt.Errorf("fetching login details: %w", err) - } - - principalType := UserType - if loginDetails.LoginType == auth.ClientIdLoginType { - principalType = ServicePrincipalType - } - - return principalType, nil + return p.authManager.CurrentPrincipalType(ctx) } diff --git a/docs/architecture/extension-framework.md b/docs/architecture/extension-framework.md index 6f961f5430a..9747e379a34 100644 --- a/docs/architecture/extension-framework.md +++ b/docs/architecture/extension-framework.md @@ -67,7 +67,7 @@ Extensions can access these azd services via gRPC: - **Environment** — Read/write environment values and secrets - **User Config** — Read user-level azd configuration - **Deployment** — Access deployment information -- **Account** — Access Azure account details +- **Account**. List subscriptions, resolve access tenants, and retrieve the current principal's resource-tenant object ID and type for role assignments. See [GetCurrentPrincipal](../../cli/azd/docs/extensions/extension-framework.md#getcurrentprincipal). - **Prompt** — Display prompts and collect user input - **AI Model** — Query AI model availability and quotas - **Event** — Subscribe to and emit events diff --git a/docs/guides/creating-an-extension.md b/docs/guides/creating-an-extension.md index 0b8b2a2ec03..6226f43fc86 100644 --- a/docs/guides/creating-an-extension.md +++ b/docs/guides/creating-an-extension.md @@ -49,6 +49,8 @@ capabilities: Implement the required interfaces for your declared capabilities. See the extension framework services documentation for interface details. +If your extension creates role assignments, use [`Account.GetCurrentPrincipal`](../../cli/azd/docs/extensions/extension-framework.md#getcurrentprincipal) with the target subscription ID. The host resolves the resource-tenant object ID and principal type without returning an access token. Consume an SDK and host release containing this method before replacing an existing lookup. + ### 4. Build ```bash From 1d8c77fb448860fe25cdb657dcbc7ee09a6ff7ae Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Tue, 15 Sep 2026 22:56:41 +0000 Subject: [PATCH 2/5] fix(auth): handle missing principal identifiers consistently Use the generic auth status message when no client ID is configured. Reject empty Graph object IDs in the shared principal resolver so core provisioning and extension callers share the same guarantee. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c24d9e3-50ce-4517-960f-a5813a362002 --- .../internal/grpcserver/account_service.go | 3 - .../grpcserver/account_service_test.go | 1 - cli/azd/pkg/azureutil/principal.go | 7 +- cli/azd/pkg/azureutil/principal_test.go | 77 ++++++++++++------- cli/azd/pkg/output/ux/auth_status.go | 8 +- cli/azd/pkg/output/ux/auth_status_test.go | 11 ++- 6 files changed, 68 insertions(+), 39 deletions(-) diff --git a/cli/azd/internal/grpcserver/account_service.go b/cli/azd/internal/grpcserver/account_service.go index f55afbfd1e9..9faa90d3da2 100644 --- a/cli/azd/internal/grpcserver/account_service.go +++ b/cli/azd/internal/grpcserver/account_service.go @@ -75,9 +75,6 @@ func (s *accountService) GetCurrentPrincipal( if err != nil { return nil, fmt.Errorf("fetching current principal information: %w", err) } - if objectID == "" { - return nil, status.Error(codes.Internal, "current principal object id is empty") - } return &azdext.GetCurrentPrincipalResponse{ ObjectId: objectID, diff --git a/cli/azd/internal/grpcserver/account_service_test.go b/cli/azd/internal/grpcserver/account_service_test.go index 1ada4cd73fc..a6a9a77f73f 100644 --- a/cli/azd/internal/grpcserver/account_service_test.go +++ b/cli/azd/internal/grpcserver/account_service_test.go @@ -229,7 +229,6 @@ func TestAccountService_GetCurrentPrincipal_GraphFallback(t *testing.T) { wantError string }{ {name: "success", graphID: "graph-object-id", statusCode: http.StatusOK}, - {name: "empty object id", statusCode: http.StatusOK, wantError: "current principal object id is empty"}, {name: "lookup failure", statusCode: http.StatusForbidden, wantError: "fetching current principal information"}, } { t.Run(tt.name, func(t *testing.T) { diff --git a/cli/azd/pkg/azureutil/principal.go b/cli/azd/pkg/azureutil/principal.go index 39d6d7329cc..9c1e71c4ec0 100644 --- a/cli/azd/pkg/azureutil/principal.go +++ b/cli/azd/pkg/azureutil/principal.go @@ -12,7 +12,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/azapi" ) -// GetCurrentPrincipalId returns the object ID of the current principal authenticated with the CLI. +// GetCurrentPrincipalId returns a non-empty object ID of the current principal authenticated with the CLI. // It prefers the oid claim from an ARM access token, falling back to Graph /me when acquiring the // token fails or when the token does not include a usable oid. func GetCurrentPrincipalId(ctx context.Context, userProfile *azapi.UserProfileService, tenantId string) (string, error) { @@ -30,7 +30,10 @@ func GetCurrentPrincipalId(ctx context.Context, userProfile *azapi.UserProfileSe principalId, graphErr := userProfile.GetSignedInUserId(ctx, tenantId) if graphErr == nil { - return principalId, nil + if principalId != "" { + return principalId, nil + } + graphErr = errors.New("signed-in user response did not contain an object id") } return "", fmt.Errorf( diff --git a/cli/azd/pkg/azureutil/principal_test.go b/cli/azd/pkg/azureutil/principal_test.go index 8cc3d41b114..40b38b496ac 100644 --- a/cli/azd/pkg/azureutil/principal_test.go +++ b/cli/azd/pkg/azureutil/principal_test.go @@ -52,39 +52,58 @@ func TestGetCurrentPrincipalId_PrefersOidFromAccessToken(t *testing.T) { func TestGetCurrentPrincipalId_FallsBackToGraphWhenOidMissing(t *testing.T) { t.Parallel() - mockContext := mocks.NewMockContext(t.Context()) - mockContext.HttpClient.When(func(request *http.Request) bool { - return request.Method == http.MethodGet && strings.Contains(request.URL.Path, "/me") - }).RespondFn(func(request *http.Request) (*http.Response, error) { - return mocks.CreateHttpResponseWithBody(request, http.StatusOK, &graphsdk.UserProfile{ - Id: "graph-user-id", - }) - }) + for _, tt := range []struct { + name string + graphID string + }{ + {name: "success", graphID: "graph-user-id"}, + {name: "empty object id"}, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + mockContext := mocks.NewMockContext(t.Context()) + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.Method == http.MethodGet && strings.Contains(request.URL.Path, "/me") + }).RespondFn(func(request *http.Request) (*http.Response, error) { + return mocks.CreateHttpResponseWithBody(request, http.StatusOK, &graphsdk.UserProfile{ + Id: tt.graphID, + }) + }) - userProfile := azapi.NewUserProfileService( - &mocks.MockMultiTenantCredentialProvider{ - TokenMap: map[string]mocks.MockCredentials{ - "resource-tenant": { - GetTokenFn: func(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) { - return azcore.AccessToken{ - Token: mocks.CreateJwtToken(t, map[string]string{ - "test": "fail", - }), - ExpiresOn: time.Now().Add(time.Hour), - }, nil + userProfile := azapi.NewUserProfileService( + &mocks.MockMultiTenantCredentialProvider{ + TokenMap: map[string]mocks.MockCredentials{ + "resource-tenant": { + GetTokenFn: func( + ctx context.Context, options policy.TokenRequestOptions, + ) (azcore.AccessToken, error) { + return azcore.AccessToken{ + Token: mocks.CreateJwtToken(t, map[string]string{ + "test": "fail", + }), + ExpiresOn: time.Now().Add(time.Hour), + }, nil + }, + }, }, }, - }, - }, - &azcore.ClientOptions{ - Transport: mockContext.HttpClient, - }, - cloud.AzurePublic(), - ) + &azcore.ClientOptions{ + Transport: mockContext.HttpClient, + }, + cloud.AzurePublic(), + ) - principalId, err := GetCurrentPrincipalId(*mockContext.Context, userProfile, "resource-tenant") - require.NoError(t, err) - require.Equal(t, "graph-user-id", principalId) + principalId, err := GetCurrentPrincipalId(*mockContext.Context, userProfile, "resource-tenant") + if tt.graphID == "" { + require.ErrorContains(t, err, "signed-in user response did not contain an object id") + require.ErrorContains(t, err, "getting oid from token: no oid claim") + require.Empty(t, principalId) + } else { + require.NoError(t, err) + require.Equal(t, tt.graphID, principalId) + } + }) + } } func TestGetCurrentPrincipalId_ReturnsJoinedErrorWhenTokenAndGraphFail(t *testing.T) { diff --git a/cli/azd/pkg/output/ux/auth_status.go b/cli/azd/pkg/output/ux/auth_status.go index 2520a9d812c..a3c50284ecb 100644 --- a/cli/azd/pkg/output/ux/auth_status.go +++ b/cli/azd/pkg/output/ux/auth_status.go @@ -38,9 +38,11 @@ func (v *AuthStatusView) ToString(currentIndentation string) string { currentIndentation, output.WithBold("%s", v.Result.Email)) case contracts.AccountTypeServicePrincipal: - return fmt.Sprintf("%sLogged in to Azure as (%s)", - currentIndentation, - output.WithGrayFormat("%s", v.Result.ClientID)) + if v.Result.ClientID != "" { + return fmt.Sprintf("%sLogged in to Azure as (%s)", + currentIndentation, + output.WithGrayFormat("%s", v.Result.ClientID)) + } } return fmt.Sprintf("%sLogged in to Azure", currentIndentation) diff --git a/cli/azd/pkg/output/ux/auth_status_test.go b/cli/azd/pkg/output/ux/auth_status_test.go index 4e3d76705e3..8b21afafa0e 100644 --- a/cli/azd/pkg/output/ux/auth_status_test.go +++ b/cli/azd/pkg/output/ux/auth_status_test.go @@ -50,6 +50,15 @@ func TestAuthStatusView_ToString(t *testing.T) { }, authMode: "azd built in", }, + { + name: "authenticated service principal without client ID", + result: &contracts.StatusResult{ + Status: contracts.AuthStatusAuthenticated, + Type: contracts.AccountTypeServicePrincipal, + }, + authMode: "azd built in", + want: "Logged in to Azure", + }, { name: "authenticated service principal", result: &contracts.StatusResult{ @@ -69,7 +78,7 @@ func TestAuthStatusView_ToString(t *testing.T) { } got := v.ToString("") - if tt.result.Status == contracts.AuthStatusUnauthenticated { + if tt.want != "" { assert.Equal(t, tt.want, got) } else { assert.Contains(t, got, "Logged in to Azure") From 29af44647533aadc957529888fa0ad0ec4278391 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Wed, 16 Sep 2026 18:03:44 +0000 Subject: [PATCH 3/5] fix(auth): omit empty account from login success output Use the generic success message for empty identities in terminal and JSON login output, preserving populated identity formatting and the event envelope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c24d9e3-50ce-4517-960f-a5813a362002 --- cli/azd/pkg/output/ux/logged_in.go | 12 ++++- cli/azd/pkg/output/ux/logged_in_test.go | 63 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 cli/azd/pkg/output/ux/logged_in_test.go diff --git a/cli/azd/pkg/output/ux/logged_in.go b/cli/azd/pkg/output/ux/logged_in.go index 59decaf2c4d..e4911f79300 100644 --- a/cli/azd/pkg/output/ux/logged_in.go +++ b/cli/azd/pkg/output/ux/logged_in.go @@ -24,6 +24,10 @@ type LoggedIn struct { } func (cr *LoggedIn) ToString(currentIndentation string) string { + if cr.LoggedInAs == "" { + return currentIndentation + cLoginSuccessMessage + } + switch cr.LoginType { case EmailLoginType: return fmt.Sprintf( @@ -47,7 +51,11 @@ func (cr *LoggedIn) ToString(currentIndentation string) string { } func (cr *LoggedIn) MarshalJSON() ([]byte, error) { + message := cLoginSuccessMessage + if cr.LoggedInAs != "" { + message = fmt.Sprintf("%s as %s", message, cr.LoggedInAs) + } + // reusing the same envelope from console messages - return json.Marshal(output.EventForMessage( - fmt.Sprintf("%s as %s", cLoginSuccessMessage, cr.LoggedInAs))) + return json.Marshal(output.EventForMessage(message)) } diff --git a/cli/azd/pkg/output/ux/logged_in_test.go b/cli/azd/pkg/output/ux/logged_in_test.go new file mode 100644 index 00000000000..695d86be41f --- /dev/null +++ b/cli/azd/pkg/output/ux/logged_in_test.go @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package ux + +import ( + "encoding/json" + "testing" + "time" + + "github.com/azure/azure-dev/cli/azd/pkg/contracts" + "github.com/azure/azure-dev/cli/azd/pkg/output" + "github.com/stretchr/testify/require" +) + +func TestLoggedIn(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + loginType LoginType + account string + wantText string + wantMessage string + }{ + { + name: "system-assigned managed identity", loginType: ClientIdLoginType, + wantText: "Logged in to Azure", wantMessage: "Logged in to Azure\n", + }, + { + name: "user without account name", loginType: EmailLoginType, + wantText: "Logged in to Azure", wantMessage: "Logged in to Azure\n", + }, + { + name: "user", loginType: EmailLoginType, account: "user@example.com", + wantText: "Logged in to Azure as " + output.WithBold("%s", "user@example.com"), + wantMessage: "Logged in to Azure as user@example.com\n", + }, + { + name: "service principal", loginType: ClientIdLoginType, account: "client-id", + wantText: "Logged in to Azure as (" + output.WithGrayFormat("%s", "client-id") + ")", + wantMessage: "Logged in to Azure as client-id\n", + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + view := &LoggedIn{LoginType: tt.loginType, LoggedInAs: tt.account} + require.Equal(t, " "+tt.wantText, view.ToString(" ")) + + data, err := json.Marshal(view) + require.NoError(t, err) + var event struct { + Type string `json:"type"` + Timestamp time.Time `json:"timestamp"` + Data contracts.ConsoleMessage `json:"data"` + } + require.NoError(t, json.Unmarshal(data, &event)) + require.Equal(t, string(contracts.ConsoleMessageEventDataType), event.Type) + require.False(t, event.Timestamp.IsZero()) + require.Equal(t, tt.wantMessage, event.Data.Message) + }) + } +} From d46e62ea0ffecc27eca36523481259bcd64154d3 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Fri, 18 Sep 2026 22:19:01 +0000 Subject: [PATCH 4/5] fix(ext): keep current principal API in v1beta Restore the stable Account contract and expose principal lookup through AccountBeta with a focused host override. Update documentation and verify stable and legacy routes remain unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c24d9e3-50ce-4517-960f-a5813a362002 --- .../docs/extensions/extension-framework.md | 11 +- .../extensions/extension-sdk-reference.md | 3 +- .../proto/azd/extensions/v1/account.proto | 21 -- .../internal/grpcserver/account_service.go | 15 +- .../grpcserver/account_service_test.go | 62 ++++-- cli/azd/internal/grpcserver/server.go | 6 +- .../versioned_services_generated.go | 9 +- cli/azd/pkg/azdext/azd_client.go | 5 + cli/azd/pkg/azdext/contracts/v1/account.pb.go | 206 ++---------------- .../azdext/contracts/v1/account_grpc.pb.go | 44 +--- .../pkg/azdext/contracts/versioning_test.go | 15 ++ cli/azd/pkg/azdext/contracts_v1_aliases.go | 9 - docs/architecture/extension-framework.md | 2 +- docs/guides/creating-an-extension.md | 2 +- 14 files changed, 108 insertions(+), 302 deletions(-) diff --git a/cli/azd/docs/extensions/extension-framework.md b/cli/azd/docs/extensions/extension-framework.md index d1b33538a91..9d516fe7c61 100644 --- a/cli/azd/docs/extensions/extension-framework.md +++ b/cli/azd/docs/extensions/extension-framework.md @@ -3062,7 +3062,7 @@ func getSubscriptionDetails(ctx context.Context, azdClient *azdext.AzdClient, su #### GetCurrentPrincipal -Resolves the current identity for role assignments in a specified subscription. The host returns the object ID in the subscription's resource tenant, which can differ from a guest user's home-tenant object ID. Unlike `LookupTenant`, this method uses the resource tenant rather than the user access tenant. +This preview method resolves the current identity for role assignments in a specified subscription. The host returns the object ID in the subscription's resource tenant, which can differ from a guest user's home-tenant object ID. Unlike `LookupTenant`, this method uses the resource tenant rather than the user access tenant. | Field | Description | |---|---| @@ -3073,7 +3073,8 @@ Resolves the current identity for role assignments in a specified subscription. The host reuses its principal lookup, including the ARM token `oid` claim and Graph fallback. Service-principal logins and both system-assigned and user-assigned managed identities return `PRINCIPAL_TYPE_SERVICE_PRINCIPAL`. Access tokens are neither accepted nor returned by this RPC. An empty subscription ID returns `InvalidArgument`; authentication, subscription, and principal lookup failures return errors rather than an empty identity. ```go -principal, err := azdClient.Account().GetCurrentPrincipal(ctx, &azdext.GetCurrentPrincipalRequest{ +// Import v1beta "github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta". +principal, err := azdClient.AccountBeta().GetCurrentPrincipal(ctx, &v1beta.GetCurrentPrincipalRequest{ SubscriptionId: subscriptionId, }) if err != nil { @@ -3082,9 +3083,9 @@ if err != nil { var principalType string switch principal.PrincipalType { -case azdext.PrincipalType_PRINCIPAL_TYPE_USER: +case v1beta.PrincipalType_PRINCIPAL_TYPE_USER: principalType = "User" -case azdext.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL: +case v1beta.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL: principalType = "ServicePrincipal" default: return fmt.Errorf("unsupported principal type: %v", principal.PrincipalType) @@ -3092,7 +3093,7 @@ default: // Pass principal.ObjectId and principalType to the role assignment. ``` -This method is available in both `v1` and `v1beta`. Older azd hosts return `Unimplemented`. Extensions must consume an SDK release containing the method and require a host release that supports it before removing their existing principal lookup. +This method and its request, response, and enum types are available only in [`v1beta`](../../grpc/proto/azd/extensions/v1beta/account.proto). `Account()` remains the unchanged stable client; use `AccountBeta()` for principal lookup. Older azd hosts return `Unimplemented`. Extensions must consume an SDK release containing the method and require a host release that supports it before removing their existing principal lookup. --- diff --git a/cli/azd/docs/extensions/extension-sdk-reference.md b/cli/azd/docs/extensions/extension-sdk-reference.md index e346c4e18ff..486f789374d 100644 --- a/cli/azd/docs/extensions/extension-sdk-reference.md +++ b/cli/azd/docs/extensions/extension-sdk-reference.md @@ -519,6 +519,7 @@ gRPC client connecting to the azd framework. Auto-discovers the socket via | `Container()` | `ContainerServiceClient` | | `Extension()` | `ExtensionServiceClient` | | `Account()` | `AccountServiceClient` | +| `AccountBeta()` | `v1beta.AccountServiceClient` (preview) | | `Ai()` | `AiModelServiceClient` | | `Copilot()` | `v1beta.CopilotServiceClient` (preview) | | `Telemetry()` | `v1beta.TelemetryServiceClient` (preview) | @@ -532,7 +533,7 @@ stable `azdext` contract facade until those services graduate to `v1`. #### AccountService -`Account().GetCurrentPrincipal(ctx, &azdext.GetCurrentPrincipalRequest{SubscriptionId: subscriptionID})` returns the current identity's `ObjectId` in the subscription's resource tenant and its `PrincipalType` enum. Use both values for role assignments instead of decoding access tokens in the extension. The subscription ID is required, and no active environment is needed. +`AccountBeta().GetCurrentPrincipal(ctx, &v1beta.GetCurrentPrincipalRequest{SubscriptionId: subscriptionID})` returns the current identity's `ObjectId` in the subscription's resource tenant and its `PrincipalType` enum. Import `github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta` for these preview types. Use both values for role assignments instead of decoding access tokens in the extension. The subscription ID is required, and no active environment is needed. The stable `Account()` client remains unchanged and does not expose this method. See [GetCurrentPrincipal](extension-framework.md#getcurrentprincipal) for the enum mapping, guest-user behavior, and host compatibility requirements. diff --git a/cli/azd/grpc/proto/azd/extensions/v1/account.proto b/cli/azd/grpc/proto/azd/extensions/v1/account.proto index 77f7b4f0d7c..09fb5c04508 100644 --- a/cli/azd/grpc/proto/azd/extensions/v1/account.proto +++ b/cli/azd/grpc/proto/azd/extensions/v1/account.proto @@ -14,9 +14,6 @@ service AccountService { // LookupTenant resolves the tenant ID required to access a specific subscription. rpc LookupTenant (LookupTenantRequest) returns (LookupTenantResponse); - - // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. - rpc GetCurrentPrincipal (GetCurrentPrincipalRequest) returns (GetCurrentPrincipalResponse); } message ListSubscriptionsRequest { @@ -37,21 +34,3 @@ message LookupTenantResponse { // The tenant ID required to access the subscription. string tenant_id = 1; } - -message GetCurrentPrincipalRequest { - // Required subscription ID. The active environment is not used as a default. - string subscription_id = 1; -} - -message GetCurrentPrincipalResponse { - // Object ID in the subscription's resource tenant, not the user's home tenant or an application client ID. - string object_id = 1; - // Principal type determined from the host's login details. - PrincipalType principal_type = 2; -} - -enum PrincipalType { - PRINCIPAL_TYPE_UNSPECIFIED = 0; - PRINCIPAL_TYPE_USER = 1; - PRINCIPAL_TYPE_SERVICE_PRINCIPAL = 2; -} diff --git a/cli/azd/internal/grpcserver/account_service.go b/cli/azd/internal/grpcserver/account_service.go index 9faa90d3da2..bd765614b57 100644 --- a/cli/azd/internal/grpcserver/account_service.go +++ b/cli/azd/internal/grpcserver/account_service.go @@ -12,6 +12,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/auth" "github.com/azure/azure-dev/cli/azd/pkg/azapi" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + v1beta "github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta" "github.com/azure/azure-dev/cli/azd/pkg/azureutil" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -42,10 +43,12 @@ func NewAccountService( } } +var _ BetaAccountServiceGetCurrentPrincipalOverride = (*accountService)(nil) + func (s *accountService) GetCurrentPrincipal( ctx context.Context, - req *azdext.GetCurrentPrincipalRequest, -) (*azdext.GetCurrentPrincipalResponse, error) { + req *v1beta.GetCurrentPrincipalRequest, +) (*v1beta.GetCurrentPrincipalResponse, error) { if strings.TrimSpace(req.GetSubscriptionId()) == "" { return nil, status.Error(codes.InvalidArgument, "subscription id is required") } @@ -55,12 +58,12 @@ func (s *accountService) GetCurrentPrincipal( return nil, err } - var protoType azdext.PrincipalType + var protoType v1beta.PrincipalType switch principalType { case auth.UserPrincipalType: - protoType = azdext.PrincipalType_PRINCIPAL_TYPE_USER + protoType = v1beta.PrincipalType_PRINCIPAL_TYPE_USER case auth.ServicePrincipalType: - protoType = azdext.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL + protoType = v1beta.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL default: return nil, status.Error(codes.Internal, "unsupported current principal type") } @@ -76,7 +79,7 @@ func (s *accountService) GetCurrentPrincipal( return nil, fmt.Errorf("fetching current principal information: %w", err) } - return &azdext.GetCurrentPrincipalResponse{ + return &v1beta.GetCurrentPrincipalResponse{ ObjectId: objectID, PrincipalType: protoType, }, nil diff --git a/cli/azd/internal/grpcserver/account_service_test.go b/cli/azd/internal/grpcserver/account_service_test.go index a6a9a77f73f..7a10cb9f909 100644 --- a/cli/azd/internal/grpcserver/account_service_test.go +++ b/cli/azd/internal/grpcserver/account_service_test.go @@ -80,13 +80,13 @@ func TestAccountService_GetCurrentPrincipal(t *testing.T) { name string accessTenant string principalType auth.PrincipalType - protoType azdext.PrincipalType + protoType v1beta.PrincipalType }{ - {"user", "resource-tenant", auth.UserPrincipalType, azdext.PrincipalType_PRINCIPAL_TYPE_USER}, - {"guest", "home-tenant", auth.UserPrincipalType, azdext.PrincipalType_PRINCIPAL_TYPE_USER}, + {"user", "resource-tenant", auth.UserPrincipalType, v1beta.PrincipalType_PRINCIPAL_TYPE_USER}, + {"guest", "home-tenant", auth.UserPrincipalType, v1beta.PrincipalType_PRINCIPAL_TYPE_USER}, { "service principal", "resource-tenant", auth.ServicePrincipalType, - azdext.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL, + v1beta.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL, }, } { t.Run(tt.name, func(t *testing.T) { @@ -96,9 +96,9 @@ func TestAccountService_GetCurrentPrincipal(t *testing.T) { subscriptions := &mockAccountSubscriptions{} subscriptions.On("GetSubscription", mock.Anything, "sub-123").Return(&account.Subscription{ Id: "sub-123", TenantId: "resource-tenant", UserAccessTenantId: tt.accessTenant, - }, nil).Twice() + }, nil).Once() principalTypes := &mockAccountPrincipalType{} - principalTypes.On("CurrentPrincipalType", mock.Anything).Return(tt.principalType, nil).Twice() + principalTypes.On("CurrentPrincipalType", mock.Anything).Return(tt.principalType, nil).Once() mockContext := mocks.NewMockContext(ctx) azureCloud := cloud.AzurePublic() @@ -133,8 +133,26 @@ func TestAccountService_GetCurrentPrincipal(t *testing.T) { subscriptionsManager: subscriptions, userProfileService: userProfile, principalTypeProvider: principalTypes, } - server := newServerWithContainerService(azdext.UnimplementedContainerServiceServer{}) - server.accountService = svc + server := NewServer( + azdext.UnimplementedProjectServiceServer{}, + azdext.UnimplementedEnvironmentServiceServer{}, + azdext.UnimplementedPromptServiceServer{}, + azdext.UnimplementedUserConfigServiceServer{}, + azdext.UnimplementedDeploymentServiceServer{}, + azdext.UnimplementedEventServiceServer{}, + v1beta.UnimplementedComposeServiceServer{}, + azdext.UnimplementedWorkflowServiceServer{}, + azdext.UnimplementedExtensionServiceServer{}, + azdext.UnimplementedServiceTargetServiceServer{}, + azdext.UnimplementedFrameworkServiceServer{}, + azdext.UnimplementedContainerServiceServer{}, + svc, + azdext.UnimplementedAiModelServiceServer{}, + v1beta.UnimplementedCopilotServiceServer{}, + azdext.UnimplementedProvisioningServiceServer{}, + azdext.UnimplementedValidationServiceServer{}, + v1beta.UnimplementedTelemetryServiceServer{}, + ) info, err := server.Start() require.NoError(t, err) t.Cleanup(func() { require.NoError(t, server.Stop()) }) @@ -145,7 +163,7 @@ func TestAccountService_GetCurrentPrincipal(t *testing.T) { require.NoError(t, err) t.Cleanup(client.Close) - response, err := client.Account().GetCurrentPrincipal(ctx, &azdext.GetCurrentPrincipalRequest{ + response, err := client.AccountBeta().GetCurrentPrincipal(ctx, &v1beta.GetCurrentPrincipalRequest{ SubscriptionId: "sub-123", }) require.NoError(t, err) @@ -155,13 +173,15 @@ func TestAccountService_GetCurrentPrincipal(t *testing.T) { connection, err := grpc.NewClient(info.Address, grpc.WithTransportCredentials(insecure.NewCredentials())) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, connection.Close()) }) - betaResponse, err := v1beta.NewAccountServiceClient(connection).GetCurrentPrincipal( - ctx, &v1beta.GetCurrentPrincipalRequest{SubscriptionId: "sub-123"}, - ) - require.NoError(t, err) - require.Equal(t, response.ObjectId, betaResponse.ObjectId) - require.Equal(t, int32(response.PrincipalType), int32(betaResponse.PrincipalType)) - require.EqualValues(t, 2, tokenCalls.Load()) + for _, service := range []string{"azd.extensions.v1.AccountService", "azdext.AccountService"} { + err := connection.Invoke( + ctx, "/"+service+"/GetCurrentPrincipal", + &v1beta.GetCurrentPrincipalRequest{SubscriptionId: "sub-123"}, + &v1beta.GetCurrentPrincipalResponse{}, + ) + require.Equal(t, codes.Unimplemented, status.Code(err)) + } + require.EqualValues(t, 1, tokenCalls.Load()) subscriptions.AssertExpectations(t) principalTypes.AssertExpectations(t) }) @@ -170,11 +190,11 @@ func TestAccountService_GetCurrentPrincipal(t *testing.T) { func TestAccountService_GetCurrentPrincipal_InvalidRequest(t *testing.T) { t.Parallel() - for _, request := range []*azdext.GetCurrentPrincipalRequest{ + for _, request := range []*v1beta.GetCurrentPrincipalRequest{ nil, {}, {SubscriptionId: " \t"}, } { // No dependencies: validation must run before authentication or subscription lookup. - response, err := NewAccountService(nil, nil, nil).GetCurrentPrincipal(t.Context(), request) + response, err := (&accountService{}).GetCurrentPrincipal(t.Context(), request) require.Nil(t, response) require.Equal(t, codes.InvalidArgument, status.Code(err)) } @@ -205,7 +225,7 @@ func TestAccountService_GetCurrentPrincipal_LookupErrors(t *testing.T) { principalTypes := &mockAccountPrincipalType{} principalTypes.On("CurrentPrincipalType", t.Context()).Return(tt.principalType, tt.loginErr).Once() svc := &accountService{principalTypeProvider: principalTypes, subscriptionsManager: subscriptions} - response, err := svc.GetCurrentPrincipal(t.Context(), &azdext.GetCurrentPrincipalRequest{ + response, err := svc.GetCurrentPrincipal(t.Context(), &v1beta.GetCurrentPrincipalRequest{ SubscriptionId: "sub-123", }) require.Nil(t, response) @@ -261,7 +281,7 @@ func TestAccountService_GetCurrentPrincipal_GraphFallback(t *testing.T) { svc := &accountService{ subscriptionsManager: subscriptions, principalTypeProvider: principalTypes, userProfileService: userProfile, } - response, err := svc.GetCurrentPrincipal(t.Context(), &azdext.GetCurrentPrincipalRequest{ + response, err := svc.GetCurrentPrincipal(t.Context(), &v1beta.GetCurrentPrincipalRequest{ SubscriptionId: "sub-123", }) if tt.wantError != "" { @@ -270,7 +290,7 @@ func TestAccountService_GetCurrentPrincipal_GraphFallback(t *testing.T) { } else { require.NoError(t, err) require.Equal(t, tt.graphID, response.ObjectId) - require.Equal(t, azdext.PrincipalType_PRINCIPAL_TYPE_USER, response.PrincipalType) + require.Equal(t, v1beta.PrincipalType_PRINCIPAL_TYPE_USER, response.PrincipalType) } subscriptions.AssertExpectations(t) principalTypes.AssertExpectations(t) diff --git a/cli/azd/internal/grpcserver/server.go b/cli/azd/internal/grpcserver/server.go index 33cfeccd91c..5cc1c66e839 100644 --- a/cli/azd/internal/grpcserver/server.go +++ b/cli/azd/internal/grpcserver/server.go @@ -72,7 +72,7 @@ func NewServer( validationService azdext.ValidationServiceServer, telemetryService v1beta.TelemetryServiceServer, ) *Server { - return &Server{ + server := &Server{ projectService: projectService, environmentService: environmentService, promptService: promptService, @@ -93,6 +93,10 @@ func NewServer( telemetryService: telemetryService, betaServiceOverrides: map[BetaService]any{}, } + if principalService, ok := accountService.(BetaAccountServiceGetCurrentPrincipalOverride); ok { + server.WithOptions(WithBetaServiceOverride(BetaAccountService, principalService)) + } + return server } // WithOptions applies optional beta service configuration before the server starts. diff --git a/cli/azd/internal/grpcserver/versioned_services_generated.go b/cli/azd/internal/grpcserver/versioned_services_generated.go index ba9b4f8fc44..fbc5366f81e 100644 --- a/cli/azd/internal/grpcserver/versioned_services_generated.go +++ b/cli/azd/internal/grpcserver/versioned_services_generated.go @@ -860,14 +860,7 @@ func (a *betaAccountServiceAdapter) GetCurrentPrincipal( if override, ok := a.override.(BetaAccountServiceGetCurrentPrincipalOverride); ok { return override.GetCurrentPrincipal(ctx, req) } - return adaptBetaUnary( - ctx, - req, - new(v1.GetCurrentPrincipalRequest), - a.stable.GetCurrentPrincipal, - new(v1beta.GetCurrentPrincipalResponse), - "AccountService.GetCurrentPrincipal", - ) + return a.UnimplementedAccountServiceServer.GetCurrentPrincipal(ctx, req) } type betaAiModelServiceAdapter struct { diff --git a/cli/azd/pkg/azdext/azd_client.go b/cli/azd/pkg/azdext/azd_client.go index 325a1fb3fa7..17f0b616496 100644 --- a/cli/azd/pkg/azdext/azd_client.go +++ b/cli/azd/pkg/azdext/azd_client.go @@ -271,6 +271,11 @@ func (c *AzdClient) Account() AccountServiceClient { return c.accountClient } +// AccountBeta returns the preview account service client, including current principal lookup. +func (c *AzdClient) AccountBeta() v1beta.AccountServiceClient { + return v1beta.NewAccountServiceClient(c.connection) +} + // Ai returns the AI model service client. func (c *AzdClient) Ai() AiModelServiceClient { if c.aiClient == nil { diff --git a/cli/azd/pkg/azdext/contracts/v1/account.pb.go b/cli/azd/pkg/azdext/contracts/v1/account.pb.go index 8987c15ff35..8c65d343fbf 100644 --- a/cli/azd/pkg/azdext/contracts/v1/account.pb.go +++ b/cli/azd/pkg/azdext/contracts/v1/account.pb.go @@ -24,55 +24,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type PrincipalType int32 - -const ( - PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED PrincipalType = 0 - PrincipalType_PRINCIPAL_TYPE_USER PrincipalType = 1 - PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL PrincipalType = 2 -) - -// Enum value maps for PrincipalType. -var ( - PrincipalType_name = map[int32]string{ - 0: "PRINCIPAL_TYPE_UNSPECIFIED", - 1: "PRINCIPAL_TYPE_USER", - 2: "PRINCIPAL_TYPE_SERVICE_PRINCIPAL", - } - PrincipalType_value = map[string]int32{ - "PRINCIPAL_TYPE_UNSPECIFIED": 0, - "PRINCIPAL_TYPE_USER": 1, - "PRINCIPAL_TYPE_SERVICE_PRINCIPAL": 2, - } -) - -func (x PrincipalType) Enum() *PrincipalType { - p := new(PrincipalType) - *p = x - return p -} - -func (x PrincipalType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (PrincipalType) Descriptor() protoreflect.EnumDescriptor { - return file_azd_extensions_v1_account_proto_enumTypes[0].Descriptor() -} - -func (PrincipalType) Type() protoreflect.EnumType { - return &file_azd_extensions_v1_account_proto_enumTypes[0] -} - -func (x PrincipalType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use PrincipalType.Descriptor instead. -func (PrincipalType) EnumDescriptor() ([]byte, []int) { - return file_azd_extensions_v1_account_proto_rawDescGZIP(), []int{0} -} - type ListSubscriptionsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Optional: filter subscriptions by tenant ID. @@ -252,105 +203,6 @@ func (x *LookupTenantResponse) GetTenantId() string { return "" } -type GetCurrentPrincipalRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Required subscription ID. The active environment is not used as a default. - SubscriptionId string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId,proto3" json:"subscription_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetCurrentPrincipalRequest) Reset() { - *x = GetCurrentPrincipalRequest{} - mi := &file_azd_extensions_v1_account_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetCurrentPrincipalRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetCurrentPrincipalRequest) ProtoMessage() {} - -func (x *GetCurrentPrincipalRequest) ProtoReflect() protoreflect.Message { - mi := &file_azd_extensions_v1_account_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetCurrentPrincipalRequest.ProtoReflect.Descriptor instead. -func (*GetCurrentPrincipalRequest) Descriptor() ([]byte, []int) { - return file_azd_extensions_v1_account_proto_rawDescGZIP(), []int{4} -} - -func (x *GetCurrentPrincipalRequest) GetSubscriptionId() string { - if x != nil { - return x.SubscriptionId - } - return "" -} - -type GetCurrentPrincipalResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Object ID in the subscription's resource tenant, not the user's home tenant or an application client ID. - ObjectId string `protobuf:"bytes,1,opt,name=object_id,json=objectId,proto3" json:"object_id,omitempty"` - // Principal type determined from the host's login details. - PrincipalType PrincipalType `protobuf:"varint,2,opt,name=principal_type,json=principalType,proto3,enum=azd.extensions.v1.PrincipalType" json:"principal_type,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetCurrentPrincipalResponse) Reset() { - *x = GetCurrentPrincipalResponse{} - mi := &file_azd_extensions_v1_account_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetCurrentPrincipalResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetCurrentPrincipalResponse) ProtoMessage() {} - -func (x *GetCurrentPrincipalResponse) ProtoReflect() protoreflect.Message { - mi := &file_azd_extensions_v1_account_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetCurrentPrincipalResponse.ProtoReflect.Descriptor instead. -func (*GetCurrentPrincipalResponse) Descriptor() ([]byte, []int) { - return file_azd_extensions_v1_account_proto_rawDescGZIP(), []int{5} -} - -func (x *GetCurrentPrincipalResponse) GetObjectId() string { - if x != nil { - return x.ObjectId - } - return "" -} - -func (x *GetCurrentPrincipalResponse) GetPrincipalType() PrincipalType { - if x != nil { - return x.PrincipalType - } - return PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED -} - var File_azd_extensions_v1_account_proto protoreflect.FileDescriptor const file_azd_extensions_v1_account_proto_rawDesc = "" + @@ -365,20 +217,10 @@ const file_azd_extensions_v1_account_proto_rawDesc = "" + "\x13LookupTenantRequest\x12'\n" + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\"3\n" + "\x14LookupTenantResponse\x12\x1b\n" + - "\ttenant_id\x18\x01 \x01(\tR\btenantId\"E\n" + - "\x1aGetCurrentPrincipalRequest\x12'\n" + - "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\"\x83\x01\n" + - "\x1bGetCurrentPrincipalResponse\x12\x1b\n" + - "\tobject_id\x18\x01 \x01(\tR\bobjectId\x12G\n" + - "\x0eprincipal_type\x18\x02 \x01(\x0e2 .azd.extensions.v1.PrincipalTypeR\rprincipalType*n\n" + - "\rPrincipalType\x12\x1e\n" + - "\x1aPRINCIPAL_TYPE_UNSPECIFIED\x10\x00\x12\x17\n" + - "\x13PRINCIPAL_TYPE_USER\x10\x01\x12$\n" + - " PRINCIPAL_TYPE_SERVICE_PRINCIPAL\x10\x022\xd7\x02\n" + + "\ttenant_id\x18\x01 \x01(\tR\btenantId2\xe1\x01\n" + "\x0eAccountService\x12n\n" + "\x11ListSubscriptions\x12+.azd.extensions.v1.ListSubscriptionsRequest\x1a,.azd.extensions.v1.ListSubscriptionsResponse\x12_\n" + - "\fLookupTenant\x12&.azd.extensions.v1.LookupTenantRequest\x1a'.azd.extensions.v1.LookupTenantResponse\x12t\n" + - "\x13GetCurrentPrincipal\x12-.azd.extensions.v1.GetCurrentPrincipalRequest\x1a..azd.extensions.v1.GetCurrentPrincipalResponseB?Z=github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1;v1b\x06proto3" + "\fLookupTenant\x12&.azd.extensions.v1.LookupTenantRequest\x1a'.azd.extensions.v1.LookupTenantResponseB?Z=github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1;v1b\x06proto3" var ( file_azd_extensions_v1_account_proto_rawDescOnce sync.Once @@ -392,32 +234,25 @@ func file_azd_extensions_v1_account_proto_rawDescGZIP() []byte { return file_azd_extensions_v1_account_proto_rawDescData } -var file_azd_extensions_v1_account_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_azd_extensions_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_azd_extensions_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_azd_extensions_v1_account_proto_goTypes = []any{ - (PrincipalType)(0), // 0: azd.extensions.v1.PrincipalType - (*ListSubscriptionsRequest)(nil), // 1: azd.extensions.v1.ListSubscriptionsRequest - (*ListSubscriptionsResponse)(nil), // 2: azd.extensions.v1.ListSubscriptionsResponse - (*LookupTenantRequest)(nil), // 3: azd.extensions.v1.LookupTenantRequest - (*LookupTenantResponse)(nil), // 4: azd.extensions.v1.LookupTenantResponse - (*GetCurrentPrincipalRequest)(nil), // 5: azd.extensions.v1.GetCurrentPrincipalRequest - (*GetCurrentPrincipalResponse)(nil), // 6: azd.extensions.v1.GetCurrentPrincipalResponse - (*Subscription)(nil), // 7: azd.extensions.v1.Subscription + (*ListSubscriptionsRequest)(nil), // 0: azd.extensions.v1.ListSubscriptionsRequest + (*ListSubscriptionsResponse)(nil), // 1: azd.extensions.v1.ListSubscriptionsResponse + (*LookupTenantRequest)(nil), // 2: azd.extensions.v1.LookupTenantRequest + (*LookupTenantResponse)(nil), // 3: azd.extensions.v1.LookupTenantResponse + (*Subscription)(nil), // 4: azd.extensions.v1.Subscription } var file_azd_extensions_v1_account_proto_depIdxs = []int32{ - 7, // 0: azd.extensions.v1.ListSubscriptionsResponse.subscriptions:type_name -> azd.extensions.v1.Subscription - 0, // 1: azd.extensions.v1.GetCurrentPrincipalResponse.principal_type:type_name -> azd.extensions.v1.PrincipalType - 1, // 2: azd.extensions.v1.AccountService.ListSubscriptions:input_type -> azd.extensions.v1.ListSubscriptionsRequest - 3, // 3: azd.extensions.v1.AccountService.LookupTenant:input_type -> azd.extensions.v1.LookupTenantRequest - 5, // 4: azd.extensions.v1.AccountService.GetCurrentPrincipal:input_type -> azd.extensions.v1.GetCurrentPrincipalRequest - 2, // 5: azd.extensions.v1.AccountService.ListSubscriptions:output_type -> azd.extensions.v1.ListSubscriptionsResponse - 4, // 6: azd.extensions.v1.AccountService.LookupTenant:output_type -> azd.extensions.v1.LookupTenantResponse - 6, // 7: azd.extensions.v1.AccountService.GetCurrentPrincipal:output_type -> azd.extensions.v1.GetCurrentPrincipalResponse - 5, // [5:8] is the sub-list for method output_type - 2, // [2:5] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 4, // 0: azd.extensions.v1.ListSubscriptionsResponse.subscriptions:type_name -> azd.extensions.v1.Subscription + 0, // 1: azd.extensions.v1.AccountService.ListSubscriptions:input_type -> azd.extensions.v1.ListSubscriptionsRequest + 2, // 2: azd.extensions.v1.AccountService.LookupTenant:input_type -> azd.extensions.v1.LookupTenantRequest + 1, // 3: azd.extensions.v1.AccountService.ListSubscriptions:output_type -> azd.extensions.v1.ListSubscriptionsResponse + 3, // 4: azd.extensions.v1.AccountService.LookupTenant:output_type -> azd.extensions.v1.LookupTenantResponse + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_azd_extensions_v1_account_proto_init() } @@ -432,14 +267,13 @@ func file_azd_extensions_v1_account_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_azd_extensions_v1_account_proto_rawDesc), len(file_azd_extensions_v1_account_proto_rawDesc)), - NumEnums: 1, - NumMessages: 6, + NumEnums: 0, + NumMessages: 4, NumExtensions: 0, NumServices: 1, }, GoTypes: file_azd_extensions_v1_account_proto_goTypes, DependencyIndexes: file_azd_extensions_v1_account_proto_depIdxs, - EnumInfos: file_azd_extensions_v1_account_proto_enumTypes, MessageInfos: file_azd_extensions_v1_account_proto_msgTypes, }.Build() File_azd_extensions_v1_account_proto = out.File diff --git a/cli/azd/pkg/azdext/contracts/v1/account_grpc.pb.go b/cli/azd/pkg/azdext/contracts/v1/account_grpc.pb.go index b1163aa334e..24e6232409f 100644 --- a/cli/azd/pkg/azdext/contracts/v1/account_grpc.pb.go +++ b/cli/azd/pkg/azdext/contracts/v1/account_grpc.pb.go @@ -22,9 +22,8 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - AccountService_ListSubscriptions_FullMethodName = "/azd.extensions.v1.AccountService/ListSubscriptions" - AccountService_LookupTenant_FullMethodName = "/azd.extensions.v1.AccountService/LookupTenant" - AccountService_GetCurrentPrincipal_FullMethodName = "/azd.extensions.v1.AccountService/GetCurrentPrincipal" + AccountService_ListSubscriptions_FullMethodName = "/azd.extensions.v1.AccountService/ListSubscriptions" + AccountService_LookupTenant_FullMethodName = "/azd.extensions.v1.AccountService/LookupTenant" ) // AccountServiceClient is the client API for AccountService service. @@ -35,8 +34,6 @@ type AccountServiceClient interface { ListSubscriptions(ctx context.Context, in *ListSubscriptionsRequest, opts ...grpc.CallOption) (*ListSubscriptionsResponse, error) // LookupTenant resolves the tenant ID required to access a specific subscription. LookupTenant(ctx context.Context, in *LookupTenantRequest, opts ...grpc.CallOption) (*LookupTenantResponse, error) - // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. - GetCurrentPrincipal(ctx context.Context, in *GetCurrentPrincipalRequest, opts ...grpc.CallOption) (*GetCurrentPrincipalResponse, error) } type accountServiceClient struct { @@ -67,16 +64,6 @@ func (c *accountServiceClient) LookupTenant(ctx context.Context, in *LookupTenan return out, nil } -func (c *accountServiceClient) GetCurrentPrincipal(ctx context.Context, in *GetCurrentPrincipalRequest, opts ...grpc.CallOption) (*GetCurrentPrincipalResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetCurrentPrincipalResponse) - err := c.cc.Invoke(ctx, AccountService_GetCurrentPrincipal_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - // AccountServiceServer is the server API for AccountService service. // All implementations must embed UnimplementedAccountServiceServer // for forward compatibility. @@ -85,8 +72,6 @@ type AccountServiceServer interface { ListSubscriptions(context.Context, *ListSubscriptionsRequest) (*ListSubscriptionsResponse, error) // LookupTenant resolves the tenant ID required to access a specific subscription. LookupTenant(context.Context, *LookupTenantRequest) (*LookupTenantResponse, error) - // GetCurrentPrincipal resolves the signed-in identity in the subscription's resource tenant. - GetCurrentPrincipal(context.Context, *GetCurrentPrincipalRequest) (*GetCurrentPrincipalResponse, error) mustEmbedUnimplementedAccountServiceServer() } @@ -103,9 +88,6 @@ func (UnimplementedAccountServiceServer) ListSubscriptions(context.Context, *Lis func (UnimplementedAccountServiceServer) LookupTenant(context.Context, *LookupTenantRequest) (*LookupTenantResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method LookupTenant not implemented") } -func (UnimplementedAccountServiceServer) GetCurrentPrincipal(context.Context, *GetCurrentPrincipalRequest) (*GetCurrentPrincipalResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetCurrentPrincipal not implemented") -} func (UnimplementedAccountServiceServer) mustEmbedUnimplementedAccountServiceServer() {} func (UnimplementedAccountServiceServer) testEmbeddedByValue() {} @@ -163,24 +145,6 @@ func _AccountService_LookupTenant_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } -func _AccountService_GetCurrentPrincipal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetCurrentPrincipalRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(AccountServiceServer).GetCurrentPrincipal(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: AccountService_GetCurrentPrincipal_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AccountServiceServer).GetCurrentPrincipal(ctx, req.(*GetCurrentPrincipalRequest)) - } - return interceptor(ctx, in, info, handler) -} - // AccountService_ServiceDesc is the grpc.ServiceDesc for AccountService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -196,10 +160,6 @@ var AccountService_ServiceDesc = grpc.ServiceDesc{ MethodName: "LookupTenant", Handler: _AccountService_LookupTenant_Handler, }, - { - MethodName: "GetCurrentPrincipal", - Handler: _AccountService_GetCurrentPrincipal_Handler, - }, }, Streams: []grpc.StreamDesc{}, Metadata: "azd/extensions/v1/account.proto", diff --git a/cli/azd/pkg/azdext/contracts/versioning_test.go b/cli/azd/pkg/azdext/contracts/versioning_test.go index 9206c46374d..059dfdba173 100644 --- a/cli/azd/pkg/azdext/contracts/versioning_test.go +++ b/cli/azd/pkg/azdext/contracts/versioning_test.go @@ -45,6 +45,21 @@ func TestPreviewOnlyServicesAreExcludedFromStable(t *testing.T) { } } +func TestCurrentPrincipalIsBetaOnly(t *testing.T) { + t.Parallel() + + stable := v1.File_azd_extensions_v1_account_proto + beta := v1beta.File_azd_extensions_v1beta_account_proto + require.Nil(t, stable.Services().ByName("AccountService").Methods().ByName("GetCurrentPrincipal")) + require.NotNil(t, beta.Services().ByName("AccountService").Methods().ByName("GetCurrentPrincipal")) + for _, name := range []protoreflect.Name{"GetCurrentPrincipalRequest", "GetCurrentPrincipalResponse"} { + require.Nil(t, stable.Messages().ByName(name)) + require.NotNil(t, beta.Messages().ByName(name)) + } + require.Nil(t, stable.Enums().ByName("PrincipalType")) + require.NotNil(t, beta.Enums().ByName("PrincipalType")) +} + func TestStableSubsetAllowsAdditiveBetaFieldsAndMethods(t *testing.T) { t.Parallel() diff --git a/cli/azd/pkg/azdext/contracts_v1_aliases.go b/cli/azd/pkg/azdext/contracts_v1_aliases.go index 55b212bc50d..fd1be5c0e19 100644 --- a/cli/azd/pkg/azdext/contracts_v1_aliases.go +++ b/cli/azd/pkg/azdext/contracts_v1_aliases.go @@ -11,7 +11,6 @@ import ( ) const ( - AccountService_GetCurrentPrincipal_FullMethodName = v1.AccountService_GetCurrentPrincipal_FullMethodName AccountService_ListSubscriptions_FullMethodName = v1.AccountService_ListSubscriptions_FullMethodName AccountService_LookupTenant_FullMethodName = v1.AccountService_LookupTenant_FullMethodName AiModelService_ListLocationsWithQuota_FullMethodName = v1.AiModelService_ListLocationsWithQuota_FullMethodName @@ -55,9 +54,6 @@ const ( LocationKind_LOCATION_KIND_LOCAL = v1.LocationKind_LOCATION_KIND_LOCAL LocationKind_LOCATION_KIND_REMOTE = v1.LocationKind_LOCATION_KIND_REMOTE LocationKind_LOCATION_KIND_UNSPECIFIED = v1.LocationKind_LOCATION_KIND_UNSPECIFIED - PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL = v1.PrincipalType_PRINCIPAL_TYPE_SERVICE_PRINCIPAL - PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED = v1.PrincipalType_PRINCIPAL_TYPE_UNSPECIFIED - PrincipalType_PRINCIPAL_TYPE_USER = v1.PrincipalType_PRINCIPAL_TYPE_USER ProjectService_AddService_FullMethodName = v1.ProjectService_AddService_FullMethodName ProjectService_GetConfigSection_FullMethodName = v1.ProjectService_GetConfigSection_FullMethodName ProjectService_GetConfigValue_FullMethodName = v1.ProjectService_GetConfigValue_FullMethodName @@ -203,8 +199,6 @@ type ( GetConfigSectionResponse = v1.GetConfigSectionResponse GetConfigStringRequest = v1.GetConfigStringRequest GetConfigStringResponse = v1.GetConfigStringResponse - GetCurrentPrincipalRequest = v1.GetCurrentPrincipalRequest - GetCurrentPrincipalResponse = v1.GetCurrentPrincipalResponse GetDeploymentContextResponse = v1.GetDeploymentContextResponse GetDeploymentResponse = v1.GetDeploymentResponse GetEnvRequest = v1.GetEnvRequest @@ -259,7 +253,6 @@ type ( ParseGitHubUrlResponse = v1.ParseGitHubUrlResponse PrepareValidationContextChunk = v1.PrepareValidationContextChunk PrepareValidationContextResponse = v1.PrepareValidationContextResponse - PrincipalType = v1.PrincipalType ProjectConfig = v1.ProjectConfig ProjectHandlerStatus = v1.ProjectHandlerStatus ProjectMetadata = v1.ProjectMetadata @@ -522,8 +515,6 @@ var ( FrameworkService_ServiceDesc = v1.FrameworkService_ServiceDesc LocationKind_name = v1.LocationKind_name LocationKind_value = v1.LocationKind_value - PrincipalType_name = v1.PrincipalType_name - PrincipalType_value = v1.PrincipalType_value ProjectService_ServiceDesc = v1.ProjectService_ServiceDesc PromptService_ServiceDesc = v1.PromptService_ServiceDesc ProvisioningService_ServiceDesc = v1.ProvisioningService_ServiceDesc diff --git a/docs/architecture/extension-framework.md b/docs/architecture/extension-framework.md index 9747e379a34..96c5f945069 100644 --- a/docs/architecture/extension-framework.md +++ b/docs/architecture/extension-framework.md @@ -67,7 +67,7 @@ Extensions can access these azd services via gRPC: - **Environment** — Read/write environment values and secrets - **User Config** — Read user-level azd configuration - **Deployment** — Access deployment information -- **Account**. List subscriptions, resolve access tenants, and retrieve the current principal's resource-tenant object ID and type for role assignments. See [GetCurrentPrincipal](../../cli/azd/docs/extensions/extension-framework.md#getcurrentprincipal). +- **Account**. List subscriptions and resolve access tenants. The `v1beta` client also retrieves the current principal's resource-tenant object ID and type for role assignments. See [GetCurrentPrincipal](../../cli/azd/docs/extensions/extension-framework.md#getcurrentprincipal). - **Prompt** — Display prompts and collect user input - **AI Model** — Query AI model availability and quotas - **Event** — Subscribe to and emit events diff --git a/docs/guides/creating-an-extension.md b/docs/guides/creating-an-extension.md index 6226f43fc86..b0d7f8c37fd 100644 --- a/docs/guides/creating-an-extension.md +++ b/docs/guides/creating-an-extension.md @@ -49,7 +49,7 @@ capabilities: Implement the required interfaces for your declared capabilities. See the extension framework services documentation for interface details. -If your extension creates role assignments, use [`Account.GetCurrentPrincipal`](../../cli/azd/docs/extensions/extension-framework.md#getcurrentprincipal) with the target subscription ID. The host resolves the resource-tenant object ID and principal type without returning an access token. Consume an SDK and host release containing this method before replacing an existing lookup. +If your extension creates role assignments, use the preview [`AccountBeta().GetCurrentPrincipal`](../../cli/azd/docs/extensions/extension-framework.md#getcurrentprincipal) method with the target subscription ID and request types from `contracts/v1beta`. The host resolves the resource-tenant object ID and principal type without returning an access token. Consume an SDK and host release containing this method before replacing an existing lookup. ### 4. Build From dc21398d7bbd228523e913008ac5e7e5d2be1226 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Fri, 18 Sep 2026 22:30:26 +0000 Subject: [PATCH 5/5] docs(ext): clarify preview account accessor guidance List AccountBeta among preview accessors, distinguish beta additions from stable account methods, and scope the SDK example to Go extensions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c24d9e3-50ce-4517-960f-a5813a362002 --- cli/azd/docs/extensions/extension-sdk-reference.md | 5 +---- docs/guides/creating-an-extension.md | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/cli/azd/docs/extensions/extension-sdk-reference.md b/cli/azd/docs/extensions/extension-sdk-reference.md index 486f789374d..dc1b84c1499 100644 --- a/cli/azd/docs/extensions/extension-sdk-reference.md +++ b/cli/azd/docs/extensions/extension-sdk-reference.md @@ -526,10 +526,7 @@ gRPC client connecting to the azd framework. Auto-discovers the socket via Always call `defer client.Close()` after creation. -`Compose()`, `Copilot()`, and `Telemetry()` are preview accessors. Import -`github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta` for their -request, response, and enum types. They are intentionally excluded from the -stable `azdext` contract facade until those services graduate to `v1`. +`AccountBeta()`, `Compose()`, `Copilot()`, and `Telemetry()` are preview accessors. Import `github.com/azure/azure-dev/cli/azd/pkg/azdext/contracts/v1beta` for their request, response, and enum types. Beta-only methods and types are not exposed through the stable `azdext` contract facade. `Account()` still provides the existing stable account methods. #### AccountService diff --git a/docs/guides/creating-an-extension.md b/docs/guides/creating-an-extension.md index b0d7f8c37fd..a6b04d20428 100644 --- a/docs/guides/creating-an-extension.md +++ b/docs/guides/creating-an-extension.md @@ -49,7 +49,7 @@ capabilities: Implement the required interfaces for your declared capabilities. See the extension framework services documentation for interface details. -If your extension creates role assignments, use the preview [`AccountBeta().GetCurrentPrincipal`](../../cli/azd/docs/extensions/extension-framework.md#getcurrentprincipal) method with the target subscription ID and request types from `contracts/v1beta`. The host resolves the resource-tenant object ID and principal type without returning an access token. Consume an SDK and host release containing this method before replacing an existing lookup. +If your Go extension creates role assignments, use the preview [`AccountBeta().GetCurrentPrincipal`](../../cli/azd/docs/extensions/extension-framework.md#getcurrentprincipal) method with the target subscription ID and request types from `contracts/v1beta`. The host resolves the resource-tenant object ID and principal type without returning an access token. Consume an SDK and host release containing this method before replacing an existing lookup. ### 4. Build