2021-06-06 02:59:27 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2020-04-05 09:20:50 +03:00
2016-12-26 04:16:37 +03:00
package lfs
import (
2022-12-03 05:48:26 +03:00
stdCtx "context"
2024-02-25 16:32:13 +03:00
"crypto/sha256"
2016-12-26 04:16:37 +03:00
"encoding/base64"
2021-08-31 16:35:08 +03:00
"encoding/hex"
2021-06-06 02:59:27 +03:00
"errors"
2016-12-26 04:16:37 +03:00
"fmt"
"io"
"net/http"
2021-11-16 21:18:25 +03:00
"net/url"
2017-11-08 16:04:19 +03:00
"path"
2016-12-26 04:16:37 +03:00
"regexp"
"strconv"
"strings"
2023-04-02 17:43:11 +03:00
actions_model "code.gitea.io/gitea/models/actions"
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"
2024-11-13 11:58:09 +03:00
perm_model "code.gitea.io/gitea/models/perm"
2022-05-11 13:09:36 +03:00
access_model "code.gitea.io/gitea/models/perm/access"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-09 22:57:58 +03:00
"code.gitea.io/gitea/models/unit"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
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"
2016-12-26 04:16:37 +03:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
2021-08-21 21:22:06 +03:00
"code.gitea.io/gitea/modules/storage"
2024-02-27 10:12:22 +03:00
"code.gitea.io/gitea/services/context"
2017-11-08 16:04:19 +03:00
2023-07-19 12:57:10 +03:00
"github.com/golang-jwt/jwt/v5"
2016-12-26 04:16:37 +03:00
)
2021-04-09 01:25:57 +03:00
// requestContext contain variables from the HTTP request.
type requestContext struct {
2016-12-26 04:16:37 +03:00
User string
Repo string
Authorization string
}
2020-03-09 22:56:18 +03:00
// Claims is a JWT Token Claims
type Claims struct {
RepoID int64
Op string
UserID int64
2022-01-21 00:52:56 +03:00
jwt . RegisteredClaims
2020-03-09 22:56:18 +03:00
}
2021-06-06 02:59:27 +03:00
// DownloadLink builds a URL to download the object.
func ( rc * requestContext ) DownloadLink ( p lfs_module . Pointer ) string {
2021-11-16 21:18:25 +03:00
return setting . AppURL + path . Join ( url . PathEscape ( rc . User ) , url . PathEscape ( rc . Repo + ".git" ) , "info/lfs/objects" , url . PathEscape ( p . Oid ) )
2017-11-08 16:04:19 +03:00
}
2021-06-06 02:59:27 +03:00
// UploadLink builds a URL to upload the object.
func ( rc * requestContext ) UploadLink ( p lfs_module . Pointer ) string {
2021-11-16 21:18:25 +03:00
return setting . AppURL + path . Join ( url . PathEscape ( rc . User ) , url . PathEscape ( rc . Repo + ".git" ) , "info/lfs/objects" , url . PathEscape ( p . Oid ) , strconv . FormatInt ( p . Size , 10 ) )
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
// VerifyLink builds a URL for verifying the object.
func ( rc * requestContext ) VerifyLink ( p lfs_module . Pointer ) string {
2021-11-16 21:18:25 +03:00
return setting . AppURL + path . Join ( url . PathEscape ( rc . User ) , url . PathEscape ( rc . Repo + ".git" ) , "info/lfs/verify" )
2018-07-19 18:39:19 +03:00
}
2021-06-06 02:59:27 +03:00
// CheckAcceptMediaType checks if the client accepts the LFS media type.
func CheckAcceptMediaType ( ctx * context . Context ) {
mediaParts := strings . Split ( ctx . Req . Header . Get ( "Accept" ) , ";" )
2019-05-25 00:21:00 +03:00
2021-06-06 02:59:27 +03:00
if mediaParts [ 0 ] != lfs_module . MediaType {
log . Trace ( "Calling a LFS method without accepting the correct media type: %s" , lfs_module . MediaType )
writeStatus ( ctx , http . StatusUnsupportedMediaType )
2016-12-26 04:16:37 +03:00
return
}
}
2024-11-13 11:58:09 +03:00
var rangeHeaderRegexp = regexp . MustCompile ( ` bytes=(\d+)-(\d*).* ` )
2023-06-21 22:57:18 +03:00
2021-06-06 02:59:27 +03:00
// DownloadHandler gets the content from the content store
func DownloadHandler ( ctx * context . Context ) {
rc := getRequestContext ( ctx )
2024-06-19 01:32:45 +03:00
p := lfs_module . Pointer { Oid : ctx . PathParam ( "oid" ) }
2017-10-30 15:11:56 +03:00
2021-06-06 02:59:27 +03:00
meta := getAuthenticatedMeta ( ctx , rc , p , false )
2017-10-30 15:11:56 +03:00
if meta == nil {
2016-12-26 04:16:37 +03:00
return
}
// Support resume download using Range header
2020-05-11 11:37:59 +03:00
var fromByte , toByte int64
toByte = meta . Size - 1
2021-06-06 02:59:27 +03:00
statusCode := http . StatusOK
2016-12-26 04:16:37 +03:00
if rangeHdr := ctx . Req . Header . Get ( "Range" ) ; rangeHdr != "" {
2023-06-21 22:57:18 +03:00
match := rangeHeaderRegexp . FindStringSubmatch ( rangeHdr )
2019-06-12 22:41:28 +03:00
if len ( match ) > 1 {
2021-06-06 02:59:27 +03:00
statusCode = http . StatusPartialContent
2016-12-26 04:16:37 +03:00
fromByte , _ = strconv . ParseInt ( match [ 1 ] , 10 , 32 )
2020-05-11 11:37:59 +03:00
2021-04-06 16:22:34 +03:00
if fromByte >= meta . Size {
writeStatus ( ctx , http . StatusRequestedRangeNotSatisfiable )
return
}
2020-05-11 11:37:59 +03:00
if match [ 2 ] != "" {
_toByte , _ := strconv . ParseInt ( match [ 2 ] , 10 , 32 )
if _toByte >= fromByte && _toByte < toByte {
toByte = _toByte
}
}
ctx . Resp . Header ( ) . Set ( "Content-Range" , fmt . Sprintf ( "bytes %d-%d/%d" , fromByte , toByte , meta . Size - fromByte ) )
2020-08-13 20:18:18 +03:00
ctx . Resp . Header ( ) . Set ( "Access-Control-Expose-Headers" , "Content-Range" )
2016-12-26 04:16:37 +03:00
}
}
2021-04-09 01:25:57 +03:00
contentStore := lfs_module . NewContentStore ( )
content , err := contentStore . Get ( meta . Pointer )
2016-12-26 04:16:37 +03:00
if err != nil {
2021-04-06 16:22:34 +03:00
writeStatus ( ctx , http . StatusNotFound )
2016-12-26 04:16:37 +03:00
return
}
2020-03-09 22:56:18 +03:00
defer content . Close ( )
2016-12-26 04:16:37 +03:00
2021-04-06 16:22:34 +03:00
if fromByte > 0 {
_ , err = content . Seek ( fromByte , io . SeekStart )
if err != nil {
log . Error ( "Whilst trying to read LFS OID[%s]: Unable to seek to %d Error: %v" , meta . Oid , fromByte , err )
writeStatus ( ctx , http . StatusInternalServerError )
return
}
}
2020-05-11 11:37:59 +03:00
contentLength := toByte + 1 - fromByte
ctx . Resp . Header ( ) . Set ( "Content-Length" , strconv . FormatInt ( contentLength , 10 ) )
2016-12-26 04:16:37 +03:00
ctx . Resp . Header ( ) . Set ( "Content-Type" , "application/octet-stream" )
2024-06-19 01:32:45 +03:00
filename := ctx . PathParam ( "filename" )
2016-12-26 04:16:37 +03:00
if len ( filename ) > 0 {
decodedFilename , err := base64 . RawURLEncoding . DecodeString ( filename )
if err == nil {
ctx . Resp . Header ( ) . Set ( "Content-Disposition" , "attachment; filename=\"" + string ( decodedFilename ) + "\"" )
2020-08-13 20:18:18 +03:00
ctx . Resp . Header ( ) . Set ( "Access-Control-Expose-Headers" , "Content-Disposition" )
2016-12-26 04:16:37 +03:00
}
}
ctx . Resp . WriteHeader ( statusCode )
2020-05-11 11:37:59 +03:00
if written , err := io . CopyN ( ctx . Resp , content , contentLength ) ; err != nil {
2020-03-09 22:56:18 +03:00
log . Error ( "Error whilst copying LFS OID[%s] to the response after %d bytes. Error: %v" , meta . Oid , written , err )
}
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
// BatchHandler provides the batch api
func BatchHandler ( ctx * context . Context ) {
var br lfs_module . BatchRequest
if err := decodeJSON ( ctx . Req , & br ) ; err != nil {
log . Trace ( "Unable to decode BATCH request vars: Error: %v" , err )
writeStatus ( ctx , http . StatusBadRequest )
2018-05-01 04:46:04 +03:00
return
}
2021-06-06 02:59:27 +03:00
var isUpload bool
if br . Operation == "upload" {
isUpload = true
} else if br . Operation == "download" {
isUpload = false
} else {
log . Trace ( "Attempt to BATCH with invalid operation: %s" , br . Operation )
writeStatus ( ctx , http . StatusBadRequest )
2018-05-01 04:46:04 +03:00
return
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
rc := getRequestContext ( ctx )
2020-02-28 07:46:57 +03:00
2021-06-06 02:59:27 +03:00
repository := getAuthenticatedRepository ( ctx , rc , isUpload )
if repository == nil {
2016-12-26 04:16:37 +03:00
return
}
2024-10-30 08:41:55 +03:00
if setting . LFS . MaxBatchSize != 0 && len ( br . Objects ) > setting . LFS . MaxBatchSize {
writeStatus ( ctx , http . StatusRequestEntityTooLarge )
return
}
2021-04-09 01:25:57 +03:00
contentStore := lfs_module . NewContentStore ( )
var responseObjects [ ] * lfs_module . ObjectResponse
2016-12-26 04:16:37 +03:00
2021-06-06 02:59:27 +03:00
for _ , p := range br . Objects {
if ! p . IsValid ( ) {
responseObjects = append ( responseObjects , buildObjectResponse ( rc , p , false , false , & lfs_module . ObjectError {
Code : http . StatusUnprocessableEntity ,
Message : "Oid or size are invalid" ,
} ) )
2018-07-19 18:39:19 +03:00
continue
}
2021-06-06 02:59:27 +03:00
exists , err := contentStore . Exists ( p )
2016-12-26 04:16:37 +03:00
if err != nil {
2021-06-06 02:59:27 +03:00
log . Error ( "Unable to check if LFS OID[%s] exist. Error: %v" , p . Oid , rc . User , rc . Repo , err )
writeStatus ( ctx , http . StatusInternalServerError )
2016-12-26 04:16:37 +03:00
return
}
2023-01-09 06:50:54 +03:00
meta , err := git_model . GetLFSMetaObjectByOid ( ctx , repository . ID , p . Oid )
2022-06-12 18:51:54 +03:00
if err != nil && err != git_model . ErrLFSObjectNotExist {
2021-06-06 02:59:27 +03:00
log . Error ( "Unable to get LFS MetaObject [%s] for %s/%s. Error: %v" , p . Oid , rc . User , rc . Repo , err )
writeStatus ( ctx , http . StatusInternalServerError )
2016-12-26 04:16:37 +03:00
return
}
2021-06-06 02:59:27 +03:00
if meta != nil && p . Size != meta . Size {
responseObjects = append ( responseObjects , buildObjectResponse ( rc , p , false , false , & lfs_module . ObjectError {
Code : http . StatusUnprocessableEntity ,
Message : fmt . Sprintf ( "Object %s is not %d bytes" , p . Oid , p . Size ) ,
} ) )
continue
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
var responseObject * lfs_module . ObjectResponse
if isUpload {
var err * lfs_module . ObjectError
if ! exists && setting . LFS . MaxFileSize > 0 && p . Size > setting . LFS . MaxFileSize {
err = & lfs_module . ObjectError {
Code : http . StatusUnprocessableEntity ,
Message : fmt . Sprintf ( "Size must be less than or equal to %d" , setting . LFS . MaxFileSize ) ,
}
}
2020-03-03 23:57:27 +03:00
2021-08-31 16:35:08 +03:00
if exists && meta == nil {
2023-01-09 06:50:54 +03:00
accessible , err := git_model . LFSObjectAccessible ( ctx , ctx . Doer , p . Oid )
2021-08-31 16:35:08 +03:00
if err != nil {
log . Error ( "Unable to check if LFS MetaObject [%s] is accessible. Error: %v" , p . Oid , err )
writeStatus ( ctx , http . StatusInternalServerError )
return
}
if accessible {
2023-12-07 10:27:36 +03:00
_ , err := git_model . NewLFSMetaObject ( ctx , repository . ID , p )
2021-06-06 02:59:27 +03:00
if err != nil {
log . Error ( "Unable to create LFS MetaObject [%s] for %s/%s. Error: %v" , p . Oid , rc . User , rc . Repo , err )
writeStatus ( ctx , http . StatusInternalServerError )
return
}
2021-08-31 16:35:08 +03:00
} else {
exists = false
2021-06-06 02:59:27 +03:00
}
2020-09-08 18:45:10 +03:00
}
2021-06-06 02:59:27 +03:00
responseObject = buildObjectResponse ( rc , p , false , ! exists , err )
2020-03-09 22:56:18 +03:00
} else {
2021-06-06 02:59:27 +03:00
var err * lfs_module . ObjectError
if ! exists || meta == nil {
err = & lfs_module . ObjectError {
Code : http . StatusNotFound ,
Message : http . StatusText ( http . StatusNotFound ) ,
}
}
responseObject = buildObjectResponse ( rc , p , true , false , err )
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
responseObjects = append ( responseObjects , responseObject )
2016-12-26 04:16:37 +03:00
}
2021-04-09 01:25:57 +03:00
respobj := & lfs_module . BatchResponse { Objects : responseObjects }
2016-12-26 04:16:37 +03:00
2021-06-06 02:59:27 +03:00
ctx . Resp . Header ( ) . Set ( "Content-Type" , lfs_module . MediaType )
2021-07-24 19:03:58 +03:00
enc := json . NewEncoder ( ctx . Resp )
2020-03-09 22:56:18 +03:00
if err := enc . Encode ( respobj ) ; err != nil {
log . Error ( "Failed to encode representation as json. Error: %v" , err )
}
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
// UploadHandler receives data from the client and puts it into the content store
func UploadHandler ( ctx * context . Context ) {
rc := getRequestContext ( ctx )
2016-12-26 04:16:37 +03:00
2024-06-19 01:32:45 +03:00
p := lfs_module . Pointer { Oid : ctx . PathParam ( "oid" ) }
2021-06-06 02:59:27 +03:00
var err error
2024-06-19 01:32:45 +03:00
if p . Size , err = strconv . ParseInt ( ctx . PathParam ( "size" ) , 10 , 64 ) ; err != nil {
2021-06-06 02:59:27 +03:00
writeStatusMessage ( ctx , http . StatusUnprocessableEntity , err . Error ( ) )
}
if ! p . IsValid ( ) {
log . Trace ( "Attempt to access invalid LFS OID[%s] in %s/%s" , p . Oid , rc . User , rc . Repo )
writeStatus ( ctx , http . StatusUnprocessableEntity )
return
}
repository := getAuthenticatedRepository ( ctx , rc , true )
if repository == nil {
return
}
2021-04-09 01:25:57 +03:00
contentStore := lfs_module . NewContentStore ( )
2021-06-06 02:59:27 +03:00
exists , err := contentStore . Exists ( p )
if err != nil {
log . Error ( "Unable to check if LFS OID[%s] exist. Error: %v" , p . Oid , err )
writeStatus ( ctx , http . StatusInternalServerError )
return
}
2021-08-31 16:35:08 +03:00
uploadOrVerify := func ( ) error {
if exists {
2023-01-09 06:50:54 +03:00
accessible , err := git_model . LFSObjectAccessible ( ctx , ctx . Doer , p . Oid )
2021-08-31 16:35:08 +03:00
if err != nil {
log . Error ( "Unable to check if LFS MetaObject [%s] is accessible. Error: %v" , p . Oid , err )
return err
}
if ! accessible {
// The file exists but the user has no access to it.
// The upload gets verified by hashing and size comparison to prove access to it.
hash := sha256 . New ( )
written , err := io . Copy ( hash , ctx . Req . Body )
if err != nil {
log . Error ( "Error creating hash. Error: %v" , err )
return err
}
if written != p . Size {
return lfs_module . ErrSizeMismatch
}
if hex . EncodeToString ( hash . Sum ( nil ) ) != p . Oid {
return lfs_module . ErrHashMismatch
}
}
} else if err := contentStore . Put ( p , ctx . Req . Body ) ; err != nil {
log . Error ( "Error putting LFS MetaObject [%s] into content store. Error: %v" , p . Oid , err )
return err
}
2023-12-07 10:27:36 +03:00
_ , err := git_model . NewLFSMetaObject ( ctx , repository . ID , p )
2021-08-31 16:35:08 +03:00
return err
2021-06-06 02:59:27 +03:00
}
2021-01-26 18:36:53 +03:00
defer ctx . Req . Body . Close ( )
2021-08-31 16:35:08 +03:00
if err := uploadOrVerify ( ) ; err != nil {
2021-06-06 02:59:27 +03:00
if errors . Is ( err , lfs_module . ErrSizeMismatch ) || errors . Is ( err , lfs_module . ErrHashMismatch ) {
2021-08-31 16:35:08 +03:00
log . Error ( "Upload does not match LFS MetaObject [%s]. Error: %v" , p . Oid , err )
2021-06-06 02:59:27 +03:00
writeStatusMessage ( ctx , http . StatusUnprocessableEntity , err . Error ( ) )
2020-03-09 22:56:18 +03:00
} else {
2023-04-12 18:01:41 +03:00
log . Error ( "Error whilst uploadOrVerify LFS OID[%s]: %v" , p . Oid , err )
2021-06-06 02:59:27 +03:00
writeStatus ( ctx , http . StatusInternalServerError )
2020-03-09 22:56:18 +03:00
}
2023-01-09 06:50:54 +03:00
if _ , err = git_model . RemoveLFSMetaObjectByOid ( ctx , repository . ID , p . Oid ) ; err != nil {
2023-04-12 18:01:41 +03:00
log . Error ( "Error whilst removing MetaObject for LFS OID[%s]: %v" , p . Oid , err )
2017-10-30 15:11:56 +03:00
}
2016-12-26 04:16:37 +03:00
return
}
2021-06-06 02:59:27 +03:00
writeStatus ( ctx , http . StatusOK )
2016-12-26 04:16:37 +03:00
}
2017-11-08 16:04:19 +03:00
// VerifyHandler verify oid and its size from the content store
func VerifyHandler ( ctx * context . Context ) {
2021-06-06 02:59:27 +03:00
var p lfs_module . Pointer
if err := decodeJSON ( ctx . Req , & p ) ; err != nil {
writeStatus ( ctx , http . StatusUnprocessableEntity )
2017-11-08 16:04:19 +03:00
return
}
2021-06-06 02:59:27 +03:00
rc := getRequestContext ( ctx )
2017-11-08 16:04:19 +03:00
2021-06-06 02:59:27 +03:00
meta := getAuthenticatedMeta ( ctx , rc , p , true )
2017-11-08 16:04:19 +03:00
if meta == nil {
return
}
2021-04-09 01:25:57 +03:00
contentStore := lfs_module . NewContentStore ( )
ok , err := contentStore . Verify ( meta . Pointer )
2021-06-06 02:59:27 +03:00
status := http . StatusOK
2017-11-08 16:04:19 +03:00
if err != nil {
2023-04-12 18:01:41 +03:00
log . Error ( "Error whilst verifying LFS OID[%s]: %v" , p . Oid , err )
2021-06-06 02:59:27 +03:00
status = http . StatusInternalServerError
} else if ! ok {
status = http . StatusNotFound
2017-11-08 16:04:19 +03:00
}
2021-06-06 02:59:27 +03:00
writeStatus ( ctx , status )
}
2023-07-04 21:36:08 +03:00
func decodeJSON ( req * http . Request , v any ) error {
2021-06-06 02:59:27 +03:00
defer req . Body . Close ( )
2017-11-08 16:04:19 +03:00
2021-07-24 19:03:58 +03:00
dec := json . NewDecoder ( req . Body )
2021-06-06 02:59:27 +03:00
return dec . Decode ( v )
2017-11-08 16:04:19 +03:00
}
2021-06-06 02:59:27 +03:00
func getRequestContext ( ctx * context . Context ) * requestContext {
return & requestContext {
2024-06-19 01:32:45 +03:00
User : ctx . PathParam ( "username" ) ,
Repo : strings . TrimSuffix ( ctx . PathParam ( "reponame" ) , ".git" ) ,
2021-06-06 02:59:27 +03:00
Authorization : ctx . Req . Header . Get ( "Authorization" ) ,
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
}
2016-12-26 04:16:37 +03:00
2022-06-12 18:51:54 +03:00
func getAuthenticatedMeta ( ctx * context . Context , rc * requestContext , p lfs_module . Pointer , requireWrite bool ) * git_model . LFSMetaObject {
2021-06-06 02:59:27 +03:00
if ! p . IsValid ( ) {
log . Info ( "Attempt to access invalid LFS OID[%s] in %s/%s" , p . Oid , rc . User , rc . Repo )
writeStatusMessage ( ctx , http . StatusUnprocessableEntity , "Oid or size are invalid" )
return nil
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
repository := getAuthenticatedRepository ( ctx , rc , requireWrite )
if repository == nil {
return nil
2016-12-26 04:16:37 +03:00
}
2023-01-09 06:50:54 +03:00
meta , err := git_model . GetLFSMetaObjectByOid ( ctx , repository . ID , p . Oid )
2021-06-06 02:59:27 +03:00
if err != nil {
log . Error ( "Unable to get LFS OID[%s] Error: %v" , p . Oid , err )
writeStatus ( ctx , http . StatusNotFound )
return nil
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
return meta
}
2019-05-25 00:21:00 +03:00
2021-12-10 04:27:50 +03:00
func getAuthenticatedRepository ( ctx * context . Context , rc * requestContext , requireWrite bool ) * repo_model . Repository {
2022-12-03 05:48:26 +03:00
repository , err := repo_model . GetRepositoryByOwnerAndName ( ctx , rc . User , rc . Repo )
2021-06-06 02:59:27 +03:00
if err != nil {
log . Error ( "Unable to get repository: %s/%s Error: %v" , rc . User , rc . Repo , err )
writeStatus ( ctx , http . StatusNotFound )
return nil
}
2019-05-25 00:21:00 +03:00
2021-06-06 02:59:27 +03:00
if ! authenticate ( ctx , repository , rc . Authorization , false , requireWrite ) {
requireAuth ( ctx )
return nil
2017-11-08 16:04:19 +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
if requireWrite {
context . CheckRepoScopedToken ( ctx , repository , auth_model . Write )
} else {
context . CheckRepoScopedToken ( ctx , repository , auth_model . Read )
}
2023-04-27 03:24:03 +03:00
if ctx . Written ( ) {
return nil
}
2021-06-06 02:59:27 +03:00
return repository
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
func buildObjectResponse ( rc * requestContext , pointer lfs_module . Pointer , download , upload bool , err * lfs_module . ObjectError ) * lfs_module . ObjectResponse {
rep := & lfs_module . ObjectResponse { Pointer : pointer }
if err != nil {
rep . Error = err
} else {
rep . Actions = make ( map [ string ] * lfs_module . Link )
2016-12-26 04:16:37 +03:00
2021-06-06 02:59:27 +03:00
header := make ( map [ string ] string )
2016-12-26 04:16:37 +03:00
2021-06-06 02:59:27 +03:00
if len ( rc . Authorization ) > 0 {
header [ "Authorization" ] = rc . Authorization
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
if download {
2022-10-22 16:36:44 +03:00
var link * lfs_module . Link
2024-05-30 10:33:50 +03:00
if setting . LFS . Storage . ServeDirect ( ) {
2022-01-20 20:46:10 +03:00
// If we have a signed url (S3, object storage), redirect to this directly.
2024-10-31 18:28:25 +03:00
u , err := storage . LFS . URL ( pointer . RelativePath ( ) , pointer . Oid , nil )
2021-08-21 21:22:06 +03:00
if u != nil && err == nil {
2022-10-22 16:36:44 +03:00
// Presigned url does not need the Authorization header
// https://github.com/go-gitea/gitea/issues/21525
delete ( header , "Authorization" )
link = & lfs_module . Link { Href : u . String ( ) , Header : header }
2021-08-21 21:22:06 +03:00
}
}
2022-10-22 16:36:44 +03:00
if link == nil {
link = & lfs_module . Link { Href : rc . DownloadLink ( pointer ) , Header : header }
}
rep . Actions [ "download" ] = link
2021-06-06 02:59:27 +03:00
}
if upload {
rep . Actions [ "upload" ] = & lfs_module . Link { Href : rc . UploadLink ( pointer ) , Header : header }
2016-12-26 04:16:37 +03:00
2021-06-06 02:59:27 +03:00
verifyHeader := make ( map [ string ] string )
for key , value := range header {
verifyHeader [ key ] = value
}
2016-12-26 04:16:37 +03:00
2021-06-06 02:59:27 +03:00
// This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
2024-06-12 01:22:28 +03:00
verifyHeader [ "Accept" ] = lfs_module . AcceptHeader
2016-12-26 04:16:37 +03:00
2021-06-06 02:59:27 +03:00
rep . Actions [ "verify" ] = & lfs_module . Link { Href : rc . VerifyLink ( pointer ) , Header : verifyHeader }
}
2016-12-26 04:16:37 +03:00
}
2021-06-06 02:59:27 +03:00
return rep
2016-12-26 04:16:37 +03:00
}
func writeStatus ( ctx * context . Context , status int ) {
2021-06-06 02:59:27 +03:00
writeStatusMessage ( ctx , status , http . StatusText ( status ) )
}
2016-12-26 04:16:37 +03:00
2021-06-06 02:59:27 +03:00
func writeStatusMessage ( ctx * context . Context , status int , message string ) {
ctx . Resp . Header ( ) . Set ( "Content-Type" , lfs_module . MediaType )
2016-12-26 04:16:37 +03:00
ctx . Resp . WriteHeader ( status )
2021-06-06 02:59:27 +03:00
er := lfs_module . ErrorResponse { Message : message }
2021-07-24 19:03:58 +03:00
enc := json . NewEncoder ( ctx . Resp )
2021-06-06 02:59:27 +03:00
if err := enc . Encode ( er ) ; err != nil {
log . Error ( "Failed to encode error response as json. Error: %v" , err )
}
2016-12-26 04:16:37 +03:00
}
// authenticate uses the authorization string to determine whether
2024-11-13 11:58:09 +03:00
// to proceed. This server assumes an HTTP Basic auth format.
2021-12-10 04:27:50 +03:00
func authenticate ( ctx * context . Context , repository * repo_model . Repository , authorization string , requireSigned , requireWrite bool ) bool {
2024-11-13 11:58:09 +03:00
accessMode := perm_model . AccessModeRead
2016-12-26 04:16:37 +03:00
if requireWrite {
2024-11-13 11:58:09 +03:00
accessMode = perm_model . AccessModeWrite
2016-12-26 04:16:37 +03:00
}
2023-04-02 17:43:11 +03:00
if ctx . Data [ "IsActionsToken" ] == true {
taskID := ctx . Data [ "ActionsTaskID" ] . ( int64 )
task , err := actions_model . GetTaskByID ( ctx , taskID )
if err != nil {
log . Error ( "Unable to GetTaskByID for task[%d] Error: %v" , taskID , err )
return false
}
if task . RepoID != repository . ID {
return false
}
if task . IsForkPullRequest {
2024-11-13 11:58:09 +03:00
return accessMode <= perm_model . AccessModeRead
2023-04-02 17:43:11 +03:00
}
2024-11-13 11:58:09 +03:00
return accessMode <= perm_model . AccessModeWrite
2023-04-02 17:43:11 +03:00
}
2019-01-31 16:36:57 +03:00
// ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess
2022-05-11 13:09:36 +03:00
perm , err := access_model . GetUserRepoPermission ( ctx , repository , ctx . Doer )
2018-11-28 14:26:14 +03:00
if err != nil {
2023-04-02 17:43:11 +03:00
log . Error ( "Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v" , ctx . Doer , repository , err )
2018-11-28 14:26:14 +03:00
return false
2016-12-26 04:16:37 +03:00
}
2019-01-31 16:36:57 +03:00
2021-11-09 22:57:58 +03:00
canRead := perm . CanAccess ( accessMode , unit . TypeCode )
2021-05-15 18:32:09 +03:00
if canRead && ( ! requireSigned || ctx . IsSigned ) {
2019-01-31 16:36:57 +03:00
return true
2016-12-26 04:16:37 +03:00
}
2022-12-03 05:48:26 +03:00
user , err := parseToken ( ctx , authorization , repository , accessMode )
2016-12-26 04:16:37 +03:00
if err != nil {
2020-03-09 22:56:18 +03:00
// Most of these are Warn level - the true internal server errors are logged in parseToken already
log . Warn ( "Authentication failure for provided token with Error: %v" , err )
2016-12-26 04:16:37 +03:00
return false
}
2022-03-22 10:03:22 +03:00
ctx . Doer = user
2021-05-15 18:32:09 +03:00
return true
}
2024-11-13 11:58:09 +03:00
func handleLFSToken ( ctx stdCtx . Context , tokenSHA string , target * repo_model . Repository , mode perm_model . AccessMode ) ( * user_model . User , error ) {
2021-05-15 18:32:09 +03:00
if ! strings . Contains ( tokenSHA , "." ) {
return nil , nil
2016-12-26 04:16:37 +03:00
}
2023-07-04 21:36:08 +03:00
token , err := jwt . ParseWithClaims ( tokenSHA , & Claims { } , func ( t * jwt . Token ) ( any , error ) {
2021-05-15 18:32:09 +03:00
if _ , ok := t . Method . ( * jwt . SigningMethodHMAC ) ; ! ok {
return nil , fmt . Errorf ( "unexpected signing method: %v" , t . Header [ "alg" ] )
2018-01-27 19:48:15 +03:00
}
2021-05-15 18:32:09 +03:00
return setting . LFS . JWTSecretBytes , nil
} )
if err != nil {
return nil , nil
2016-12-26 04:16:37 +03:00
}
2021-05-15 18:32:09 +03:00
claims , claimsOk := token . Claims . ( * Claims )
if ! token . Valid || ! claimsOk {
return nil , fmt . Errorf ( "invalid token claim" )
2018-01-27 19:48:15 +03:00
}
2021-05-15 18:32:09 +03:00
if claims . RepoID != target . ID {
return nil , fmt . Errorf ( "invalid token claim" )
2016-12-26 04:16:37 +03:00
}
2024-11-13 11:58:09 +03:00
if mode == perm_model . AccessModeWrite && claims . Op != "upload" {
2021-05-15 18:32:09 +03:00
return nil , fmt . Errorf ( "invalid token claim" )
2016-12-26 04:16:37 +03:00
}
2022-12-03 05:48:26 +03:00
u , err := user_model . GetUserByID ( ctx , claims . UserID )
2021-05-15 18:32:09 +03:00
if err != nil {
log . Error ( "Unable to GetUserById[%d]: Error: %v" , claims . UserID , err )
return nil , err
}
return u , nil
}
2024-11-13 11:58:09 +03:00
func parseToken ( ctx stdCtx . Context , authorization string , target * repo_model . Repository , mode perm_model . AccessMode ) ( * user_model . User , error ) {
2021-05-15 18:32:09 +03:00
if authorization == "" {
return nil , fmt . Errorf ( "no token" )
}
parts := strings . SplitN ( authorization , " " , 2 )
if len ( parts ) != 2 {
return nil , fmt . Errorf ( "no token" )
}
tokenSHA := parts [ 1 ]
switch strings . ToLower ( parts [ 0 ] ) {
case "bearer" :
fallthrough
case "token" :
2022-12-03 05:48:26 +03:00
return handleLFSToken ( ctx , tokenSHA , target , mode )
2021-05-15 18:32:09 +03:00
}
return nil , fmt . Errorf ( "token not found" )
2016-12-26 04:16:37 +03:00
}
func requireAuth ( ctx * context . Context ) {
2024-11-13 11:58:09 +03:00
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="gitea-lfs" ` )
2021-06-06 02:59:27 +03:00
writeStatus ( ctx , http . StatusUnauthorized )
2016-12-26 04:16:37 +03:00
}