2022-03-30 11:42:47 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2022-03-30 11:42:47 +03:00
2022-09-02 22:18:23 +03:00
package integration
2022-03-30 11:42:47 +03:00
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"testing"
2023-04-27 03:24:03 +03:00
auth_model "code.gitea.io/gitea/models/auth"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/packages/npm"
"code.gitea.io/gitea/modules/setting"
2022-09-02 22:18:23 +03:00
"code.gitea.io/gitea/tests"
2022-03-30 11:42:47 +03:00
"github.com/stretchr/testify/assert"
)
func TestPackageNpm ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrepareTestEnv ( t ) ( )
2023-02-23 17:11:56 +03:00
2022-08-16 05:22:25 +03:00
user := unittest . AssertExistsAndLoadBean ( t , & user_model . User { ID : 2 } )
2022-03-30 11:42:47 +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
token := fmt . Sprintf ( "Bearer %s" , getTokenForLoggedInUser ( t , loginUser ( t , user . Name ) , auth_model . AccessTokenScopeWritePackage ) )
2022-03-30 11:42:47 +03:00
packageName := "@scope/test-package"
packageVersion := "1.0.1-pre"
packageTag := "latest"
packageTag2 := "release"
packageAuthor := "KN4CK3R"
packageDescription := "Test Description"
2022-10-08 08:24:44 +03:00
packageBinName := "cli"
packageBinPath := "./cli.sh"
2023-03-17 21:39:19 +03:00
repoType := "gitea"
repoURL := "http://localhost:3000/gitea/test.git"
2022-03-30 11:42:47 +03:00
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
2022-08-09 10:23:43 +03:00
buildUpload := func ( version string ) string {
return ` {
"_id" : "` + packageName + `" ,
2022-03-30 11:42:47 +03:00
"name" : "` + packageName + `" ,
"description" : "` + packageDescription + `" ,
2022-08-09 10:23:43 +03:00
"dist-tags" : {
"` + packageTag + `" : "` + version + `"
} ,
"versions" : {
"` + version + `" : {
"name" : "` + packageName + `" ,
"version" : "` + version + `" ,
"description" : "` + packageDescription + `" ,
"author" : {
"name" : "` + packageAuthor + `"
} ,
2022-10-08 08:24:44 +03:00
"bin" : {
"` + packageBinName + `" : "` + packageBinPath + `"
} ,
2022-08-09 10:23:43 +03:00
"dist" : {
"integrity" : "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==" ,
"shasum" : "aaa7eaf852a948b0aa05afeda35b1badca155d90"
2023-03-17 21:39:19 +03:00
} ,
"repository" : {
"type" : "` + repoType + `" ,
"url" : "` + repoURL + `"
2022-08-09 10:23:43 +03:00
}
}
2022-03-30 11:42:47 +03:00
} ,
2022-08-09 10:23:43 +03:00
"_attachments" : {
"` + packageName + `-` + version + `.tgz" : {
"data" : "` + data + `"
}
2022-03-30 11:42:47 +03:00
}
2022-08-09 10:23:43 +03:00
} `
}
2022-03-30 11:42:47 +03:00
root := fmt . Sprintf ( "/api/packages/%s/npm/%s" , user . Name , url . QueryEscape ( packageName ) )
tagsRoot := fmt . Sprintf ( "/api/packages/%s/npm/-/package/%s/dist-tags" , user . Name , url . QueryEscape ( packageName ) )
filename := fmt . Sprintf ( "%s-%s.tgz" , strings . Split ( packageName , "/" ) [ 1 ] , packageVersion )
t . Run ( "Upload" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-03-30 11:42:47 +03:00
2023-12-22 02:59:59 +03:00
req := NewRequestWithBody ( t , "PUT" , root , strings . NewReader ( buildUpload ( packageVersion ) ) ) .
AddTokenAuth ( token )
2022-03-30 11:42:47 +03:00
MakeRequest ( t , req , http . StatusCreated )
pvs , err := packages . GetVersionsByPackageType ( db . DefaultContext , user . ID , packages . TypeNpm )
assert . NoError ( t , err )
assert . Len ( t , pvs , 1 )
pd , err := packages . GetPackageDescriptor ( db . DefaultContext , pvs [ 0 ] )
assert . NoError ( t , err )
assert . NotNil ( t , pd . SemVer )
assert . IsType ( t , & npm . Metadata { } , pd . Metadata )
assert . Equal ( t , packageName , pd . Package . Name )
assert . Equal ( t , packageVersion , pd . Version . Version )
2022-07-28 06:59:39 +03:00
assert . Len ( t , pd . VersionProperties , 1 )
assert . Equal ( t , npm . TagProperty , pd . VersionProperties [ 0 ] . Name )
assert . Equal ( t , packageTag , pd . VersionProperties [ 0 ] . Value )
2022-03-30 11:42:47 +03:00
pfs , err := packages . GetFilesByVersionID ( db . DefaultContext , pvs [ 0 ] . ID )
assert . NoError ( t , err )
assert . Len ( t , pfs , 1 )
assert . Equal ( t , filename , pfs [ 0 ] . Name )
assert . True ( t , pfs [ 0 ] . IsLead )
pb , err := packages . GetBlobByID ( db . DefaultContext , pfs [ 0 ] . BlobID )
assert . NoError ( t , err )
assert . Equal ( t , int64 ( 192 ) , pb . Size )
} )
t . Run ( "UploadExists" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-03-30 11:42:47 +03:00
2023-12-22 02:59:59 +03:00
req := NewRequestWithBody ( t , "PUT" , root , strings . NewReader ( buildUpload ( packageVersion ) ) ) .
AddTokenAuth ( token )
2023-10-10 16:39:58 +03:00
MakeRequest ( t , req , http . StatusConflict )
2022-03-30 11:42:47 +03:00
} )
t . Run ( "Download" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-03-30 11:42:47 +03:00
2023-12-22 02:59:59 +03:00
req := NewRequest ( t , "GET" , fmt . Sprintf ( "%s/-/%s/%s" , root , packageVersion , filename ) ) .
AddTokenAuth ( token )
2022-03-30 11:42:47 +03:00
resp := MakeRequest ( t , req , http . StatusOK )
b , _ := base64 . StdEncoding . DecodeString ( data )
assert . Equal ( t , b , resp . Body . Bytes ( ) )
2023-12-22 02:59:59 +03:00
req = NewRequest ( t , "GET" , fmt . Sprintf ( "%s/-/%s" , root , filename ) ) .
AddTokenAuth ( token )
2022-10-24 16:50:22 +03:00
resp = MakeRequest ( t , req , http . StatusOK )
assert . Equal ( t , b , resp . Body . Bytes ( ) )
2022-03-30 11:42:47 +03:00
pvs , err := packages . GetVersionsByPackageType ( db . DefaultContext , user . ID , packages . TypeNpm )
assert . NoError ( t , err )
assert . Len ( t , pvs , 1 )
2022-10-24 16:50:22 +03:00
assert . Equal ( t , int64 ( 2 ) , pvs [ 0 ] . DownloadCount )
2022-03-30 11:42:47 +03:00
} )
t . Run ( "PackageMetadata" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-03-30 11:42:47 +03:00
2023-12-22 02:59:59 +03:00
req := NewRequest ( t , "GET" , fmt . Sprintf ( "/api/packages/%s/npm/%s" , user . Name , "does-not-exist" ) ) .
AddTokenAuth ( token )
2022-03-30 11:42:47 +03:00
MakeRequest ( t , req , http . StatusNotFound )
2023-12-22 02:59:59 +03:00
req = NewRequest ( t , "GET" , root ) .
AddTokenAuth ( token )
2022-03-30 11:42:47 +03:00
resp := MakeRequest ( t , req , http . StatusOK )
var result npm . PackageMetadata
DecodeJSON ( t , resp , & result )
assert . Equal ( t , packageName , result . ID )
assert . Equal ( t , packageName , result . Name )
assert . Equal ( t , packageDescription , result . Description )
assert . Contains ( t , result . DistTags , packageTag )
assert . Equal ( t , packageVersion , result . DistTags [ packageTag ] )
assert . Equal ( t , packageAuthor , result . Author . Name )
assert . Contains ( t , result . Versions , packageVersion )
pmv := result . Versions [ packageVersion ]
assert . Equal ( t , fmt . Sprintf ( "%s@%s" , packageName , packageVersion ) , pmv . ID )
assert . Equal ( t , packageName , pmv . Name )
assert . Equal ( t , packageDescription , pmv . Description )
assert . Equal ( t , packageAuthor , pmv . Author . Name )
2022-10-08 08:24:44 +03:00
assert . Equal ( t , packageBinPath , pmv . Bin [ packageBinName ] )
2022-03-30 11:42:47 +03:00
assert . Equal ( t , "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==" , pmv . Dist . Integrity )
assert . Equal ( t , "aaa7eaf852a948b0aa05afeda35b1badca155d90" , pmv . Dist . Shasum )
assert . Equal ( t , fmt . Sprintf ( "%s%s/-/%s/%s" , setting . AppURL , root [ 1 : ] , packageVersion , filename ) , pmv . Dist . Tarball )
2023-03-17 21:39:19 +03:00
assert . Equal ( t , repoType , result . Repository . Type )
assert . Equal ( t , repoURL , result . Repository . URL )
2022-03-30 11:42:47 +03:00
} )
t . Run ( "AddTag" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-03-30 11:42:47 +03:00
test := func ( t * testing . T , status int , tag , version string ) {
2023-12-22 02:59:59 +03:00
req := NewRequestWithBody ( t , "PUT" , fmt . Sprintf ( "%s/%s" , tagsRoot , tag ) , strings . NewReader ( ` " ` + version + ` " ` ) ) .
AddTokenAuth ( token )
2022-03-30 11:42:47 +03:00
MakeRequest ( t , req , status )
}
test ( t , http . StatusBadRequest , "1.0" , packageVersion )
test ( t , http . StatusBadRequest , "v1.0" , packageVersion )
test ( t , http . StatusNotFound , packageTag2 , "1.2" )
test ( t , http . StatusOK , packageTag , packageVersion )
test ( t , http . StatusOK , packageTag2 , packageVersion )
} )
t . Run ( "ListTags" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-03-30 11:42:47 +03:00
2023-12-22 02:59:59 +03:00
req := NewRequest ( t , "GET" , tagsRoot ) .
AddTokenAuth ( token )
2022-03-30 11:42:47 +03:00
resp := MakeRequest ( t , req , http . StatusOK )
var result map [ string ] string
DecodeJSON ( t , resp , & result )
assert . Len ( t , result , 2 )
assert . Contains ( t , result , packageTag )
assert . Equal ( t , packageVersion , result [ packageTag ] )
assert . Contains ( t , result , packageTag2 )
assert . Equal ( t , packageVersion , result [ packageTag2 ] )
} )
t . Run ( "PackageMetadataDistTags" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-03-30 11:42:47 +03:00
2023-12-22 02:59:59 +03:00
req := NewRequest ( t , "GET" , root ) .
AddTokenAuth ( token )
2022-03-30 11:42:47 +03:00
resp := MakeRequest ( t , req , http . StatusOK )
var result npm . PackageMetadata
DecodeJSON ( t , resp , & result )
assert . Len ( t , result . DistTags , 2 )
assert . Contains ( t , result . DistTags , packageTag )
assert . Equal ( t , packageVersion , result . DistTags [ packageTag ] )
assert . Contains ( t , result . DistTags , packageTag2 )
assert . Equal ( t , packageVersion , result . DistTags [ packageTag2 ] )
} )
t . Run ( "DeleteTag" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-03-30 11:42:47 +03:00
test := func ( t * testing . T , status int , tag string ) {
2023-12-22 02:59:59 +03:00
req := NewRequest ( t , "DELETE" , fmt . Sprintf ( "%s/%s" , tagsRoot , tag ) ) .
AddTokenAuth ( token )
2022-03-30 11:42:47 +03:00
MakeRequest ( t , req , status )
}
test ( t , http . StatusBadRequest , "v1.0" )
test ( t , http . StatusBadRequest , "1.0" )
test ( t , http . StatusOK , "dummy" )
test ( t , http . StatusOK , packageTag2 )
} )
2022-08-09 10:23:43 +03:00
2022-09-24 14:24:33 +03:00
t . Run ( "Search" , func ( t * testing . T ) {
defer tests . PrintCurrentTest ( t ) ( )
url := fmt . Sprintf ( "/api/packages/%s/npm/-/v1/search" , user . Name )
cases := [ ] struct {
Query string
Skip int
Take int
ExpectedTotal int64
ExpectedResults int
} {
{ "" , 0 , 0 , 1 , 1 } ,
{ "" , 0 , 10 , 1 , 1 } ,
{ "gitea" , 0 , 10 , 0 , 0 } ,
{ "test" , 0 , 10 , 1 , 1 } ,
{ "test" , 1 , 10 , 1 , 0 } ,
}
for i , c := range cases {
req := NewRequest ( t , "GET" , fmt . Sprintf ( "%s?text=%s&from=%d&size=%d" , url , c . Query , c . Skip , c . Take ) )
resp := MakeRequest ( t , req , http . StatusOK )
var result npm . PackageSearch
DecodeJSON ( t , resp , & result )
assert . Equal ( t , c . ExpectedTotal , result . Total , "case %d: unexpected total hits" , i )
assert . Len ( t , result . Objects , c . ExpectedResults , "case %d: unexpected result count" , i )
}
} )
2022-08-09 10:23:43 +03:00
t . Run ( "Delete" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-08-09 10:23:43 +03:00
2023-12-22 02:59:59 +03:00
req := NewRequestWithBody ( t , "PUT" , root , strings . NewReader ( buildUpload ( packageVersion + "-dummy" ) ) ) .
AddTokenAuth ( token )
2022-08-09 10:23:43 +03:00
MakeRequest ( t , req , http . StatusCreated )
req = NewRequest ( t , "PUT" , root + "/-rev/dummy" )
MakeRequest ( t , req , http . StatusUnauthorized )
2023-12-22 02:59:59 +03:00
req = NewRequest ( t , "PUT" , root + "/-rev/dummy" ) .
AddTokenAuth ( token )
2022-08-09 10:23:43 +03:00
MakeRequest ( t , req , http . StatusOK )
t . Run ( "Version" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-08-09 10:23:43 +03:00
pvs , err := packages . GetVersionsByPackageType ( db . DefaultContext , user . ID , packages . TypeNpm )
assert . NoError ( t , err )
assert . Len ( t , pvs , 2 )
req := NewRequest ( t , "DELETE" , fmt . Sprintf ( "%s/-/%s/%s/-rev/dummy" , root , packageVersion , filename ) )
MakeRequest ( t , req , http . StatusUnauthorized )
2023-12-22 02:59:59 +03:00
req = NewRequest ( t , "DELETE" , fmt . Sprintf ( "%s/-/%s/%s/-rev/dummy" , root , packageVersion , filename ) ) .
AddTokenAuth ( token )
2022-08-09 10:23:43 +03:00
MakeRequest ( t , req , http . StatusOK )
pvs , err = packages . GetVersionsByPackageType ( db . DefaultContext , user . ID , packages . TypeNpm )
assert . NoError ( t , err )
assert . Len ( t , pvs , 1 )
} )
t . Run ( "Full" , func ( t * testing . T ) {
2022-09-02 22:18:23 +03:00
defer tests . PrintCurrentTest ( t ) ( )
2022-08-09 10:23:43 +03:00
pvs , err := packages . GetVersionsByPackageType ( db . DefaultContext , user . ID , packages . TypeNpm )
assert . NoError ( t , err )
assert . Len ( t , pvs , 1 )
req := NewRequest ( t , "DELETE" , root + "/-rev/dummy" )
MakeRequest ( t , req , http . StatusUnauthorized )
2023-12-22 02:59:59 +03:00
req = NewRequest ( t , "DELETE" , root + "/-rev/dummy" ) .
AddTokenAuth ( token )
2022-08-09 10:23:43 +03:00
MakeRequest ( t , req , http . StatusOK )
pvs , err = packages . GetVersionsByPackageType ( db . DefaultContext , user . ID , packages . TypeNpm )
assert . NoError ( t , err )
assert . Len ( t , pvs , 0 )
} )
} )
2022-03-30 11:42:47 +03:00
}