Compare commits

...

26 Commits

Author SHA1 Message Date
Richard Nordström 20aea8fd03
Merge ca08796f77 into 4b069bb6e1 2024-11-25 16:31:42 +01:00
dependabot[bot] 4b069bb6e1
Bump golang.org/x/term from 0.25.0 to 0.26.0 (#1907)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.25.0 to
0.26.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b725e362a8"><code>b725e36</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="54df7da90d"><code>54df7da</code></a>
README: don't recommend go get</li>
<li>See full diff in <a
href="https://github.com/golang/term/compare/v0.25.0...v0.26.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/term&package-manager=go_modules&previous-version=0.25.0&new-version=0.26.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 13:46:20 +00:00
Richard Nordström ca08796f77
Merge branch 'main' into feature/logout 2024-11-06 16:50:52 +01:00
Richard Nordström fc23aa584d
Merge branch 'main' into feature/logout 2024-10-22 21:10:07 +02:00
Richard Nordström 6af6b55832
Merge branch 'main' into feature/logout 2024-10-16 01:31:40 +02:00
Richard Nordström 865964e029
reduce scope for logout cmd to only remove the OAuth token 2024-10-06 23:44:11 +02:00
Richard Nordström 41999fbe87
Merge branch 'main' into feature/logout 2024-10-06 22:44:06 +02:00
Richard Nordström d2bead3fe6
Merge branch 'main' into feature/logout 2024-10-01 22:01:40 +02:00
Richard Nordström 11c37673a6
make tokenCacheMock consistent naming with struct 2024-09-23 21:55:31 +02:00
Richard Nordström 18d3fea34e
Merge branch 'main' into feature/logout 2024-09-23 21:43:07 +02:00
Richard Nordström b7ff019b60
add test for file write 2024-09-23 21:20:56 +02:00
Richard Nordström bb35ca090f
logoutSession not exportable 2024-09-23 20:37:53 +02:00
Richard Nordström d037ec32a1
add new write function to persist to disk 2024-09-23 20:26:43 +02:00
Richard Nordström 89d3b1a4df
remove redundant version specification 2024-09-23 20:23:09 +02:00
Richard Nordström 37067ef933
rename DeleteKey to Delete 2024-09-23 20:21:38 +02:00
Richard Nordström 171c3fdd75
Merge branch 'main' into feature/logout 2024-09-19 21:02:09 +02:00
Richard Nordström dc44dbd667
Merge branch 'main' into feature/logout 2024-09-07 00:45:28 +02:00
Richard Nordström b044a6c0e0
Merge branch 'main' into feature/logout 2024-09-04 14:00:23 +02:00
Richard Nordström 7636c55ba9
Merge branch 'main' into feature/logout 2024-09-03 21:02:45 +02:00
Richard Nordström e88fd0a5c0
Merge branch 'main' into feature/logout 2024-09-02 17:26:33 +02:00
Richard Nordström 6c32a0df7a
improve profile handling and add tests 2024-09-02 00:15:48 +02:00
Richard Nordström 7eca34a7b2
fix typo 2024-09-01 20:25:40 +02:00
Richard Nordström 6277cf24c6
allow no OAuth in case PAT is used 2024-09-01 20:20:22 +02:00
Richard Nordström 6a8b2f452f
use PersistentAuth struc 2024-09-01 20:20:21 +02:00
Richard Nordström 712e2919f5
add logout cmd 2024-09-01 20:20:21 +02:00
Richard Nordström 882ccba0f5
add DeleteKey to TokenCache for logout cmd 2024-09-01 20:20:21 +02:00
12 changed files with 383 additions and 11 deletions

View File

@ -31,6 +31,7 @@ GCP: https://docs.gcp.databricks.com/dev-tools/auth/index.html`,
cmd.AddCommand(newProfilesCommand())
cmd.AddCommand(newTokenCommand(&perisistentAuth))
cmd.AddCommand(newDescribeCommand())
cmd.AddCommand(newLogoutCommand(&perisistentAuth))
return cmd
}

110
cmd/auth/logout.go Normal file
View File

