Skip to content

Commit

Permalink
CUS-377: engflow_auth: add logout command (#22)
Browse files Browse the repository at this point in the history
It's nice to have a way to delete a token without having to rummage
around in Keychain Access.
  • Loading branch information
jayconrod authored Aug 12, 2024
1 parent c5ffd2f commit 0611fd4
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 15 deletions.
25 changes: 24 additions & 1 deletion cmd/engflow_auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,21 @@ Visit %s for help.`,
return nil
}

func (r *appState) logout(cliCtx *cli.Context) error {
if cliCtx.NArg() != 1 {
return autherr.CodedErrorf(autherr.CodeBadParams, "expected exactly 1 positional argument, a cluster name")
}
clusterURL, err := sanitizedURL(cliCtx.Args().Get(0))
if err != nil {
return autherr.CodedErrorf(autherr.CodeBadParams, "invalid cluster: %w", err)
}

if err := r.tokenStore.Delete(cliCtx.Context, clusterURL.Host); err != nil {
return &autherr.CodedError{Code: autherr.CodeTokenStoreFailure, Err: err}
}
return nil
}

func (r *appState) version(cliCtx *cli.Context) error {
fmt.Fprintf(cliCtx.App.Writer, "%s\n", buildstamp.Values)
return nil
Expand Down Expand Up @@ -295,7 +310,7 @@ credential helper protocol.`),
Name: "login",
Usage: "Generate and store credentials for a particular cluster",
UsageText: strings.TrimSpace(`
Initiates an interactive OAuth flow to log into the cluster at
Initiates an interactive OAuth2 flow to log into the cluster at
CLUSTER_URL.`),
Action: root.login,
Flags: []cli.Flag{
Expand All @@ -305,6 +320,14 @@ CLUSTER_URL.`),
},
},
},
{
Name: "logout",
Usage: "Remove a cluster's credentials from this machine",
ArgsUsage: " CLUSTER_URL",
UsageText: strings.TrimSpace(`
Erases the credentials for the named cluster from the local machine.`),
Action: root.logout,
},
{
Name: "version",
Usage: "Print version info for this application",
Expand Down
28 changes: 28 additions & 0 deletions cmd/engflow_auth/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,34 @@ func TestRun(t *testing.T) {
wantCode: autherr.CodeTokenStoreFailure,
wantErr: "token_store_fail",
},
{
desc: "logout without cluster",
args: []string{"logout"},
wantCode: autherr.CodeBadParams,
wantErr: "expected exactly 1 positional argument",
},
{
desc: "logout with unknown cluster",
args: []string{"logout", "unknown.example.com"},
},
{
desc: "logout with cluster",
args: []string{"logout", "cluster.example.com"},
tokenStore: &oauthtoken.FakeTokenStore{
Tokens: map[string]*oauth2.Token{
"cluster.example.com": {},
},
},
},
{
desc: "logout with error",
args: []string{"logout", "cluster.example.com"},
tokenStore: &oauthtoken.FakeTokenStore{
DeleteErr: errors.New("token_delete_error"),
},
wantCode: autherr.CodeTokenStoreFailure,
wantErr: "token_delete_error",
},
{
desc: "export with no args",
args: []string{"export"},
Expand Down
20 changes: 8 additions & 12 deletions internal/oauthtoken/cache_alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,24 @@ import (
// a different cluster is changing, and produces a warning over an appropriate
// communication channel.
type CacheAlert struct {
impl LoadStorer
LoadStorer
stderr io.Writer
}

func NewCacheAlert(impl LoadStorer, stderr io.Writer) LoadStorer {
return &CacheAlert{
impl: impl,
stderr: stderr,
LoadStorer: impl,
stderr: stderr,
}
}

func (a *CacheAlert) Store(ctx context.Context, cluster string, token *oauth2.Token) error {
oldToken, err := a.impl.Load(ctx, cluster)
oldToken, err := a.Load(ctx, cluster)
if err != nil || oldToken == nil {
// Failed to fetch any sort of previous valid token. Defer to the
// wrapped implementation; we'll assume that the token didn't exist
// previously (and therefore no need to issue a warning).
return a.impl.Store(ctx, cluster, token)
return a.LoadStorer.Store(ctx, cluster, token)
}

// Disable claims validation, since expired tokens should be allowed to
Expand All @@ -55,20 +55,16 @@ func (a *CacheAlert) Store(ctx context.Context, cluster string, token *oauth2.To
// concern.
_, _, err = parser.ParseUnverified(oldToken.AccessToken, oldClaims)
if err != nil {
return a.impl.Store(ctx, cluster, token)
return a.LoadStorer.Store(ctx, cluster, token)
}
_, _, err = parser.ParseUnverified(token.AccessToken, newClaims)
if err != nil {
return a.impl.Store(ctx, cluster, token)
return a.LoadStorer.Store(ctx, cluster, token)
}

if oldClaims.Subject != newClaims.Subject {
fmt.Fprintf(a.stderr, "WARNING: Login identity has changed since last login to %q.\nPlease run `bazel shutdown` in current workspaces in order to ensure bazel picks up new credentials.\n", cluster)
}

return a.impl.Store(ctx, cluster, token)
}

func (a *CacheAlert) Load(ctx context.Context, cluster string) (*oauth2.Token, error) {
return a.impl.Load(ctx, cluster)
return a.LoadStorer.Store(ctx, cluster, token)
}
12 changes: 10 additions & 2 deletions internal/oauthtoken/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (
// FakeTokenStore is a test implementation of LoadStorer that stores tokens in
// memory instead of the system keychain.
type FakeTokenStore struct {
Tokens map[string]*oauth2.Token
LoadErr, StoreErr error
Tokens map[string]*oauth2.Token
LoadErr, StoreErr, DeleteErr error
}

var _ LoadStorer = (*FakeTokenStore)(nil)
Expand Down Expand Up @@ -54,3 +54,11 @@ func (f *FakeTokenStore) Store(ctx context.Context, cluster string, token *oauth
f.Tokens[cluster] = token
return nil
}

func (f *FakeTokenStore) Delete(ctx context.Context, cluster string) error {
if f.DeleteErr != nil {
return f.DeleteErr
}
delete(f.Tokens, cluster)
return nil
}
10 changes: 10 additions & 0 deletions internal/oauthtoken/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ func (f *Keyring) Store(ctx context.Context, cluster string, token *oauth2.Token
return nil
}

func (f *Keyring) Delete(ctx context.Context, cluster string) error {
serviceName := f.secretServiceName(cluster)
if err := keyring.Delete(serviceName, f.username); errors.Is(err, keyring.ErrNotFound) {
return nil
} else if err != nil {
return fmt.Errorf("failed to delete oauth2 token from keyring service %q: %w", serviceName, err)
}
return nil
}

func (f *Keyring) secretServiceName(cluster string) string {
return fmt.Sprintf("engflow.com/engflow_auth/%s", cluster)
}
1 change: 1 addition & 0 deletions internal/oauthtoken/load_storer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ import (
type LoadStorer interface {
Load(context.Context, string) (*oauth2.Token, error)
Store(context.Context, string, *oauth2.Token) error
Delete(context.Context, string) error
}

0 comments on commit 0611fd4

Please sign in to comment.