mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
- `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>
153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
// 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)
|
|
}
|
|
}
|