2022-03-30 11:42:47 +03:00
// Copyright 2022 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
package packages
import (
"net/http"
"regexp"
"strings"
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/perm"
refactor auth interface to return error when verify failure (#22119)
This PR changed the Auth interface signature from
`Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess
SessionStore) *user_model.User`
to
`Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess
SessionStore) (*user_model.User, error)`.
There is a new return argument `error` which means the verification
condition matched but verify process failed, we should stop the auth
process.
Before this PR, when return a `nil` user, we don't know the reason why
it returned `nil`. If the match condition is not satisfied or it
verified failure? For these two different results, we should have
different handler. If the match condition is not satisfied, we should
try next auth method and if there is no more auth method, it's an
anonymous user. If the condition matched but verify failed, the auth
process should be stop and return immediately.
This will fix #20563
Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
Co-authored-by: Jason Song <i@wolfogre.com>
2022-12-28 08:53:28 +03:00
"code.gitea.io/gitea/modules/log"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
2023-05-12 20:27:50 +03:00
"code.gitea.io/gitea/routers/api/packages/alpine"
2023-02-05 13:12:31 +03:00
"code.gitea.io/gitea/routers/api/packages/cargo"
2023-02-06 04:49:21 +03:00
"code.gitea.io/gitea/routers/api/packages/chef"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/routers/api/packages/composer"
"code.gitea.io/gitea/routers/api/packages/conan"
2023-02-01 21:30:39 +03:00
"code.gitea.io/gitea/routers/api/packages/conda"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/routers/api/packages/container"
2023-05-22 05:57:49 +03:00
"code.gitea.io/gitea/routers/api/packages/cran"
2023-05-02 19:31:35 +03:00
"code.gitea.io/gitea/routers/api/packages/debian"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/routers/api/packages/generic"
2023-05-14 18:38:40 +03:00
"code.gitea.io/gitea/routers/api/packages/goproxy"
2022-04-19 19:55:35 +03:00
"code.gitea.io/gitea/routers/api/packages/helm"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/routers/api/packages/maven"
"code.gitea.io/gitea/routers/api/packages/npm"
"code.gitea.io/gitea/routers/api/packages/nuget"
2022-08-07 13:09:54 +03:00
"code.gitea.io/gitea/routers/api/packages/pub"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/routers/api/packages/pypi"
2023-05-05 23:33:37 +03:00
"code.gitea.io/gitea/routers/api/packages/rpm"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/routers/api/packages/rubygems"
2023-03-13 23:28:39 +03:00
"code.gitea.io/gitea/routers/api/packages/swift"
2022-08-29 10:04:45 +03:00
"code.gitea.io/gitea/routers/api/packages/vagrant"
2022-03-30 11:42:47 +03:00
"code.gitea.io/gitea/services/auth"
2024-02-27 10:12:22 +03:00
"code.gitea.io/gitea/services/context"
2022-03-30 11:42:47 +03:00
)
func reqPackageAccess ( accessMode perm . AccessMode ) func ( ctx * context . Context ) {
return func ( ctx * context . Context ) {
2023-04-27 03:24:03 +03:00
if ctx . Data [ "IsApiToken" ] == true {
scope , ok := ctx . Data [ "ApiTokenScope" ] . ( auth_model . AccessTokenScope )
if ok { // it's a personal access token but not oauth2 token
scopeMatched := false
var err error
if accessMode == perm . AccessModeRead {
scopeMatched , err = scope . HasScope ( auth_model . AccessTokenScopeReadPackage )
if err != nil {
ctx . Error ( http . StatusInternalServerError , "HasScope" , err . Error ( ) )
return
}
} else if accessMode == perm . AccessModeWrite {
scopeMatched , err = scope . HasScope ( auth_model . AccessTokenScopeWritePackage )
if err != nil {
ctx . Error ( http . StatusInternalServerError , "HasScope" , err . Error ( ) )
return
}
}
if ! scopeMatched {
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="Gitea Package API" ` )
ctx . Error ( http . StatusUnauthorized , "reqPackageAccess" , "user should have specific permission or be a site admin" )
return
}
2024-10-08 12:51:09 +03:00
// check if scope only applies to public resources
publicOnly , err := scope . PublicOnly ( )
if err != nil {
ctx . Error ( http . StatusForbidden , "tokenRequiresScope" , "parsing public resource scope failed: " + err . Error ( ) )
return
}
if publicOnly {
if ctx . Package != nil && ctx . Package . Owner . Visibility . IsPrivate ( ) {
ctx . Error ( http . StatusForbidden , "reqToken" , "token scope is limited to public packages" )
return
}
}
2023-04-27 03:24:03 +03:00
}
}
2022-03-30 11:42:47 +03:00
if ctx . Package . AccessMode < accessMode && ! ctx . IsUserSiteAdmin ( ) {
ctx . Resp . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="Gitea Package API" ` )
ctx . Error ( http . StatusUnauthorized , "reqPackageAccess" , "user should have specific permission or be a site admin" )
return
}
}
}
2024-06-19 01:32:45 +03:00
func verifyAuth ( r * web . Router , authMethods [ ] auth . Method ) {
2022-03-30 11:42:47 +03:00
if setting . Service . EnableReverseProxyAuth {
authMethods = append ( authMethods , & auth . ReverseProxy { } )
}
authGroup := auth . NewGroup ( authMethods ... )
2023-04-10 10:21:03 +03:00
2022-03-30 11:42:47 +03:00
r . Use ( func ( ctx * context . Context ) {
refactor auth interface to return error when verify failure (#22119)
This PR changed the Auth interface signature from
`Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess
SessionStore) *user_model.User`
to
`Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess
SessionStore) (*user_model.User, error)`.
There is a new return argument `error` which means the verification
condition matched but verify process failed, we should stop the auth
process.
Before this PR, when return a `nil` user, we don't know the reason why
it returned `nil`. If the match condition is not satisfied or it
verified failure? For these two different results, we should have
different handler. If the match condition is not satisfied, we should
try next auth method and if there is no more auth method, it's an
anonymous user. If the condition matched but verify failed, the auth
process should be stop and return immediately.
This will fix #20563
Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
Co-authored-by: Jason Song <i@wolfogre.com>
2022-12-28 08:53:28 +03:00
var err error
ctx . Doer , err = authGroup . Verify ( ctx . Req , ctx . Resp , ctx , ctx . Session )
if err != nil {
2023-04-10 10:21:03 +03:00
log . Error ( "Failed to verify user: %v" , err )
refactor auth interface to return error when verify failure (#22119)
This PR changed the Auth interface signature from
`Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess
SessionStore) *user_model.User`
to
`Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess
SessionStore) (*user_model.User, error)`.
There is a new return argument `error` which means the verification
condition matched but verify process failed, we should stop the auth
process.
Before this PR, when return a `nil` user, we don't know the reason why
it returned `nil`. If the match condition is not satisfied or it
verified failure? For these two different results, we should have
different handler. If the match condition is not satisfied, we should
try next auth method and if there is no more auth method, it's an
anonymous user. If the condition matched but verify failed, the auth
process should be stop and return immediately.
This will fix #20563
Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
Co-authored-by: Jason Song <i@wolfogre.com>
2022-12-28 08:53:28 +03:00
ctx . Error ( http . StatusUnauthorized , "authGroup.Verify" )
return
}
2022-10-24 22:23:25 +03:00
ctx . IsSigned = ctx . Doer != nil
2022-03-30 11:42:47 +03:00
} )
2023-04-10 10:21:03 +03:00
}
// CommonRoutes provide endpoints for most package managers (except containers - see below)
// These are mounted on `/api/packages` (not `/api/v1/packages`)
2024-06-19 01:32:45 +03:00
func CommonRoutes ( ) * web . Router {
r := web . NewRouter ( )
2023-04-10 10:21:03 +03:00
2023-05-21 04:50:53 +03:00
r . Use ( context . PackageContexter ( ) )
2023-04-10 10:21:03 +03:00
verifyAuth ( r , [ ] auth . Method {
& auth . OAuth2 { } ,
& auth . Basic { } ,
& nuget . Auth { } ,
& conan . Auth { } ,
& chef . Auth { } ,
} )
2022-03-30 11:42:47 +03:00
r . Group ( "/{username}" , func ( ) {
2023-05-12 20:27:50 +03:00
r . Group ( "/alpine" , func ( ) {
r . Get ( "/key" , alpine . GetRepositoryKey )
r . Group ( "/{branch}/{repository}" , func ( ) {
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , alpine . UploadPackageFile )
r . Group ( "/{architecture}" , func ( ) {
r . Get ( "/APKINDEX.tar.gz" , alpine . GetRepositoryFile )
r . Group ( "/{filename}" , func ( ) {
r . Get ( "" , alpine . DownloadPackageFile )
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , alpine . DeletePackageFile )
} )
} )
} )
} , reqPackageAccess ( perm . AccessModeRead ) )
2023-02-05 13:12:31 +03:00
r . Group ( "/cargo" , func ( ) {
r . Group ( "/api/v1/crates" , func ( ) {
r . Get ( "" , cargo . SearchPackages )
r . Put ( "/new" , reqPackageAccess ( perm . AccessModeWrite ) , cargo . UploadPackage )
r . Group ( "/{package}" , func ( ) {
r . Group ( "/{version}" , func ( ) {
r . Get ( "/download" , cargo . DownloadPackageFile )
r . Delete ( "/yank" , reqPackageAccess ( perm . AccessModeWrite ) , cargo . YankPackage )
r . Put ( "/unyank" , reqPackageAccess ( perm . AccessModeWrite ) , cargo . UnyankPackage )
} )
r . Get ( "/owners" , cargo . ListOwners )
} )
} )
2023-05-03 23:58:43 +03:00
r . Get ( "/config.json" , cargo . RepositoryConfig )
r . Get ( "/1/{package}" , cargo . EnumeratePackageVersions )
r . Get ( "/2/{package}" , cargo . EnumeratePackageVersions )
// Use dummy placeholders because these parts are not of interest
r . Get ( "/3/{_}/{package}" , cargo . EnumeratePackageVersions )
r . Get ( "/{_}/{__}/{package}" , cargo . EnumeratePackageVersions )
2023-02-05 13:12:31 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2023-02-06 04:49:21 +03:00
r . Group ( "/chef" , func ( ) {
r . Group ( "/api/v1" , func ( ) {
r . Get ( "/universe" , chef . PackagesUniverse )
r . Get ( "/search" , chef . EnumeratePackages )
r . Group ( "/cookbooks" , func ( ) {
r . Get ( "" , chef . EnumeratePackages )
r . Post ( "" , reqPackageAccess ( perm . AccessModeWrite ) , chef . UploadPackage )
r . Group ( "/{name}" , func ( ) {
r . Get ( "" , chef . PackageMetadata )
r . Group ( "/versions/{version}" , func ( ) {
r . Get ( "" , chef . PackageVersionMetadata )
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , chef . DeletePackageVersion )
r . Get ( "/download" , chef . DownloadPackage )
} )
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , chef . DeletePackage )
} )
} )
} )
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
r . Group ( "/composer" , func ( ) {
r . Get ( "/packages.json" , composer . ServiceIndex )
r . Get ( "/search.json" , composer . SearchPackages )
r . Get ( "/list.json" , composer . EnumeratePackages )
r . Get ( "/p2/{vendorname}/{projectname}~dev.json" , composer . PackageMetadata )
r . Get ( "/p2/{vendorname}/{projectname}.json" , composer . PackageMetadata )
r . Get ( "/files/{package}/{version}/{filename}" , composer . DownloadPackageFile )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , composer . UploadPackage )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
r . Group ( "/conan" , func ( ) {
r . Group ( "/v1" , func ( ) {
r . Get ( "/ping" , conan . Ping )
r . Group ( "/users" , func ( ) {
r . Get ( "/authenticate" , conan . Authenticate )
r . Get ( "/check_credentials" , conan . CheckCredentials )
} )
r . Group ( "/conans" , func ( ) {
r . Get ( "/search" , conan . SearchRecipes )
r . Group ( "/{name}/{version}/{user}/{channel}" , func ( ) {
r . Get ( "" , conan . RecipeSnapshot )
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . DeleteRecipeV1 )
r . Get ( "/search" , conan . SearchPackagesV1 )
r . Get ( "/digest" , conan . RecipeDownloadURLs )
r . Post ( "/upload_urls" , reqPackageAccess ( perm . AccessModeWrite ) , conan . RecipeUploadURLs )
r . Get ( "/download_urls" , conan . RecipeDownloadURLs )
r . Group ( "/packages" , func ( ) {
r . Post ( "/delete" , reqPackageAccess ( perm . AccessModeWrite ) , conan . DeletePackageV1 )
r . Group ( "/{package_reference}" , func ( ) {
r . Get ( "" , conan . PackageSnapshot )
r . Get ( "/digest" , conan . PackageDownloadURLs )
r . Post ( "/upload_urls" , reqPackageAccess ( perm . AccessModeWrite ) , conan . PackageUploadURLs )
r . Get ( "/download_urls" , conan . PackageDownloadURLs )
} )
} )
} , conan . ExtractPathParameters )
} )
r . Group ( "/files/{name}/{version}/{user}/{channel}/{recipe_revision}" , func ( ) {
r . Group ( "/recipe/{filename}" , func ( ) {
r . Get ( "" , conan . DownloadRecipeFile )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . UploadRecipeFile )
} )
r . Group ( "/package/{package_reference}/{package_revision}/{filename}" , func ( ) {
r . Get ( "" , conan . DownloadPackageFile )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . UploadPackageFile )
} )
} , conan . ExtractPathParameters )
} )
r . Group ( "/v2" , func ( ) {
r . Get ( "/ping" , conan . Ping )
r . Group ( "/users" , func ( ) {
r . Get ( "/authenticate" , conan . Authenticate )
r . Get ( "/check_credentials" , conan . CheckCredentials )
} )
r . Group ( "/conans" , func ( ) {
r . Get ( "/search" , conan . SearchRecipes )
r . Group ( "/{name}/{version}/{user}/{channel}" , func ( ) {
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . DeleteRecipeV2 )
r . Get ( "/search" , conan . SearchPackagesV2 )
r . Get ( "/latest" , conan . LatestRecipeRevision )
r . Group ( "/revisions" , func ( ) {
r . Get ( "" , conan . ListRecipeRevisions )
r . Group ( "/{recipe_revision}" , func ( ) {
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . DeleteRecipeV2 )
r . Get ( "/search" , conan . SearchPackagesV2 )
r . Group ( "/files" , func ( ) {
r . Get ( "" , conan . ListRecipeRevisionFiles )
r . Group ( "/{filename}" , func ( ) {
r . Get ( "" , conan . DownloadRecipeFile )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . UploadRecipeFile )
} )
} )
r . Group ( "/packages" , func ( ) {
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . DeletePackageV2 )
r . Group ( "/{package_reference}" , func ( ) {
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . DeletePackageV2 )
r . Get ( "/latest" , conan . LatestPackageRevision )
r . Group ( "/revisions" , func ( ) {
r . Get ( "" , conan . ListPackageRevisions )
r . Group ( "/{package_revision}" , func ( ) {
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . DeletePackageV2 )
r . Group ( "/files" , func ( ) {
r . Get ( "" , conan . ListPackageRevisionFiles )
r . Group ( "/{filename}" , func ( ) {
r . Get ( "" , conan . DownloadPackageFile )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , conan . UploadPackageFile )
} )
} )
} )
} )
} )
} )
} )
} )
} , conan . ExtractPathParameters )
} )
} )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2023-02-01 21:30:39 +03:00
r . Group ( "/conda" , func ( ) {
var (
downloadPattern = regexp . MustCompile ( ` \A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z ` )
uploadPattern = regexp . MustCompile ( ` \A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z ` )
)
r . Get ( "/*" , func ( ctx * context . Context ) {
2024-06-19 01:32:45 +03:00
m := downloadPattern . FindStringSubmatch ( ctx . PathParam ( "*" ) )
2023-02-01 21:30:39 +03:00
if len ( m ) == 0 {
ctx . Status ( http . StatusNotFound )
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "channel" , strings . TrimSuffix ( m [ 1 ] , "/" ) )
ctx . SetPathParam ( "architecture" , m [ 2 ] )
ctx . SetPathParam ( "filename" , m [ 3 ] )
2023-02-01 21:30:39 +03:00
switch m [ 3 ] {
case "repodata.json" , "repodata.json.bz2" , "current_repodata.json" , "current_repodata.json.bz2" :
conda . EnumeratePackages ( ctx )
default :
conda . DownloadPackageFile ( ctx )
}
} )
r . Put ( "/*" , reqPackageAccess ( perm . AccessModeWrite ) , func ( ctx * context . Context ) {
2024-06-19 01:32:45 +03:00
m := uploadPattern . FindStringSubmatch ( ctx . PathParam ( "*" ) )
2023-02-01 21:30:39 +03:00
if len ( m ) == 0 {
ctx . Status ( http . StatusNotFound )
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "channel" , strings . TrimSuffix ( m [ 1 ] , "/" ) )
ctx . SetPathParam ( "filename" , m [ 2 ] )
2023-02-01 21:30:39 +03:00
conda . UploadPackageFile ( ctx )
} )
} , reqPackageAccess ( perm . AccessModeRead ) )
2023-05-22 05:57:49 +03:00
r . Group ( "/cran" , func ( ) {
r . Group ( "/src" , func ( ) {
r . Group ( "/contrib" , func ( ) {
r . Get ( "/PACKAGES" , cran . EnumerateSourcePackages )
r . Get ( "/PACKAGES{format}" , cran . EnumerateSourcePackages )
r . Get ( "/{filename}" , cran . DownloadSourcePackageFile )
} )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , cran . UploadSourcePackageFile )
} )
r . Group ( "/bin" , func ( ) {
r . Group ( "/{platform}/contrib/{rversion}" , func ( ) {
r . Get ( "/PACKAGES" , cran . EnumerateBinaryPackages )
r . Get ( "/PACKAGES{format}" , cran . EnumerateBinaryPackages )
r . Get ( "/{filename}" , cran . DownloadBinaryPackageFile )
} )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , cran . UploadBinaryPackageFile )
} )
} , reqPackageAccess ( perm . AccessModeRead ) )
2023-05-02 19:31:35 +03:00
r . Group ( "/debian" , func ( ) {
r . Get ( "/repository.key" , debian . GetRepositoryKey )
r . Group ( "/dists/{distribution}" , func ( ) {
r . Get ( "/{filename}" , debian . GetRepositoryFile )
r . Get ( "/by-hash/{algorithm}/{hash}" , debian . GetRepositoryFileByHash )
r . Group ( "/{component}/{architecture}" , func ( ) {
r . Get ( "/{filename}" , debian . GetRepositoryFile )
r . Get ( "/by-hash/{algorithm}/{hash}" , debian . GetRepositoryFileByHash )
} )
} )
r . Group ( "/pool/{distribution}/{component}" , func ( ) {
r . Get ( "/{name}_{version}_{architecture}.deb" , debian . DownloadPackageFile )
r . Group ( "" , func ( ) {
r . Put ( "/upload" , debian . UploadPackageFile )
r . Delete ( "/{name}/{version}/{architecture}" , debian . DeletePackageFile )
} , reqPackageAccess ( perm . AccessModeWrite ) )
} )
} , reqPackageAccess ( perm . AccessModeRead ) )
2023-05-14 18:38:40 +03:00
r . Group ( "/go" , func ( ) {
r . Put ( "/upload" , reqPackageAccess ( perm . AccessModeWrite ) , goproxy . UploadPackage )
r . Get ( "/sumdb/sum.golang.org/supported" , func ( ctx * context . Context ) {
ctx . Status ( http . StatusNotFound )
} )
// Manual mapping of routes because the package name contains slashes which chi does not support
// https://go.dev/ref/mod#goproxy-protocol
r . Get ( "/*" , func ( ctx * context . Context ) {
2024-06-19 01:32:45 +03:00
path := ctx . PathParam ( "*" )
2023-05-14 18:38:40 +03:00
if strings . HasSuffix ( path , "/@latest" ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "name" , path [ : len ( path ) - len ( "/@latest" ) ] )
ctx . SetPathParam ( "version" , "latest" )
2023-05-14 18:38:40 +03:00
goproxy . PackageVersionMetadata ( ctx )
return
}
parts := strings . SplitN ( path , "/@v/" , 2 )
if len ( parts ) != 2 {
ctx . Status ( http . StatusNotFound )
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "name" , parts [ 0 ] )
2023-05-14 18:38:40 +03:00
// <package/name>/@v/list
if parts [ 1 ] == "list" {
goproxy . EnumeratePackageVersions ( ctx )
return
}
// <package/name>/@v/<version>.zip
if strings . HasSuffix ( parts [ 1 ] , ".zip" ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "version" , parts [ 1 ] [ : len ( parts [ 1 ] ) - len ( ".zip" ) ] )
2023-05-14 18:38:40 +03:00
goproxy . DownloadPackageFile ( ctx )
return
}
// <package/name>/@v/<version>.info
if strings . HasSuffix ( parts [ 1 ] , ".info" ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "version" , parts [ 1 ] [ : len ( parts [ 1 ] ) - len ( ".info" ) ] )
2023-05-14 18:38:40 +03:00
goproxy . PackageVersionMetadata ( ctx )
return
}
// <package/name>/@v/<version>.mod
if strings . HasSuffix ( parts [ 1 ] , ".mod" ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "version" , parts [ 1 ] [ : len ( parts [ 1 ] ) - len ( ".mod" ) ] )
2023-05-14 18:38:40 +03:00
goproxy . PackageVersionGoModContent ( ctx )
return
}
ctx . Status ( http . StatusNotFound )
} )
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
r . Group ( "/generic" , func ( ) {
2022-08-09 07:39:24 +03:00
r . Group ( "/{packagename}/{packageversion}" , func ( ) {
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , generic . DeletePackage )
r . Group ( "/{filename}" , func ( ) {
r . Get ( "" , generic . DownloadPackageFile )
r . Group ( "" , func ( ) {
r . Put ( "" , generic . UploadPackage )
r . Delete ( "" , generic . DeletePackageFile )
} , reqPackageAccess ( perm . AccessModeWrite ) )
} )
2022-03-30 11:42:47 +03:00
} )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-04-19 19:55:35 +03:00
r . Group ( "/helm" , func ( ) {
r . Get ( "/index.yaml" , helm . Index )
r . Get ( "/{filename}" , helm . DownloadPackageFile )
r . Post ( "/api/charts" , reqPackageAccess ( perm . AccessModeWrite ) , helm . UploadPackage )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
r . Group ( "/maven" , func ( ) {
r . Put ( "/*" , reqPackageAccess ( perm . AccessModeWrite ) , maven . UploadPackageFile )
r . Get ( "/*" , maven . DownloadPackageFile )
2022-11-24 17:25:13 +03:00
r . Head ( "/*" , maven . ProvidePackageFileHeader )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
r . Group ( "/nuget" , func ( ) {
2022-10-13 13:19:39 +03:00
r . Group ( "" , func ( ) { // Needs to be unauthenticated for the NuGet client.
r . Get ( "/" , nuget . ServiceIndexV2 )
r . Get ( "/index.json" , nuget . ServiceIndexV3 )
r . Get ( "/$metadata" , nuget . FeedCapabilityResource )
} )
2022-03-30 11:42:47 +03:00
r . Group ( "" , func ( ) {
2022-10-13 13:19:39 +03:00
r . Get ( "/query" , nuget . SearchServiceV3 )
2022-09-24 18:17:08 +03:00
r . Group ( "/registration/{id}" , func ( ) {
r . Get ( "/index.json" , nuget . RegistrationIndex )
2022-10-13 13:19:39 +03:00
r . Get ( "/{version}" , nuget . RegistrationLeafV3 )
2022-09-24 18:17:08 +03:00
} )
r . Group ( "/package/{id}" , func ( ) {
2022-10-13 13:19:39 +03:00
r . Get ( "/index.json" , nuget . EnumeratePackageVersionsV3 )
2022-09-24 18:17:08 +03:00
r . Get ( "/{version}/{filename}" , nuget . DownloadPackageFile )
} )
r . Group ( "" , func ( ) {
r . Put ( "/" , nuget . UploadPackage )
r . Put ( "/symbolpackage" , nuget . UploadSymbolPackage )
r . Delete ( "/{id}/{version}" , nuget . DeletePackage )
} , reqPackageAccess ( perm . AccessModeWrite ) )
2022-10-12 09:53:56 +03:00
r . Get ( "/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}" , nuget . DownloadSymbolFile )
2022-10-13 13:19:39 +03:00
r . Get ( "/Packages(Id='{id:[^']+}',Version='{version:[^']+}')" , nuget . RegistrationLeafV2 )
2023-02-11 14:30:44 +03:00
r . Group ( "/Packages()" , func ( ) {
r . Get ( "" , nuget . SearchServiceV2 )
r . Get ( "/$count" , nuget . SearchServiceV2Count )
} )
r . Group ( "/FindPackagesById()" , func ( ) {
r . Get ( "" , nuget . EnumeratePackageVersionsV2 )
r . Get ( "/$count" , nuget . EnumeratePackageVersionsV2Count )
} )
r . Group ( "/Search()" , func ( ) {
r . Get ( "" , nuget . SearchServiceV2 )
r . Get ( "/$count" , nuget . SearchServiceV2Count )
} )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
} )
r . Group ( "/npm" , func ( ) {
r . Group ( "/@{scope}/{id}" , func ( ) {
r . Get ( "" , npm . PackageMetadata )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , npm . UploadPackage )
2022-08-09 10:23:43 +03:00
r . Group ( "/-/{version}/{filename}" , func ( ) {
r . Get ( "" , npm . DownloadPackageFile )
r . Delete ( "/-rev/{revision}" , reqPackageAccess ( perm . AccessModeWrite ) , npm . DeletePackageVersion )
} )
2022-10-24 16:50:22 +03:00
r . Get ( "/-/{filename}" , npm . DownloadPackageFileByName )
2022-08-09 10:23:43 +03:00
r . Group ( "/-rev/{revision}" , func ( ) {
r . Delete ( "" , npm . DeletePackage )
r . Put ( "" , npm . DeletePreview )
} , reqPackageAccess ( perm . AccessModeWrite ) )
2022-03-30 11:42:47 +03:00
} )
r . Group ( "/{id}" , func ( ) {
r . Get ( "" , npm . PackageMetadata )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , npm . UploadPackage )
2022-08-09 10:23:43 +03:00
r . Group ( "/-/{version}/{filename}" , func ( ) {
r . Get ( "" , npm . DownloadPackageFile )
r . Delete ( "/-rev/{revision}" , reqPackageAccess ( perm . AccessModeWrite ) , npm . DeletePackageVersion )
} )
2022-10-24 16:50:22 +03:00
r . Get ( "/-/{filename}" , npm . DownloadPackageFileByName )
2022-08-09 10:23:43 +03:00
r . Group ( "/-rev/{revision}" , func ( ) {
r . Delete ( "" , npm . DeletePackage )
r . Put ( "" , npm . DeletePreview )
} , reqPackageAccess ( perm . AccessModeWrite ) )
2022-03-30 11:42:47 +03:00
} )
r . Group ( "/-/package/@{scope}/{id}/dist-tags" , func ( ) {
r . Get ( "" , npm . ListPackageTags )
r . Group ( "/{tag}" , func ( ) {
r . Put ( "" , npm . AddPackageTag )
r . Delete ( "" , npm . DeletePackageTag )
} , reqPackageAccess ( perm . AccessModeWrite ) )
} )
r . Group ( "/-/package/{id}/dist-tags" , func ( ) {
r . Get ( "" , npm . ListPackageTags )
r . Group ( "/{tag}" , func ( ) {
r . Put ( "" , npm . AddPackageTag )
r . Delete ( "" , npm . DeletePackageTag )
} , reqPackageAccess ( perm . AccessModeWrite ) )
} )
2022-09-24 14:24:33 +03:00
r . Group ( "/-/v1/search" , func ( ) {
r . Get ( "" , npm . PackageSearch )
} )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-08-07 13:09:54 +03:00
r . Group ( "/pub" , func ( ) {
r . Group ( "/api/packages" , func ( ) {
r . Group ( "/versions/new" , func ( ) {
r . Get ( "" , pub . RequestUpload )
r . Post ( "/upload" , pub . UploadPackageFile )
r . Get ( "/finalize/{id}/{version}" , pub . FinalizePackage )
} , reqPackageAccess ( perm . AccessModeWrite ) )
r . Group ( "/{id}" , func ( ) {
r . Get ( "" , pub . EnumeratePackageVersions )
r . Get ( "/files/{version}" , pub . DownloadPackageFile )
r . Get ( "/{version}" , pub . PackageVersionMetadata )
} )
} )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
r . Group ( "/pypi" , func ( ) {
r . Post ( "/" , reqPackageAccess ( perm . AccessModeWrite ) , pypi . UploadPackageFile )
r . Get ( "/files/{id}/{version}/{filename}" , pypi . DownloadPackageFile )
r . Get ( "/simple/{id}" , pypi . PackageMetadata )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2024-01-19 14:37:10 +03:00
r . Group ( "/rpm" , func ( ) {
r . Group ( "/repository.key" , func ( ) {
r . Head ( "" , rpm . GetRepositoryKey )
r . Get ( "" , rpm . GetRepositoryKey )
} )
var (
repoPattern = regexp . MustCompile ( ` \A(.*?)\.repo\z ` )
uploadPattern = regexp . MustCompile ( ` \A(.*?)/upload\z ` )
filePattern = regexp . MustCompile ( ` \A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z ` )
repoFilePattern = regexp . MustCompile ( ` \A(.*?)/repodata/([^/]+)\z ` )
)
r . Methods ( "HEAD,GET,PUT,DELETE" , "*" , func ( ctx * context . Context ) {
2024-06-19 01:32:45 +03:00
path := ctx . PathParam ( "*" )
2024-01-19 14:37:10 +03:00
isHead := ctx . Req . Method == "HEAD"
isGetHead := ctx . Req . Method == "HEAD" || ctx . Req . Method == "GET"
isPut := ctx . Req . Method == "PUT"
isDelete := ctx . Req . Method == "DELETE"
m := repoPattern . FindStringSubmatch ( path )
if len ( m ) == 2 && isGetHead {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "group" , strings . Trim ( m [ 1 ] , "/" ) )
2024-01-19 14:37:10 +03:00
rpm . GetRepositoryConfig ( ctx )
return
}
m = repoFilePattern . FindStringSubmatch ( path )
if len ( m ) == 3 && isGetHead {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "group" , strings . Trim ( m [ 1 ] , "/" ) )
ctx . SetPathParam ( "filename" , m [ 2 ] )
2024-01-19 14:37:10 +03:00
if isHead {
rpm . CheckRepositoryFileExistence ( ctx )
} else {
rpm . GetRepositoryFile ( ctx )
}
return
}
m = uploadPattern . FindStringSubmatch ( path )
if len ( m ) == 2 && isPut {
reqPackageAccess ( perm . AccessModeWrite ) ( ctx )
if ctx . Written ( ) {
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "group" , strings . Trim ( m [ 1 ] , "/" ) )
2024-01-19 14:37:10 +03:00
rpm . UploadPackageFile ( ctx )
return
}
m = filePattern . FindStringSubmatch ( path )
if len ( m ) == 6 && ( isGetHead || isDelete ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "group" , strings . Trim ( m [ 1 ] , "/" ) )
ctx . SetPathParam ( "name" , m [ 2 ] )
ctx . SetPathParam ( "version" , m [ 3 ] )
ctx . SetPathParam ( "architecture" , m [ 4 ] )
2024-01-19 14:37:10 +03:00
if isGetHead {
rpm . DownloadPackageFile ( ctx )
} else {
reqPackageAccess ( perm . AccessModeWrite ) ( ctx )
if ctx . Written ( ) {
return
}
rpm . DeletePackageFile ( ctx )
}
return
}
ctx . Status ( http . StatusNotFound )
} )
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
r . Group ( "/rubygems" , func ( ) {
r . Get ( "/specs.4.8.gz" , rubygems . EnumeratePackages )
r . Get ( "/latest_specs.4.8.gz" , rubygems . EnumeratePackagesLatest )
r . Get ( "/prerelease_specs.4.8.gz" , rubygems . EnumeratePackagesPreRelease )
r . Get ( "/quick/Marshal.4.8/{filename}" , rubygems . ServePackageSpecification )
r . Get ( "/gems/{filename}" , rubygems . DownloadPackageFile )
2024-06-17 11:42:46 +03:00
r . Get ( "/info/{packagename}" , rubygems . GetPackageInfo )
r . Get ( "/versions" , rubygems . GetAllPackagesVersions )
2022-03-30 11:42:47 +03:00
r . Group ( "/api/v1/gems" , func ( ) {
r . Post ( "/" , rubygems . UploadPackageFile )
r . Delete ( "/yank" , rubygems . DeletePackage )
} , reqPackageAccess ( perm . AccessModeWrite ) )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2023-03-13 23:28:39 +03:00
r . Group ( "/swift" , func ( ) {
r . Group ( "/{scope}/{name}" , func ( ) {
r . Group ( "" , func ( ) {
r . Get ( "" , swift . EnumeratePackageVersions )
r . Get ( ".json" , swift . EnumeratePackageVersions )
} , swift . CheckAcceptMediaType ( swift . AcceptJSON ) )
r . Group ( "/{version}" , func ( ) {
r . Get ( "/Package.swift" , swift . CheckAcceptMediaType ( swift . AcceptSwift ) , swift . DownloadManifest )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , swift . CheckAcceptMediaType ( swift . AcceptJSON ) , swift . UploadPackageFile )
r . Get ( "" , func ( ctx * context . Context ) {
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781
2024-06-19 01:32:45 +03:00
version := ctx . PathParam ( "version" )
2023-03-13 23:28:39 +03:00
if strings . HasSuffix ( version , ".zip" ) {
swift . CheckAcceptMediaType ( swift . AcceptZip ) ( ctx )
if ctx . Written ( ) {
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "version" , version [ : len ( version ) - 4 ] )
2023-03-13 23:28:39 +03:00
swift . DownloadPackageFile ( ctx )
} else {
swift . CheckAcceptMediaType ( swift . AcceptJSON ) ( ctx )
if ctx . Written ( ) {
return
}
if strings . HasSuffix ( version , ".json" ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "version" , version [ : len ( version ) - 5 ] )
2023-03-13 23:28:39 +03:00
}
swift . PackageVersionMetadata ( ctx )
}
} )
} )
} )
r . Get ( "/identifiers" , swift . CheckAcceptMediaType ( swift . AcceptJSON ) , swift . LookupPackageIdentifiers )
} , reqPackageAccess ( perm . AccessModeRead ) )
2022-08-29 10:04:45 +03:00
r . Group ( "/vagrant" , func ( ) {
r . Group ( "/authenticate" , func ( ) {
r . Get ( "" , vagrant . CheckAuthenticate )
} )
r . Group ( "/{name}" , func ( ) {
r . Head ( "" , vagrant . CheckBoxAvailable )
r . Get ( "" , vagrant . EnumeratePackageVersions )
r . Group ( "/{version}/{provider}" , func ( ) {
r . Get ( "" , vagrant . DownloadPackageFile )
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , vagrant . UploadPackageFile )
} )
} )
2022-09-24 18:17:08 +03:00
} , reqPackageAccess ( perm . AccessModeRead ) )
2024-02-27 10:12:22 +03:00
} , context . UserAssignmentWeb ( ) , context . PackageAssignment ( ) )
2022-03-30 11:42:47 +03:00
return r
}
2022-11-12 21:59:15 +03:00
// ContainerRoutes provides endpoints that implement the OCI API to serve containers
// These have to be mounted on `/v2/...` to comply with the OCI spec:
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
2024-06-19 01:32:45 +03:00
func ContainerRoutes ( ) * web . Router {
r := web . NewRouter ( )
2022-03-30 11:42:47 +03:00
2023-05-21 04:50:53 +03:00
r . Use ( context . PackageContexter ( ) )
2022-03-30 11:42:47 +03:00
2023-04-10 10:21:03 +03:00
verifyAuth ( r , [ ] auth . Method {
2022-03-30 11:42:47 +03:00
& auth . Basic { } ,
& container . Auth { } ,
} )
r . Get ( "" , container . ReqContainerAccess , container . DetermineSupport )
2023-12-13 23:23:53 +03:00
r . Group ( "/token" , func ( ) {
r . Get ( "" , container . Authenticate )
r . Post ( "" , container . AuthenticateNotImplemented )
} )
2022-07-28 06:59:39 +03:00
r . Get ( "/_catalog" , container . ReqContainerAccess , container . GetRepositoryList )
2022-03-30 11:42:47 +03:00
r . Group ( "/{username}" , func ( ) {
r . Group ( "/{image}" , func ( ) {
r . Group ( "/blobs/uploads" , func ( ) {
r . Post ( "" , container . InitiateUploadBlob )
r . Group ( "/{uuid}" , func ( ) {
2022-10-07 18:30:59 +03:00
r . Get ( "" , container . GetUploadBlob )
2022-03-30 11:42:47 +03:00
r . Patch ( "" , container . UploadBlob )
r . Put ( "" , container . EndUploadBlob )
2022-10-07 18:30:59 +03:00
r . Delete ( "" , container . CancelUploadBlob )
2022-03-30 11:42:47 +03:00
} )
} , reqPackageAccess ( perm . AccessModeWrite ) )
r . Group ( "/blobs/{digest}" , func ( ) {
r . Head ( "" , container . HeadBlob )
r . Get ( "" , container . GetBlob )
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , container . DeleteBlob )
} )
r . Group ( "/manifests/{reference}" , func ( ) {
r . Put ( "" , reqPackageAccess ( perm . AccessModeWrite ) , container . UploadManifest )
r . Head ( "" , container . HeadManifest )
r . Get ( "" , container . GetManifest )
r . Delete ( "" , reqPackageAccess ( perm . AccessModeWrite ) , container . DeleteManifest )
} )
r . Get ( "/tags/list" , container . GetTagList )
} , container . VerifyImageName )
var (
blobsUploadsPattern = regexp . MustCompile ( ` \A(.+)/blobs/uploads/([a-zA-Z0-9-_.=]+)\z ` )
blobsPattern = regexp . MustCompile ( ` \A(.+)/blobs/([^/]+)\z ` )
manifestsPattern = regexp . MustCompile ( ` \A(.+)/manifests/([^/]+)\z ` )
)
// Manual mapping of routes because {image} can contain slashes which chi does not support
2023-07-21 01:43:49 +03:00
r . Methods ( "HEAD,GET,POST,PUT,PATCH,DELETE" , "/*" , func ( ctx * context . Context ) {
2024-06-19 01:32:45 +03:00
path := ctx . PathParam ( "*" )
2022-03-30 11:42:47 +03:00
isHead := ctx . Req . Method == "HEAD"
isGet := ctx . Req . Method == "GET"
isPost := ctx . Req . Method == "POST"
isPut := ctx . Req . Method == "PUT"
isPatch := ctx . Req . Method == "PATCH"
isDelete := ctx . Req . Method == "DELETE"
if isPost && strings . HasSuffix ( path , "/blobs/uploads" ) {
reqPackageAccess ( perm . AccessModeWrite ) ( ctx )
if ctx . Written ( ) {
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "image" , path [ : len ( path ) - 14 ] )
2022-03-30 11:42:47 +03:00
container . VerifyImageName ( ctx )
if ctx . Written ( ) {
return
}
container . InitiateUploadBlob ( ctx )
return
}
if isGet && strings . HasSuffix ( path , "/tags/list" ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "image" , path [ : len ( path ) - 10 ] )
2022-03-30 11:42:47 +03:00
container . VerifyImageName ( ctx )
if ctx . Written ( ) {
return
}
container . GetTagList ( ctx )
return
}
m := blobsUploadsPattern . FindStringSubmatch ( path )
2022-10-07 18:30:59 +03:00
if len ( m ) == 3 && ( isGet || isPut || isPatch || isDelete ) {
2022-03-30 11:42:47 +03:00
reqPackageAccess ( perm . AccessModeWrite ) ( ctx )
if ctx . Written ( ) {
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "image" , m [ 1 ] )
2022-03-30 11:42:47 +03:00
container . VerifyImageName ( ctx )
if ctx . Written ( ) {
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "uuid" , m [ 2 ] )
2022-03-30 11:42:47 +03:00
2022-10-07 18:30:59 +03:00
if isGet {
container . GetUploadBlob ( ctx )
} else if isPatch {
2022-03-30 11:42:47 +03:00
container . UploadBlob ( ctx )
2022-10-07 18:30:59 +03:00
} else if isPut {
2022-03-30 11:42:47 +03:00
container . EndUploadBlob ( ctx )
2022-10-07 18:30:59 +03:00
} else {
container . CancelUploadBlob ( ctx )
2022-03-30 11:42:47 +03:00
}
return
}
m = blobsPattern . FindStringSubmatch ( path )
if len ( m ) == 3 && ( isHead || isGet || isDelete ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "image" , m [ 1 ] )
2022-03-30 11:42:47 +03:00
container . VerifyImageName ( ctx )
if ctx . Written ( ) {
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "digest" , m [ 2 ] )
2022-03-30 11:42:47 +03:00
if isHead {
container . HeadBlob ( ctx )
} else if isGet {
container . GetBlob ( ctx )
} else {
reqPackageAccess ( perm . AccessModeWrite ) ( ctx )
if ctx . Written ( ) {
return
}
container . DeleteBlob ( ctx )
}
return
}
m = manifestsPattern . FindStringSubmatch ( path )
if len ( m ) == 3 && ( isHead || isGet || isPut || isDelete ) {
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "image" , m [ 1 ] )
2022-03-30 11:42:47 +03:00
container . VerifyImageName ( ctx )
if ctx . Written ( ) {
return
}
2024-06-19 01:32:45 +03:00
ctx . SetPathParam ( "reference" , m [ 2 ] )
2022-03-30 11:42:47 +03:00
if isHead {
container . HeadManifest ( ctx )
} else if isGet {
container . GetManifest ( ctx )
} else {
reqPackageAccess ( perm . AccessModeWrite ) ( ctx )
if ctx . Written ( ) {
return
}
if isPut {
container . UploadManifest ( ctx )
} else {
container . DeleteManifest ( ctx )
}
}
return
}
ctx . Status ( http . StatusNotFound )
} )
2024-02-27 10:12:22 +03:00
} , container . ReqContainerAccess , context . UserAssignmentWeb ( ) , context . PackageAssignment ( ) , reqPackageAccess ( perm . AccessModeRead ) )
2022-03-30 11:42:47 +03:00
return r
}