Skip to content

Commit

Permalink
Return organizations from Backend.CurrentUser (#9211)
Browse files Browse the repository at this point in the history
* Return organizations from Backend.CurrentUser

Organizations are shown by `pulumi about` and `pulumi whoami --verbose`

e.g.
```
$ pulumi whoami --verbose
User: Frassle
Organizations: Frassle
Backend URL: https://app.pulumi.com/Frassle
```

Like usernames these are cached in the credentials file.

* lint

* Add to CHANGELOG
  • Loading branch information
Frassle committed Mar 31, 2022
1 parent d07886d commit 50ade97
Show file tree
Hide file tree
Showing 14 changed files with 92 additions and 48 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG_PENDING.md
Expand Up @@ -8,6 +8,9 @@
- Clear pending operations during `pulumi refresh` or `pulumi up -r`.
[#8435](https://github.com/pulumi/pulumi/pull/8435)

- [cli] - `pulumi whoami --verbose` and `pulumi about` include a list of the current users organizations.
[#9211](https://github.com/pulumi/pulumi/pull/9211)

### Bug Fixes

- [codegen/go] - Fix Go SDK function output to check for errors
Expand Down
4 changes: 2 additions & 2 deletions pkg/backend/backend.go
Expand Up @@ -200,8 +200,8 @@ type Backend interface {
Logout() error
// LogoutAll logs you out of all the backend and removes any stored credentials.
LogoutAll() error
// Returns the identity of the current user for the backend.
CurrentUser() (string, error)
// Returns the identity of the current user and any organizations they are in for the backend.
CurrentUser() (string, []string, error)

// Cancel the current update for the given stack.
CancelCurrentUpdate(ctx context.Context, stackRef StackReference) error
Expand Down
6 changes: 3 additions & 3 deletions pkg/backend/filestate/backend.go
Expand Up @@ -810,12 +810,12 @@ func (b *localBackend) LogoutAll() error {
return workspace.DeleteAllAccounts()
}

func (b *localBackend) CurrentUser() (string, error) {
func (b *localBackend) CurrentUser() (string, []string, error) {
user, err := user.Current()
if err != nil {
return "", err
return "", nil, err
}
return user.Username, nil
return user.Username, nil, nil
}

func (b *localBackend) getLocalStacks() ([]tokens.Name, error) {
Expand Down
52 changes: 32 additions & 20 deletions pkg/backend/httpstate/backend.go
Expand Up @@ -212,13 +212,18 @@ func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string, opts di

accessToken := <-c

username, err := client.NewClient(cloudURL, accessToken, d).GetPulumiAccountName(ctx)
username, organizations, err := client.NewClient(cloudURL, accessToken, d).GetPulumiAccountDetails(ctx)
if err != nil {
return nil, err
}

// Save the token and return the backend
account := workspace.Account{AccessToken: accessToken, Username: username, LastValidatedAt: time.Now()}
account := workspace.Account{
AccessToken: accessToken,
Username: username,
Organizations: organizations,
LastValidatedAt: time.Now(),
}
if err = workspace.StoreAccount(cloudURL, account, true); err != nil {
return nil, err
}
Expand All @@ -237,9 +242,9 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
existingAccount, err := workspace.GetAccount(cloudURL)
if err == nil && existingAccount.AccessToken != "" {
// If the account was last verified less than an hour ago, assume the token is valid.
valid, username := true, existingAccount.Username
valid, username, organizations := true, existingAccount.Username, existingAccount.Organizations
if username == "" || existingAccount.LastValidatedAt.Add(1*time.Hour).Before(time.Now()) {
valid, username, err = IsValidAccessToken(ctx, cloudURL, existingAccount.AccessToken)
valid, username, organizations, err = IsValidAccessToken(ctx, cloudURL, existingAccount.AccessToken)
if err != nil {
return nil, err
}
Expand All @@ -249,6 +254,7 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
if valid {
// Save the token. While it hasn't changed this will update the current cloud we are logged into, as well.
existingAccount.Username = username
existingAccount.Organizations = organizations
if err = workspace.StoreAccount(cloudURL, existingAccount, true); err != nil {
return nil, err
}
Expand Down Expand Up @@ -328,15 +334,20 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
}

// Try and use the credentials to see if they are valid.
valid, username, err := IsValidAccessToken(ctx, cloudURL, accessToken)
valid, username, organizations, err := IsValidAccessToken(ctx, cloudURL, accessToken)
if err != nil {
return nil, err
} else if !valid {
return nil, fmt.Errorf("invalid access token")
}

// Save them.
account := workspace.Account{AccessToken: accessToken, Username: username, LastValidatedAt: time.Now()}
account := workspace.Account{
AccessToken: accessToken,
Username: username,
Organizations: organizations,
LastValidatedAt: time.Now(),
}
if err = workspace.StoreAccount(cloudURL, account, true); err != nil {
return nil, err
}
Expand Down Expand Up @@ -389,28 +400,29 @@ func (b *cloudBackend) Name() string {
}

func (b *cloudBackend) URL() string {
user, err := b.CurrentUser()
user, _, err := b.CurrentUser()
if err != nil {
return cloudConsoleURL(b.url)
}
return cloudConsoleURL(b.url, user)
}

func (b *cloudBackend) CurrentUser() (string, error) {
func (b *cloudBackend) CurrentUser() (string, []string, error) {
return b.currentUser(context.Background())
}

func (b *cloudBackend) currentUser(ctx context.Context) (string, error) {
func (b *cloudBackend) currentUser(ctx context.Context) (string, []string, error) {
account, err := workspace.GetAccount(b.CloudURL())
if err != nil {
return "", err
return "", nil, err
}
if account.Username != "" {
logging.V(1).Infof("found username for access token")
return account.Username, nil
return account.Username, account.Organizations, nil
}
logging.V(1).Infof("no username for access token")
return b.client.GetPulumiAccountName(ctx)
name, orgs, err := b.client.GetPulumiAccountDetails(ctx)
return name, orgs, err
}

func (b *cloudBackend) CloudURL() string { return b.url }
Expand All @@ -430,7 +442,7 @@ func (b *cloudBackend) parsePolicyPackReference(s string) (backend.PolicyPackRef
}

if orgName == "" {
currentUser, userErr := b.CurrentUser()
currentUser, _, userErr := b.CurrentUser()
if userErr != nil {
return nil, userErr
}
Expand Down Expand Up @@ -535,7 +547,7 @@ func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, er
if defaultOrg != "" {
qualifiedName.Owner = defaultOrg
} else {
currentUser, userErr := b.CurrentUser()
currentUser, _, userErr := b.CurrentUser()
if userErr != nil {
return nil, userErr
}
Expand Down Expand Up @@ -674,7 +686,7 @@ func (b *cloudBackend) LogoutAll() error {

// DoesProjectExist returns true if a project with the given name exists in this backend, or false otherwise.
func (b *cloudBackend) DoesProjectExist(ctx context.Context, projectName string) (bool, error) {
owner, err := b.currentUser(ctx)
owner, _, err := b.currentUser(ctx)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -1464,19 +1476,19 @@ func (b *cloudBackend) tryNextUpdate(ctx context.Context, update client.UpdateId

// IsValidAccessToken tries to use the provided Pulumi access token and returns if it is accepted
// or not. Returns error on any unexpected error.
func IsValidAccessToken(ctx context.Context, cloudURL, accessToken string) (bool, string, error) {
func IsValidAccessToken(ctx context.Context, cloudURL, accessToken string) (bool, string, []string, error) {
// Make a request to get the authenticated user. If it returns a successful response,
// we know the access token is legit. We also parse the response as JSON and confirm
// it has a githubLogin field that is non-empty (like the Pulumi Service would return).
username, err := client.NewClient(cloudURL, accessToken, cmdutil.Diag()).GetPulumiAccountName(ctx)
username, organizations, err := client.NewClient(cloudURL, accessToken, cmdutil.Diag()).GetPulumiAccountDetails(ctx)
if err != nil {
if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == 401 {
return false, "", nil
return false, "", nil, nil
}
return false, "", fmt.Errorf("getting user info from %v: %w", cloudURL, err)
return false, "", nil, fmt.Errorf("getting user info from %v: %w", cloudURL, err)
}

return true, username, nil
return true, username, organizations, nil
}

// UpdateStackTags updates the stacks's tags, replacing all existing tags.
Expand Down
37 changes: 31 additions & 6 deletions pkg/backend/httpstate/client/client.go
Expand Up @@ -45,6 +45,7 @@ type Client struct {
apiURL string
apiToken apiAccessToken
apiUser string
apiOrgs []string
diag diag.Sink
client restClient
}
Expand Down Expand Up @@ -168,26 +169,50 @@ func getUpdatePath(update UpdateIdentifier, components ...string) string {
return getStackPath(update.StackIdentifier, components...)
}

type getUserResponse struct {
// Copied from https://github.com/pulumi/pulumi-service/blob/master/pkg/apitype/users.go#L7-L16
type serviceUserInfo struct {
Name string `json:"name"`
GitHubLogin string `json:"githubLogin"`
AvatarURL string `json:"avatarUrl"`
Email string `json:"email,omitempty"`
}

// Copied from https://github.com/pulumi/pulumi-service/blob/master/pkg/apitype/users.go#L20-L34
type serviceUser struct {
ID string `json:"id"`
GitHubLogin string `json:"githubLogin"`
Name string `json:"name"`
Email string `json:"email"`
AvatarURL string `json:"avatarUrl"`
Organizations []serviceUserInfo `json:"organizations"`
Identities []string `json:"identities"`
SiteAdmin *bool `json:"siteAdmin,omitempty"`
}

// GetPulumiAccountName returns the user implied by the API token associated with this client.
func (pc *Client) GetPulumiAccountName(ctx context.Context) (string, error) {
func (pc *Client) GetPulumiAccountDetails(ctx context.Context) (string, []string, error) {
if pc.apiUser == "" {
resp := getUserResponse{}
resp := serviceUser{}
if err := pc.restCall(ctx, "GET", "/api/user", nil, nil, &resp); err != nil {
return "", err
return "", nil, err
}

if resp.GitHubLogin == "" {
return "", errors.New("unexpected response from server")
return "", nil, errors.New("unexpected response from server")
}

pc.apiUser = resp.GitHubLogin
pc.apiOrgs = make([]string, len(resp.Organizations))
for i, org := range resp.Organizations {
if org.GitHubLogin == "" {
return "", nil, errors.New("unexpected response from server")
}

pc.apiOrgs[i] = org.GitHubLogin
}
}

return pc.apiUser, nil
return pc.apiUser, pc.apiOrgs, nil
}

// GetCLIVersionInfo asks the service for information about versions of the CLI (the newest version as well as the
Expand Down
2 changes: 1 addition & 1 deletion pkg/backend/httpstate/stack.go
Expand Up @@ -60,7 +60,7 @@ func (c cloudBackendReference) String() string {
return string(c.name)
}
} else {
currentUser, userErr := c.b.CurrentUser()
currentUser, _, userErr := c.b.CurrentUser()
if userErr == nil && c.owner == currentUser {
return string(c.name)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/backend/mock.go
Expand Up @@ -55,7 +55,7 @@ type MockBackend struct {
ImportDeploymentF func(context.Context, Stack, *apitype.UntypedDeployment) error
LogoutF func() error
LogoutAllF func() error
CurrentUserF func() (string, error)
CurrentUserF func() (string, []string, error)
PreviewF func(context.Context, Stack,
UpdateOperation) (*deploy.Plan, engine.ResourceChanges, result.Result)
UpdateF func(context.Context, Stack,
Expand Down Expand Up @@ -319,7 +319,7 @@ func (be *MockBackend) LogoutAll() error {
panic("not implemented")
}

func (be *MockBackend) CurrentUser() (string, error) {
func (be *MockBackend) CurrentUser() (string, []string, error) {
if be.CurrentUserF != nil {
return be.CurrentUserF()
}
Expand Down
19 changes: 10 additions & 9 deletions pkg/cmd/pulumi/about.go
Expand Up @@ -280,22 +280,22 @@ func (host hostAbout) String() string {
}

type backendAbout struct {
Name string `json:"name"`
URL string `json:"url"`
User string `json:"user"`
Name string `json:"name"`
URL string `json:"url"`
User string `json:"user"`
Organizations []string `json:"organizations"`
}

func getBackendAbout(b backend.Backend) backendAbout {
var err error
var currentUser string
currentUser, err = b.CurrentUser()
currentUser, currentOrgs, err := b.CurrentUser()
if err != nil {
currentUser = "Unknown"
}
return backendAbout{
Name: b.Name(),
URL: b.URL(),
User: currentUser,
Name: b.Name(),
URL: b.URL(),
User: currentUser,
Organizations: currentOrgs,
}
}

Expand All @@ -306,6 +306,7 @@ func (b backendAbout) String() string {
{"Name", b.Name},
{"URL", b.URL},
{"User", b.User},
{"Organizations", strings.Join(b.Organizations, ", ")},
}),
}.String()
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/pulumi/login.go
Expand Up @@ -151,7 +151,7 @@ func newLoginCmd() *cobra.Command {
return fmt.Errorf("problem logging in: %w", err)
}

if currentUser, err := be.CurrentUser(); err == nil {
if currentUser, _, err := be.CurrentUser(); err == nil {
fmt.Printf("Logged in to %s as %s (%s)\n", be.Name(), currentUser, be.URL())
} else {
fmt.Printf("Logged in to %s (%s)\n", be.Name(), be.URL())
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/pulumi/new_test.go
Expand Up @@ -878,7 +878,7 @@ func loadProject(t *testing.T, dir string) *workspace.Project {
func currentUser(t *testing.T) string {
b, err := currentBackend(display.Options{})
assert.NoError(t, err)
currentUser, err := b.CurrentUser()
currentUser, _, err := b.CurrentUser()
assert.NoError(t, err)
return currentUser
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/pulumi/policy_group_ls.go
Expand Up @@ -57,7 +57,7 @@ func newPolicyGroupLsCmd() *cobra.Command {
if len(cliArgs) > 0 {
orgName = cliArgs[0]
} else {
orgName, err = b.CurrentUser()
orgName, _, err = b.CurrentUser()
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/pulumi/policy_ls.go
Expand Up @@ -47,7 +47,7 @@ func newPolicyLsCmd() *cobra.Command {
if len(cliArgs) > 0 {
orgName = cliArgs[0]
} else {
orgName, err = b.CurrentUser()
orgName, _, err = b.CurrentUser()
if err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/cmd/pulumi/whoami.go
Expand Up @@ -16,6 +16,7 @@ package main

import (
"fmt"
"strings"

"github.com/pulumi/pulumi/pkg/v3/backend/display"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
Expand All @@ -42,13 +43,14 @@ func newWhoAmICmd() *cobra.Command {
return err
}

name, err := b.CurrentUser()
name, orgs, err := b.CurrentUser()
if err != nil {
return err
}

if verbose {
fmt.Printf("User: %s\n", name)
fmt.Printf("Organizations: %s\n", strings.Join(orgs, ", "))
fmt.Printf("Backend URL: %s\n", b.URL())
} else {
fmt.Println(name)
Expand Down
1 change: 1 addition & 0 deletions sdk/go/common/workspace/creds.go
Expand Up @@ -113,6 +113,7 @@ func StoreAccount(key string, account Account, current bool) error {
type Account struct {
AccessToken string `json:"accessToken,omitempty"` // The access token for this account.
Username string `json:"username,omitempty"` // The username for this account.
Organizations []string `json:"organizations,omitempty"` // The organizations for this account.
LastValidatedAt time.Time `json:"lastValidatedAt,omitempty"` // The last time this token was validated.
}

Expand Down

0 comments on commit 50ade97

Please sign in to comment.