mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
refactor: add credential and Git packages (#1123)
- `internal/auth`: credentials in the OS keyring or a private file, OAuth login with token refresh, SSH HTTP-signature signers (key files with passphrase prompt, ssh-agent keys and certificates), and the Git credential helper protocol. - `internal/git`: cancellable Git subprocesses, repository and remote discovery (tracked remote via `branch.<name>.remote`), PR checkout by full ref, AGit push, branch cleanup. Nothing imports these packages yet. Reviewed-on: https://gitea.com/gitea/tea/pulls/1123 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
1 parent
2b218a5bae
commit
88b91faa54
28 files changed
+3044
-3
No files matched your search
@@ -0,0 +1,152 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gitea.dev/tea/internal/settings"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
// keyringService namespaces credentials in the platform credential manager.
|
||||
const keyringService = "tea-cli-v2"
|
||||
|
||||
// Credential holds the secret token material kept outside instance metadata.
|
||||
type Credential struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitzero"`
|
||||
}
|
||||
|
||||
// Keyring is the operating-system credential storage boundary.
|
||||
type Keyring interface {
|
||||
Get(string, string) (string, error)
|
||||
Set(string, string, string) error
|
||||
Delete(string, string) error
|
||||
}
|
||||
|
||||
// SystemKeyring delegates to the platform credential manager.
|
||||
type SystemKeyring struct{}
|
||||
|
||||
// Get retrieves a secret from the platform credential manager.
|
||||
func (SystemKeyring) Get(service, user string) (string, error) { return keyring.Get(service, user) }
|
||||
|
||||
// Set persists a secret in the platform credential manager.
|
||||
func (SystemKeyring) Set(service, user, value string) error { return keyring.Set(service, user, value) }
|
||||
|
||||
// Delete removes only the selected instance/account credential.
|
||||
func (SystemKeyring) Delete(service, user string) error { return keyring.Delete(service, user) }
|
||||
|
||||
// errNoKeyring rejects the keyring backend on a Store without a Keyring.
|
||||
var errNoKeyring = errors.New("keyring storage unavailable")
|
||||
|
||||
// Store selects secure storage or explicitly opted-in private files.
|
||||
type Store struct {
|
||||
Dir string
|
||||
Keyring Keyring
|
||||
}
|
||||
|
||||
// Identity builds a stable, opaque key from an instance URL and account.
|
||||
func Identity(instance, user string) string {
|
||||
sum := sha256.Sum256([]byte(instance + "\x00" + user))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func (s *Store) filename(instance, user string) string {
|
||||
return filepath.Join(s.Dir, "credentials", Identity(instance, user)+".json")
|
||||
}
|
||||
|
||||
// Load returns a credential without exposing storage contents in errors.
|
||||
func (s *Store) Load(instance, user, backend string) (Credential, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
switch backend {
|
||||
case settings.StoreKeyring:
|
||||
if s.Keyring == nil {
|
||||
return Credential{}, errNoKeyring
|
||||
}
|
||||
value, loadErr := s.Keyring.Get(keyringService, Identity(instance, user))
|
||||
data, err = []byte(value), loadErr
|
||||
case settings.StoreFile:
|
||||
data, err = os.ReadFile(s.filename(instance, user))
|
||||
default:
|
||||
err = errors.New("unsupported credential store")
|
||||
}
|
||||
if errors.Is(err, keyring.ErrNotFound) || errors.Is(err, os.ErrNotExist) {
|
||||
return Credential{}, errors.New("credential unavailable; run tea auth login for this instance and account")
|
||||
}
|
||||
if err != nil {
|
||||
return Credential{}, fmt.Errorf("credential storage unavailable: %w", err)
|
||||
}
|
||||
var credential Credential
|
||||
if err := json.Unmarshal(data, &credential); err != nil {
|
||||
return Credential{}, errors.New("invalid stored credential; run tea auth login")
|
||||
}
|
||||
if credential.AccessToken == "" {
|
||||
return Credential{}, errors.New("stored credential has no access token")
|
||||
}
|
||||
return credential, nil
|
||||
}
|
||||
|
||||
// Save persists credentials without falling back to a different backend.
|
||||
func (s *Store) Save(instance, user, backend string, credential Credential) error {
|
||||
if credential.AccessToken == "" {
|
||||
return errors.New("access token cannot be empty")
|
||||
}
|
||||
data, err := json.Marshal(credential)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch backend {
|
||||
case settings.StoreKeyring:
|
||||
if s.Keyring == nil {
|
||||
return errNoKeyring
|
||||
}
|
||||
if err := s.Keyring.Set(keyringService, Identity(instance, user), string(data)); err != nil {
|
||||
return errors.New("secure credential storage unavailable; use --credential-store file only if you accept private file storage")
|
||||
}
|
||||
return nil
|
||||
case settings.StoreFile:
|
||||
if err := os.MkdirAll(filepath.Dir(s.filename(instance, user)), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
return settings.AtomicWrite(s.filename(instance, user), data)
|
||||
default:
|
||||
return errors.New("unsupported credential store")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete removes only the selected instance/account credential.
|
||||
func (s *Store) Delete(instance, user, backend string) error {
|
||||
switch backend {
|
||||
case settings.StoreKeyring:
|
||||
if s.Keyring == nil {
|
||||
return errNoKeyring
|
||||
}
|
||||
err := s.Keyring.Delete(keyringService, Identity(instance, user))
|
||||
if errors.Is(err, keyring.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New("failed to remove credential from secure storage")
|
||||
}
|
||||
return nil
|
||||
case settings.StoreFile:
|
||||
err := os.Remove(s.filename(instance, user))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unsupported credential store %q", backend)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user