2021-05-13 21:59:12 +03:00
package api
import (
"context"
"encoding/base64"
"net/http"
"strings"
"time"
2021-09-10 18:23:26 +03:00
glob "github.com/bmatcuk/doublestar/v4"
2021-05-13 21:59:12 +03:00
"github.com/gorilla/mux"
2021-12-04 03:50:58 +00:00
"zotregistry.io/zot/pkg/api/config"
2021-12-29 17:14:56 +02:00
"zotregistry.io/zot/pkg/common"
2021-12-04 03:50:58 +00:00
"zotregistry.io/zot/pkg/log"
2021-05-13 21:59:12 +03:00
)
type contextKey int
const (
2021-09-18 00:00:59 +00:00
// actions.
2021-05-13 21:59:12 +03:00
CREATE = "create"
READ = "read"
UPDATE = "update"
DELETE = "delete"
2021-09-18 00:00:59 +00:00
// request-local context key.
2021-05-13 21:59:12 +03:00
authzCtxKey contextKey = 0
)
// AccessController authorizes users to act on resources.
type AccessController struct {
2021-06-08 23:11:18 +03:00
Config * config . AccessControlConfig
2021-05-13 21:59:12 +03:00
Log log . Logger
}
// AccessControlContext context passed down to http.Handlers.
type AccessControlContext struct {
2021-09-10 18:23:26 +03:00
globPatterns map [ string ] bool
isAdmin bool
2021-05-13 21:59:12 +03:00
}
2021-06-08 23:11:18 +03:00
func NewAccessController ( config * config . Config ) * AccessController {
2021-05-13 21:59:12 +03:00
return & AccessController {
Config : config . AccessControl ,
Log : log . NewLogger ( config . Log . Level , config . Log . Output ) ,
}
}
2021-09-10 18:23:26 +03:00
// getReadRepos get glob patterns from config file that the user has or doesn't have READ perms.
// used to filter /v2/_catalog repositories based on user rights.
func ( ac * AccessController ) getReadGlobPatterns ( username string ) map [ string ] bool {
globPatterns := make ( map [ string ] bool )
2021-05-13 21:59:12 +03:00
2021-09-10 18:23:26 +03:00
for pattern , policyGroup := range ac . Config . Repositories {
// check default policy
2021-12-29 17:14:56 +02:00
if common . Contains ( policyGroup . DefaultPolicy , READ ) {
2021-09-10 18:23:26 +03:00
globPatterns [ pattern ] = true
}
// check user based policy
for _ , p := range policyGroup . Policies {
2021-12-29 17:14:56 +02:00
if common . Contains ( p . Users , username ) && common . Contains ( p . Actions , READ ) {
2021-09-10 18:23:26 +03:00
globPatterns [ pattern ] = true
2021-05-13 21:59:12 +03:00
}
}
2021-09-10 18:23:26 +03:00
// if not allowed then mark it
if _ , ok := globPatterns [ pattern ] ; ! ok {
globPatterns [ pattern ] = false
}
2021-05-13 21:59:12 +03:00
}
2021-09-10 18:23:26 +03:00
return globPatterns
2021-05-13 21:59:12 +03:00
}
// can verifies if a user can do action on repository.
func ( ac * AccessController ) can ( username , action , repository string ) bool {
can := false
2021-09-10 18:23:26 +03:00
var longestMatchedPattern string
for pattern := range ac . Config . Repositories {
matched , err := glob . Match ( pattern , repository )
if err == nil {
if matched && len ( pattern ) > len ( longestMatchedPattern ) {
longestMatchedPattern = pattern
}
}
}
// check matched repo based policy
pg , ok := ac . Config . Repositories [ longestMatchedPattern ]
2021-05-13 21:59:12 +03:00
if ok {
can = isPermitted ( username , action , pg )
}
2021-12-13 19:23:31 +00:00
// check admins based policy
2021-05-13 21:59:12 +03:00
if ! can {
2021-12-29 17:14:56 +02:00
if ac . isAdmin ( username ) && common . Contains ( ac . Config . AdminPolicy . Actions , action ) {
2021-05-13 21:59:12 +03:00
can = true
}
}
return can
}
// isAdmin .
func ( ac * AccessController ) isAdmin ( username string ) bool {
2021-12-29 17:14:56 +02:00
return common . Contains ( ac . Config . AdminPolicy . Users , username )
2021-05-13 21:59:12 +03:00
}
// getContext builds ac context(allowed to read repos and if user is admin) and returns it.
2021-12-13 19:23:31 +00:00
func ( ac * AccessController ) getContext ( username string , request * http . Request ) context . Context {
2021-09-10 18:23:26 +03:00
readGlobPatterns := ac . getReadGlobPatterns ( username )
acCtx := AccessControlContext { globPatterns : readGlobPatterns }
2021-05-13 21:59:12 +03:00
if ac . isAdmin ( username ) {
acCtx . isAdmin = true
} else {
acCtx . isAdmin = false
}
2021-12-13 19:23:31 +00:00
ctx := context . WithValue ( request . Context ( ) , authzCtxKey , acCtx )
2021-05-13 21:59:12 +03:00
return ctx
}
// isPermitted returns true if username can do action on a repository policy.
2021-12-13 19:23:31 +00:00
func isPermitted ( username , action string , policyGroup config . PolicyGroup ) bool {
2021-05-13 21:59:12 +03:00
var result bool
// check repo/system based policies
2021-12-13 19:23:31 +00:00
for _ , p := range policyGroup . Policies {
2021-12-29 17:14:56 +02:00
if common . Contains ( p . Users , username ) && common . Contains ( p . Actions , action ) {
2021-05-13 21:59:12 +03:00
result = true
2021-12-13 19:23:31 +00:00
2021-05-13 21:59:12 +03:00
break
}
}
// check defaultPolicy
if ! result {
2021-12-29 17:14:56 +02:00
if common . Contains ( policyGroup . DefaultPolicy , action ) {
2021-05-13 21:59:12 +03:00
result = true
}
}
return result
}
2021-09-10 18:23:26 +03:00
// returns either a user has or not rights on 'repository'.
func matchesRepo ( globPatterns map [ string ] bool , repository string ) bool {
var longestMatchedPattern string
// because of the longest path matching rule, we need to check all patterns from config
for pattern := range globPatterns {
matched , err := glob . Match ( pattern , repository )
if err == nil {
if matched && len ( pattern ) > len ( longestMatchedPattern ) {
longestMatchedPattern = pattern
}
2021-05-13 21:59:12 +03:00
}
}
2021-09-10 18:23:26 +03:00
allowed := globPatterns [ longestMatchedPattern ]
return allowed
2021-05-13 21:59:12 +03:00
}
2021-12-13 19:23:31 +00:00
func AuthzHandler ( ctlr * Controller ) mux . MiddlewareFunc {
2021-05-13 21:59:12 +03:00
return func ( next http . Handler ) http . Handler {
2021-12-13 19:23:31 +00:00
return http . HandlerFunc ( func ( response http . ResponseWriter , request * http . Request ) {
vars := mux . Vars ( request )
2021-05-13 21:59:12 +03:00
resource := vars [ "name" ]
reference , ok := vars [ "reference" ]
2021-09-10 18:23:26 +03:00
// bypass authz for /v2/ route
if request . RequestURI == "/v2/" {
next . ServeHTTP ( response , request )
return
}
2021-12-13 19:23:31 +00:00
acCtrlr := NewAccessController ( ctlr . Config )
username := getUsername ( request )
ctx := acCtrlr . getContext ( username , request )
2021-09-10 18:23:26 +03:00
// will return only repos on which client is authorized to read
if request . RequestURI == "/v2/_catalog" {
2021-12-13 19:23:31 +00:00
next . ServeHTTP ( response , request . WithContext ( ctx ) )
2021-05-13 21:59:12 +03:00
return
}
var action string
2021-12-13 19:23:31 +00:00
if request . Method == http . MethodGet || request . Method == http . MethodHead {
2021-05-13 21:59:12 +03:00
action = READ
}
2021-12-13 19:23:31 +00:00
if request . Method == http . MethodPut || request . Method == http . MethodPatch || request . Method == http . MethodPost {
2021-05-13 21:59:12 +03:00
// assume user wants to create
action = CREATE
// if we get a reference (tag)
if ok {
2021-12-13 19:23:31 +00:00
is := ctlr . StoreController . GetImageStore ( resource )
2021-05-13 21:59:12 +03:00
tags , err := is . GetImageTags ( resource )
// if repo exists and request's tag doesn't exist yet then action is UPDATE
2021-12-29 17:14:56 +02:00
if err == nil && common . Contains ( tags , reference ) && reference != "latest" {
2021-05-13 21:59:12 +03:00
action = UPDATE
}
}
}
2021-12-13 19:23:31 +00:00
if request . Method == http . MethodDelete {
2021-05-13 21:59:12 +03:00
action = DELETE
}
2021-12-13 19:23:31 +00:00
can := acCtrlr . can ( username , action , resource )
2021-05-13 21:59:12 +03:00
if ! can {
2021-12-13 19:23:31 +00:00
authzFail ( response , ctlr . Config . HTTP . Realm , ctlr . Config . HTTP . Auth . FailDelay )
2021-05-13 21:59:12 +03:00
} else {
2021-12-13 19:23:31 +00:00
next . ServeHTTP ( response , request . WithContext ( ctx ) )
2021-05-13 21:59:12 +03:00
}
} )
}
}
func getUsername ( r * http . Request ) string {
2021-09-10 18:23:26 +03:00
// this should work because it was already parsed in authn middleware
2021-05-13 21:59:12 +03:00
basicAuth := r . Header . Get ( "Authorization" )
2021-12-13 19:23:31 +00:00
s := strings . SplitN ( basicAuth , " " , 2 ) //nolint:gomnd
2021-05-13 21:59:12 +03:00
b , _ := base64 . StdEncoding . DecodeString ( s [ 1 ] )
2021-12-13 19:23:31 +00:00
pair := strings . SplitN ( string ( b ) , ":" , 2 ) //nolint:gomnd
2021-05-13 21:59:12 +03:00
return pair [ 0 ]
}
func authzFail ( w http . ResponseWriter , realm string , delay int ) {
time . Sleep ( time . Duration ( delay ) * time . Second )
w . Header ( ) . Set ( "WWW-Authenticate" , realm )
w . Header ( ) . Set ( "Content-Type" , "application/json" )
WriteJSON ( w , http . StatusForbidden , NewErrorList ( NewError ( DENIED ) ) )
}