0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-23 22:27:35 -05:00
zot/pkg/api/authn.go

349 lines
9.5 KiB
Go
Raw Normal View History

2019-06-20 18:36:40 -05:00
package api
import (
"bufio"
"context"
2019-08-15 11:34:54 -05:00
"crypto/x509"
2019-06-20 18:36:40 -05:00
"encoding/base64"
2019-08-15 11:34:54 -05:00
"fmt"
2019-06-20 18:36:40 -05:00
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/chartmuseum/auth"
"github.com/gorilla/mux"
2019-06-20 18:36:40 -05:00
"golang.org/x/crypto/bcrypt"
"zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
localCtx "zotregistry.io/zot/pkg/requestcontext"
2019-06-20 18:36:40 -05:00
)
const (
bearerAuthDefaultAccessEntryType = "repository"
)
func AuthHandler(c *Controller) mux.MiddlewareFunc {
if isBearerAuthEnabled(c.Config) {
return bearerAuthHandler(c)
}
return basicAuthHandler(c)
}
func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
authorizer, err := auth.NewAuthorizer(&auth.AuthorizerOptions{
Realm: ctlr.Config.HTTP.Auth.Bearer.Realm,
Service: ctlr.Config.HTTP.Auth.Bearer.Service,
PublicKeyPath: ctlr.Config.HTTP.Auth.Bearer.Cert,
AccessEntryType: bearerAuthDefaultAccessEntryType,
EmptyDefaultNamespace: true,
})
if err != nil {
ctlr.Log.Panic().Err(err).Msg("error creating bearer authorizer")
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)
response.WriteHeader(http.StatusNoContent)
return
}
vars := mux.Vars(request)
name := vars["name"]
// we want to bypass auth for mgmt route
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
header := request.Header.Get("Authorization")
if (header == "" || header == "Basic Og==") && isMgmtRequested {
next.ServeHTTP(response, request)
return
}
action := auth.PullAction
if m := request.Method; m != http.MethodGet && m != http.MethodHead {
action = auth.PushAction
}
permissions, err := authorizer.Authorize(header, action, name)
if err != nil {
ctlr.Log.Error().Err(err).Msg("issue parsing Authorization header")
response.Header().Set("Content-Type", "application/json")
WriteJSON(response, http.StatusInternalServerError, NewErrorList(NewError(UNSUPPORTED)))
return
}
if !permissions.Allowed {
authFail(response, permissions.WWWAuthenticateHeader, 0)
return
}
next.ServeHTTP(response, request)
})
}
2019-06-20 18:36:40 -05:00
}
func noPasswdAuth(realm string, config *config.Config) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)
response.WriteHeader(http.StatusNoContent)
return
}
// Process request
ctx := getReqContextWithAuthorization("", []string{}, request)
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
})
}
}
//nolint:gocyclo // we use closure making this a complex subroutine
func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
realm := ctlr.Config.HTTP.Realm
if realm == "" {
realm = "Authorization Required"
}
realm = "Basic realm=" + strconv.Quote(realm)
2019-08-15 11:34:54 -05:00
// no password based authN, if neither LDAP nor HTTP BASIC is enabled
if ctlr.Config.HTTP.Auth == nil ||
(ctlr.Config.HTTP.Auth.HTPasswd.Path == "" && ctlr.Config.HTTP.Auth.LDAP == nil) {
return noPasswdAuth(realm, ctlr.Config)
2019-06-20 18:36:40 -05:00
}
credMap := make(map[string]string)
delay := ctlr.Config.HTTP.Auth.FailDelay
2019-08-15 11:34:54 -05:00
var ldapClient *LDAPClient
if ctlr.Config.HTTP.Auth != nil {
if ctlr.Config.HTTP.Auth.LDAP != nil {
ldapConfig := ctlr.Config.HTTP.Auth.LDAP
2019-08-15 11:34:54 -05:00
ldapClient = &LDAPClient{
Host: ldapConfig.Address,
Port: ldapConfig.Port,
UseSSL: !ldapConfig.Insecure,
SkipTLS: !ldapConfig.StartTLS,
Base: ldapConfig.BaseDN,
BindDN: ldapConfig.BindDN,
UserGroupAttribute: ldapConfig.UserGroupAttribute, // from config
BindPassword: ldapConfig.BindPassword,
UserFilter: fmt.Sprintf("(%s=%%s)", ldapConfig.UserAttribute),
InsecureSkipVerify: ldapConfig.SkipVerify,
ServerName: ldapConfig.Address,
Log: ctlr.Log,
SubtreeSearch: ldapConfig.SubtreeSearch,
2019-08-15 11:34:54 -05:00
}
if ctlr.Config.HTTP.Auth.LDAP.CACert != "" {
caCert, err := os.ReadFile(ctlr.Config.HTTP.Auth.LDAP.CACert)
2019-08-15 11:34:54 -05:00
if err != nil {
panic(err)
}
2019-08-15 11:34:54 -05:00
caCertPool := x509.NewCertPool()
2019-08-15 11:34:54 -05:00
if !caCertPool.AppendCertsFromPEM(caCert) {
panic(errors.ErrBadCACert)
}
ldapClient.ClientCAs = caCertPool
2019-08-15 11:34:54 -05:00
} else {
// default to system cert pool
caCertPool, err := x509.SystemCertPool()
if err != nil {
panic(errors.ErrBadCACert)
}
ldapClient.ClientCAs = caCertPool
2019-08-15 11:34:54 -05:00
}
}
if ctlr.Config.HTTP.Auth.HTPasswd.Path != "" {
credsFile, err := os.Open(ctlr.Config.HTTP.Auth.HTPasswd.Path)
2019-08-15 11:34:54 -05:00
if err != nil {
panic(err)
}
defer credsFile.Close()
2019-06-20 18:36:40 -05:00
scanner := bufio.NewScanner(credsFile)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, ":") {
tokens := strings.Split(scanner.Text(), ":")
credMap[tokens[0]] = tokens[1]
2019-08-15 11:34:54 -05:00
}
}
2019-06-20 18:36:40 -05:00
}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)
response.WriteHeader(http.StatusNoContent)
return
}
// we want to bypass auth for mgmt route
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
if request.Header.Get("Authorization") == "" {
if anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) || isMgmtRequested {
// Process request
ctx := getReqContextWithAuthorization("", []string{}, request)
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
return
}
}
username, passphrase, err := getUsernamePasswordBasicAuth(request)
if err != nil {
ctlr.Log.Error().Err(err).Msg("failed to parse authorization header")
authFail(response, realm, delay)
return
}
2019-06-20 18:36:40 -05:00
// some client tools might send Authorization: Basic Og== (decoded into ":")
// empty username and password
if username == "" && passphrase == "" {
if anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) || isMgmtRequested {
// Process request
ctx := getReqContextWithAuthorization("", []string{}, request)
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
return
}
}
2019-06-20 18:36:40 -05:00
// first, HTTPPassword authN (which is local)
passphraseHash, ok := credMap[username]
if ok {
if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err == nil {
// Process request
var userGroups []string
if ctlr.Config.HTTP.AccessControl != nil {
ac := NewAccessController(ctlr.Config)
userGroups = ac.getUserGroups(username)
}
ctx := getReqContextWithAuthorization(username, userGroups, request)
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
return
}
}
// next, LDAP if configured (network-based which can lose connectivity)
if ctlr.Config.HTTP.Auth != nil && ctlr.Config.HTTP.Auth.LDAP != nil {
ok, _, ldapgroups, err := ldapClient.Authenticate(username, passphrase)
2019-08-15 11:34:54 -05:00
if ok && err == nil {
// Process request
var userGroups []string
if ctlr.Config.HTTP.AccessControl != nil {
ac := NewAccessController(ctlr.Config)
userGroups = ac.getUserGroups(username)
}
userGroups = append(userGroups, ldapgroups...)
ctx := getReqContextWithAuthorization(username, userGroups, request)
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
2019-08-15 11:34:54 -05:00
return
}
}
authFail(response, realm, delay)
})
2019-06-20 18:36:40 -05:00
}
}
func getReqContextWithAuthorization(username string, groups []string, request *http.Request) context.Context {
acCtx := localCtx.AccessControlContext{
Username: username,
Groups: groups,
}
authzCtxKey := localCtx.GetContextKey()
ctx := context.WithValue(request.Context(), authzCtxKey, acCtx)
return ctx
}
func isAuthnEnabled(config *config.Config) bool {
if config.HTTP.Auth != nil &&
(config.HTTP.Auth.HTPasswd.Path != "" || config.HTTP.Auth.LDAP != nil) {
return true
}
return false
}
func isBearerAuthEnabled(config *config.Config) bool {
if config.HTTP.Auth != nil &&
config.HTTP.Auth.Bearer != nil &&
config.HTTP.Auth.Bearer.Cert != "" &&
config.HTTP.Auth.Bearer.Realm != "" &&
config.HTTP.Auth.Bearer.Service != "" {
return true
}
return false
}
func authFail(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.StatusUnauthorized, NewErrorList(NewError(UNAUTHORIZED)))
}
func getUsernamePasswordBasicAuth(request *http.Request) (string, string, error) {
basicAuth := request.Header.Get("Authorization")
if basicAuth == "" {
return "", "", errors.ErrParsingAuthHeader
}
splitStr := strings.SplitN(basicAuth, " ", 2) //nolint:gomnd
if len(splitStr) != 2 || strings.ToLower(splitStr[0]) != "basic" {
return "", "", errors.ErrParsingAuthHeader
}
decodedStr, err := base64.StdEncoding.DecodeString(splitStr[1])
if err != nil {
return "", "", err
}
pair := strings.SplitN(string(decodedStr), ":", 2) //nolint:gomnd
if len(pair) != 2 { //nolint:gomnd
return "", "", errors.ErrParsingAuthHeader
}
username := pair[0]
passphrase := pair[1]
return username, passphrase, nil
}