@ -0,0 +1,110 @@
package auth
import (
"context"
"errors"
"fmt"
"io/fs"
"github.com/databricks/cli/libs/auth"
"github.com/databricks/cli/libs/auth/cache"
"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/cli/libs/databrickscfg/profile"
"github.com/databricks/databricks-sdk-go/config"
"github.com/spf13/cobra"
)
type logoutSession struct {
profile string
file config.File
persistentAuth *auth.PersistentAuth
}
func (l *logoutSession) load(ctx context.Context, profileName string, persistentAuth *auth.PersistentAuth) error {
l.profile = profileName
l.persistentAuth = persistentAuth
iniFile, err := profile.DefaultProfiler.Get(ctx)
if errors.Is(err, fs.ErrNotExist) {
return err
} else if err != nil {
return fmt.Errorf("cannot parse config file: %w", err)
}
l.file = *iniFile
if err := l.setHostAndAccountIdFromProfile(); err != nil {
return err
}
return nil
}
func (l *logoutSession) setHostAndAccountIdFromProfile() error {
sectionMap, err := l.getConfigSectionMap()
if err != nil {
return err
}
if sectionMap["host"] == "" {
return fmt.Errorf("no host configured for profile %s", l.profile)
}
l.persistentAuth.Host = sectionMap["host"]
l.persistentAuth.AccountID = sectionMap["account_id"]
return nil
}
func (l *logoutSession) getConfigSectionMap() (map[string]string, error) {
section, err := l.file.GetSection(l.profile)
if err != nil {
return map[string]string{}, fmt.Errorf("profile does not exist in config file: %w", err)
}
return section.KeysHash(), nil
}
// clear token from ~/.databricks/token-cache.json
func (l *logoutSession) clearTokenCache(ctx context.Context) error {
return l.persistentAuth.ClearToken(ctx)
}
func newLogoutCommand(persistentAuth *auth.PersistentAuth) *cobra.Command {
cmd := &cobra.Command{
Use: "logout [PROFILE]",
Short: "Logout from specified profile",
Long: "Removes the OAuth token from the token-cache",
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
profileNameFromFlag := cmd.Flag("profile").Value.String()
// If both [PROFILE] and --profile are provided, return an error.
if len(args) > 0 && profileNameFromFlag != "" {
return fmt.Errorf("please only provide a profile as an argument or a flag, not both")
}
// Determine the profile name from either args or the flag.
profileName := profileNameFromFlag
if len(args) > 0 {
profileName = args[0]
}
// If the user has not specified a profile name, prompt for one.
if profileName == "" {
var err error
profileName, err = promptForProfile(ctx, persistentAuth.ProfileName())
if err != nil {
return err
}
}
defer persistentAuth.Close()
logoutSession := &logoutSession{}
err := logoutSession.load(ctx, profileName, persistentAuth)
if err != nil {
return err
}
err = logoutSession.clearTokenCache(ctx)
if err != nil {
if errors.Is(err, cache.ErrNotConfigured) {
// It is OK to not have OAuth configured
} else {
return err
}
}
cmdio.LogString(ctx, fmt.Sprintf("Profile %s is logged out", profileName))
return nil
}
return cmd
}

62
cmd/auth/logout_test.go Normal file
View File

@ -0,0 +1,62 @@
package auth
import (
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/databricks/cli/libs/auth"
"github.com/databricks/cli/libs/databrickscfg"
"github.com/databricks/databricks-sdk-go/config"
)
func TestLogout_setHostAndAccountIdFromProfile(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "databrickscfg")
err := databrickscfg.SaveToProfile(ctx, &config.Config{
ConfigFile: path,
Profile: "abc",
Host: "https://foo",
Token: "xyz",
})
require.NoError(t, err)
iniFile, err := config.LoadFile(path)
require.NoError(t, err)
logout := &logoutSession{
profile: "abc",
file: *iniFile,
persistentAuth: &auth.PersistentAuth{},
}
err = logout.setHostAndAccountIdFromProfile()
assert.NoError(t, err)
assert.Equal(t, logout.persistentAuth.Host, "https://foo")
assert.Empty(t, logout.persistentAuth.AccountID)
}
func TestLogout_getConfigSectionMap(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "databrickscfg")
err := databrickscfg.SaveToProfile(ctx, &config.Config{
ConfigFile: path,
Profile: "abc",
Host: "https://foo",
Token: "xyz",
})
require.NoError(t, err)
iniFile, err := config.LoadFile(path)
require.NoError(t, err)
logout := &logoutSession{
profile: "abc",
file: *iniFile,
persistentAuth: &auth.PersistentAuth{},
}
configSectionMap, err := logout.getConfigSectionMap()
assert.NoError(t, err)
assert.Equal(t, configSectionMap["host"], "https://foo")
assert.Equal(t, configSectionMap["token"], "xyz")
}

