mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-28 21:30:06 +00:00
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:
@@ -27,14 +27,7 @@ func (attempts ActionRunAttemptList) LoadTriggerUser(ctx context.Context) error
|
||||
return err
|
||||
}
|
||||
for _, attempt := range attempts {
|
||||
if attempt.TriggerUserID == user_model.ActionsUserID {
|
||||
attempt.TriggerUser = user_model.NewActionsUser()
|
||||
} else {
|
||||
attempt.TriggerUser = users[attempt.TriggerUserID]
|
||||
if attempt.TriggerUser == nil {
|
||||
attempt.TriggerUser = user_model.NewGhostUser()
|
||||
}
|
||||
}
|
||||
attempt.TriggerUser = user_model.GetPossibleUserFromMap(attempt.TriggerUserID, users)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/asymkey"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type KeyType int // SSH public key or HTTP auth token
|
||||
|
||||
const (
|
||||
KeyTypeSSH KeyType = iota + 1
|
||||
KeyTypeToken
|
||||
)
|
||||
|
||||
type DeployKey struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
KeyID int64 `xorm:"INDEX"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
|
||||
KeyType KeyType `xorm:"NOT NULL DEFAULT 1"`
|
||||
|
||||
Fingerprint string
|
||||
PublicKey *asymkey.PublicKey `xorm:"-"`
|
||||
|
||||
TokenHash string `xorm:"INDEX"` // sha256 of the token, which carries enough entropy to need no salt
|
||||
Token string `xorm:"-"` // only set when the token is created
|
||||
|
||||
Mode perm.AccessMode `xorm:"NOT NULL DEFAULT 1"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
// these methods below are mainly used by templates
|
||||
|
||||
func (key *DeployKey) HasRecentActivity() bool {
|
||||
return key.UpdatedUnix.AddDuration(7*24*time.Hour) > timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
func (key *DeployKey) HasUsed() bool { return key.UpdatedUnix > key.CreatedUnix }
|
||||
|
||||
func (key *DeployKey) IsReadOnly() bool { return key.Mode == perm.AccessModeRead }
|
||||
|
||||
func (key *DeployKey) IsKeyTypeToken() bool { return key.KeyType == KeyTypeToken }
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(DeployKey))
|
||||
}
|
||||
|
||||
func checkDeployKeyName(ctx context.Context, repoID int64, name string) error {
|
||||
has, err := db.Exist[DeployKey](ctx, builder.Eq{"repo_id": repoID, "name": name})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
return ErrDeployKeyNameAlreadyUsed{repoID, name}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDeployKeyLastUsed marks the key as used now.
|
||||
func UpdateDeployKeyLastUsed(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Cols("updated_unix").Update(&DeployKey{UpdatedUnix: timeutil.TimeStampNow()})
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/models/asymkey"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// AddDeployKeySSH add new deploy-key to database and authorized_keys file.
|
||||
func AddDeployKeySSH(ctx context.Context, repoID int64, name, content string, accessMode perm.AccessMode) (*DeployKey, error) {
|
||||
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, err := asymkey.FindOrAddDeployPublicKey(ctx, content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has, err := db.Exist[DeployKey](ctx, builder.Eq{"repo_id": repoID, "key_id": pkey.ID}); err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return nil, ErrDeployKeyAlreadyExist{pkey.ID, repoID}
|
||||
}
|
||||
if err := checkDeployKeyName(ctx, repoID, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := &DeployKey{KeyID: pkey.ID, RepoID: repoID, KeyType: KeyTypeSSH, Name: name, Fingerprint: pkey.Fingerprint, Mode: accessMode}
|
||||
return key, db.Insert(ctx, key)
|
||||
})
|
||||
}
|
||||
|
||||
func (key *DeployKey) LoadPublicKey(ctx context.Context) (err error) {
|
||||
if key.PublicKey != nil {
|
||||
return nil
|
||||
}
|
||||
key.PublicKey, err = asymkey.GetPublicKeyByID(ctx, key.KeyID)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// the type is part of the condition because every token row carries key id 0
|
||||
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"key_id": publicKeyID, "repo_id": repoID, "key_type": KeyTypeSSH})
|
||||
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.Exist[DeployKey](ctx, builder.Eq{"key_id": keyID, "key_type": KeyTypeSSH})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
const (
|
||||
DeployTokenPrefix = "gdt_" // lets a secret scanner recognize a leaked token
|
||||
deployTokenLength = 43 // 256 bits of entropy over the 62 alphanumerical characters
|
||||
)
|
||||
|
||||
func (key *DeployKey) generateToken() {
|
||||
key.Token = DeployTokenPrefix + util.CryptoRandomString(deployTokenLength)
|
||||
key.TokenHash = base.EncodeSha256(key.Token)
|
||||
key.Fingerprint = key.Token[:len(DeployTokenPrefix)+2] + "********" + key.Token[len(key.Token)-2:]
|
||||
}
|
||||
|
||||
// AddDeployKeyToken adds a token that authenticates git HTTP requests for one repository.
|
||||
// The plaintext token is only readable on the returned key.
|
||||
func AddDeployKeyToken(ctx context.Context, repoID int64, name string, accessMode perm.AccessMode) (*DeployKey, error) {
|
||||
key := &DeployKey{
|
||||
RepoID: repoID,
|
||||
KeyType: KeyTypeToken,
|
||||
Name: name,
|
||||
Mode: accessMode,
|
||||
}
|
||||
key.generateToken()
|
||||
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*DeployKey, error) {
|
||||
if err := checkDeployKeyName(ctx, repoID, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, db.Insert(ctx, key)
|
||||
})
|
||||
}
|
||||
|
||||
// RegenerateDeployKeyToken replaces the token value of an existing deploy token, keeping its name and access mode.
|
||||
func RegenerateDeployKeyToken(ctx context.Context, repoID, keyID int64) (*DeployKey, error) {
|
||||
key, err := GetDeployKeyByID(ctx, repoID, keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if key.KeyType != KeyTypeToken {
|
||||
return nil, ErrDeployKeyNotExist{keyID, 0, repoID}
|
||||
}
|
||||
|
||||
key.generateToken()
|
||||
_, err = db.GetEngine(ctx).ID(key.ID).Cols("token_hash", "fingerprint").NoAutoTime().Update(key)
|
||||
return key, err
|
||||
}
|
||||
|
||||
// VerifyDeployKeyToken returns the deploy-key which the given plaintext token authenticates.
|
||||
func VerifyDeployKeyToken(ctx context.Context, token string) (*DeployKey, error) {
|
||||
if !strings.HasPrefix(token, DeployTokenPrefix) { // spares a query for every password of a normal user
|
||||
return nil, ErrDeployKeyNotExist{}
|
||||
}
|
||||
|
||||
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"token_hash": base.EncodeSha256(token), "key_type": KeyTypeToken})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
return nil, ErrDeployKeyNotExist{}
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDeployToken(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
key, err := AddDeployKeyToken(t.Context(), 1, "ci", perm.AccessModeWrite)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, key.IsReadOnly())
|
||||
assert.Len(t, key.Token, len(DeployTokenPrefix)+deployTokenLength)
|
||||
|
||||
got, err := VerifyDeployKeyToken(t.Context(), key.Token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, key.ID, got.ID)
|
||||
assert.Empty(t, got.Token, "the token itself is never stored")
|
||||
|
||||
_, err = VerifyDeployKeyToken(t.Context(), "not-a-token")
|
||||
assert.True(t, IsErrDeployKeyNotExist(err))
|
||||
|
||||
_, err = AddDeployKeyToken(t.Context(), 1, "ci", perm.AccessModeWrite)
|
||||
assert.True(t, IsErrDeployKeyNameAlreadyUsed(err))
|
||||
|
||||
regenerated, err := RegenerateDeployKeyToken(t.Context(), 1, key.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, key.Name, regenerated.Name)
|
||||
assert.Equal(t, key.Mode, regenerated.Mode)
|
||||
assert.False(t, unittest.AssertExistsAndLoadBean(t, &DeployKey{ID: key.ID}).HasUsed(), "regenerating is not a use")
|
||||
|
||||
_, err = VerifyDeployKeyToken(t.Context(), key.Token)
|
||||
assert.True(t, IsErrDeployKeyNotExist(err), "the old token stops working")
|
||||
_, err = VerifyDeployKeyToken(t.Context(), regenerated.Token)
|
||||
require.NoError(t, err)
|
||||
|
||||
// an SSH deploy-key has no token to regenerate
|
||||
sshKey := &DeployKey{RepoID: 1, KeyType: KeyTypeSSH, Name: "ssh"}
|
||||
require.NoError(t, db.Insert(t.Context(), sshKey))
|
||||
_, err = RegenerateDeployKeyToken(t.Context(), 1, sshKey.ID)
|
||||
assert.True(t, IsErrDeployKeyNotExist(err))
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{FixtureFiles: []string{}}) // the tests insert what they assert on
|
||||
}
|
||||
@@ -748,4 +748,18 @@
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 113
|
||||
repo_id: 19
|
||||
type: 1
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 114
|
||||
repo_id: 20
|
||||
type: 1
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
deploykey_model "gitea.dev/models/deploykey"
|
||||
"gitea.dev/models/organization"
|
||||
perm_model "gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
@@ -383,12 +384,39 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// getDeployKeyRepoPermission returns the permissions that a deploy key grants on a repository.
|
||||
// A key only ever reaches the git data of the one repository it was added to, at its own access mode.
|
||||
func getDeployKeyRepoPermission(ctx context.Context, repo *repo_model.Repository, keyID int64) (perm Permission, err error) {
|
||||
key, err := deploykey_model.GetDeployKeyByID(ctx, repo.ID, keyID)
|
||||
if err != nil {
|
||||
if deploykey_model.IsErrDeployKeyNotExist(err) {
|
||||
return perm, nil // the key belongs to another repository, so it grants nothing here
|
||||
}
|
||||
return perm, err
|
||||
}
|
||||
if err = repo.LoadUnits(ctx); err != nil {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
perm.units = repo.Units
|
||||
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
|
||||
for _, u := range repo.Units {
|
||||
if u.Type == unit.TypeCode || u.Type == unit.TypeWiki { // a deploy-key only ever reaches git data
|
||||
perm.unitsMode[u.Type] = key.Mode
|
||||
}
|
||||
}
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// GetDoerRepoPermission returns the repository permission for the current actor,
|
||||
// dispatching to GetActionsUserRepoPermission when the actor is an Actions token user.
|
||||
// dispatching to the credential-scoped permissions when the actor is a token or key user.
|
||||
func GetDoerRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) {
|
||||
if taskID, ok := user_model.GetActionsUserTaskID(user); ok {
|
||||
return GetActionsUserRepoPermission(ctx, repo, user, taskID)
|
||||
}
|
||||
if keyID, ok := user_model.GetDeployKeyUserDeployKeyID(user); ok {
|
||||
return getDeployKeyRepoPermission(ctx, repo, keyID)
|
||||
}
|
||||
return GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +47,7 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
|
||||
|
||||
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
|
||||
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||
// ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
|
||||
// which comes from git configure information
|
||||
if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
|
||||
if u.ID <= 0 {
|
||||
return avatars.DefaultAvatarLink()
|
||||
}
|
||||
|
||||
|
||||
+21
-11
@@ -159,6 +159,11 @@ type User struct {
|
||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
Theme string `xorm:"NOT NULL DEFAULT ''"`
|
||||
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
|
||||
|
||||
// When the user model is used as a doer (all existing code does so), the doer can have extra details.
|
||||
// * Actions task doer needs to bind to the task
|
||||
// * Deploy-key doer needs to bind to the key
|
||||
ExtDoerData ExtDoerData `xorm:"-"`
|
||||
}
|
||||
|
||||
// Meta defines the meta information of a user, to be stored in the K/V table
|
||||
@@ -418,9 +423,9 @@ func (u *User) IsOrganization() bool {
|
||||
return u.Type == UserTypeOrganization
|
||||
}
|
||||
|
||||
// IsIndividual returns true if user is actually a individual user.
|
||||
// IsIndividual returns true if user is actually an individual user.
|
||||
func (u *User) IsIndividual() bool {
|
||||
return u.Type == UserTypeIndividual
|
||||
return u.ID > 0 && u.Type == UserTypeIndividual
|
||||
}
|
||||
|
||||
// IsTypeBot returns whether the user is of type bot
|
||||
@@ -513,9 +518,8 @@ func (u *User) GitName() string {
|
||||
}
|
||||
|
||||
// IsMailable checks if a user is eligible to receive emails.
|
||||
// System users like Ghost and Gitea Actions are excluded.
|
||||
func (u *User) IsMailable() bool {
|
||||
return u.IsActive && !u.IsGiteaActions() && !u.IsGhost()
|
||||
return u.ID > 0 && u.IsActive && u.IsIndividual()
|
||||
}
|
||||
|
||||
// IsUserExist checks if given username exist,
|
||||
@@ -551,10 +555,11 @@ type globalVarsStruct struct {
|
||||
emailToReplacer *strings.Replacer
|
||||
emailRegexp *regexp.Regexp
|
||||
systemUserNewFuncs map[int64]func() *User
|
||||
systemUserNameIdMap map[string]int64
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
return &globalVarsStruct{
|
||||
ret := &globalVarsStruct{
|
||||
// Note: The set of characters here can safely expand without a breaking change,
|
||||
// but characters removed from this set can cause user account linking to break
|
||||
customCharsReplacement: strings.NewReplacer("Æ", "AE"),
|
||||
@@ -573,12 +578,17 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
";", "",
|
||||
),
|
||||
emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"),
|
||||
|
||||
systemUserNewFuncs: map[int64]func() *User{
|
||||
GhostUserID: NewGhostUser,
|
||||
ActionsUserID: NewActionsUser,
|
||||
},
|
||||
}
|
||||
|
||||
userFuncs := []func() *User{NewGhostUser, NewActionsUser, NewDeployKeyUser}
|
||||
ret.systemUserNewFuncs = map[int64]func() *User{}
|
||||
ret.systemUserNameIdMap = map[string]int64{}
|
||||
for _, fn := range userFuncs {
|
||||
u := fn()
|
||||
ret.systemUserNewFuncs[u.ID] = fn
|
||||
ret.systemUserNameIdMap[u.LowerName] = u.ID
|
||||
}
|
||||
return ret
|
||||
})
|
||||
|
||||
// NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
|
||||
@@ -1023,7 +1033,7 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
return users, err
|
||||
}
|
||||
|
||||
// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
|
||||
// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user
|
||||
func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) {
|
||||
if id < 0 {
|
||||
if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ExtDoerData interface {
|
||||
EncodeToString() string
|
||||
DecodeFromString(string) error
|
||||
}
|
||||
|
||||
type extDoerGiteaActions struct {
|
||||
TaskID int64
|
||||
}
|
||||
|
||||
var _ ExtDoerData = (*extDoerGiteaActions)(nil)
|
||||
|
||||
func (e *extDoerGiteaActions) EncodeToString() string {
|
||||
return "gitea-actions:" + strconv.FormatInt(e.TaskID, 10)
|
||||
}
|
||||
|
||||
func (e *extDoerGiteaActions) DecodeFromString(s string) (err error) {
|
||||
idStr, _ := strings.CutPrefix(s, "gitea-actions:")
|
||||
e.TaskID, err = strconv.ParseInt(idStr, 10, 64)
|
||||
return err
|
||||
}
|
||||
|
||||
type extDoerDeployKey struct {
|
||||
DeployKeyID int64
|
||||
}
|
||||
|
||||
var _ ExtDoerData = (*extDoerDeployKey)(nil)
|
||||
|
||||
func (e *extDoerDeployKey) EncodeToString() string {
|
||||
return "deploy-key:" + strconv.FormatInt(e.DeployKeyID, 10)
|
||||
}
|
||||
|
||||
func (e *extDoerDeployKey) DecodeFromString(s string) (err error) {
|
||||
idStr, _ := strings.CutPrefix(s, "deploy-key:")
|
||||
e.DeployKeyID, err = strconv.ParseInt(idStr, 10, 64)
|
||||
return err
|
||||
}
|
||||
@@ -31,18 +31,15 @@ func GetUsersMapByIDs(ctx context.Context, userIDs []int64) (map[int64]*User, er
|
||||
}
|
||||
|
||||
func GetPossibleUserFromMap(userID int64, usererMaps map[int64]*User) *User {
|
||||
switch userID {
|
||||
case GhostUserID:
|
||||
return NewGhostUser()
|
||||
case ActionsUserID:
|
||||
return NewActionsUser()
|
||||
case 0:
|
||||
if userID == 0 {
|
||||
return nil
|
||||
default:
|
||||
user, ok := usererMaps[userID]
|
||||
if !ok {
|
||||
return NewGhostUser()
|
||||
}
|
||||
return user
|
||||
}
|
||||
if newFunc, ok := globalVars().systemUserNewFuncs[userID]; ok {
|
||||
return newFunc()
|
||||
}
|
||||
user, ok := usererMaps[userID]
|
||||
if !ok {
|
||||
return NewGhostUser()
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
+63
-36
@@ -4,7 +4,7 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/structs"
|
||||
@@ -32,59 +32,86 @@ func (u *User) IsGhost() bool {
|
||||
return u.ID == GhostUserID && u.Name == GhostUserName
|
||||
}
|
||||
|
||||
// newSystemUser creates and returns a fake user for system use.
|
||||
// The builtin username can be wrapped in parentheses to avoid conflicts with real usernames.
|
||||
func newSystemUser(id int64, name, fullName string) *User {
|
||||
return &User{
|
||||
ID: id,
|
||||
Name: name,
|
||||
LowerName: strings.ToLower(name),
|
||||
IsActive: true,
|
||||
FullName: fullName,
|
||||
Type: UserTypeBot,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ActionsUserID int64 = -2
|
||||
ActionsUserName = "gitea-actions"
|
||||
ActionsUserEmail = "teabot@gitea.io"
|
||||
ActionsUserID int64 = -2
|
||||
DeployKeyUserID int64 = -3
|
||||
)
|
||||
|
||||
// NewActionsUser creates and returns a fake user for running the actions.
|
||||
func NewActionsUser() *User {
|
||||
return &User{
|
||||
ID: ActionsUserID,
|
||||
Name: ActionsUserName,
|
||||
LowerName: ActionsUserName,
|
||||
IsActive: true,
|
||||
FullName: "Gitea Actions",
|
||||
Email: ActionsUserEmail,
|
||||
KeepEmailPrivate: true,
|
||||
LoginName: ActionsUserName,
|
||||
Type: UserTypeBot,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
return newSystemUser(ActionsUserID, "gitea-actions", "Gitea Actions")
|
||||
}
|
||||
|
||||
func GetActionsUserTaskID(u *User) (int64, bool) {
|
||||
if u == nil || u.ExtDoerData == nil || u.ID != ActionsUserID {
|
||||
return 0, false
|
||||
}
|
||||
extData := u.ExtDoerData.(*extDoerGiteaActions) //nolint:forcetypeassert // must be valid
|
||||
return extData.TaskID, true
|
||||
}
|
||||
|
||||
func NewActionsUserWithTaskID(id int64) *User {
|
||||
u := NewActionsUser()
|
||||
// LoginName is for only internal usage in this case, so it can be moved to other fields in the future
|
||||
u.LoginSource = -1
|
||||
u.LoginName = "@" + ActionsUserName + "/" + strconv.FormatInt(id, 10)
|
||||
u.ExtDoerData = &extDoerGiteaActions{TaskID: id}
|
||||
return u
|
||||
}
|
||||
|
||||
func GetActionsUserTaskID(u *User) (int64, bool) {
|
||||
if u == nil || u.ID != ActionsUserID {
|
||||
return 0, false
|
||||
}
|
||||
prefix, payload, _ := strings.Cut(u.LoginName, "/")
|
||||
if prefix != "@"+ActionsUserName {
|
||||
return 0, false
|
||||
} else if taskID, err := strconv.ParseInt(payload, 10, 64); err == nil {
|
||||
return taskID, true
|
||||
}
|
||||
return 0, false
|
||||
func NewDeployKeyUser() *User {
|
||||
return newSystemUser(DeployKeyUserID, "(deploy-key)", "Deploy Key")
|
||||
}
|
||||
|
||||
func (u *User) IsGiteaActions() bool {
|
||||
return u != nil && u.ID == ActionsUserID
|
||||
func GetDeployKeyUserDeployKeyID(u *User) (int64, bool) {
|
||||
// ok, the function name seems wordy, it is intentionally to distinguish from other "keys" like "public key id"
|
||||
// it was a mess in the "pre-receive" hook code
|
||||
if u == nil || u.ExtDoerData == nil || u.ID != DeployKeyUserID {
|
||||
return 0, false
|
||||
}
|
||||
extData := u.ExtDoerData.(*extDoerDeployKey) //nolint:forcetypeassert // must be valid
|
||||
return extData.DeployKeyID, true
|
||||
}
|
||||
|
||||
func NewDeployKeyUserWithKeyID(id int64) *User {
|
||||
u := NewDeployKeyUser()
|
||||
u.ExtDoerData = &extDoerDeployKey{DeployKeyID: id}
|
||||
return u
|
||||
}
|
||||
|
||||
func GetSystemUserByName(name string) *User {
|
||||
if strings.EqualFold(name, GhostUserName) {
|
||||
return NewGhostUser()
|
||||
}
|
||||
if strings.EqualFold(name, ActionsUserName) {
|
||||
return NewActionsUser()
|
||||
lowerName := strings.ToLower(name)
|
||||
uid := globalVars().systemUserNameIdMap[lowerName]
|
||||
if fn := globalVars().systemUserNewFuncs[uid]; fn != nil {
|
||||
return fn()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDoerUser(ctx context.Context, id int64, extDoerData string) (u *User, _ error) {
|
||||
if id > 0 {
|
||||
return GetUserByID(ctx, id)
|
||||
}
|
||||
switch id {
|
||||
case ActionsUserID:
|
||||
u = NewActionsUser()
|
||||
u.ExtDoerData = &extDoerGiteaActions{}
|
||||
case DeployKeyUserID:
|
||||
u = NewDeployKeyUser()
|
||||
u.ExtDoerData = &extDoerDeployKey{}
|
||||
default:
|
||||
return nil, ErrUserNotExist{UID: id}
|
||||
}
|
||||
return u, u.ExtDoerData.DecodeFromString(extDoerData)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ func TestSystemUser(t *testing.T) {
|
||||
assert.Equal(t, int64(-2), uid)
|
||||
assert.Equal(t, "gitea-actions", u.Name)
|
||||
assert.Equal(t, "gitea-actions", u.LowerName)
|
||||
assert.True(t, u.IsGiteaActions())
|
||||
|
||||
u = GetSystemUserByName("Gitea-actionS")
|
||||
require.NotNil(t, u)
|
||||
|
||||
Reference in New Issue
Block a user