diff --git a/cmd/actions.go b/cmd/actions.go index f582c16c81c48..e0742f6e7ebfe 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -19,6 +19,7 @@ var ( Usage: "Manage Gitea Actions", Subcommands: []*cli.Command{ subcmdActionsGenRunnerToken, + subcmdActionsSetRunnerToken, }, } @@ -36,6 +37,27 @@ var ( }, }, } + + subcmdActionsSetRunnerToken = &cli.Command{ + Name: "set-runner-token", + Usage: "Set a new token for a runner to as register token", + Action: runSetActionsRunnerToken, + Aliases: []string{"srt"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "scope", + Aliases: []string{"s"}, + Value: "", + Usage: "{owner}[/{repo}] - leave empty for a global runner", + }, + &cli.StringFlag{ + Name: "token", + Aliases: []string{"t"}, + Value: "", + Usage: "[{token}] - leave empty will generate a new token, otherwise will update the token to database. The token MUST be a 40 digital string containing only [0-9a-zA-Z]", + }, + }, + } ) func runGenerateActionsRunnerToken(c *cli.Context) error { @@ -53,3 +75,20 @@ func runGenerateActionsRunnerToken(c *cli.Context) error { _, _ = fmt.Printf("%s\n", respText.Text) return nil } + +func runSetActionsRunnerToken(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + setting.MustInstalled() + + scope := c.String("scope") + token := c.String("token") + + respText, extra := private.SetActionsRunnerToken(ctx, scope, token) + if extra.HasError() { + return handleCliResponseExtra(extra) + } + _, _ = fmt.Printf("%s\n", respText.Text) + return nil +} diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go index fd6ba7ecadb3f..e5cbc2db4480a 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -70,17 +70,24 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string // NewRunnerToken creates a new active runner token and invalidate all old tokens // ownerID will be ignored and treated as 0 if repoID is non-zero. -func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { +func NewRunnerToken(ctx context.Context, ownerID, repoID int64, preDefinedToken string) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // Remove OwnerID to avoid confusion; it's not worth returning an error here. ownerID = 0 } - token, err := util.CryptoRandomString(40) - if err != nil { - return nil, err + token := preDefinedToken + if token == "" { + var err error + token, err = util.CryptoRandomString(40) + if err != nil { + return nil, err + } + } else if len(token) != 40 || !util.IsRandomStringValid(token) { + return nil, util.NewInvalidArgumentErrorf("invalid token: %s", token) } + runnerToken := &ActionRunnerToken{ OwnerID: ownerID, RepoID: repoID, @@ -95,7 +102,7 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo return err } - _, err = db.GetEngine(ctx).Insert(runnerToken) + _, err := db.GetEngine(ctx).Insert(runnerToken) return err }) } diff --git a/models/actions/runner_token_test.go b/models/actions/runner_token_test.go index 159805e5f7ce5..d81432d8e5727 100644 --- a/models/actions/runner_token_test.go +++ b/models/actions/runner_token_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -22,11 +23,25 @@ func TestGetLatestRunnerToken(t *testing.T) { func TestNewRunnerToken(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - token, err := NewRunnerToken(db.DefaultContext, 1, 0) + token, err := NewRunnerToken(db.DefaultContext, 1, 0, "") assert.NoError(t, err) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) assert.NoError(t, err) assert.EqualValues(t, expectedToken, token) + + predefinedToken, err := util.CryptoRandomString(40) + assert.NoError(t, err) + + token, err = NewRunnerToken(db.DefaultContext, 1, 0, predefinedToken) + assert.NoError(t, err) + assert.EqualValues(t, predefinedToken, token.Token) + + expectedToken, err = GetLatestRunnerToken(db.DefaultContext, 1, 0) + assert.NoError(t, err) + assert.EqualValues(t, expectedToken, token) + + _, err = NewRunnerToken(db.DefaultContext, 1, 0, "invalid-token") + assert.Error(t, err) } func TestUpdateRunnerToken(t *testing.T) { diff --git a/modules/private/actions.go b/modules/private/actions.go index 311a28365004b..0f144170b1aeb 100644 --- a/modules/private/actions.go +++ b/modules/private/actions.go @@ -23,3 +23,20 @@ func GenerateActionsRunnerToken(ctx context.Context, scope string) (*ResponseTex return requestJSONResp(req, &ResponseText{}) } + +type SetTokenRequest struct { + Scope string + Token string +} + +// SetActionsRunnerToken calls the internal GenerateActionsRunnerToken function +func SetActionsRunnerToken(ctx context.Context, scope, token string) (*ResponseText, ResponseExtra) { + reqURL := setting.LocalURL + "api/internal/actions/set_actions_runner_token" + + req := newInternalRequest(ctx, reqURL, "POST", SetTokenRequest{ + Scope: scope, + Token: token, + }) + + return requestJSONResp(req, &ResponseText{}) +} diff --git a/modules/util/util.go b/modules/util/util.go index 1fb4cb21cb884..7319406c4cd26 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -78,6 +78,15 @@ func CryptoRandomInt(limit int64) (int64, error) { const alphanumericalChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +func IsRandomStringValid(s string) bool { + for _, c := range s { + if !strings.ContainsRune(alphanumericalChars, c) { + return false + } + } + return true +} + // CryptoRandomString generates a crypto random alphanumerical string, each byte is generated by [0,61] range func CryptoRandomString(length int64) (string, error) { buf := make([]byte, length) diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go index 329242d9f6ee5..518888f3813e9 100644 --- a/routers/api/v1/admin/runners.go +++ b/routers/api/v1/admin/runners.go @@ -18,6 +18,11 @@ func GetRegistrationToken(ctx *context.APIContext) { // produces: // - application/json // parameters: + // - name: set_token + // in: body + // description: set a runner register token instead of generating one. + // type: string + // required: false // responses: // "200": // "$ref": "#/responses/RegistrationToken" diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 199ee7d7773b9..78b2e4bf7e46f 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -182,6 +182,11 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { // description: name of the organization // type: string // required: true + // - name: set_token + // in: body + // description: set a runner register token instead of generating one. + // type: string + // required: false // responses: // "200": // "$ref": "#/responses/RegistrationToken" diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d27e8d2427b39..a161b05a21602 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -500,6 +500,11 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { // description: name of the repo // type: string // required: true + // - name: set_token + // in: body + // description: set a runner register token instead of generating one. + // type: string + // required: false // responses: // "200": // "$ref": "#/responses/RegistrationToken" diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index f088e9a2d411f..97be75fc859a5 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -19,14 +19,18 @@ type RegistrationToken struct { } func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) { - token, err := actions_model.GetLatestRunnerToken(ctx, ownerID, repoID) - if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) { - token, err = actions_model.NewRunnerToken(ctx, ownerID, repoID) + setToken := ctx.FormString("set_token") + var token *actions_model.ActionRunnerToken + var err error + if setToken == "" { + token, err = actions_model.GetLatestRunnerToken(ctx, ownerID, repoID) + } + if setToken != "" || errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) { + token, err = actions_model.NewRunnerToken(ctx, ownerID, repoID, setToken) } if err != nil { ctx.InternalServerError(err) return } - ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token}) } diff --git a/routers/private/actions.go b/routers/private/actions.go index 696634b5e757d..a3646e1340a2b 100644 --- a/routers/private/actions.go +++ b/routers/private/actions.go @@ -43,7 +43,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) { token, err := actions_model.GetLatestRunnerToken(ctx, owner, repo) if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) { - token, err = actions_model.NewRunnerToken(ctx, owner, repo) + token, err = actions_model.NewRunnerToken(ctx, owner, repo, "") if err != nil { errMsg := fmt.Sprintf("error while creating runner token: %v", err) log.Error("NewRunnerToken failed: %v", errMsg) @@ -90,3 +90,44 @@ func parseScope(ctx *context.PrivateContext, scope string) (ownerID, repoID int6 repoID = r.ID return ownerID, repoID, nil } + +// SetActionsRunnerToken set a runner token for a given scope +func SetActionsRunnerToken(ctx *context.PrivateContext) { + var setRequest private.SetTokenRequest + rd := ctx.Req.Body + defer rd.Close() + + if err := json.NewDecoder(rd).Decode(&setRequest); err != nil { + log.Error("JSON Decode failed: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + + owner, repo, err := parseScope(ctx, setRequest.Scope) + if err != nil { + log.Error("parseScope failed: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + } + if setRequest.Token == "" { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: "token is empty", + }) + return + } + + token, err := actions_model.NewRunnerToken(ctx, owner, repo, setRequest.Token) + if err != nil { + errMsg := fmt.Sprintf("error while creating runner token: %v", err) + log.Error("NewRunnerToken failed: %v", errMsg) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: errMsg, + }) + return + } + + ctx.PlainText(http.StatusOK, token.Token) +} diff --git a/routers/private/internal.go b/routers/private/internal.go index 1fb72f13d9cc1..2600afb1cbf4f 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -82,6 +82,7 @@ func Routes() *web.Router { r.Post("/mail/send", SendEmail) r.Post("/restore_repo", RestoreRepo) r.Post("/actions/generate_actions_runner_token", GenerateActionsRunnerToken) + r.Post("/actions/set_actions_runner_token", SetActionsRunnerToken) r.Group("/repo", func() { // FIXME: it is not right to use context.Contexter here because all routes here should use PrivateContext diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index f38933226b883..81f0dfd750b6d 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -32,7 +32,7 @@ func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) { var token *actions_model.ActionRunnerToken token, err = actions_model.GetLatestRunnerToken(ctx, opts.OwnerID, opts.RepoID) if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) { - token, err = actions_model.NewRunnerToken(ctx, opts.OwnerID, opts.RepoID) + token, err = actions_model.NewRunnerToken(ctx, opts.OwnerID, opts.RepoID, "") if err != nil { ctx.ServerError("CreateRunnerToken", err) return @@ -131,7 +131,7 @@ func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64 // RunnerResetRegistrationToken reset registration token func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, redirectTo string) { - _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID) + _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID, "") if err != nil { ctx.ServerError("ResetRunnerRegistrationToken", err) return diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 82a301da2fe99..0d0fc39d1d7c8 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -402,6 +402,14 @@ ], "summary": "Get an global actions runner registration token", "operationId": "adminGetRunnerRegistrationToken", + "parameters": [ + { + "type": "string", + "description": "set a runner register token instead of generating one.", + "name": "set_token", + "in": "body" + } + ], "responses": { "200": { "$ref": "#/responses/RegistrationToken" @@ -1702,6 +1710,12 @@ "name": "org", "in": "path", "required": true + }, + { + "type": "string", + "description": "set a runner register token instead of generating one.", + "name": "set_token", + "in": "body" } ], "responses": { @@ -3891,6 +3905,12 @@ "name": "repo", "in": "path", "required": true + }, + { + "type": "string", + "description": "set a runner register token instead of generating one.", + "name": "set_token", + "in": "body" } ], "responses": {