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
+31 -2
View File
@@ -11,6 +11,7 @@ import (
asymkey_model "gitea.dev/models/asymkey"
auth_model "gitea.dev/models/auth"
deploykey_model "gitea.dev/models/deploykey"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
@@ -66,7 +67,7 @@ func TestCreateReadOnlyDeployKey(t *testing.T) {
resp := MakeRequest(t, req, http.StatusCreated)
newDeployKey := DecodeJSON(t, resp, &api.DeployKey{})
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
unittest.AssertExistsAndLoadBean(t, &deploykey_model.DeployKey{
ID: newDeployKey.ID,
Name: rawKeyBody.Title,
Mode: perm.AccessModeRead,
@@ -103,7 +104,7 @@ func TestCreateReadWriteDeployKey(t *testing.T) {
resp := MakeRequest(t, req, http.StatusCreated)
newDeployKey := DecodeJSON(t, resp, &api.DeployKey{})
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
unittest.AssertExistsAndLoadBean(t, &deploykey_model.DeployKey{
ID: newDeployKey.ID,
Name: rawKeyBody.Title,
Mode: perm.AccessModeWrite,
@@ -204,3 +205,31 @@ func TestCreateUserKey(t *testing.T) {
fingerprintPublicKeys = DecodeJSON(t, resp, []api.PublicKey{})
assert.Empty(t, fingerprintPublicKeys)
}
func TestCreateDeployToken(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo1"})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name)
req := NewRequestWithJSON(t, "POST", keysURL+"/tokens", api.CreateDeployKeyTokenOption{Title: "ci", ReadOnly: true}).
AddTokenAuth(token)
created := DecodeJSON(t, MakeRequest(t, req, http.StatusCreated), &api.DeployKey{})
assert.NotEmpty(t, created.Token)
assert.True(t, created.ReadOnly)
// a token is listed and deleted like a deploy key, but it is never readable again
resp := MakeRequest(t, NewRequest(t, "GET", keysURL).AddTokenAuth(token), http.StatusOK)
listed := DecodeJSON(t, resp, []api.DeployKey{})
assert.Len(t, listed, 1)
assert.NotContains(t, resp.Body.String(), created.Token)
assert.Equal(t, created.Fingerprint, listed[0].Fingerprint)
assert.Contains(t, created.Fingerprint, "********")
MakeRequest(t, NewRequest(t, "DELETE", fmt.Sprintf("%s/%d", keysURL, created.ID)).AddTokenAuth(token), http.StatusNoContent)
resp = MakeRequest(t, NewRequest(t, "GET", keysURL).AddTokenAuth(token), http.StatusOK)
assert.Empty(t, DecodeJSON(t, resp, []api.DeployKey{}))
}