4
go.mod
View File

@ -27,7 +27,7 @@ require (
golang.org/x/mod v0.22.0
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.9.0
golang.org/x/term v0.25.0
golang.org/x/term v0.26.0
golang.org/x/text v0.20.0
gopkg.in/ini.v1 v1.67.0 // Apache 2.0
gopkg.in/yaml.v3 v3.0.1
@ -64,7 +64,7 @@ require (
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.182.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect

8
go.sum generated
View File

@ -212,10 +212,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=

View File

@ -9,6 +9,7 @@ import (
type TokenCache interface {
Store(key string, t *oauth2.Token) error
Lookup(key string) (*oauth2.Token, error)
Delete(key string) error
}
var tokenCache int

View File

@ -52,11 +52,7 @@ func (c *FileTokenCache) Store(key string, t *oauth2.Token) error {
c.Tokens = map[string]*oauth2.Token{}
}
c.Tokens[key] = t
raw, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
return os.WriteFile(c.fileLocation, raw, ownerReadWrite)
return c.write()
}
func (c *FileTokenCache) Lookup(key string) (*oauth2.Token, error) {
@ -73,6 +69,24 @@ func (c *FileTokenCache) Lookup(key string) (*oauth2.Token, error) {
return t, nil
}
func (c *FileTokenCache) Delete(key string) error {
err := c.load()
if errors.Is(err, fs.ErrNotExist) {
return ErrNotConfigured
} else if err != nil {
return fmt.Errorf("load: %w", err)
}
if c.Tokens == nil {
c.Tokens = map[string]*oauth2.Token{}
}
_, ok := c.Tokens[key]
if !ok {
return ErrNotConfigured
}
delete(c.Tokens, key)
return c.write()
}
func (c *FileTokenCache) location() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
@ -105,4 +119,12 @@ func (c *FileTokenCache) load() error {
return nil
}
func (c *FileTokenCache) write() error {
raw, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
return os.WriteFile(c.fileLocation, raw, ownerReadWrite)
}
var _ TokenCache = (*FileTokenCache)(nil)

View File

@ -1,6 +1,7 @@
package cache
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
@ -103,3 +104,64 @@ func TestStoreOnDev(t *testing.T) {
// macOS: read-only file system
assert.Error(t, err)
}
func TestStoreAndDeleteKey(t *testing.T) {
setup(t)
c := &FileTokenCache{}
err := c.Store("x", &oauth2.Token{
AccessToken: "abc",
})
require.NoError(t, err)
err = c.Store("y", &oauth2.Token{
AccessToken: "bcd",
})
require.NoError(t, err)
l := &FileTokenCache{}
err = l.Delete("x")
require.NoError(t, err)
assert.Equal(t, 1, len(l.Tokens))
_, err = l.Lookup("x")
assert.Equal(t, ErrNotConfigured, err)
tok, err := l.Lookup("y")
require.NoError(t, err)
assert.Equal(t, "bcd", tok.AccessToken)
}
func TestDeleteKeyNotExist(t *testing.T) {
c := &FileTokenCache{
Tokens: map[string]*oauth2.Token{},
}
err := c.Delete("x")
assert.Equal(t, ErrNotConfigured, err)
_, err = c.Lookup("x")
assert.Equal(t, ErrNotConfigured, err)
}
func TestWrite(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "token-cache.json")
tokenMap := map[string]*oauth2.Token{}
token := &oauth2.Token{
AccessToken: "some-access-token",
}
tokenMap["test"] = token
cache := &FileTokenCache{
fileLocation: tempFile,
Tokens: tokenMap,
}
err := cache.write()
assert.NoError(t, err)
content, err := os.ReadFile(tempFile)
require.NoError(t, err)
expected, _ := json.MarshalIndent(&cache, "", " ")
assert.Equal(t, content, expected)
}

View File

@ -23,4 +23,14 @@ func (i *InMemoryTokenCache) Store(key string, t *oauth2.Token) error {
return nil
}
// Delete implements TokenCache.
func (i *InMemoryTokenCache) Delete(key string) error {
_, ok := i.Tokens[key]
if !ok {
return ErrNotConfigured
}
delete(i.Tokens, key)
return nil
}
var _ TokenCache = (*InMemoryTokenCache)(nil)

View File

@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
)
@ -42,3 +43,40 @@ func TestInMemoryCacheStore(t *testing.T) {
assert.Equal(t, res, token)
assert.NoError(t, err)
}
func TestInMemoryDeleteKey(t *testing.T) {
c := &InMemoryTokenCache{
Tokens: map[string]*oauth2.Token{},
}
err := c.Store("x", &oauth2.Token{
AccessToken: "abc",
})
require.NoError(t, err)
err = c.Store("y", &oauth2.Token{
AccessToken: "bcd",
})
require.NoError(t, err)
err = c.Delete("x")
require.NoError(t, err)
assert.Equal(t, 1, len(c.Tokens))
_, err = c.Lookup("x")
assert.Equal(t, ErrNotConfigured, err)
tok, err := c.Lookup("y")
require.NoError(t, err)
assert.Equal(t, "bcd", tok.AccessToken)
}
func TestInMemoryDeleteKeyNotExist(t *testing.T) {
c := &InMemoryTokenCache{
Tokens: map[string]*oauth2.Token{},
}
err := c.Delete("x")
assert.Equal(t, ErrNotConfigured, err)
_, err = c.Lookup("x")
assert.Equal(t, ErrNotConfigured, err)
}

View File

@ -144,6 +144,18 @@ func (a *PersistentAuth) Challenge(ctx context.Context) error {
return nil
}
func (a *PersistentAuth) ClearToken(ctx context.Context) error {
if a.Host == "" && a.AccountID == "" {
return ErrFetchCredentials
}
if a.cache == nil {
a.cache = cache.GetTokenCache(ctx)
}
// lookup token identified by host (and possibly the account id)
key := a.key()
return a.cache.Delete(key)
}
// This function cleans up the host URL by only retaining the scheme and the host.
// This function thus removes any path, query arguments, or fragments from the URL.
func (a *PersistentAuth) cleanHost() {

View File

@ -55,6 +55,7 @@ func TestOidcForWorkspace(t *testing.T) {
type tokenCacheMock struct {
store func(key string, t *oauth2.Token) error
lookup func(key string) (*oauth2.Token, error)
delete func(key string) error
}
func (m *tokenCacheMock) Store(key string, t *oauth2.Token) error {
@ -71,6 +72,13 @@ func (m *tokenCacheMock) Lookup(key string) (*oauth2.Token, error) {
return m.lookup(key)
}
func (m *tokenCacheMock) Delete(key string) error {
if m.delete == nil {
panic("no deleteKey mock")
}
return m.delete(key)
}
func TestLoad(t *testing.T) {
p := &PersistentAuth{
Host: "abc",
@ -229,6 +237,52 @@ func TestChallengeFailed(t *testing.T) {
})
}
func TestClearToken(t *testing.T) {
p := &PersistentAuth{
Host: "abc",
AccountID: "xyz",
cache: &tokenCacheMock{
lookup: func(key string) (*oauth2.Token, error) {
assert.Equal(t, "https://abc/oidc/accounts/xyz", key)
return &oauth2.Token{}, ErrNotConfigured
},
delete: func(key string) error {
assert.Equal(t, "https://abc/oidc/accounts/xyz", key)
return nil
},
},
}
defer p.Close()
err := p.ClearToken(context.Background())
assert.NoError(t, err)
key := p.key()
_, err = p.cache.Lookup(key)
assert.Equal(t, ErrNotConfigured, err)
}
func TestClearTokenNotExist(t *testing.T) {
p := &PersistentAuth{
Host: "abc",
AccountID: "xyz",
cache: &tokenCacheMock{
lookup: func(key string) (*oauth2.Token, error) {
assert.Equal(t, "https://abc/oidc/accounts/xyz", key)
return &oauth2.Token{}, ErrNotConfigured
},
delete: func(key string) error {
assert.Equal(t, "https://abc/oidc/accounts/xyz", key)
return ErrNotConfigured
},
},
}
defer p.Close()
err := p.ClearToken(context.Background())
assert.Equal(t, ErrNotConfigured, err)
key := p.key()
_, err = p.cache.Lookup(key)
assert.Equal(t, ErrNotConfigured, err)
}
func TestPersistentAuthCleanHost(t *testing.T) {
for _, tcases := range []struct {
in string