mirror of
https://github.com/project-zot/zot.git
synced 2024-12-23 22:27:35 -05:00
78be4cbe3c
This is useful if we want to roll out experimental versions of zot pointing to some storage shared with another zot instance. Also, when under storage full conditions, will be useful to turn on this flag to prevent further writes.
249 lines
6.4 KiB
Go
249 lines
6.4 KiB
Go
package api
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/anuvu/zot/errors"
|
|
"github.com/chartmuseum/auth"
|
|
"github.com/gorilla/mux"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const (
|
|
bearerAuthDefaultAccessEntryType = "repository"
|
|
)
|
|
|
|
func AuthHandler(c *Controller) mux.MiddlewareFunc {
|
|
if c.Config.HTTP.Auth != nil &&
|
|
c.Config.HTTP.Auth.Bearer != nil &&
|
|
c.Config.HTTP.Auth.Bearer.Cert != "" &&
|
|
c.Config.HTTP.Auth.Bearer.Realm != "" &&
|
|
c.Config.HTTP.Auth.Bearer.Service != "" {
|
|
return bearerAuthHandler(c)
|
|
}
|
|
|
|
return basicAuthHandler(c)
|
|
}
|
|
|
|
func bearerAuthHandler(c *Controller) mux.MiddlewareFunc {
|
|
authorizer, err := auth.NewAuthorizer(&auth.AuthorizerOptions{
|
|
Realm: c.Config.HTTP.Auth.Bearer.Realm,
|
|
Service: c.Config.HTTP.Auth.Bearer.Service,
|
|
PublicKeyPath: c.Config.HTTP.Auth.Bearer.Cert,
|
|
AccessEntryType: bearerAuthDefaultAccessEntryType,
|
|
EmptyDefaultNamespace: true,
|
|
})
|
|
if err != nil {
|
|
c.Log.Panic().Err(err).Msg("error creating bearer authorizer")
|
|
}
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
name := vars["name"]
|
|
header := r.Header.Get("Authorization")
|
|
action := auth.PullAction
|
|
if m := r.Method; m != http.MethodGet && m != http.MethodHead {
|
|
action = auth.PushAction
|
|
}
|
|
permissions, err := authorizer.Authorize(header, action, name)
|
|
if err != nil {
|
|
c.Log.Error().Err(err).Msg("issue parsing Authorization header")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
WriteJSON(w, http.StatusInternalServerError, NewErrorList(NewError(UNSUPPORTED)))
|
|
return
|
|
}
|
|
if !permissions.Allowed {
|
|
authFail(w, permissions.WWWAuthenticateHeader, 0)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// nolint:gocyclo // we use closure making this a complex subroutine
|
|
func basicAuthHandler(c *Controller) mux.MiddlewareFunc {
|
|
realm := c.Config.HTTP.Realm
|
|
if realm == "" {
|
|
realm = "Authorization Required"
|
|
}
|
|
|
|
realm = "Basic realm=" + strconv.Quote(realm)
|
|
|
|
// no password based authN, if neither LDAP nor HTTP BASIC is enabled
|
|
if c.Config.HTTP.Auth == nil || (c.Config.HTTP.Auth.HTPasswd.Path == "" && c.Config.HTTP.Auth.LDAP == nil) {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if c.Config.HTTP.AllowReadAccess &&
|
|
c.Config.HTTP.TLS.CACert != "" &&
|
|
r.TLS.VerifiedChains == nil &&
|
|
r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
authFail(w, realm, 5)
|
|
return
|
|
}
|
|
|
|
if (r.Method != http.MethodGet && r.Method != http.MethodHead) && c.Config.HTTP.ReadOnly {
|
|
// Reject modification requests in read-only mode
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
// Process request
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
credMap := make(map[string]string)
|
|
|
|
delay := c.Config.HTTP.Auth.FailDelay
|
|
|
|
var ldapClient *LDAPClient
|
|
|
|
if c.Config.HTTP.Auth != nil {
|
|
if c.Config.HTTP.Auth.LDAP != nil {
|
|
l := c.Config.HTTP.Auth.LDAP
|
|
ldapClient = &LDAPClient{
|
|
Host: l.Address,
|
|
Port: l.Port,
|
|
UseSSL: !l.Insecure,
|
|
SkipTLS: !l.StartTLS,
|
|
Base: l.BaseDN,
|
|
BindDN: l.BindDN,
|
|
BindPassword: l.BindPassword,
|
|
UserFilter: fmt.Sprintf("(%s=%%s)", l.UserAttribute),
|
|
InsecureSkipVerify: l.SkipVerify,
|
|
ServerName: l.Address,
|
|
Log: c.Log,
|
|
SubtreeSearch: l.SubtreeSearch,
|
|
}
|
|
|
|
if c.Config.HTTP.Auth.LDAP.CACert != "" {
|
|
caCert, err := ioutil.ReadFile(c.Config.HTTP.Auth.LDAP.CACert)
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
caCertPool := x509.NewCertPool()
|
|
|
|
if !caCertPool.AppendCertsFromPEM(caCert) {
|
|
panic(errors.ErrBadCACert)
|
|
}
|
|
|
|
ldapClient.ClientCAs = caCertPool
|
|
} else {
|
|
// default to system cert pool
|
|
caCertPool, err := x509.SystemCertPool()
|
|
|
|
if err != nil {
|
|
panic(errors.ErrBadCACert)
|
|
}
|
|
|
|
ldapClient.ClientCAs = caCertPool
|
|
}
|
|
}
|
|
|
|
if c.Config.HTTP.Auth.HTPasswd.Path != "" {
|
|
f, err := os.Open(c.Config.HTTP.Auth.HTPasswd.Path)
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.Contains(line, ":") {
|
|
tokens := strings.Split(scanner.Text(), ":")
|
|
credMap[tokens[0]] = tokens[1]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if (r.Method == http.MethodGet || r.Method == http.MethodHead) && c.Config.HTTP.AllowReadAccess {
|
|
// Process request
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
if (r.Method != http.MethodGet && r.Method != http.MethodHead) && c.Config.HTTP.ReadOnly {
|
|
// Reject modification requests in read-only mode
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
basicAuth := r.Header.Get("Authorization")
|
|
if basicAuth == "" {
|
|
authFail(w, realm, delay)
|
|
return
|
|
}
|
|
|
|
s := strings.SplitN(basicAuth, " ", 2)
|
|
|
|
if len(s) != 2 || strings.ToLower(s[0]) != "basic" {
|
|
authFail(w, realm, delay)
|
|
return
|
|
}
|
|
|
|
b, err := base64.StdEncoding.DecodeString(s[1])
|
|
if err != nil {
|
|
authFail(w, realm, delay)
|
|
return
|
|
}
|
|
|
|
pair := strings.SplitN(string(b), ":", 2)
|
|
// nolint:gomnd
|
|
if len(pair) != 2 {
|
|
authFail(w, realm, delay)
|
|
return
|
|
}
|
|
|
|
username := pair[0]
|
|
passphrase := pair[1]
|
|
|
|
// first, HTTPPassword authN (which is local)
|
|
passphraseHash, ok := credMap[username]
|
|
if ok {
|
|
if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err == nil {
|
|
// Process request
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
// next, LDAP if configured (network-based which can lose connectivity)
|
|
if c.Config.HTTP.Auth != nil && c.Config.HTTP.Auth.LDAP != nil {
|
|
ok, _, err := ldapClient.Authenticate(username, passphrase)
|
|
if ok && err == nil {
|
|
// Process request
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
authFail(w, realm, delay)
|
|
})
|
|
}
|
|
}
|
|
|
|
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)))
|
|
}
|