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
8 changes: 8 additions & 0 deletions cli/azd/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ func authActions(root *actions.ActionDescriptor) *actions.ActionDescriptor {
DefaultFormat: output.NoneFormat,
})

group.Add("status", &actions.ActionDescriptorOptions{
Command: newAuthStatusCmd(),
FlagsResolver: newAuthStatusFlags,
ActionResolver: newAuthStatusAction,
OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat},
DefaultFormat: output.NoneFormat,
})

group.Add("logout", &actions.ActionDescriptorOptions{
Command: newLogoutCmd("auth"),
ActionResolver: newLogoutAction,
Expand Down
135 changes: 135 additions & 0 deletions cli/azd/cmd/auth_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"errors"
"fmt"
"io"
"log"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/auth"
"github.com/azure/azure-dev/cli/azd/pkg/contracts"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

type authStatusFlags struct {
global *internal.GlobalCommandOptions
}

func newAuthStatusFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *authStatusFlags {
flags := &authStatusFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}

func (f *authStatusFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.global = global
}

func newAuthStatusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "Show the current authentication status.",
Long: "Display whether you are logged in to Azure and the associated account information.",
}
}

type authStatusAction struct {
formatter output.Formatter
writer io.Writer
console input.Console
authManager *auth.Manager
flags *authStatusFlags
}

func newAuthStatusAction(
formatter output.Formatter,
writer io.Writer,
authManager *auth.Manager,
flags *authStatusFlags,
console input.Console,
) actions.Action {
return &authStatusAction{
formatter: formatter,
writer: writer,
console: console,
authManager: authManager,
flags: flags,
}
}

func (a *authStatusAction) Run(ctx context.Context) (*actions.ActionResult, error) {
scopes := a.authManager.LoginScopes()

// get user account information
details, err := a.authManager.LogInDetails(ctx)
var loginExpiryError *auth.ReLoginRequiredError
if err != nil {
if !errors.Is(err, auth.ErrNoCurrentUser) &&
!errors.As(err, &loginExpiryError) {
// print a useful message for unknown errors
fmt.Fprintln(a.console.Handles().Stderr, err.Error())
}
log.Printf("error: getting signed in account: %v", err)
}

res := contracts.StatusResult{}
if err != nil {
res.Status = contracts.AuthStatusUnauthenticated
} else {
res.Status = contracts.AuthStatusAuthenticated
_, err := a.verifyLoggedIn(ctx, scopes)
if err != nil {
res.Status = contracts.AuthStatusUnauthenticated
log.Printf("error: verifying logged in status: %v", err)
}

switch details.LoginType {
case auth.EmailLoginType:
res.Type = contracts.AccountTypeUser
res.Email = details.Account
case auth.ClientIdLoginType:
res.Type = contracts.AccountTypeServicePrincipal
res.ClientID = details.Account
}
}

if a.formatter.Kind() != output.NoneFormat {
a.formatter.Format(res, a.writer, nil)
return nil, nil
}

a.console.MessageUxItem(ctx, &ux.AuthStatusView{Result: &res})
return nil, nil
}

// Verifies that the user has credentials stored,
// and that the credentials stored is accepted by the identity server (can be exchanged for access token).
func (a *authStatusAction) verifyLoggedIn(ctx context.Context, scopes []string) (*azcore.AccessToken, error) {
cred, err := a.authManager.CredentialForCurrentUser(ctx, nil)
if err != nil {
return nil, err
}

// Ensure credential is valid, and can be exchanged for an access token
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: scopes,
})

if err != nil {
return nil, err
}

