forgejo/models/actions/runner_token.go
Mathieu Fenniak a012b8bf36 refactor: replace ActionRunnerToken.OwnerID & RepoID with optional.Option[int64] (#11601)
Currently:
- In the database, `NULL` is used in `action_runner_token.owner_id` & `.repo_id` to represent an absent value, as required by the foreign key
- In the code, `0` is used in `ActionRunnerToken.OwnerID` and `.RepoID` to represent an absent value

This PR replaces the `int64` fields with `optional.Option[int64]` which allows a single data type to be used for both cases, and removes the usage of the value `0` as a placeholder.

This change has a limited scope -- although `ActionRunnerToken` uses `NULL` values in the database, the related table `ActionRunner` still uses zero-values for `OwnerID` and `RepoID`.  This means a lot of code interacting with both of these tables still uses `0` values, such as the UI.  The changes here were stopped at a reasonable point to avoid cascading into all places that use the `ActionRunner` table.  (I'll continue this work in the future to enable foreign keys on `ActionRunner`, but likely after #11516 is completed to avoid serious conflict resolution problems.)

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests for Go changes

(can be removed for JavaScript changes)

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [x] `make pr-go` before pushing

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11601
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2026-03-10 03:19:16 +01:00

135 lines
4.4 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"context"
"fmt"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
"forgejo.org/modules/optional"
"forgejo.org/modules/timeutil"
"forgejo.org/modules/util"
"xorm.io/builder"
)
// ActionRunnerToken represents runner tokens
//
// It can be:
// 1. global token, OwnerID is 0 and RepoID is 0
// 2. org/user level token, OwnerID is org/user ID and RepoID is 0
// 3. repo level token, OwnerID is 0 and RepoID is repo ID
//
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
// or it will be complicated to find tokens belonging to a specific owner.
// For example, conditions like `OwnerID = 1` will also return token {OwnerID: 1, RepoID: 1},
// but it's a repo level token, not an org/user level token.
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level tokens.
type ActionRunnerToken struct {
ID int64
Token string `xorm:"UNIQUE"`
OwnerID optional.Option[int64] `xorm:"index REFERENCES(user, id)"`
Owner *user_model.User `xorm:"-"`
RepoID optional.Option[int64] `xorm:"index REFERENCES(repository, id)"`
Repo *repo_model.Repository `xorm:"-"`
IsActive bool // true means it can be used
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(ActionRunnerToken))
}
// GetRunnerToken returns a action runner via token
func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, error) {
var runnerToken ActionRunnerToken
has, err := db.GetEngine(ctx).Where("token=?", token).Get(&runnerToken)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist)
}
return &runnerToken, nil
}
// UpdateRunnerToken updates runner token information.
func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string) (err error) {
e := db.GetEngine(ctx)
if len(cols) == 0 {
_, err = e.ID(r.ID).AllCols().Update(r)
} else {
_, err = e.ID(r.ID).Cols(cols...).Update(r)
}
return err
}
// 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 optional.Option[int64]) (*ActionRunnerToken, error) {
if ownerID.Has() && repoID.Has() {
// 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 = optional.None[int64]()
}
token := util.CryptoRandomString(util.RandomStringHigh)
runnerToken := &ActionRunnerToken{
OwnerID: ownerID,
RepoID: repoID,
IsActive: true,
Token: token,
}
return runnerToken, db.WithTx(ctx, func(ctx context.Context) error {
if _, err := db.GetEngine(ctx).Where(runnerTokenCond(ownerID, repoID)).Cols("is_active").Update(&ActionRunnerToken{
IsActive: false,
}); err != nil {
return err
}
_, err := db.GetEngine(ctx).Insert(runnerToken)
return err
})
}
func runnerTokenCond(ownerID, repoID optional.Option[int64]) builder.Cond {
var condOwnerID builder.Cond
if has, value := ownerID.Get(); !has {
condOwnerID = builder.IsNull{"owner_id"}
} else {
condOwnerID = builder.Eq{"owner_id": value}
}
var condRepoID builder.Cond
if has, value := repoID.Get(); !has {
condRepoID = builder.IsNull{"repo_id"}
} else {
condRepoID = builder.Eq{"repo_id": value}
}
return builder.And(condOwnerID, condRepoID)
}
// GetLatestRunnerToken returns the latest runner token
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID optional.Option[int64]) (*ActionRunnerToken, error) {
if ownerID.Has() && repoID.Has() {
// 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 = optional.None[int64]()
}
var runnerToken ActionRunnerToken
has, err := db.GetEngine(ctx).Where(runnerTokenCond(ownerID, repoID)).
OrderBy("id DESC").Get(&runnerToken)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("runner token: %w", util.ErrNotExist)
}
return &runnerToken, nil
}