Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions cli/azd/docs/extensions/extension-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -3060,6 +3060,41 @@ 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

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 |
|---|---|
| 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
// 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 {
return fmt.Errorf("resolving current principal: %w", err)
}

var principalType string
switch principal.PrincipalType {
case v1beta.PrincipalType_PRINCIPAL_TYPE_USER:
principalType = "User"
case v1beta.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 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.

---

### Copilot Service
Expand Down
12 changes: 8 additions & 4 deletions cli/azd/docs/extensions/extension-sdk-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,16 +519,20 @@ gRPC client connecting to the azd framework. Auto-discovers the socket via
| `Container()` | `ContainerServiceClient` |
| `Extension()` | `ExtensionServiceClient` |
| `Account()` | `AccountServiceClient` |
| `AccountBeta()` | `v1beta.AccountServiceClient` (preview) |
Comment thread
JeffreyCA marked this conversation as resolved.
| `Ai()` | `AiModelServiceClient` |
| `Copilot()` | `v1beta.CopilotServiceClient` (preview) |
| `Telemetry()` | `v1beta.TelemetryServiceClient` (preview) |

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

`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.

#### TelemetryService

Expand Down
21 changes: 21 additions & 0 deletions cli/azd/grpc/proto/azd/extensions/v1beta/account.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
68 changes: 65 additions & 3 deletions cli/azd/internal/grpcserver/account_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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"
)

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,
}
}

var _ BetaAccountServiceGetCurrentPrincipalOverride = (*accountService)(nil)

func (s *accountService) GetCurrentPrincipal(
ctx context.Context,
req *v1beta.GetCurrentPrincipalRequest,
) (*v1beta.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 v1beta.PrincipalType
switch principalType {
case auth.UserPrincipalType:
protoType = v1beta.PrincipalType_PRINCIPAL_TYPE_USER
case auth.ServicePrincipalType:
protoType = v1beta.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)
}

return &v1beta.GetCurrentPrincipalResponse{
ObjectId: objectID,
PrincipalType: protoType,
}, nil
}

func (s *accountService) ListSubscriptions(
Expand Down
Loading
Loading