mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: backend DB model for fine-grained repo access tokens
This commit is contained in:
parent
2d4a3e5658
commit
a1eff6f0dc
13 changed files with 166 additions and 1 deletions
|
|
@ -14,6 +14,7 @@ forgejo.org/models
|
|||
IsErrMergeDivergingFastForwardOnly
|
||||
|
||||
forgejo.org/models/auth
|
||||
GetRepositoriesAccessibleWithToken
|
||||
WebAuthnCredentials
|
||||
|
||||
forgejo.org/models/db
|
||||
|
|
|
|||
|
|
@ -205,7 +205,15 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
|||
|
||||
// create the access token
|
||||
if accessTokenScope != "" {
|
||||
t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
|
||||
t := &auth_model.AccessToken{
|
||||
Name: accessTokenName,
|
||||
UID: u.ID,
|
||||
Scope: accessTokenScope,
|
||||
|
||||
// maintain legacy behaviour until new CLI options are added -- token has access to all resources, is not
|
||||
// fine-grained
|
||||
ResourceAllRepos: true,
|
||||
}
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
|
|||
}
|
||||
t.Scope = accessTokenScope
|
||||
|
||||
// maintain legacy behaviour until new CLI options are added -- token has access to all resources, is not
|
||||
// fine-grained
|
||||
t.ResourceAllRepos = true
|
||||
|
||||
// create the token
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
- token_id: 3
|
||||
repo_id: 1
|
||||
created_unix: 1772158384
|
||||
- token_id: 3
|
||||
repo_id: 2
|
||||
created_unix: 1772158384
|
||||
- token_id: 3
|
||||
repo_id: 3
|
||||
created_unix: 1772158384
|
||||
|
|
@ -73,6 +73,8 @@ type AccessToken struct {
|
|||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
HasRecentActivity bool `xorm:"-"`
|
||||
HasUsed bool `xorm:"-"`
|
||||
|
||||
ResourceAllRepos bool `xorm:"NOT NULL DEFAULT TRUE"` // flag for whether AccessTokenResourceRepo instances will limit the resources this access token can access (false) or won't limit them (true).
|
||||
}
|
||||
|
||||
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||
|
|
|
|||
36
models/auth/access_token_resource.go
Normal file
36
models/auth/access_token_resource.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/modules/timeutil"
|
||||
)
|
||||
|
||||
// Represents a many-to-many join table which indicates specific repositories (RepoID) that can be accessed by an access
|
||||
// token (TokenID). An access token's ResourceAllRepos field must be false for records in this table to become active.
|
||||
type AccessTokenResourceRepo struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
TokenID int64 `xorm:"NOT NULL REFERENCES(access_token, id)"` // needs to be shortened from "AccessTokenID" for the index to fit MySQL table identifier length restrictions
|
||||
RepoID int64 `xorm:"NOT NULL REFERENCES(repository, id)"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(AccessTokenResourceRepo))
|
||||
}
|
||||
|
||||
func GetRepositoriesAccessibleWithToken(ctx context.Context, accessTokenID int64) ([]*AccessTokenResourceRepo, error) {
|
||||
var resources []*AccessTokenResourceRepo
|
||||
err := db.GetEngine(ctx).
|
||||
Where("token_id = ?", accessTokenID).
|
||||
Find(&resources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
41
models/auth/access_token_resource_test.go
Normal file
41
models/auth/access_token_resource_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetRepositoriesAccessibleWithToken(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("models/auth/TestGetRepositoriesAccessibleWithToken")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("No Resources", func(t *testing.T) {
|
||||
resources, err := auth_model.GetRepositoriesAccessibleWithToken(db.DefaultContext, 999)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, resources)
|
||||
})
|
||||
|
||||
t.Run("Has Resources", func(t *testing.T) {
|
||||
resources, err := auth_model.GetRepositoriesAccessibleWithToken(db.DefaultContext, 3)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resources, 3)
|
||||
|
||||
// Verify all expected repo IDs are present
|
||||
repoIDs := make([]int64, len(resources))
|
||||
for i, res := range resources {
|
||||
repoIDs[i] = res.RepoID
|
||||
}
|
||||
assert.Contains(t, repoIDs, int64(1))
|
||||
assert.Contains(t, repoIDs, int64(2))
|
||||
assert.Contains(t, repoIDs, int64(3))
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package forgejo_migrations
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerMigration(&Migration{
|
||||
Description: "add resource_all_owned_repositories to table access_token",
|
||||
Upgrade: addAllOwnedRepositoriesToAccessToken,
|
||||
})
|
||||
}
|
||||
|
||||
func addAllOwnedRepositoriesToAccessToken(x *xorm.Engine) error {
|
||||
type AccessToken struct {
|
||||
ResourceAllRepos bool `xorm:"NOT NULL DEFAULT TRUE"`
|
||||
}
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(AccessToken))
|
||||
return err
|
||||
}
|
||||
29
models/forgejo_migrations/v15b_add-access_token_resource.go
Normal file
29
models/forgejo_migrations/v15b_add-access_token_resource.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package forgejo_migrations
|
||||
|
||||
import (
|
||||
"forgejo.org/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerMigration(&Migration{
|
||||
Description: "add access_token_resource table",
|
||||
Upgrade: addAccessTokenResource,
|
||||
})
|
||||
}
|
||||
|
||||
func addAccessTokenResource(x *xorm.Engine) error {
|
||||
type AccessTokenResourceRepo struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
TokenID int64 `xorm:"NOT NULL REFERENCES(access_token, id)"` // needs to be shortened from "AccessTokenID" for the index to fit MySQL table identifier length restrictions
|
||||
RepoID int64 `xorm:"NOT NULL REFERENCES(repository, id)"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||
}
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(AccessTokenResourceRepo))
|
||||
return err
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"forgejo.org/models/unittest"
|
||||
|
||||
_ "forgejo.org/modules/testimport"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import (
|
|||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/optional"
|
||||
|
||||
_ "forgejo.org/modules/testimport"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -128,6 +128,10 @@ func CreateAccessToken(ctx *context.APIContext) {
|
|||
}
|
||||
t.Scope = scope
|
||||
|
||||
// maintain legacy behaviour until new API options are added -- token has access to all resources, is not
|
||||
// fine-grained
|
||||
t.ResourceAllRepos = true
|
||||
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "NewAccessToken", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ func ApplicationsPost(ctx *context.Context) {
|
|||
UID: ctx.Doer.ID,
|
||||
Name: form.Name,
|
||||
Scope: scope,
|
||||
|
||||
// maintain legacy behaviour until new UI options are added -- token has access to all resources, is not
|
||||
// fine-grained
|
||||
ResourceAllRepos: true,
|
||||
}
|
||||
|
||||
exist, err := auth_model.AccessTokenByNameExists(ctx, t)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue