-
Notifications
You must be signed in to change notification settings - Fork 6
feat(auth): initial implementation of auth subcommand #217
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
Open
zeroshade
wants to merge
18
commits into
main
Choose a base branch
from
auth-command-stuff
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
955f76a
feat(auth): initial implementation of auth subcommand
zeroshade c62e6e2
implement refresh oauth token
zeroshade 9342015
add unit tests for auth package
zeroshade ca9eaac
update default URI
zeroshade d48d194
fix auth panic
zeroshade 07bcbb0
don't print Auth Success when interrupted
zeroshade 5ecbe4e
rebased
zeroshade 95b2ef7
macos-13 is deprecated
zeroshade 7fa73ea
Update auth/credentials.go
zeroshade c20a331
remove audience, use stdin
zeroshade 3ba5bcb
use "registry" instead of "index"
zeroshade b8258fe
more rename index to registry
zeroshade e511838
more updates from comments
zeroshade aff7d70
add user-agent and allow overwriting creds
zeroshade c8a452d
add unit tests
zeroshade 79acb8e
fix failing on header
zeroshade a2513ee
updates from comments
zeroshade 0a8ba3d
check openid-configuration first
zeroshade File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| // Copyright 2025 Columnar Technologies Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package auth | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
| "net/url" | ||
| "os" | ||
| "path/filepath" | ||
| "runtime" | ||
| "slices" | ||
| "sync" | ||
|
|
||
| "github.com/pelletier/go-toml/v2" | ||
| ) | ||
|
|
||
| type Type string | ||
|
|
||
| const ( | ||
| TypeApiKey Type = "apikey" | ||
| TypeToken Type = "oauth" | ||
| ) | ||
|
|
||
| func (a *Type) UnmarshalText(text []byte) error { | ||
| switch string(text) { | ||
| case "apikey": | ||
| *a = TypeApiKey | ||
| case "oauth": | ||
| *a = TypeToken | ||
| default: | ||
| return fmt.Errorf("invalid auth type: %s", string(text)) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| type Uri url.URL | ||
|
|
||
| func (u *Uri) String() string { | ||
| return (*url.URL)(u).String() | ||
| } | ||
|
|
||
| func (u *Uri) MarshalText() ([]byte, error) { | ||
| return (*url.URL)(u).MarshalBinary() | ||
| } | ||
|
|
||
| func (u *Uri) UnmarshalText(text []byte) error { | ||
| return (*url.URL)(u).UnmarshalBinary(text) | ||
| } | ||
|
|
||
| type Credential struct { | ||
| Type Type `toml:"type"` | ||
| AuthURI Uri `toml:"auth_uri"` | ||
| RegistryURL Uri `toml:"registry_url"` | ||
| ApiKey string `toml:"api_key,omitempty"` | ||
| Token string `toml:"token"` | ||
| RefreshToken string `toml:"refresh_token,omitempty"` | ||
| ClientID string `toml:"client_id,omitempty"` | ||
| Audience string `toml:"audience,omitempty"` | ||
| } | ||
|
|
||
| func (t *Credential) Refresh() bool { | ||
| switch t.Type { | ||
| case TypeApiKey: | ||
| rsp, err := http.DefaultClient.Do(&http.Request{ | ||
| Method: http.MethodGet, | ||
| URL: (*url.URL)(&t.AuthURI), | ||
| Header: http.Header{ | ||
| "authorization": []string{"Bearer " + t.ApiKey}, | ||
| }, | ||
| }) | ||
| if err != nil || rsp.StatusCode != http.StatusOK { | ||
| return false | ||
| } | ||
| defer rsp.Body.Close() | ||
|
|
||
| var tokenResp struct { | ||
| Token string `json:"access_token"` | ||
| } | ||
| if err := json.NewDecoder(rsp.Body).Decode(&tokenResp); err != nil { | ||
| return false | ||
| } | ||
|
|
||
| t.Token = tokenResp.Token | ||
| return true | ||
| case TypeToken: | ||
| if err := refreshOauth(t); err != nil { | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| func (t *Credential) GetAuthToken() string { | ||
| if t.Token != "" { | ||
| return t.Token | ||
| } | ||
|
|
||
| if t.Refresh() { | ||
| _ = UpdateCreds() | ||
| return t.Token | ||
| } | ||
|
|
||
| return "" | ||
| } | ||
|
|
||
| var ( | ||
| loadedCredentials []Credential | ||
| credentialErr error | ||
| loaded sync.Once | ||
| credPath string | ||
| ) | ||
|
|
||
| func init() { | ||
| var err error | ||
| credPath, err = getCredentialPath() | ||
| if err != nil { | ||
| panic(fmt.Sprintf("failed to get credential path: %s", err)) | ||
| } | ||
| } | ||
|
|
||
| func getCredentialPath() (string, error) { | ||
| dir := os.Getenv("XDG_DATA_HOME") | ||
| if dir == "" { | ||
| switch runtime.GOOS { | ||
| case "windows": | ||
| dir = os.Getenv("LocalAppData") | ||
| if dir == "" { | ||
| return "", errors.New("%LocalAppData% is not set") | ||
| } | ||
| case "darwin": | ||
| home, err := os.UserHomeDir() | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to get user home directory: %w", err) | ||
| } | ||
| dir = filepath.Join(home, "Library") | ||
| default: // unix | ||
| home, err := os.UserHomeDir() | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to get user home directory: %w", err) | ||
| } | ||
| dir = filepath.Join(home, ".local", "share") | ||
| } | ||
| } else if !filepath.IsAbs(dir) { | ||
| return "", errors.New("path in $XDG_DATA_HOME is relative") | ||
| } | ||
|
|
||
| return filepath.Join(dir, "dbc", "credentials", "credentials.toml"), nil | ||
| } | ||
|
|
||
| func loadCreds() ([]Credential, error) { | ||
| credFile, err := os.Open(credPath) | ||
| if err != nil { | ||
| if errors.Is(err, os.ErrNotExist) { | ||
| return []Credential{}, nil | ||
| } | ||
| return nil, err | ||
| } | ||
| defer credFile.Close() | ||
|
|
||
| creds := struct { | ||
| Credentials []Credential `toml:"credentials"` | ||
| }{} | ||
|
|
||
| if err := toml.NewDecoder(credFile).Decode(&creds); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return creds.Credentials, nil | ||
| } | ||
|
|
||
| func GetCredentials(u *url.URL) (*Credential, error) { | ||
| if err := LoadCredentials(); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| for i, cred := range loadedCredentials { | ||
| if cred.RegistryURL.Host == u.Host { | ||
| return &loadedCredentials[i], nil | ||
| } | ||
| } | ||
|
|
||
| return nil, nil | ||
| } | ||
|
|
||
| func LoadCredentials() error { | ||
| loaded.Do(func() { | ||
| loadedCredentials, credentialErr = loadCreds() | ||
| }) | ||
| return credentialErr | ||
| } | ||
|
|
||
| func AddCredential(cred Credential, allowOverwrite bool) error { | ||
| if err := LoadCredentials(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| idx := slices.IndexFunc(loadedCredentials, func(c Credential) bool { | ||
| return c.RegistryURL.Host == cred.RegistryURL.Host | ||
| }) | ||
|
|
||
| if idx != -1 { | ||
| if !allowOverwrite { | ||
| return fmt.Errorf("credentials for %s already exist", cred.RegistryURL.Host) | ||
| } | ||
| loadedCredentials[idx] = cred | ||
| } else { | ||
| loadedCredentials = append(loadedCredentials, cred) | ||
| } | ||
| return UpdateCreds() | ||
| } | ||
|
|
||
| func RemoveCredential(host Uri) error { | ||
| if err := LoadCredentials(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| idx := slices.IndexFunc(loadedCredentials, func(c Credential) bool { | ||
| return c.RegistryURL.Host == host.Host | ||
| }) | ||
|
|
||
| if idx == -1 { | ||
| return fmt.Errorf("no credentials found for %s", host.Host) | ||
| } | ||
|
|
||
| loadedCredentials = append(loadedCredentials[:idx], loadedCredentials[idx+1:]...) | ||
| return UpdateCreds() | ||
| } | ||
|
|
||
| func UpdateCreds() error { | ||
| if err := LoadCredentials(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err := os.MkdirAll(filepath.Dir(credPath), 0o700) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| f, err := os.OpenFile(credPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o600) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer f.Close() | ||
|
|
||
| return toml.NewEncoder(f).Encode(struct { | ||
| Credentials []Credential `toml:"credentials"` | ||
| }{ | ||
| Credentials: loadedCredentials, | ||
| }) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would you feel if I asked you to kick this change out of this PR so we can put it in the releasenotes as a standalone item?