2014-11-12 14:48:50 +03:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2019-05-04 18:45:34 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2014-11-12 14:48:50 +03:00
2022-08-25 05:31:57 +03:00
package auth
2014-11-12 14:48:50 +03:00
import (
2023-09-15 09:13:19 +03:00
"context"
2019-05-04 18:45:34 +03:00
"crypto/subtle"
2022-11-28 18:37:42 +03:00
"encoding/hex"
2021-09-19 14:49:59 +03:00
"fmt"
2014-11-12 14:48:50 +03:00
"time"
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
2019-08-15 17:46:21 +03:00
"code.gitea.io/gitea/modules/timeutil"
2021-05-10 09:45:17 +03:00
"code.gitea.io/gitea/modules/util"
2019-08-15 17:46:21 +03:00
2023-07-14 06:00:31 +03:00
lru "github.com/hashicorp/golang-lru/v2"
2023-11-24 06:49:41 +03:00
"xorm.io/builder"
2014-11-12 14:48:50 +03:00
)
2022-08-25 05:31:57 +03:00
// ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error.
type ErrAccessTokenNotExist struct {
Token string
}
// IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist.
func IsErrAccessTokenNotExist ( err error ) bool {
_ , ok := err . ( ErrAccessTokenNotExist )
return ok
}
func ( err ErrAccessTokenNotExist ) Error ( ) string {
return fmt . Sprintf ( "access token does not exist [sha: %s]" , err . Token )
}
2022-10-18 08:50:37 +03:00
func ( err ErrAccessTokenNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2022-08-25 05:31:57 +03:00
// ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error.
type ErrAccessTokenEmpty struct { }
// IsErrAccessTokenEmpty checks if an error is a ErrAccessTokenEmpty.
func IsErrAccessTokenEmpty ( err error ) bool {
_ , ok := err . ( ErrAccessTokenEmpty )
return ok
}
func ( err ErrAccessTokenEmpty ) Error ( ) string {
return "access token is empty"
}
2022-10-18 08:50:37 +03:00
func ( err ErrAccessTokenEmpty ) Unwrap ( ) error {
return util . ErrInvalidArgument
}
2023-07-14 06:00:31 +03:00
var successfulAccessTokenCache * lru . Cache [ string , any ]
2021-08-17 21:30:42 +03:00
2014-11-12 14:48:50 +03:00
// AccessToken represents a personal access token.
type AccessToken struct {
2019-05-04 18:45:34 +03:00
ID int64 ` xorm:"pk autoincr" `
UID int64 ` xorm:"INDEX" `
Name string
Token string ` xorm:"-" `
TokenHash string ` xorm:"UNIQUE" ` // sha256 of token
TokenSalt string
2022-11-24 05:49:41 +03:00
TokenLastEight string ` xorm:"INDEX token_last_eight" `
2023-01-18 00:46:03 +03:00
Scope AccessTokenScope
2016-03-10 03:53:30 +03:00
2019-08-15 17:46:21 +03:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
HasRecentActivity bool ` xorm:"-" `
HasUsed bool ` xorm:"-" `
2014-11-12 14:48:50 +03:00
}
2017-10-01 19:52:35 +03:00
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func ( t * AccessToken ) AfterLoad ( ) {
2017-12-11 07:37:04 +03:00
t . HasUsed = t . UpdatedUnix > t . CreatedUnix
2019-08-15 17:46:21 +03:00
t . HasRecentActivity = t . UpdatedUnix . AddDuration ( 7 * 24 * time . Hour ) > timeutil . TimeStampNow ( )
2016-03-10 03:53:30 +03:00
}
2021-09-19 14:49:59 +03:00
func init ( ) {
db . RegisterModel ( new ( AccessToken ) , func ( ) error {
if setting . SuccessfulTokensCacheSize > 0 {
var err error
2023-07-14 06:00:31 +03:00
successfulAccessTokenCache , err = lru . New [ string , any ] ( setting . SuccessfulTokensCacheSize )
2021-09-19 14:49:59 +03:00
if err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "unable to allocate AccessToken cache: %w" , err )
2021-09-19 14:49:59 +03:00
}
} else {
successfulAccessTokenCache = nil
}
return nil
} )
}
2014-11-12 14:48:50 +03:00
// NewAccessToken creates new access token.
2023-09-15 09:13:19 +03:00
func NewAccessToken ( ctx context . Context , t * AccessToken ) error {
2022-01-26 07:10:10 +03:00
salt , err := util . CryptoRandomString ( 10 )
2019-05-04 18:45:34 +03:00
if err != nil {
return err
}
2022-11-28 18:37:42 +03:00
token , err := util . CryptoRandomBytes ( 20 )
if err != nil {
return err
}
2019-05-04 18:45:34 +03:00
t . TokenSalt = salt
2022-11-28 18:37:42 +03:00
t . Token = hex . EncodeToString ( token )
2022-08-25 05:31:57 +03:00
t . TokenHash = HashToken ( t . Token , t . TokenSalt )
2019-05-04 18:45:34 +03:00
t . TokenLastEight = t . Token [ len ( t . Token ) - 8 : ]
2023-09-15 09:13:19 +03:00
_ , err = db . GetEngine ( ctx ) . Insert ( t )
2014-11-12 14:48:50 +03:00
return err
}
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>
2023-06-04 21:57:16 +03:00
// 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
}
2021-08-17 21:30:42 +03:00
func getAccessTokenIDFromCache ( token string ) int64 {
if successfulAccessTokenCache == nil {
return 0
}
tInterface , ok := successfulAccessTokenCache . Get ( token )
if ! ok {
return 0
}
t , ok := tInterface . ( int64 )
if ! ok {
return 0
}
return t
}
2019-05-04 18:45:34 +03:00
// GetAccessTokenBySHA returns access token by given token value
2023-09-15 09:13:19 +03:00
func GetAccessTokenBySHA ( ctx context . Context , token string ) ( * AccessToken , error ) {
2019-05-04 18:45:34 +03:00
if token == "" {
2016-06-27 12:02:39 +03:00
return nil , ErrAccessTokenEmpty { }
}
2021-06-16 01:29:25 +03:00
// A token is defined as being SHA1 sum these are 40 hexadecimal bytes long
if len ( token ) != 40 {
2019-05-04 18:45:34 +03:00
return nil , ErrAccessTokenNotExist { token }
}
2021-06-16 01:29:25 +03:00
for _ , x := range [ ] byte ( token ) {
if x < '0' || ( x > '9' && x < 'a' ) || x > 'f' {
return nil , ErrAccessTokenNotExist { token }
}
}
2021-08-17 21:30:42 +03:00
2019-05-04 18:45:34 +03:00
lastEight := token [ len ( token ) - 8 : ]
2021-08-17 21:30:42 +03:00
if id := getAccessTokenIDFromCache ( token ) ; id > 0 {
2023-07-14 06:00:31 +03:00
accessToken := & AccessToken {
2021-08-17 21:30:42 +03:00
TokenLastEight : lastEight ,
}
// Re-get the token from the db in case it has been deleted in the intervening period
2023-09-15 09:13:19 +03:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( accessToken )
2021-08-17 21:30:42 +03:00
if err != nil {
return nil , err
}
if has {
2023-07-14 06:00:31 +03:00
return accessToken , nil
2021-08-17 21:30:42 +03:00
}
successfulAccessTokenCache . Remove ( token )
}
var tokens [ ] AccessToken
2023-09-15 09:13:19 +03:00
err := db . GetEngine ( ctx ) . Table ( & AccessToken { } ) . Where ( "token_last_eight = ?" , lastEight ) . Find ( & tokens )
2014-11-12 14:48:50 +03:00
if err != nil {
return nil , err
2019-05-04 18:45:34 +03:00
} else if len ( tokens ) == 0 {
return nil , ErrAccessTokenNotExist { token }
}
2021-08-17 21:30:42 +03:00
2019-05-04 18:45:34 +03:00
for _ , t := range tokens {
2022-08-25 05:31:57 +03:00
tempHash := HashToken ( token , t . TokenSalt )
2019-05-04 18:45:34 +03:00
if subtle . ConstantTimeCompare ( [ ] byte ( t . TokenHash ) , [ ] byte ( tempHash ) ) == 1 {
2021-08-17 21:30:42 +03:00
if successfulAccessTokenCache != nil {
successfulAccessTokenCache . Add ( token , t . ID )
}
2019-05-04 18:45:34 +03:00
return & t , nil
}
2014-11-12 14:48:50 +03:00
}
2019-05-04 18:45:34 +03:00
return nil , ErrAccessTokenNotExist { token }
2014-11-12 14:48:50 +03:00
}
2020-04-13 22:02:48 +03:00
// AccessTokenByNameExists checks if a token name has been used already by a user.
2023-09-15 09:13:19 +03:00
func AccessTokenByNameExists ( ctx context . Context , token * AccessToken ) ( bool , error ) {
return db . GetEngine ( ctx ) . Table ( "access_token" ) . Where ( "name = ?" , token . Name ) . And ( "uid = ?" , token . UID ) . Exist ( )
2020-04-13 22:02:48 +03:00
}
2020-08-28 11:09:33 +03:00
// ListAccessTokensOptions contain filter options
type ListAccessTokensOptions struct {
2021-09-24 14:32:56 +03:00
db . ListOptions
2020-08-28 11:09:33 +03:00
Name string
UserID int64
}
2023-11-24 06:49:41 +03:00
func ( opts ListAccessTokensOptions ) ToConds ( ) builder . Cond {
cond := builder . NewCond ( )
// user id is required, otherwise it will return all result which maybe a possible bug
cond = cond . And ( builder . Eq { "uid" : opts . UserID } )
if len ( opts . Name ) > 0 {
cond = cond . And ( builder . Eq { "name" : opts . Name } )
2020-01-24 22:00:29 +03:00
}
2023-11-24 06:49:41 +03:00
return cond
}
2020-01-24 22:00:29 +03:00
2023-11-24 06:49:41 +03:00
func ( opts ListAccessTokensOptions ) ToOrders ( ) string {
return "created_unix DESC"
2014-11-12 14:48:50 +03:00
}
2016-01-06 22:41:42 +03:00
// UpdateAccessToken updates information of access token.
2023-09-15 09:13:19 +03:00
func UpdateAccessToken ( ctx context . Context , t * AccessToken ) error {
_ , err := db . GetEngine ( ctx ) . ID ( t . ID ) . AllCols ( ) . Update ( t )
2015-08-19 01:22:33 +03:00
return err
}
2015-08-18 22:36:16 +03:00
// DeleteAccessTokenByID deletes access token by given ID.
2023-09-15 09:13:19 +03:00
func DeleteAccessTokenByID ( ctx context . Context , id , userID int64 ) error {
cnt , err := db . GetEngine ( ctx ) . ID ( id ) . Delete ( & AccessToken {
2016-12-15 11:49:06 +03:00
UID : userID ,
} )
if err != nil {
return err
} else if cnt != 1 {
return ErrAccessTokenNotExist { }
}
return nil
2014-11-12 14:48:50 +03:00
}