mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-10-23 18:42:26 +00:00
Fixes: https://codeberg.org/forgejo/forgejo/issues/820 (cherry picked from commit6a7022ebbb) (cherry picked from commit764eac47b5) (cherry picked from commit1141eb7b6f) (cherry picked from commit826b6509b6) (cherry picked from commit9990d932b8) (cherry picked from commit7eca570743) (cherry picked from commit66e1d3f082) (cherry picked from commit188226a8e6) (cherry picked from commit4cd1bff25c) (cherry picked from commitfad6b6d2c4) (cherry picked from commit5b25c3d851) (cherry picked from commit4746ece4dd) (cherry picked from commit2a6f85afb3) (cherry picked from commitc027d724ee) (cherry picked from commitbe2f1eeaeb) (cherry picked from commit3058a54fe9) (cherry picked from commit53936d38a0) (cherry picked from commit311983cc97) (cherry picked from commit1651ae757b) (cherry picked from commitd3dd8ea24d) (cherry picked from commit9a80326ff3) (cherry picked from commit66eb33235e) (cherry picked from commit769e24d5a8) (cherry picked from commit436cc21217) (cherry picked from commit817faca7f0) (cherry picked from commit80ee08aef1) (cherry picked from commit15f8885d0c) (cherry picked from commit0944a4442c) (cherry picked from commit91631d41b0) (cherry picked from commit0fbda3386f) (cherry picked from commita464b0e2ba) (cherry picked from commit0b98d50c92) (cherry picked from commit6365d4b761) (cherry picked from commit3af5715dbc)
350 lines
13 KiB
Go
350 lines
13 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/perm"
|
|
)
|
|
|
|
// AccessTokenScopeCategory represents the scope category for an access token
|
|
type AccessTokenScopeCategory int
|
|
|
|
const (
|
|
AccessTokenScopeCategoryActivityPub = iota
|
|
AccessTokenScopeCategoryAdmin
|
|
AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values
|
|
AccessTokenScopeCategoryNotification
|
|
AccessTokenScopeCategoryOrganization
|
|
AccessTokenScopeCategoryPackage
|
|
AccessTokenScopeCategoryIssue
|
|
AccessTokenScopeCategoryRepository
|
|
AccessTokenScopeCategoryUser
|
|
)
|
|
|
|
// AllAccessTokenScopeCategories contains all access token scope categories
|
|
var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{
|
|
AccessTokenScopeCategoryActivityPub,
|
|
AccessTokenScopeCategoryAdmin,
|
|
AccessTokenScopeCategoryMisc,
|
|
AccessTokenScopeCategoryNotification,
|
|
AccessTokenScopeCategoryOrganization,
|
|
AccessTokenScopeCategoryPackage,
|
|
AccessTokenScopeCategoryIssue,
|
|
AccessTokenScopeCategoryRepository,
|
|
AccessTokenScopeCategoryUser,
|
|
}
|
|
|
|
// AccessTokenScopeLevel represents the access levels without a given scope category
|
|
type AccessTokenScopeLevel int
|
|
|
|
const (
|
|
NoAccess AccessTokenScopeLevel = iota
|
|
Read
|
|
Write
|
|
)
|
|
|
|
// 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,
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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)
|
|
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 == "" || singleScope == "sudo" {
|
|
continue
|
|
}
|
|
if singleScope == AccessTokenScopeAll {
|
|
bitmap |= accessTokenScopeAllBits
|
|
continue
|
|
}
|
|
|
|
bits, ok := allAccessTokenScopeBits[singleScope]
|
|
if !ok {
|
|
return 0, fmt.Errorf("invalid access token scope: %s", singleScope)
|
|
}
|
|
bitmap |= bits
|
|
}
|
|
|
|
return bitmap, nil
|
|
}
|
|
|
|
// StringSlice returns the AccessTokenScope as a []string
|
|
func (s AccessTokenScope) StringSlice() []string {
|
|
return strings.Split(string(s), ",")
|
|
}
|
|
|
|
// Normalize returns a normalized scope string without any duplicates.
|
|
func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
|
|
bitmap, err := s.parse()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return bitmap.toScope(), nil
|
|
}
|
|
|
|
// 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(AccessTokenScopePublicOnly)
|
|
}
|
|
|
|
// HasScope returns true if the string has the given scope
|
|
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)
|
|
}
|
|
|
|
return bitmap&expectedBits == expectedBits, nil
|
|
}
|
|
|
|
// 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
|
|
|
|
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
|
|
}
|