2017-11-28 23:58:37 +03:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2017-11-28 23:58:37 +03:00
package lfs
import (
2021-04-05 18:30:52 +03:00
"net/http"
2017-11-28 23:58:37 +03:00
"strconv"
2017-11-29 02:35:23 +03:00
"strings"
2017-11-28 23:58:37 +03:00
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
auth_model "code.gitea.io/gitea/models/auth"
2022-06-12 18:51:54 +03:00
git_model "code.gitea.io/gitea/models/git"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-07-24 19:03:58 +03:00
"code.gitea.io/gitea/modules/json"
2021-04-09 01:25:57 +03:00
lfs_module "code.gitea.io/gitea/modules/lfs"
2019-05-28 13:32:41 +03:00
"code.gitea.io/gitea/modules/log"
2017-11-28 23:58:37 +03:00
"code.gitea.io/gitea/modules/setting"
2019-05-11 13:21:34 +03:00
api "code.gitea.io/gitea/modules/structs"
2024-02-27 10:12:22 +03:00
"code.gitea.io/gitea/services/context"
2022-12-29 05:57:15 +03:00
"code.gitea.io/gitea/services/convert"
2017-11-28 23:58:37 +03:00
)
2022-06-12 18:51:54 +03:00
func handleLockListOut ( ctx * context . Context , repo * repo_model . Repository , lock * git_model . LFSLock , err error ) {
2017-11-28 23:58:37 +03:00
if err != nil {
2022-06-12 18:51:54 +03:00
if git_model . IsErrLFSLockNotExist ( err ) {
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , api . LFSLockList {
2017-11-28 23:58:37 +03:00
Locks : [ ] * api . LFSLock { } ,
} )
return
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusInternalServerError , api . LFSLockError {
2020-03-09 22:56:18 +03:00
Message : "unable to list locks : Internal Server Error" ,
2017-11-28 23:58:37 +03:00
} )
return
}
2019-05-28 13:32:41 +03:00
if repo . ID != lock . RepoID {
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , api . LFSLockList {
2017-11-28 23:58:37 +03:00
Locks : [ ] * api . LFSLock { } ,
} )
return
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , api . LFSLockList {
2022-12-03 05:48:26 +03:00
Locks : [ ] * api . LFSLock { convert . ToLFSLock ( ctx , lock ) } ,
2017-11-28 23:58:37 +03:00
} )
}
// GetListLockHandler list locks
func GetListLockHandler ( ctx * context . Context ) {
2021-06-06 02:59:27 +03:00
rv := getRequestContext ( ctx )
2019-05-28 13:32:41 +03:00
2022-12-03 05:48:26 +03:00
repository , err := repo_model . GetRepositoryByOwnerAndName ( ctx , rv . User , rv . Repo )
2017-11-28 23:58:37 +03:00
if err != nil {
2019-05-28 13:32:41 +03:00
log . Debug ( "Could not find repository: %s/%s - %s" , rv . User , rv . Repo , err )
2021-05-15 18:32:09 +03:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2022-03-23 07:54:07 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2021-05-15 18:32:09 +03:00
Message : "You must have pull access to list locks" ,
} )
2019-05-28 13:32:41 +03:00
return
}
2022-11-19 11:12:33 +03:00
repository . MustOwner ( ctx )
2019-05-28 13:32:41 +03:00
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
context . CheckRepoScopedToken ( ctx , repository , auth_model . Read )
2023-04-27 03:24:03 +03:00
if ctx . Written ( ) {
return
}
2021-05-15 18:32:09 +03:00
authenticated := authenticate ( ctx , repository , rv . Authorization , true , false )
2019-05-28 13:32:41 +03:00
if ! authenticated {
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2019-05-28 13:32:41 +03:00
Message : "You must have pull access to list locks" ,
2017-11-28 23:58:37 +03:00
} )
return
}
2021-05-15 18:32:09 +03:00
ctx . Resp . Header ( ) . Set ( "Content-Type" , lfs_module . MediaType )
2020-03-09 22:56:18 +03:00
2021-07-29 04:42:15 +03:00
cursor := ctx . FormInt ( "cursor" )
2020-03-09 22:56:18 +03:00
if cursor < 0 {
cursor = 0
}
2021-07-29 04:42:15 +03:00
limit := ctx . FormInt ( "limit" )
2020-03-09 22:56:18 +03:00
if limit > setting . LFS . LocksPagingNum && setting . LFS . LocksPagingNum > 0 {
limit = setting . LFS . LocksPagingNum
} else if limit < 0 {
limit = 0
}
2021-08-11 03:31:13 +03:00
id := ctx . FormString ( "id" )
2022-01-20 20:46:10 +03:00
if id != "" { // Case where we request a specific id
2017-11-28 23:58:37 +03:00
v , err := strconv . ParseInt ( id , 10 , 64 )
if err != nil {
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusBadRequest , api . LFSLockError {
2017-11-28 23:58:37 +03:00
Message : "bad request : " + err . Error ( ) ,
} )
return
}
2022-06-12 18:51:54 +03:00
lock , err := git_model . GetLFSLockByID ( ctx , v )
if err != nil && ! git_model . IsErrLFSLockNotExist ( err ) {
2020-03-09 22:56:18 +03:00
log . Error ( "Unable to get lock with ID[%s]: Error: %v" , v , err )
}
2019-05-28 13:32:41 +03:00
handleLockListOut ( ctx , repository , lock , err )
2017-11-28 23:58:37 +03:00
return
}
2021-08-11 03:31:13 +03:00
path := ctx . FormString ( "path" )
2022-01-20 20:46:10 +03:00
if path != "" { // Case where we request a specific id
2022-06-12 18:51:54 +03:00
lock , err := git_model . GetLFSLock ( ctx , repository , path )
if err != nil && ! git_model . IsErrLFSLockNotExist ( err ) {
2020-03-09 22:56:18 +03:00
log . Error ( "Unable to get lock for repository %-v with path %s: Error: %v" , repository , path , err )
}
2019-05-28 13:32:41 +03:00
handleLockListOut ( ctx , repository , lock , err )
2017-11-28 23:58:37 +03:00
return
}
2022-01-20 20:46:10 +03:00
// If no query params path or id
2023-01-09 06:50:54 +03:00
lockList , err := git_model . GetLFSLockByRepoID ( ctx , repository . ID , cursor , limit )
2017-11-28 23:58:37 +03:00
if err != nil {
2020-03-09 22:56:18 +03:00
log . Error ( "Unable to list locks for repository ID[%d]: Error: %v" , repository . ID , err )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusInternalServerError , api . LFSLockError {
2020-03-09 22:56:18 +03:00
Message : "unable to list locks : Internal Server Error" ,
2017-11-28 23:58:37 +03:00
} )
return
}
lockListAPI := make ( [ ] * api . LFSLock , len ( lockList ) )
2020-03-09 22:56:18 +03:00
next := ""
2017-11-28 23:58:37 +03:00
for i , l := range lockList {
2022-12-03 05:48:26 +03:00
lockListAPI [ i ] = convert . ToLFSLock ( ctx , l )
2017-11-28 23:58:37 +03:00
}
2020-03-09 22:56:18 +03:00
if limit > 0 && len ( lockList ) == limit {
next = strconv . Itoa ( cursor + 1 )
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , api . LFSLockList {
2017-11-28 23:58:37 +03:00
Locks : lockListAPI ,
2020-03-09 22:56:18 +03:00
Next : next ,
2017-11-28 23:58:37 +03:00
} )
}
// PostLockHandler create lock
func PostLockHandler ( ctx * context . Context ) {
2019-05-28 13:32:41 +03:00
userName := ctx . Params ( "username" )
repoName := strings . TrimSuffix ( ctx . Params ( "reponame" ) , ".git" )
authorization := ctx . Req . Header . Get ( "Authorization" )
2022-12-03 05:48:26 +03:00
repository , err := repo_model . GetRepositoryByOwnerAndName ( ctx , userName , repoName )
2019-05-28 13:32:41 +03:00
if err != nil {
2020-03-09 22:56:18 +03:00
log . Error ( "Unable to get repository: %s/%s Error: %v" , userName , repoName , err )
2021-05-15 18:32:09 +03:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2022-03-23 07:54:07 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2021-05-15 18:32:09 +03:00
Message : "You must have push access to create locks" ,
} )
2019-05-28 13:32:41 +03:00
return
}
2022-11-19 11:12:33 +03:00
repository . MustOwner ( ctx )
2019-05-28 13:32:41 +03:00
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
context . CheckRepoScopedToken ( ctx , repository , auth_model . Write )
2023-04-27 03:24:03 +03:00
if ctx . Written ( ) {
return
}
2021-05-15 18:32:09 +03:00
authenticated := authenticate ( ctx , repository , authorization , true , true )
2019-05-28 13:32:41 +03:00
if ! authenticated {
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2019-05-28 13:32:41 +03:00
Message : "You must have push access to create locks" ,
} )
return
}
2021-05-15 18:32:09 +03:00
ctx . Resp . Header ( ) . Set ( "Content-Type" , lfs_module . MediaType )
2017-11-28 23:58:37 +03:00
var req api . LFSLockRequest
2021-01-26 18:36:53 +03:00
bodyReader := ctx . Req . Body
2019-10-10 20:42:28 +03:00
defer bodyReader . Close ( )
2021-07-24 19:03:58 +03:00
2019-10-10 20:42:28 +03:00
dec := json . NewDecoder ( bodyReader )
2019-05-28 13:32:41 +03:00
if err := dec . Decode ( & req ) ; err != nil {
2020-03-09 22:56:18 +03:00
log . Warn ( "Failed to decode lock request as json. Error: %v" , err )
2022-03-23 07:54:07 +03:00
writeStatus ( ctx , http . StatusBadRequest )
2017-11-28 23:58:37 +03:00
return
}
2023-01-09 06:50:54 +03:00
lock , err := git_model . CreateLFSLock ( ctx , repository , & git_model . LFSLock {
2021-11-24 12:49:20 +03:00
Path : req . Path ,
2022-03-22 10:03:22 +03:00
OwnerID : ctx . Doer . ID ,
2017-11-28 23:58:37 +03:00
} )
if err != nil {
2022-06-12 18:51:54 +03:00
if git_model . IsErrLFSLockAlreadyExist ( err ) {
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusConflict , api . LFSLockError {
2022-12-03 05:48:26 +03:00
Lock : convert . ToLFSLock ( ctx , lock ) ,
2017-11-28 23:58:37 +03:00
Message : "already created lock" ,
} )
return
}
2022-06-12 18:51:54 +03:00
if git_model . IsErrLFSUnauthorizedAction ( err ) {
2018-01-27 19:48:15 +03:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2017-11-28 23:58:37 +03:00
Message : "You must have push access to create locks : " + err . Error ( ) ,
} )
return
}
2022-03-22 10:03:22 +03:00
log . Error ( "Unable to CreateLFSLock in repository %-v at %s for user %-v: Error: %v" , repository , req . Path , ctx . Doer , err )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusInternalServerError , api . LFSLockError {
2020-03-09 22:56:18 +03:00
Message : "internal server error : Internal Server Error" ,
2017-11-28 23:58:37 +03:00
} )
return
}
2022-12-03 05:48:26 +03:00
ctx . JSON ( http . StatusCreated , api . LFSLockResponse { Lock : convert . ToLFSLock ( ctx , lock ) } )
2017-11-28 23:58:37 +03:00
}
// VerifyLockHandler list locks for verification
func VerifyLockHandler ( ctx * context . Context ) {
2019-05-28 13:32:41 +03:00
userName := ctx . Params ( "username" )
repoName := strings . TrimSuffix ( ctx . Params ( "reponame" ) , ".git" )
authorization := ctx . Req . Header . Get ( "Authorization" )
2022-12-03 05:48:26 +03:00
repository , err := repo_model . GetRepositoryByOwnerAndName ( ctx , userName , repoName )
2017-11-28 23:58:37 +03:00
if err != nil {
2020-03-09 22:56:18 +03:00
log . Error ( "Unable to get repository: %s/%s Error: %v" , userName , repoName , err )
2021-05-15 18:32:09 +03:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2022-03-23 07:54:07 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2021-05-15 18:32:09 +03:00
Message : "You must have push access to verify locks" ,
} )
2019-05-28 13:32:41 +03:00
return
}
2022-11-19 11:12:33 +03:00
repository . MustOwner ( ctx )
2019-05-28 13:32:41 +03:00
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
context . CheckRepoScopedToken ( ctx , repository , auth_model . Read )
2023-04-27 03:24:03 +03:00
if ctx . Written ( ) {
return
}
2021-05-15 18:32:09 +03:00
authenticated := authenticate ( ctx , repository , authorization , true , true )
2019-05-28 13:32:41 +03:00
if ! authenticated {
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2019-05-28 13:32:41 +03:00
Message : "You must have push access to verify locks" ,
2017-11-28 23:58:37 +03:00
} )
return
}
2021-05-15 18:32:09 +03:00
ctx . Resp . Header ( ) . Set ( "Content-Type" , lfs_module . MediaType )
2021-07-29 04:42:15 +03:00
cursor := ctx . FormInt ( "cursor" )
2020-03-09 22:56:18 +03:00
if cursor < 0 {
cursor = 0
}
2021-07-29 04:42:15 +03:00
limit := ctx . FormInt ( "limit" )
2020-03-09 22:56:18 +03:00
if limit > setting . LFS . LocksPagingNum && setting . LFS . LocksPagingNum > 0 {
limit = setting . LFS . LocksPagingNum
} else if limit < 0 {
limit = 0
}
2023-01-09 06:50:54 +03:00
lockList , err := git_model . GetLFSLockByRepoID ( ctx , repository . ID , cursor , limit )
2017-11-28 23:58:37 +03:00
if err != nil {
2020-03-09 22:56:18 +03:00
log . Error ( "Unable to list locks for repository ID[%d]: Error: %v" , repository . ID , err )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusInternalServerError , api . LFSLockError {
2020-03-09 22:56:18 +03:00
Message : "unable to list locks : Internal Server Error" ,
2017-11-28 23:58:37 +03:00
} )
return
}
2020-03-09 22:56:18 +03:00
next := ""
if limit > 0 && len ( lockList ) == limit {
next = strconv . Itoa ( cursor + 1 )
}
2017-11-28 23:58:37 +03:00
lockOursListAPI := make ( [ ] * api . LFSLock , 0 , len ( lockList ) )
lockTheirsListAPI := make ( [ ] * api . LFSLock , 0 , len ( lockList ) )
for _ , l := range lockList {
2022-03-22 10:03:22 +03:00
if l . OwnerID == ctx . Doer . ID {
2022-12-03 05:48:26 +03:00
lockOursListAPI = append ( lockOursListAPI , convert . ToLFSLock ( ctx , l ) )
2017-11-28 23:58:37 +03:00
} else {
2022-12-03 05:48:26 +03:00
lockTheirsListAPI = append ( lockTheirsListAPI , convert . ToLFSLock ( ctx , l ) )
2017-11-28 23:58:37 +03:00
}
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , api . LFSLockListVerify {
2017-11-28 23:58:37 +03:00
Ours : lockOursListAPI ,
Theirs : lockTheirsListAPI ,
2020-03-09 22:56:18 +03:00
Next : next ,
2017-11-28 23:58:37 +03:00
} )
}
// UnLockHandler delete locks
func UnLockHandler ( ctx * context . Context ) {
2019-05-28 13:32:41 +03:00
userName := ctx . Params ( "username" )
repoName := strings . TrimSuffix ( ctx . Params ( "reponame" ) , ".git" )
authorization := ctx . Req . Header . Get ( "Authorization" )
2022-12-03 05:48:26 +03:00
repository , err := repo_model . GetRepositoryByOwnerAndName ( ctx , userName , repoName )
2019-05-28 13:32:41 +03:00
if err != nil {
2020-03-09 22:56:18 +03:00
log . Error ( "Unable to get repository: %s/%s Error: %v" , userName , repoName , err )
2021-05-15 18:32:09 +03:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2022-03-23 07:54:07 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2021-05-15 18:32:09 +03:00
Message : "You must have push access to delete locks" ,
} )
2019-05-28 13:32:41 +03:00
return
}
2022-11-19 11:12:33 +03:00
repository . MustOwner ( ctx )
2019-05-28 13:32:41 +03:00
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
context . CheckRepoScopedToken ( ctx , repository , auth_model . Write )
2023-04-27 03:24:03 +03:00
if ctx . Written ( ) {
return
}
2021-05-15 18:32:09 +03:00
authenticated := authenticate ( ctx , repository , authorization , true , true )
2019-05-28 13:32:41 +03:00
if ! authenticated {
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2019-05-28 13:32:41 +03:00
Message : "You must have push access to delete locks" ,
} )
return
}
2021-05-15 18:32:09 +03:00
ctx . Resp . Header ( ) . Set ( "Content-Type" , lfs_module . MediaType )
2017-11-28 23:58:37 +03:00
var req api . LFSLockDeleteRequest
2021-01-26 18:36:53 +03:00
bodyReader := ctx . Req . Body
2019-10-10 20:42:28 +03:00
defer bodyReader . Close ( )
2021-07-24 19:03:58 +03:00
2019-10-10 20:42:28 +03:00
dec := json . NewDecoder ( bodyReader )
2019-05-28 13:32:41 +03:00
if err := dec . Decode ( & req ) ; err != nil {
2020-03-09 22:56:18 +03:00
log . Warn ( "Failed to decode lock request as json. Error: %v" , err )
2022-03-23 07:54:07 +03:00
writeStatus ( ctx , http . StatusBadRequest )
2017-11-28 23:58:37 +03:00
return
}
2023-01-09 06:50:54 +03:00
lock , err := git_model . DeleteLFSLockByID ( ctx , ctx . ParamsInt64 ( "lid" ) , repository , ctx . Doer , req . Force )
2017-11-28 23:58:37 +03:00
if err != nil {
2022-06-12 18:51:54 +03:00
if git_model . IsErrLFSUnauthorizedAction ( err ) {
2018-01-27 19:48:15 +03:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=gitea-lfs" )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusUnauthorized , api . LFSLockError {
2017-11-28 23:58:37 +03:00
Message : "You must have push access to delete locks : " + err . Error ( ) ,
} )
return
}
2022-03-22 10:03:22 +03:00
log . Error ( "Unable to DeleteLFSLockByID[%d] by user %-v with force %t: Error: %v" , ctx . ParamsInt64 ( "lid" ) , ctx . Doer , req . Force , err )
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusInternalServerError , api . LFSLockError {
2020-03-09 22:56:18 +03:00
Message : "unable to delete lock : Internal Server Error" ,
2017-11-28 23:58:37 +03:00
} )
return
}
2022-12-03 05:48:26 +03:00
ctx . JSON ( http . StatusOK , api . LFSLockResponse { Lock : convert . ToLFSLock ( ctx , lock ) } )
2017-11-28 23:58:37 +03:00
}