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> pirms 1 gada |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- // 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
- }
|