aboutsummaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorJack Hay <jack@allspice.io>2023-06-04 14:57:16 -0400
committerGitHub <noreply@github.com>2023-06-04 20:57:16 +0200
commit18de83b2a3fc120922096b7348d6375094ae1532 (patch)
treea9724bcb6f00a040e5a16970ce56931cd1aa3d51 /models
parent520eb57d7642a5fca3df319e5b5d1c7c9018087c (diff)
downloadgitea-18de83b2a3fc120922096b7348d6375094ae1532.tar.gz
gitea-18de83b2a3fc120922096b7348d6375094ae1532.zip
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>
Diffstat (limited to 'models')
-rw-r--r--models/auth/token.go9
-rw-r--r--models/auth/token_scope.go348
-rw-r--r--models/auth/token_scope_test.go106
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v1_20/main_test.go14
-rw-r--r--models/migrations/v1_20/v259.go360
-rw-r--r--models/migrations/v1_20/v259_test.go110
7 files changed, 762 insertions, 187 deletions
diff --git a/models/auth/token.go b/models/auth/token.go
index 3f9f117f73..9374fe38c2 100644
--- a/models/auth/token.go
+++ b/models/auth/token.go
@@ -112,6 +112,15 @@ func NewAccessToken(t *AccessToken) error {
return err
}
+// DisplayPublicOnly whether to display this as a public-only token.
+func (t *AccessToken) DisplayPublicOnly() bool {
+ publicOnly, err := t.Scope.PublicOnly()
+ if err != nil {
+ return false
+ }
+ return publicOnly
+}
+
func getAccessTokenIDFromCache(token string) int64 {
if successfulAccessTokenCache == nil {
return 0
diff --git a/models/auth/token_scope.go b/models/auth/token_scope.go
index 06c89fecc2..61e684ea27 100644
--- a/models/auth/token_scope.go
+++ b/models/auth/token_scope.go
@@ -6,113 +6,122 @@ package auth
import (
"fmt"
"strings"
+
+ "code.gitea.io/gitea/models/perm"
)
-// AccessTokenScope represents the scope for an access token.
-type AccessTokenScope string
+// AccessTokenScopeCategory represents the scope category for an access token
+type AccessTokenScopeCategory int
const (
- AccessTokenScopeAll AccessTokenScope = "all"
+ AccessTokenScopeCategoryActivityPub = iota
+ AccessTokenScopeCategoryAdmin
+ AccessTokenScopeCategoryMisc
+ AccessTokenScopeCategoryNotification
+ AccessTokenScopeCategoryOrganization
+ AccessTokenScopeCategoryPackage
+ AccessTokenScopeCategoryIssue
+ AccessTokenScopeCategoryRepository
+ AccessTokenScopeCategoryUser
+)
- AccessTokenScopeRepo AccessTokenScope = "repo"
- AccessTokenScopeRepoStatus AccessTokenScope = "repo:status"
- AccessTokenScopePublicRepo AccessTokenScope = "public_repo"
+// AllAccessTokenScopeCategories contains all access token scope categories
+var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{
+ AccessTokenScopeCategoryActivityPub,
+ AccessTokenScopeCategoryAdmin,
+ AccessTokenScopeCategoryMisc,
+ AccessTokenScopeCategoryNotification,
+ AccessTokenScopeCategoryOrganization,
+ AccessTokenScopeCategoryPackage,
+ AccessTokenScopeCategoryIssue,
+ AccessTokenScopeCategoryRepository,
+ AccessTokenScopeCategoryUser,
+}
- AccessTokenScopeAdminOrg AccessTokenScope = "admin:org"
- AccessTokenScopeWriteOrg AccessTokenScope = "write:org"
- AccessTokenScopeReadOrg AccessTokenScope = "read:org"
+// AccessTokenScopeLevel represents the access levels without a given scope category
+type AccessTokenScopeLevel int
- AccessTokenScopeAdminPublicKey AccessTokenScope = "admin:public_key"
- AccessTokenScopeWritePublicKey AccessTokenScope = "write:public_key"
- AccessTokenScopeReadPublicKey AccessTokenScope = "read:public_key"
+const (
+ NoAccess AccessTokenScopeLevel = iota
+ Read
+ Write
+)
+
+// AccessTokenScope represents the scope for an access token.
+type AccessTokenScope string
- AccessTokenScopeAdminRepoHook AccessTokenScope = "admin:repo_hook"
- AccessTokenScopeWriteRepoHook AccessTokenScope = "write:repo_hook"
- AccessTokenScopeReadRepoHook AccessTokenScope = "read:repo_hook"
+// for all categories, write implies read
+const (
+ AccessTokenScopeAll AccessTokenScope = "all"
+ AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos
- AccessTokenScopeAdminOrgHook AccessTokenScope = "admin:org_hook"
+ AccessTokenScopeReadActivityPub AccessTokenScope = "read:activitypub"
+ AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub"
- AccessTokenScopeAdminUserHook AccessTokenScope = "admin:user_hook"
+ AccessTokenScopeReadAdmin AccessTokenScope = "read:admin"
+ AccessTokenScopeWriteAdmin AccessTokenScope = "write:admin"
- AccessTokenScopeNotification AccessTokenScope = "notification"
+ AccessTokenScopeReadMisc AccessTokenScope = "read:misc"
+ AccessTokenScopeWriteMisc AccessTokenScope = "write:misc"
- AccessTokenScopeUser AccessTokenScope = "user"
- AccessTokenScopeReadUser AccessTokenScope = "read:user"
- AccessTokenScopeUserEmail AccessTokenScope = "user:email"
- AccessTokenScopeUserFollow AccessTokenScope = "user:follow"
+ AccessTokenScopeReadNotification AccessTokenScope = "read:notification"
+ AccessTokenScopeWriteNotification AccessTokenScope = "write:notification"
- AccessTokenScopeDeleteRepo AccessTokenScope = "delete_repo"
+ AccessTokenScopeReadOrganization AccessTokenScope = "read:organization"
+ AccessTokenScopeWriteOrganization AccessTokenScope = "write:organization"
- AccessTokenScopePackage AccessTokenScope = "package"
- AccessTokenScopeWritePackage AccessTokenScope = "write:package"
- AccessTokenScopeReadPackage AccessTokenScope = "read:package"
- AccessTokenScopeDeletePackage AccessTokenScope = "delete:package"
+ AccessTokenScopeReadPackage AccessTokenScope = "read:package"
+ AccessTokenScopeWritePackage AccessTokenScope = "write:package"
- AccessTokenScopeAdminGPGKey AccessTokenScope = "admin:gpg_key"
- AccessTokenScopeWriteGPGKey AccessTokenScope = "write:gpg_key"
- AccessTokenScopeReadGPGKey AccessTokenScope = "read:gpg_key"
+ AccessTokenScopeReadIssue AccessTokenScope = "read:issue"
+ AccessTokenScopeWriteIssue AccessTokenScope = "write:issue"
- AccessTokenScopeAdminApplication AccessTokenScope = "admin:application"
- AccessTokenScopeWriteApplication AccessTokenScope = "write:application"
- AccessTokenScopeReadApplication AccessTokenScope = "read:application"
+ AccessTokenScopeReadRepository AccessTokenScope = "read:repository"
+ AccessTokenScopeWriteRepository AccessTokenScope = "write:repository"
- AccessTokenScopeSudo AccessTokenScope = "sudo"
+ AccessTokenScopeReadUser AccessTokenScope = "read:user"
+ AccessTokenScopeWriteUser AccessTokenScope = "write:user"
)
-// AccessTokenScopeBitmap represents a bitmap of access token scopes.
-type AccessTokenScopeBitmap uint64
+// 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, except `sudo`.
- AccessTokenScopeAllBits AccessTokenScopeBitmap = AccessTokenScopeRepoBits |
- AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | AccessTokenScopeAdminUserHookBits |
- AccessTokenScopeNotificationBits | AccessTokenScopeUserBits | AccessTokenScopeDeleteRepoBits |
- AccessTokenScopePackageBits | AccessTokenScopeAdminGPGKeyBits | AccessTokenScopeAdminApplicationBits
-
- AccessTokenScopeRepoBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeRepoStatusBits | AccessTokenScopePublicRepoBits | AccessTokenScopeAdminRepoHookBits
- AccessTokenScopeRepoStatusBits AccessTokenScopeBitmap = 1 << iota
- AccessTokenScopePublicRepoBits AccessTokenScopeBitmap = 1 << iota
-
- AccessTokenScopeAdminOrgBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteOrgBits
- AccessTokenScopeWriteOrgBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadOrgBits
- AccessTokenScopeReadOrgBits AccessTokenScopeBitmap = 1 << iota
-
- AccessTokenScopeAdminPublicKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWritePublicKeyBits
- AccessTokenScopeWritePublicKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadPublicKeyBits
- AccessTokenScopeReadPublicKeyBits AccessTokenScopeBitmap = 1 << iota
+ // AccessTokenScopeAllBits is the bitmap of all access token scopes
+ accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
+ accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
+ accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
+ accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits
- AccessTokenScopeAdminRepoHookBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteRepoHookBits
- AccessTokenScopeWriteRepoHookBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadRepoHookBits
- AccessTokenScopeReadRepoHookBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
- AccessTokenScopeAdminOrgHookBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopeReadActivityPubBits accessTokenScopeBitmap = 1 << iota
+ accessTokenScopeWriteActivityPubBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadActivityPubBits
- AccessTokenScopeAdminUserHookBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopeReadAdminBits accessTokenScopeBitmap = 1 << iota
+ accessTokenScopeWriteAdminBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadAdminBits
- AccessTokenScopeNotificationBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopeReadMiscBits accessTokenScopeBitmap = 1 << iota
+ accessTokenScopeWriteMiscBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadMiscBits
- AccessTokenScopeUserBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadUserBits | AccessTokenScopeUserEmailBits | AccessTokenScopeUserFollowBits
- AccessTokenScopeReadUserBits AccessTokenScopeBitmap = 1 << iota
- AccessTokenScopeUserEmailBits AccessTokenScopeBitmap = 1 << iota
- AccessTokenScopeUserFollowBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopeReadNotificationBits accessTokenScopeBitmap = 1 << iota
+ accessTokenScopeWriteNotificationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadNotificationBits
- AccessTokenScopeDeleteRepoBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopeReadOrganizationBits accessTokenScopeBitmap = 1 << iota
+ accessTokenScopeWriteOrganizationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadOrganizationBits
- AccessTokenScopePackageBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWritePackageBits | AccessTokenScopeDeletePackageBits
- AccessTokenScopeWritePackageBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadPackageBits
- AccessTokenScopeReadPackageBits AccessTokenScopeBitmap = 1 << iota
- AccessTokenScopeDeletePackageBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopeReadPackageBits accessTokenScopeBitmap = 1 << iota
+ accessTokenScopeWritePackageBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadPackageBits
- AccessTokenScopeAdminGPGKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteGPGKeyBits
- AccessTokenScopeWriteGPGKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadGPGKeyBits
- AccessTokenScopeReadGPGKeyBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopeReadIssueBits accessTokenScopeBitmap = 1 << iota
+ accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits
- AccessTokenScopeAdminApplicationBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteApplicationBits
- AccessTokenScopeWriteApplicationBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadApplicationBits
- AccessTokenScopeReadApplicationBits AccessTokenScopeBitmap = 1 << iota
+ accessTokenScopeReadRepositoryBits accessTokenScopeBitmap = 1 << iota
+ accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits
- AccessTokenScopeSudoBits AccessTokenScopeBitmap = 1 << iota
+ 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,
@@ -120,61 +129,110 @@ const (
)
// allAccessTokenScopes contains all access token scopes.
-// The order is important: parent scope must precedes child scopes.
+// The order is important: parent scope must precede child scopes.
var allAccessTokenScopes = []AccessTokenScope{
- AccessTokenScopeRepo, AccessTokenScopeRepoStatus, AccessTokenScopePublicRepo,
- AccessTokenScopeAdminOrg, AccessTokenScopeWriteOrg, AccessTokenScopeReadOrg,
- AccessTokenScopeAdminPublicKey, AccessTokenScopeWritePublicKey, AccessTokenScopeReadPublicKey,
- AccessTokenScopeAdminRepoHook, AccessTokenScopeWriteRepoHook, AccessTokenScopeReadRepoHook,
- AccessTokenScopeAdminOrgHook,
- AccessTokenScopeAdminUserHook,
- AccessTokenScopeNotification,
- AccessTokenScopeUser, AccessTokenScopeReadUser, AccessTokenScopeUserEmail, AccessTokenScopeUserFollow,
- AccessTokenScopeDeleteRepo,
- AccessTokenScopePackage, AccessTokenScopeWritePackage, AccessTokenScopeReadPackage, AccessTokenScopeDeletePackage,
- AccessTokenScopeAdminGPGKey, AccessTokenScopeWriteGPGKey, AccessTokenScopeReadGPGKey,
- AccessTokenScopeAdminApplication, AccessTokenScopeWriteApplication, AccessTokenScopeReadApplication,
- AccessTokenScopeSudo,
+ 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{
- AccessTokenScopeRepo: AccessTokenScopeRepoBits,
- AccessTokenScopeRepoStatus: AccessTokenScopeRepoStatusBits,
- AccessTokenScopePublicRepo: AccessTokenScopePublicRepoBits,
- AccessTokenScopeAdminOrg: AccessTokenScopeAdminOrgBits,
- AccessTokenScopeWriteOrg: AccessTokenScopeWriteOrgBits,
- AccessTokenScopeReadOrg: AccessTokenScopeReadOrgBits,
- AccessTokenScopeAdminPublicKey: AccessTokenScopeAdminPublicKeyBits,
- AccessTokenScopeWritePublicKey: AccessTokenScopeWritePublicKeyBits,
- AccessTokenScopeReadPublicKey: AccessTokenScopeReadPublicKeyBits,
- AccessTokenScopeAdminRepoHook: AccessTokenScopeAdminRepoHookBits,
- AccessTokenScopeWriteRepoHook: AccessTokenScopeWriteRepoHookBits,
- AccessTokenScopeReadRepoHook: AccessTokenScopeReadRepoHookBits,
- AccessTokenScopeAdminOrgHook: AccessTokenScopeAdminOrgHookBits,
- AccessTokenScopeAdminUserHook: AccessTokenScopeAdminUserHookBits,
- AccessTokenScopeNotification: AccessTokenScopeNotificationBits,
- AccessTokenScopeUser: AccessTokenScopeUserBits,
- AccessTokenScopeReadUser: AccessTokenScopeReadUserBits,
- AccessTokenScopeUserEmail: AccessTokenScopeUserEmailBits,
- AccessTokenScopeUserFollow: AccessTokenScopeUserFollowBits,
- AccessTokenScopeDeleteRepo: AccessTokenScopeDeleteRepoBits,
- AccessTokenScopePackage: AccessTokenScopePackageBits,
- AccessTokenScopeWritePackage: AccessTokenScopeWritePackageBits,
- AccessTokenScopeReadPackage: AccessTokenScopeReadPackageBits,
- AccessTokenScopeDeletePackage: AccessTokenScopeDeletePackageBits,
- AccessTokenScopeAdminGPGKey: AccessTokenScopeAdminGPGKeyBits,
- AccessTokenScopeWriteGPGKey: AccessTokenScopeWriteGPGKeyBits,
- AccessTokenScopeReadGPGKey: AccessTokenScopeReadGPGKeyBits,
- AccessTokenScopeAdminApplication: AccessTokenScopeAdminApplicationBits,
- AccessTokenScopeWriteApplication: AccessTokenScopeWriteApplicationBits,
- AccessTokenScopeReadApplication: AccessTokenScopeReadApplicationBits,
- AccessTokenScopeSudo: AccessTokenScopeSudoBits,
+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,
+}
+
+// readAccessTokenScopes maps a scope category to the read permission scope
+var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]AccessTokenScope{
+ Read: {
+ AccessTokenScopeCategoryActivityPub: AccessTokenScopeReadActivityPub,
+ AccessTokenScopeCategoryAdmin: AccessTokenScopeReadAdmin,
+ AccessTokenScopeCategoryMisc: AccessTokenScopeReadMisc,
+ AccessTokenScopeCategoryNotification: AccessTokenScopeReadNotification,
+ AccessTokenScopeCategoryOrganization: AccessTokenScopeReadOrganization,
+ AccessTokenScopeCategoryPackage: AccessTokenScopeReadPackage,
+ AccessTokenScopeCategoryIssue: AccessTokenScopeReadIssue,
+ AccessTokenScopeCategoryRepository: AccessTokenScopeReadRepository,
+ AccessTokenScopeCategoryUser: AccessTokenScopeReadUser,
+ },
+ Write: {
+ AccessTokenScopeCategoryActivityPub: AccessTokenScopeWriteActivityPub,
+ AccessTokenScopeCategoryAdmin: AccessTokenScopeWriteAdmin,
+ AccessTokenScopeCategoryMisc: AccessTokenScopeWriteMisc,
+ AccessTokenScopeCategoryNotification: AccessTokenScopeWriteNotification,
+ AccessTokenScopeCategoryOrganization: AccessTokenScopeWriteOrganization,
+ AccessTokenScopeCategoryPackage: AccessTokenScopeWritePackage,
+ AccessTokenScopeCategoryIssue: AccessTokenScopeWriteIssue,
+ AccessTokenScopeCategoryRepository: AccessTokenScopeWriteRepository,
+ AccessTokenScopeCategoryUser: AccessTokenScopeWriteUser,
+ },
+}
+
+// GetRequiredScopes gets the specific scopes for a given level and categories
+func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
+ scopes := make([]AccessTokenScope, 0, len(scopeCategories))
+ for _, cat := range scopeCategories {
+ scopes = append(scopes, accessTokenScopes[level][cat])
+ }
+ return scopes
}
-// Parse parses the scope string into a bitmap, thus removing possible duplicates.
-func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) {
- var bitmap AccessTokenScopeBitmap
+// ContainsCategory checks if a list of categories contains a specific category
+func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool {
+ for _, c := range categories {
+ if c == category {
+ return true
+ }
+ }
+ return false
+}
+
+// GetScopeLevelFromAccessMode converts permission access mode to scope level
+func GetScopeLevelFromAccessMode(mode perm.AccessMode) AccessTokenScopeLevel {
+ switch mode {
+ case perm.AccessModeNone:
+ return NoAccess
+ case perm.AccessModeRead:
+ return Read
+ case perm.AccessModeWrite:
+ return Write
+ case perm.AccessModeAdmin:
+ return Write
+ case perm.AccessModeOwner:
+ return Write
+ default:
+ return NoAccess
+ }
+}
+
+// parse the scope string into a bitmap, thus removing possible duplicates.
+func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
+ var bitmap accessTokenScopeBitmap
// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
remainingScopes := string(s)
@@ -196,7 +254,7 @@ func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) {
continue
}
if singleScope == AccessTokenScopeAll {
- bitmap |= AccessTokenScopeAllBits
+ bitmap |= accessTokenScopeAllBits
continue
}
@@ -217,26 +275,42 @@ func (s AccessTokenScope) StringSlice() []string {
// Normalize returns a normalized scope string without any duplicates.
func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
- bitmap, err := s.Parse()
+ bitmap, err := s.parse()
if err != nil {
return "", err
}
- return bitmap.ToScope(), nil
+ return bitmap.toScope(), nil
}
-// HasScope returns true if the string has the given scope
-func (s AccessTokenScope) HasScope(scope AccessTokenScope) (bool, error) {
- bitmap, err := s.Parse()
+// PublicOnly checks if this token scope is limited to public resources
+func (s AccessTokenScope) PublicOnly() (bool, error) {
+ bitmap, err := s.parse()
if err != nil {
return false, err
}
- return bitmap.HasScope(scope)
+ return bitmap.hasScope(AccessTokenScopePublicOnly)
}
// HasScope returns true if the string has the given scope
-func (bitmap AccessTokenScopeBitmap) HasScope(scope AccessTokenScope) (bool, error) {
+func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
+ bitmap, err := s.parse()
+ if err != nil {
+ return false, err
+ }
+
+ for _, s := range scopes {
+ if has, err := bitmap.hasScope(s); !has || err != nil {
+ return has, err
+ }
+ }
+
+ return true, nil
+}
+
+// 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)
@@ -245,17 +319,17 @@ func (bitmap AccessTokenScopeBitmap) HasScope(scope AccessTokenScope) (bool, err
return bitmap&expectedBits == expectedBits, nil
}
-// ToScope returns a normalized scope string without any duplicates.
-func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope {
+// toScope returns a normalized scope string without any duplicates.
+func (bitmap accessTokenScopeBitmap) toScope() AccessTokenScope {
var scopes []string
// iterate over all scopes, and reconstruct the bitmap
// if the reconstructed bitmap doesn't change, then the scope is already included
- var reconstruct AccessTokenScopeBitmap
+ 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 {
+ if ok, _ := bitmap.hasScope(singleScope); ok {
current := reconstruct | allAccessTokenScopeBits[singleScope]
if current == reconstruct {
continue
@@ -269,7 +343,7 @@ func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope {
scope := AccessTokenScope(strings.Join(scopes, ","))
scope = AccessTokenScope(strings.ReplaceAll(
string(scope),
- "repo,admin:org,admin:public_key,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application",
+ "write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user",
"all",
))
return scope
diff --git a/models/auth/token_scope_test.go b/models/auth/token_scope_test.go
index b96a5fd469..a6097e45d7 100644
--- a/models/auth/token_scope_test.go
+++ b/models/auth/token_scope_test.go
@@ -4,44 +4,35 @@
package auth
import (
+ "fmt"
"testing"
"github.com/stretchr/testify/assert"
)
+type scopeTestNormalize struct {
+ in AccessTokenScope
+ out AccessTokenScope
+ err error
+}
+
func TestAccessTokenScope_Normalize(t *testing.T) {
- tests := []struct {
- in AccessTokenScope
- out AccessTokenScope
- err error
- }{
+ tests := []scopeTestNormalize{
{"", "", nil},
- {"repo", "repo", nil},
- {"repo,repo:status", "repo", nil},
- {"repo,public_repo", "repo", nil},
- {"admin:public_key,write:public_key", "admin:public_key", nil},
- {"admin:public_key,read:public_key", "admin:public_key", nil},
- {"write:public_key,read:public_key", "write:public_key", nil}, // read is include in write
- {"admin:repo_hook,write:repo_hook", "admin:repo_hook", nil},
- {"admin:repo_hook,read:repo_hook", "admin:repo_hook", nil},
- {"repo,admin:repo_hook,read:repo_hook", "repo", nil}, // admin:repo_hook is a child scope of repo
- {"repo,read:repo_hook", "repo", nil}, // read:repo_hook is a child scope of repo
- {"user", "user", nil},
- {"user,read:user", "user", nil},
- {"user,admin:org,write:org", "admin:org,user", nil},
- {"admin:org,write:org,user", "admin:org,user", nil},
- {"package", "package", nil},
- {"package,write:package", "package", nil},
- {"package,write:package,delete:package", "package", nil},
- {"write:package,read:package", "write:package", nil}, // read is include in write
- {"write:package,delete:package", "write:package,delete:package", nil}, // write and delete are not include in each other
- {"admin:gpg_key", "admin:gpg_key", nil},
- {"admin:gpg_key,write:gpg_key", "admin:gpg_key", nil},
- {"admin:gpg_key,write:gpg_key,user", "user,admin:gpg_key", nil},
- {"admin:application,write:application,user", "user,admin:application", nil},
+ {"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
{"all", "all", nil},
- {"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", "all", nil},
- {"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application,sudo", "all,sudo", nil},
+ {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
+ {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
+ }
+
+ for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
+ tests = append(tests,
+ scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
+ scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
+ scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
+ scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
+ scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
+ )
}
for _, test := range tests {
@@ -53,31 +44,46 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
}
}
+type scopeTestHasScope struct {
+ in AccessTokenScope
+ scope AccessTokenScope
+ out bool
+ err error
+}
+
func TestAccessTokenScope_HasScope(t *testing.T) {
- tests := []struct {
- in AccessTokenScope
- scope AccessTokenScope
- out bool
- err error
- }{
- {"repo", "repo", true, nil},
- {"repo", "repo:status", true, nil},
- {"repo", "public_repo", true, nil},
- {"repo", "admin:org", false, nil},
- {"repo", "admin:public_key", false, nil},
- {"repo:status", "repo", false, nil},
- {"repo:status", "public_repo", false, nil},
- {"admin:org", "write:org", true, nil},
- {"admin:org", "read:org", true, nil},
- {"admin:org", "admin:org", true, nil},
- {"user", "read:user", true, nil},
- {"package", "write:package", true, nil},
+ tests := []scopeTestHasScope{
+ {"read:admin", "write:package", false, nil},
+ {"all", "write:package", true, nil},
+ {"write:package", "all", false, nil},
+ {"public-only", "read:issue", false, nil},
+ }
+
+ for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
+ tests = append(tests,
+ scopeTestHasScope{
+ AccessTokenScope(fmt.Sprintf("read:%s", scope)),
+ AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
+ },
+ scopeTestHasScope{
+ AccessTokenScope(fmt.Sprintf("write:%s", scope)),
+ AccessTokenScope(fmt.Sprintf("write:%s", scope)), true, nil,
+ },
+ scopeTestHasScope{
+ AccessTokenScope(fmt.Sprintf("write:%s", scope)),
+ AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
+ },
+ scopeTestHasScope{
+ AccessTokenScope(fmt.Sprintf("read:%s", scope)),
+ AccessTokenScope(fmt.Sprintf("write:%s", scope)), false, nil,
+ },
+ )
}
for _, test := range tests {
t.Run(string(test.in), func(t *testing.T) {
- scope, err := test.in.HasScope(test.scope)
- assert.Equal(t, test.out, scope)
+ hasScope, err := test.in.HasScope(test.scope)
+ assert.Equal(t, test.out, hasScope)
assert.Equal(t, test.err, err)
})
}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 231c93cc74..d96c17bfb5 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -495,6 +495,8 @@ var migrations = []Migration{
NewMigration("Add Actions Artifact table", v1_20.CreateActionArtifactTable),
// v258 -> 259
NewMigration("Add PinOrder Column", v1_20.AddPinOrderToIssue),
+ // v259 -> 260
+ NewMigration("Convert scoped access tokens", v1_20.ConvertScopedAccessTokens),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_20/main_test.go b/models/migrations/v1_20/main_test.go
new file mode 100644
index 0000000000..92a1a9f622
--- /dev/null
+++ b/models/migrations/v1_20/main_test.go
@@ -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)
+}
diff --git a/models/migrations/v1_20/v259.go b/models/migrations/v1_20/v259.go
new file mode 100644
index 0000000000..5b8ced4ad7
--- /dev/null
+++ b/models/migrations/v1_20/v259.go
@@ -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
+}
diff --git a/models/migrations/v1_20/v259_test.go b/models/migrations/v1_20/v259_test.go
new file mode 100644
index 0000000000..5bc9a71391
--- /dev/null
+++ b/models/migrations/v1_20/v259_test.go
@@ -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)
+ }
+}