feat: add deploy tokens (#37306)

Deploy keys only work over SSH. A deploy token is their counterpart for HTTPS: a repository scoped credential, used as the password of a Git request, with read or read and write access. It covers Git operations and LFS, and can be regenerated in place.

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude Mythos <noreply@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
ToastyTheBot
2026-08-27 03:32:44 +08:00
committed by GitHub
parent 3c4d5a6a5c
commit 646ea0f253
76 changed files with 1594 additions and 831 deletions
-61
View File
@@ -215,67 +215,6 @@ func (err ErrKeyAccessDenied) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrDeployKeyNotExist represents a "DeployKeyNotExist" kind of error.
type ErrDeployKeyNotExist struct {
ID int64
KeyID int64
RepoID int64
}
// IsErrDeployKeyNotExist checks if an error is a ErrDeployKeyNotExist.
func IsErrDeployKeyNotExist(err error) bool {
_, ok := err.(ErrDeployKeyNotExist)
return ok
}
func (err ErrDeployKeyNotExist) Error() string {
return fmt.Sprintf("Deploy key does not exist [id: %d, key_id: %d, repo_id: %d]", err.ID, err.KeyID, err.RepoID)
}
func (err ErrDeployKeyNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrDeployKeyAlreadyExist represents a "DeployKeyAlreadyExist" kind of error.
type ErrDeployKeyAlreadyExist struct {
KeyID int64
RepoID int64
}
// IsErrDeployKeyAlreadyExist checks if an error is a ErrDeployKeyAlreadyExist.
func IsErrDeployKeyAlreadyExist(err error) bool {
_, ok := err.(ErrDeployKeyAlreadyExist)
return ok
}
func (err ErrDeployKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
}
func (err ErrDeployKeyAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrDeployKeyNameAlreadyUsed represents a "DeployKeyNameAlreadyUsed" kind of error.
type ErrDeployKeyNameAlreadyUsed struct {
RepoID int64
Name string
}
// IsErrDeployKeyNameAlreadyUsed checks if an error is a ErrDeployKeyNameAlreadyUsed.
func IsErrDeployKeyNameAlreadyUsed(err error) bool {
_, ok := err.(ErrDeployKeyNameAlreadyUsed)
return ok
}
func (err ErrDeployKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key with name already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
}
func (err ErrDeployKeyNameAlreadyUsed) Unwrap() error {
return util.ErrNotExist
}
// ErrSSHInvalidTokenSignature represents a "ErrSSHInvalidTokenSignature" kind of error.
type ErrSSHInvalidTokenSignature struct {
Wrapped error
+30
View File
@@ -89,6 +89,36 @@ func addPublicKey(ctx context.Context, key *PublicKey) (err error) {
return appendAuthorizedKeysToFile(key)
}
// FindOrAddDeployPublicKey returns the shared public key that deploy keys of the given content link to, adding it on first use.
func FindOrAddDeployPublicKey(ctx context.Context, content string) (*PublicKey, error) {
fingerprint, err := CalcFingerprint(content)
if err != nil {
return nil, err
}
pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
if err != nil {
return nil, err
} else if exist {
if pkey.Type != KeyTypeDeploy {
return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
}
return pkey, nil
}
pkey = &PublicKey{
Mode: perm.AccessModeNone,
Type: KeyTypeDeploy,
Name: "(DeployKey)",
Content: content,
Fingerprint: fingerprint,
}
if err = addPublicKey(ctx, pkey); err != nil {
return nil, fmt.Errorf("addPublicKey: %w", err)
}
return pkey, nil
}
// AddPublicKey adds new public key to database and authorized_keys file.
func AddPublicKey(ctx context.Context, ownerID int64, name, content string, authSourceID int64, verified bool) (*PublicKey, error) {
log.Trace(content)
-175
View File
@@ -1,175 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package asymkey
import (
"context"
"fmt"
"time"
"gitea.dev/models/db"
"gitea.dev/models/perm"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// DeployKey represents deploy key information and its relation with repository.
type DeployKey struct {
ID int64 `xorm:"pk autoincr"`
KeyID int64 `xorm:"UNIQUE(s) INDEX"`
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
Name string
Fingerprint string
Mode perm.AccessMode `xorm:"NOT NULL DEFAULT 1"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
PublicKey *PublicKey `xorm:"-"`
}
func (key *DeployKey) HasUsed() bool {
return key.UpdatedUnix > key.CreatedUnix
}
func (key *DeployKey) HasRecentActivity() bool {
return key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
}
func (key *DeployKey) LoadPublicKey(ctx context.Context) (err error) {
if key.PublicKey != nil {
return nil
}
key.PublicKey, err = GetPublicKeyByID(ctx, key.KeyID)
return err
}
// IsReadOnly checks if the key can only be used for read operations, used by template
func (key *DeployKey) IsReadOnly() bool {
return key.Mode == perm.AccessModeRead
}
func init() {
db.RegisterModel(new(DeployKey))
}
func checkDeployKey(ctx context.Context, repoID, publicKeyID int64, name string) error {
// Note: We want error detail, not just true or false here.
has, err := db.GetEngine(ctx).
Where("repo_id=? AND (key_id=? OR name=?)", repoID, publicKeyID, name).
Get(new(DeployKey))
if err != nil {
return err
} else if has {
return ErrDeployKeyAlreadyExist{publicKeyID, repoID}
}
return nil
}
// addDeployKey adds new key-repo relation.
func addDeployKey(ctx context.Context, repoID, publicKeyID int64, name, fingerprint string, mode perm.AccessMode) (*DeployKey, error) {
if err := checkDeployKey(ctx, repoID, publicKeyID, name); err != nil {
return nil, err
}
key := &DeployKey{KeyID: publicKeyID, RepoID: repoID, Name: name, Fingerprint: fingerprint, Mode: mode}
return key, db.Insert(ctx, key)
}
// AddDeployKey add new deploy key to database and authorized_keys file.
func AddDeployKey(ctx context.Context, repoID int64, name, content string, accessMode perm.AccessMode) (*DeployKey, error) {
fingerprint, err := CalcFingerprint(content)
if err != nil {
return nil, err
}
if accessMode != perm.AccessModeRead && accessMode != perm.AccessModeWrite {
return nil, util.NewInvalidArgumentErrorf("invalid access mode")
}
return db.WithTx2(ctx, func(ctx context.Context) (*DeployKey, error) {
pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
if err != nil {
return nil, err
} else if exist {
if pkey.Type != KeyTypeDeploy {
return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
}
} else {
// First time use this deploy key, add a shared public key
pkey = &PublicKey{
Mode: perm.AccessModeNone,
Type: KeyTypeDeploy,
Name: "(DeployKey)",
Content: content,
Fingerprint: fingerprint,
}
if err = addPublicKey(ctx, pkey); err != nil {
return nil, fmt.Errorf("addPublicKey: %w", err)
}
}
return addDeployKey(ctx, repoID, pkey.ID, name, fingerprint, accessMode)
})
}
// GetDeployKeyByID returns deploy key by given ID.
func GetDeployKeyByID(ctx context.Context, repoID, deployKeyID int64) (*DeployKey, error) {
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"id": deployKeyID, "repo_id": repoID})
if err != nil {
return nil, err
} else if !exist {
return nil, ErrDeployKeyNotExist{deployKeyID, 0, repoID}
}
return key, nil
}
// GetDeployKeyByRepoPublicKey returns deploy key by given public key ID and repository ID.
func GetDeployKeyByRepoPublicKey(ctx context.Context, repoID, publicKeyID int64) (*DeployKey, error) {
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"key_id": publicKeyID, "repo_id": repoID})
if err != nil {
return nil, err
} else if !exist {
return nil, ErrDeployKeyNotExist{0, publicKeyID, repoID}
}
return key, nil
}
// IsDeployKeyExistByPublicKeyID return true if there is at least one deploy-key with the key id
func IsDeployKeyExistByPublicKeyID(ctx context.Context, keyID int64) (bool, error) {
return db.GetEngine(ctx).
Where("key_id = ?", keyID).
Get(new(DeployKey))
}
// UpdateDeployKeyCols updates deploy key information in the specified columns.
func UpdateDeployKeyCols(ctx context.Context, key *DeployKey, cols ...string) error {
_, err := db.GetEngine(ctx).ID(key.ID).Cols(cols...).Update(key)
return err
}
// ListDeployKeysOptions are options for ListDeployKeys
type ListDeployKeysOptions struct {
db.ListOptions
RepoID int64
KeyID int64
Fingerprint string
}
func (opt ListDeployKeysOptions) ToOrders() string {
return "name"
}
func (opt ListDeployKeysOptions) ToConds() builder.Cond {
cond := builder.NewCond()
cond = cond.And(builder.Eq{"repo_id": opt.RepoID}) // repo ID must be used
if opt.KeyID != 0 {
cond = cond.And(builder.Eq{"key_id": opt.KeyID})
}
if opt.Fingerprint != "" {
cond = cond.And(builder.Eq{"fingerprint": opt.Fingerprint})
}
return cond
}