mirror of https://github.com/go-gitea/gitea
Redesign Scoped Access Tokens (#24767)
## Changes - Adds the following high level access scopes, each with `read` and `write` levels: - `activitypub` - `admin` (hidden if user is not a site admin) - `misc` - `notification` - `organization` - `package` - `issue` - `repository` - `user` - Adds new middleware function `tokenRequiresScopes()` in addition to `reqToken()` - `tokenRequiresScopes()` is used for each high-level api section - _if_ a scoped token is present, checks that the required scope is included based on the section and HTTP method - `reqToken()` is used for individual routes - checks that required authentication is present (but does not check scope levels as this will already have been handled by `tokenRequiresScopes()` - Adds migration to convert old scoped access tokens to the new set of scopes - Updates the user interface for scope selection ### User interface example <img width="903" alt="Screen Shot 2023-05-31 at 1 56 55 PM" src="https://github.com/go-gitea/gitea/assets/23248839/654766ec-2143-4f59-9037-3b51600e32f3"> <img width="917" alt="Screen Shot 2023-05-31 at 1 56 43 PM" src="https://github.com/go-gitea/gitea/assets/23248839/1ad64081-012c-4a73-b393-66b30352654c"> ## tokenRequiresScopes Design Decision - `tokenRequiresScopes()` was added to more reliably cover api routes. For an incoming request, this function uses the given scope category (say `AccessTokenScopeCategoryOrganization`) and the HTTP method (say `DELETE`) and verifies that any scoped tokens in use include `delete:organization`. - `reqToken()` is used to enforce auth for individual routes that require it. If a scoped token is not present for a request, `tokenRequiresScopes()` will not return an error ## TODO - [x] Alphabetize scope categories - [x] Change 'public repos only' to a radio button (private vs public). Also expand this to organizations - [X] Disable token creation if no scopes selected. Alternatively, show warning - [x] `reqToken()` is missing from many `POST/DELETE` routes in the api. `tokenRequiresScopes()` only checks that a given token has the correct scope, `reqToken()` must be used to check that a token (or some other auth) is present. - _This should be addressed in this PR_ - [x] The migration should be reviewed very carefully in order to minimize access changes to existing user tokens. - _This should be addressed in this PR_ - [x] Link to api to swagger documentation, clarify what read/write/delete levels correspond to - [x] Review cases where more than one scope is needed as this directly deviates from the api definition. - _This should be addressed in this PR_ - For example: ```go m.Group("/users/{username}/orgs", func() { m.Get("", reqToken(), org.ListUserOrgs) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI()) ``` ## Future improvements - [ ] Add required scopes to swagger documentation - [ ] Redesign `reqToken()` to be opt-out rather than opt-in - [ ] Subdivide scopes like `repository` - [ ] Once a token is created, if it has no scopes, we should display text instead of an empty bullet point - [ ] If the 'public repos only' option is selected, should read categories be selected by default Closes #24501 Closes #24799 Co-authored-by: Jonathan Tran <jon@allspice.io> Co-authored-by: Kyle D <kdumontnu@gmail.com> Co-authored-by: silverwind <me@silverwind.io>pull/25050/head
parent
520eb57d76
commit
18de83b2a3
@ -0,0 +1,14 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_20 //nolint
|
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"code.gitea.io/gitea/models/migrations/base" |
||||
) |
||||
|
||||
func TestMain(m *testing.M) { |
||||
base.MainTest(m) |
||||
} |
@ -0,0 +1,360 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_20 //nolint
|
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/modules/log" |
||||
|
||||
"xorm.io/xorm" |
||||
) |
||||
|
||||
// unknownAccessTokenScope represents the scope for an access token that isn't
|
||||
// known be an old token or a new token.
|
||||
type unknownAccessTokenScope string |
||||
|
||||
// AccessTokenScope represents the scope for an access token.
|
||||
type AccessTokenScope string |
||||
|
||||
// for all categories, write implies read
|
||||
const ( |
||||
AccessTokenScopeAll AccessTokenScope = "all" |
||||
AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos
|
||||
|
||||
AccessTokenScopeReadActivityPub AccessTokenScope = "read:activitypub" |
||||
AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub" |
||||
|
||||
AccessTokenScopeReadAdmin AccessTokenScope = "read:admin" |
||||
AccessTokenScopeWriteAdmin AccessTokenScope = "write:admin" |
||||
|
||||
AccessTokenScopeReadMisc AccessTokenScope = "read:misc" |
||||
AccessTokenScopeWriteMisc AccessTokenScope = "write:misc" |
||||
|
||||
AccessTokenScopeReadNotification AccessTokenScope = "read:notification" |
||||
AccessTokenScopeWriteNotification AccessTokenScope = "write:notification" |
||||
|
||||
AccessTokenScopeReadOrganization AccessTokenScope = "read:organization" |
||||
AccessTokenScopeWriteOrganization AccessTokenScope = "write:organization" |
||||
|
||||
AccessTokenScopeReadPackage AccessTokenScope = "read:package" |
||||
AccessTokenScopeWritePackage AccessTokenScope = "write:package" |
||||
|
||||
AccessTokenScopeReadIssue AccessTokenScope = "read:issue" |
||||
AccessTokenScopeWriteIssue AccessTokenScope = "write:issue" |
||||
|
||||
AccessTokenScopeReadRepository AccessTokenScope = "read:repository" |
||||
AccessTokenScopeWriteRepository AccessTokenScope = "write:repository" |
||||
|
||||
AccessTokenScopeReadUser AccessTokenScope = "read:user" |
||||
AccessTokenScopeWriteUser AccessTokenScope = "write:user" |
||||
) |
||||
|
||||
// accessTokenScopeBitmap represents a bitmap of access token scopes.
|
||||
type accessTokenScopeBitmap uint64 |
||||
|
||||
// Bitmap of each scope, including the child scopes.
|
||||
const ( |
||||
// AccessTokenScopeAllBits is the bitmap of all access token scopes
|
||||
accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits | |
||||
accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits | |
||||
accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits | |
||||
accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits |
||||
|
||||
accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota |
||||
|
||||
accessTokenScopeReadActivityPubBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWriteActivityPubBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadActivityPubBits |
||||
|
||||
accessTokenScopeReadAdminBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWriteAdminBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadAdminBits |
||||
|
||||
accessTokenScopeReadMiscBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWriteMiscBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadMiscBits |
||||
|
||||
accessTokenScopeReadNotificationBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWriteNotificationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadNotificationBits |
||||
|
||||
accessTokenScopeReadOrganizationBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWriteOrganizationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadOrganizationBits |
||||
|
||||
accessTokenScopeReadPackageBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWritePackageBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadPackageBits |
||||
|
||||
accessTokenScopeReadIssueBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits |
||||
|
||||
accessTokenScopeReadRepositoryBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits |
||||
|
||||
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota |
||||
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits |
||||
|
||||
// The current implementation only supports up to 64 token scopes.
|
||||
// If we need to support > 64 scopes,
|
||||
// refactoring the whole implementation in this file (and only this file) is needed.
|
||||
) |
||||
|
||||
// allAccessTokenScopes contains all access token scopes.
|
||||
// The order is important: parent scope must precede child scopes.
|
||||
var allAccessTokenScopes = []AccessTokenScope{ |
||||
AccessTokenScopePublicOnly, |
||||
AccessTokenScopeWriteActivityPub, AccessTokenScopeReadActivityPub, |
||||
AccessTokenScopeWriteAdmin, AccessTokenScopeReadAdmin, |
||||
AccessTokenScopeWriteMisc, AccessTokenScopeReadMisc, |
||||
AccessTokenScopeWriteNotification, AccessTokenScopeReadNotification, |
||||
AccessTokenScopeWriteOrganization, AccessTokenScopeReadOrganization, |
||||
AccessTokenScopeWritePackage, AccessTokenScopeReadPackage, |
||||
AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue, |
||||
AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository, |
||||
AccessTokenScopeWriteUser, AccessTokenScopeReadUser, |
||||
} |
||||
|
||||
// allAccessTokenScopeBits contains all access token scopes.
|
||||
var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{ |
||||
AccessTokenScopeAll: accessTokenScopeAllBits, |
||||
AccessTokenScopePublicOnly: accessTokenScopePublicOnlyBits, |
||||
AccessTokenScopeReadActivityPub: accessTokenScopeReadActivityPubBits, |
||||
AccessTokenScopeWriteActivityPub: accessTokenScopeWriteActivityPubBits, |
||||
AccessTokenScopeReadAdmin: accessTokenScopeReadAdminBits, |
||||
AccessTokenScopeWriteAdmin: accessTokenScopeWriteAdminBits, |
||||
AccessTokenScopeReadMisc: accessTokenScopeReadMiscBits, |
||||
AccessTokenScopeWriteMisc: accessTokenScopeWriteMiscBits, |
||||
AccessTokenScopeReadNotification: accessTokenScopeReadNotificationBits, |
||||
AccessTokenScopeWriteNotification: accessTokenScopeWriteNotificationBits, |
||||
AccessTokenScopeReadOrganization: accessTokenScopeReadOrganizationBits, |
||||
AccessTokenScopeWriteOrganization: accessTokenScopeWriteOrganizationBits, |
||||
AccessTokenScopeReadPackage: accessTokenScopeReadPackageBits, |
||||
AccessTokenScopeWritePackage: accessTokenScopeWritePackageBits, |
||||
AccessTokenScopeReadIssue: accessTokenScopeReadIssueBits, |
||||
AccessTokenScopeWriteIssue: accessTokenScopeWriteIssueBits, |
||||
AccessTokenScopeReadRepository: accessTokenScopeReadRepositoryBits, |
||||
AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits, |
||||
AccessTokenScopeReadUser: accessTokenScopeReadUserBits, |
||||
AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits, |
||||
} |
||||
|
||||
// hasScope returns true if the string has the given scope
|
||||
func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) { |
||||
expectedBits, ok := allAccessTokenScopeBits[scope] |
||||
if !ok { |
||||
return false, fmt.Errorf("invalid access token scope: %s", scope) |
||||
} |
||||
|
||||
return bitmap&expectedBits == expectedBits, nil |
||||
} |
||||
|
||||
// toScope returns a normalized scope string without any duplicates.
|
||||
func (bitmap accessTokenScopeBitmap) toScope(unknownScopes *[]unknownAccessTokenScope) AccessTokenScope { |
||||
var scopes []string |
||||
|
||||
// Preserve unknown scopes, and put them at the beginning so that it's clear
|
||||
// when debugging.
|
||||
if unknownScopes != nil { |
||||
for _, unknownScope := range *unknownScopes { |
||||
scopes = append(scopes, string(unknownScope)) |
||||
} |
||||
} |
||||
|
||||
// iterate over all scopes, and reconstruct the bitmap
|
||||
// if the reconstructed bitmap doesn't change, then the scope is already included
|
||||
var reconstruct accessTokenScopeBitmap |
||||
|
||||
for _, singleScope := range allAccessTokenScopes { |
||||
// no need for error checking here, since we know the scope is valid
|
||||
if ok, _ := bitmap.hasScope(singleScope); ok { |
||||
current := reconstruct | allAccessTokenScopeBits[singleScope] |
||||
if current == reconstruct { |
||||
continue |
||||
} |
||||
|
||||
reconstruct = current |
||||
scopes = append(scopes, string(singleScope)) |
||||
} |
||||
} |
||||
|
||||
scope := AccessTokenScope(strings.Join(scopes, ",")) |
||||
scope = AccessTokenScope(strings.ReplaceAll( |
||||
string(scope), |
||||
"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", |
||||
"all", |
||||
)) |
||||
return scope |
||||
} |
||||
|
||||
// parse the scope string into a bitmap, thus removing possible duplicates.
|
||||
func (s AccessTokenScope) parse() (accessTokenScopeBitmap, *[]unknownAccessTokenScope) { |
||||
var bitmap accessTokenScopeBitmap |
||||
var unknownScopes []unknownAccessTokenScope |
||||
|
||||
// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
|
||||
remainingScopes := string(s) |
||||
for len(remainingScopes) > 0 { |
||||
i := strings.IndexByte(remainingScopes, ',') |
||||
var v string |
||||
if i < 0 { |
||||
v = remainingScopes |
||||
remainingScopes = "" |
||||
} else if i+1 >= len(remainingScopes) { |
||||
v = remainingScopes[:i] |
||||
remainingScopes = "" |
||||
} else { |
||||
v = remainingScopes[:i] |
||||
remainingScopes = remainingScopes[i+1:] |
||||
} |
||||
singleScope := AccessTokenScope(v) |
||||
if singleScope == "" { |
||||
continue |
||||
} |
||||
if singleScope == AccessTokenScopeAll { |
||||
bitmap |= accessTokenScopeAllBits |
||||
continue |
||||
} |
||||
|
||||
bits, ok := allAccessTokenScopeBits[singleScope] |
||||
if !ok { |
||||
unknownScopes = append(unknownScopes, unknownAccessTokenScope(string(singleScope))) |
||||
} |
||||
bitmap |= bits |
||||
} |
||||
|
||||
return bitmap, &unknownScopes |
||||
} |
||||
|
||||
// NormalizePreservingUnknown returns a normalized scope string without any
|
||||
// duplicates. Unknown scopes are included.
|
||||
func (s AccessTokenScope) NormalizePreservingUnknown() AccessTokenScope { |
||||
bitmap, unknownScopes := s.parse() |
||||
|
||||
return bitmap.toScope(unknownScopes) |
||||
} |
||||
|
||||
// OldAccessTokenScope represents the scope for an access token.
|
||||
type OldAccessTokenScope string |
||||
|
||||
const ( |
||||
OldAccessTokenScopeAll OldAccessTokenScope = "all" |
||||
|
||||
OldAccessTokenScopeRepo OldAccessTokenScope = "repo" |
||||
OldAccessTokenScopeRepoStatus OldAccessTokenScope = "repo:status" |
||||
OldAccessTokenScopePublicRepo OldAccessTokenScope = "public_repo" |
||||
|
||||
OldAccessTokenScopeAdminOrg OldAccessTokenScope = "admin:org" |
||||
OldAccessTokenScopeWriteOrg OldAccessTokenScope = "write:org" |
||||
OldAccessTokenScopeReadOrg OldAccessTokenScope = "read:org" |
||||
|
||||
OldAccessTokenScopeAdminPublicKey OldAccessTokenScope = "admin:public_key" |
||||
OldAccessTokenScopeWritePublicKey OldAccessTokenScope = "write:public_key" |
||||
OldAccessTokenScopeReadPublicKey OldAccessTokenScope = "read:public_key" |
||||
|
||||
OldAccessTokenScopeAdminRepoHook OldAccessTokenScope = "admin:repo_hook" |
||||
OldAccessTokenScopeWriteRepoHook OldAccessTokenScope = "write:repo_hook" |
||||
OldAccessTokenScopeReadRepoHook OldAccessTokenScope = "read:repo_hook" |
||||
|
||||
OldAccessTokenScopeAdminOrgHook OldAccessTokenScope = "admin:org_hook" |
||||
|
||||
OldAccessTokenScopeNotification OldAccessTokenScope = "notification" |
||||
|
||||
OldAccessTokenScopeUser OldAccessTokenScope = "user" |
||||
OldAccessTokenScopeReadUser OldAccessTokenScope = "read:user" |
||||
OldAccessTokenScopeUserEmail OldAccessTokenScope = "user:email" |
||||
OldAccessTokenScopeUserFollow OldAccessTokenScope = "user:follow" |
||||
|
||||
OldAccessTokenScopeDeleteRepo OldAccessTokenScope = "delete_repo" |
||||
|
||||
OldAccessTokenScopePackage OldAccessTokenScope = "package" |
||||
OldAccessTokenScopeWritePackage OldAccessTokenScope = "write:package" |
||||
OldAccessTokenScopeReadPackage OldAccessTokenScope = "read:package" |
||||
OldAccessTokenScopeDeletePackage OldAccessTokenScope = "delete:package" |
||||
|
||||
OldAccessTokenScopeAdminGPGKey OldAccessTokenScope = "admin:gpg_key" |
||||
OldAccessTokenScopeWriteGPGKey OldAccessTokenScope = "write:gpg_key" |
||||
OldAccessTokenScopeReadGPGKey OldAccessTokenScope = "read:gpg_key" |
||||
|
||||
OldAccessTokenScopeAdminApplication OldAccessTokenScope = "admin:application" |
||||
OldAccessTokenScopeWriteApplication OldAccessTokenScope = "write:application" |
||||
OldAccessTokenScopeReadApplication OldAccessTokenScope = "read:application" |
||||
|
||||
OldAccessTokenScopeSudo OldAccessTokenScope = "sudo" |
||||
) |
||||
|
||||
var accessTokenScopeMap = map[OldAccessTokenScope][]AccessTokenScope{ |
||||
OldAccessTokenScopeAll: {AccessTokenScopeAll}, |
||||
OldAccessTokenScopeRepo: {AccessTokenScopeWriteRepository}, |
||||
OldAccessTokenScopeRepoStatus: {AccessTokenScopeWriteRepository}, |
||||
OldAccessTokenScopePublicRepo: {AccessTokenScopePublicOnly, AccessTokenScopeWriteRepository}, |
||||
OldAccessTokenScopeAdminOrg: {AccessTokenScopeWriteOrganization}, |
||||
OldAccessTokenScopeWriteOrg: {AccessTokenScopeWriteOrganization}, |
||||
OldAccessTokenScopeReadOrg: {AccessTokenScopeReadOrganization}, |
||||
OldAccessTokenScopeAdminPublicKey: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeWritePublicKey: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeReadPublicKey: {AccessTokenScopeReadUser}, |
||||
OldAccessTokenScopeAdminRepoHook: {AccessTokenScopeWriteRepository}, |
||||
OldAccessTokenScopeWriteRepoHook: {AccessTokenScopeWriteRepository}, |
||||
OldAccessTokenScopeReadRepoHook: {AccessTokenScopeReadRepository}, |
||||
OldAccessTokenScopeAdminOrgHook: {AccessTokenScopeWriteOrganization}, |
||||
OldAccessTokenScopeNotification: {AccessTokenScopeWriteNotification}, |
||||
OldAccessTokenScopeUser: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeReadUser: {AccessTokenScopeReadUser}, |
||||
OldAccessTokenScopeUserEmail: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeUserFollow: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeDeleteRepo: {AccessTokenScopeWriteRepository}, |
||||
OldAccessTokenScopePackage: {AccessTokenScopeWritePackage}, |
||||
OldAccessTokenScopeWritePackage: {AccessTokenScopeWritePackage}, |
||||
OldAccessTokenScopeReadPackage: {AccessTokenScopeReadPackage}, |
||||
OldAccessTokenScopeDeletePackage: {AccessTokenScopeWritePackage}, |
||||
OldAccessTokenScopeAdminGPGKey: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeWriteGPGKey: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeReadGPGKey: {AccessTokenScopeReadUser}, |
||||
OldAccessTokenScopeAdminApplication: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeWriteApplication: {AccessTokenScopeWriteUser}, |
||||
OldAccessTokenScopeReadApplication: {AccessTokenScopeReadUser}, |
||||
OldAccessTokenScopeSudo: {AccessTokenScopeWriteAdmin}, |
||||
} |
||||
|
||||
type AccessToken struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
Scope string |
||||
} |
||||
|
||||
func ConvertScopedAccessTokens(x *xorm.Engine) error { |
||||
var tokens []*AccessToken |
||||
|
||||
if err := x.Find(&tokens); err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, token := range tokens { |
||||
var scopes []string |
||||
allNewScopesMap := make(map[AccessTokenScope]bool) |
||||
for _, oldScope := range strings.Split(token.Scope, ",") { |
||||
if newScopes, exists := accessTokenScopeMap[OldAccessTokenScope(oldScope)]; exists { |
||||
for _, newScope := range newScopes { |
||||
allNewScopesMap[newScope] = true |
||||
} |
||||
} else { |
||||
log.Debug("access token scope not recognized as old token scope %s; preserving it", oldScope) |
||||
scopes = append(scopes, oldScope) |
||||
} |
||||
} |
||||
|
||||
for s := range allNewScopesMap { |
||||
scopes = append(scopes, string(s)) |
||||
} |
||||
scope := AccessTokenScope(strings.Join(scopes, ",")) |
||||
|
||||
// normalize the scope
|
||||
normScope := scope.NormalizePreservingUnknown() |
||||
|
||||
token.Scope = string(normScope) |
||||
|
||||
// update the db entry with the new scope
|
||||
if _, err := x.Cols("scope").ID(token.ID).Update(token); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,110 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_20 //nolint
|
||||
|
||||
import ( |
||||
"sort" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"code.gitea.io/gitea/models/migrations/base" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
type testCase struct { |
||||
Old OldAccessTokenScope |
||||
New AccessTokenScope |
||||
} |
||||
|
||||
func createOldTokenScope(scopes ...OldAccessTokenScope) OldAccessTokenScope { |
||||
s := make([]string, 0, len(scopes)) |
||||
for _, os := range scopes { |
||||
s = append(s, string(os)) |
||||
} |
||||
return OldAccessTokenScope(strings.Join(s, ",")) |
||||
} |
||||
|
||||
func createNewTokenScope(scopes ...AccessTokenScope) AccessTokenScope { |
||||
s := make([]string, 0, len(scopes)) |
||||
for _, os := range scopes { |
||||
s = append(s, string(os)) |
||||
} |
||||
return AccessTokenScope(strings.Join(s, ",")) |
||||
} |
||||
|
||||
func Test_ConvertScopedAccessTokens(t *testing.T) { |
||||
tests := []testCase{ |
||||
{ |
||||
createOldTokenScope(OldAccessTokenScopeRepo, OldAccessTokenScopeUserFollow), |
||||
createNewTokenScope(AccessTokenScopeWriteRepository, AccessTokenScopeWriteUser), |
||||
}, |
||||
{ |
||||
createOldTokenScope(OldAccessTokenScopeUser, OldAccessTokenScopeWritePackage, OldAccessTokenScopeSudo), |
||||
createNewTokenScope(AccessTokenScopeWriteAdmin, AccessTokenScopeWritePackage, AccessTokenScopeWriteUser), |
||||
}, |
||||
{ |
||||
createOldTokenScope(), |
||||
createNewTokenScope(), |
||||
}, |
||||
{ |
||||
createOldTokenScope(OldAccessTokenScopeReadGPGKey, OldAccessTokenScopeReadOrg, OldAccessTokenScopeAll), |
||||
createNewTokenScope(AccessTokenScopeAll), |
||||
}, |
||||
{ |
||||
createOldTokenScope(OldAccessTokenScopeReadGPGKey, "invalid"), |
||||
createNewTokenScope("invalid", AccessTokenScopeReadUser), |
||||
}, |
||||
} |
||||
|
||||
// add a test for each individual mapping
|
||||
for oldScope, newScope := range accessTokenScopeMap { |
||||
tests = append(tests, testCase{ |
||||
oldScope, |
||||
createNewTokenScope(newScope...), |
||||
}) |
||||
} |
||||
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(AccessToken)) |
||||
defer deferable() |
||||
if x == nil || t.Failed() { |
||||
t.Skip() |
||||
return |
||||
} |
||||
|
||||
// verify that no fixtures were loaded
|
||||
count, err := x.Count(&AccessToken{}) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, int64(0), count) |
||||
|
||||
for _, tc := range tests { |
||||
_, err = x.Insert(&AccessToken{ |
||||
Scope: string(tc.Old), |
||||
}) |
||||
assert.NoError(t, err) |
||||
} |
||||
|
||||
// migrate the scopes
|
||||
err = ConvertScopedAccessTokens(x) |
||||
assert.NoError(t, err) |
||||
|
||||
// migrate the scopes again (migration should be idempotent)
|
||||
err = ConvertScopedAccessTokens(x) |
||||
assert.NoError(t, err) |
||||
|
||||
tokens := make([]AccessToken, 0) |
||||
err = x.Find(&tokens) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, len(tests), len(tokens)) |
||||
|
||||
// sort the tokens (insertion order by auto-incrementing primary key)
|
||||
sort.Slice(tokens, func(i, j int) bool { |
||||
return tokens[i].ID < tokens[j].ID |
||||
}) |
||||
|
||||
// verify that the converted scopes are equal to the expected test result
|
||||
for idx, newToken := range tokens { |
||||
assert.Equal(t, string(tests[idx].New), newToken.Scope) |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue