0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00
zot/pkg/api/authn.go
Petu Eusebiu 19003e8a71 Added new extension "sync"
Periodically poll registries and pull images according to sync's config
Added sync on demand, syncing when clients asks for an image which
zot doesn't have.

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
2021-10-21 10:32:46 -07:00

267 lines
6.7 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/anuvu/zot/pkg/api/config"
"github.com/chartmuseum/auth"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
)
const (
bearerAuthDefaultAccessEntryType = "repository"
)
func AuthHandler(c *Controller) mux.MiddlewareFunc {
if isBearerAuthEnabled(c.Config) {
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 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)))
}