summaryrefslogtreecommitdiffstats
path: root/models/auth
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/auth
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/auth')
-rw-r--r--models/auth/token.go9
-rw-r--r--models/auth/token_scope.go348
-rw-r--r--models/auth/token_scope_test.go106
3 files changed, 276 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)
})
}