Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return organizations from Backend.CurrentUser #9211

Merged
merged 4 commits into from Mar 31, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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