return &token, nil
}
8 changes: 8 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ const completionSpec: Fig.Spec = {
name: ['logout'],
description: 'Log out of Azure.',
},
{
name: ['status'],
description: 'Show the current authentication status.',
},
],
},
{
Expand Down Expand Up @@ -1605,6 +1609,10 @@ const completionSpec: Fig.Spec = {
name: ['logout'],
description: 'Log out of Azure.',
},
{
name: ['status'],
description: 'Show the current authentication status.',
},
],
},
{
Expand Down
16 changes: 16 additions & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

Show the current authentication status.

Usage
azd auth status [flags]

Global Flags
-C, --cwd string : Sets the current working directory.
--debug : Enables debugging and diagnostics logging.
--docs : Opens the documentation for azd auth status in your web browser.
-h, --help : Gets help for status.
--no-prompt : Accepts the default value instead of prompting, or it fails if there is no default.

Find a bug? Want to let us know how we're doing? Fill out this brief survey: https://aka.ms/azure-dev/hats.


1 change: 1 addition & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-auth.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Usage
Available Commands
login : Log in to Azure.
logout : Log out of Azure.
status : Show the current authentication status.

Global Flags
-C, --cwd string : Sets the current working directory.
Expand Down
62 changes: 62 additions & 0 deletions cli/azd/pkg/contracts/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package contracts

import "time"

// LoginStatus are the values of the "status" property of a LoginResult
type LoginStatus string

const (
// The user is logged in and we were able to obtain an access token for them.
// The "ExpiresOn" property of the result will contain information on when the
// access token expires.
LoginStatusSuccess LoginStatus = "success"
// The user is not logged in.
LoginStatusUnauthenticated LoginStatus = "unauthenticated"
)

// LoginResult is the contract for the output of `azd auth login`.
type LoginResult struct {
// The result of checking for a valid access token.
Status LoginStatus `json:"status"`
// When status is `LoginStatusSuccess`, the time at which the access token
// expires.
ExpiresOn *time.Time `json:"expiresOn,omitempty"`
}

// AuthStatus represents the authentication state for `azd auth status`.
type AuthStatus string

const (
AuthStatusAuthenticated AuthStatus = "authenticated"
AuthStatusUnauthenticated AuthStatus = "unauthenticated"
)

// AccountType represents the type of account signed in.
type AccountType string

const (
// AccountTypeUser indicates a user account (email-based login).
AccountTypeUser AccountType = "user"
// AccountTypeServicePrincipal indicates a service principal (client ID-based login).
AccountTypeServicePrincipal AccountType = "servicePrincipal"
)

// StatusResult is the contract for the output of `azd auth status`.
type StatusResult struct {
// The authentication state.
// When value is AuthStatusUnauthenticated, the user is not logged in and no other
// properties will be set.
Status AuthStatus `json:"status"`

// The type of account signed in.
Type AccountType `json:"type,omitempty"`

// The email of the signed-in user. Only set when Type is AccountTypeUser.
Email string `json:"email,omitempty"`

// The client ID of the service principal. Only set when Type is AccountTypeServicePrincipal.
ClientID string `json:"clientId,omitempty"`
}
2 changes: 1 addition & 1 deletion cli/azd/pkg/contracts/auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"time"
)

// AuthTokenResult is the value returned by `azd get-access-token`. It matches the shape of `azcore.AccessToken`
// AuthTokenResult is the value returned by `azd auth token`. It matches the shape of `azcore.AccessToken`
type AuthTokenResult struct {
// Token is the opaque access token, which may be provided to an Azure service.
Token string `json:"token"`
Expand Down
27 changes: 0 additions & 27 deletions cli/azd/pkg/contracts/login.go

This file was deleted.

40 changes: 40 additions & 0 deletions cli/azd/pkg/output/ux/auth_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package ux

import (
"encoding/json"
"fmt"

"github.com/azure/azure-dev/cli/azd/pkg/contracts"
"github.com/azure/azure-dev/cli/azd/pkg/output"
)

// AuthStatusView renders a contracts.StatusResult for console output.
type AuthStatusView struct {
Result *contracts.StatusResult
}

func (v *AuthStatusView) ToString(currentIndentation string) string {
if v.Result.Status == contracts.AuthStatusUnauthenticated {
return fmt.Sprintf("%sNot logged in, run `azd auth login` to login to Azure", currentIndentation)
}

switch v.Result.Type {
case contracts.AccountTypeUser:
return fmt.Sprintf("%sLogged in to Azure as %s",
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))
}

return fmt.Sprintf("%sLogged in to Azure", currentIndentation)
}

func (v *AuthStatusView) MarshalJSON() ([]byte, error) {
return json.Marshal(v.Result)
}
Loading
Loading