Files
fff32e9469 refactor: prepare to decouple the "model migration" package and "models" package (#38533)
Migrations should never use model structs directly, because the model
structs can be different in different releases. e.g. if one migration uses
"User" model, it works in the early releases, then one day, when the
User model changes, the migration breaks because it will use the
new (incorrect) User model, it should only use the old User model.

The same to "modules/structs".

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
2026-07-20 03:42:02 +00:00

360 lines
15 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_20
import (
"fmt"
"strings"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/log"
)
// 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 base.EngineMigration) 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.SplitSeq(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
}