github.com/anuvu/zot@v1.3.4/pkg/api/authn.go (about)

     1  package api
     2  
     3  import (
     4  	"bufio"
     5  	"crypto/x509"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/anuvu/zot/errors"
    16  	"github.com/anuvu/zot/pkg/api/config"
    17  	"github.com/chartmuseum/auth"
    18  	"github.com/gorilla/mux"
    19  	"golang.org/x/crypto/bcrypt"
    20  )
    21  
    22  const (
    23  	bearerAuthDefaultAccessEntryType = "repository"
    24  )
    25  
    26  func AuthHandler(c *Controller) mux.MiddlewareFunc {
    27  	if isBearerAuthEnabled(c.Config) {
    28  		return bearerAuthHandler(c)
    29  	}
    30  
    31  	return basicAuthHandler(c)
    32  }
    33  
    34  func bearerAuthHandler(c *Controller) mux.MiddlewareFunc {
    35  	authorizer, err := auth.NewAuthorizer(&auth.AuthorizerOptions{
    36  		Realm:                 c.Config.HTTP.Auth.Bearer.Realm,
    37  		Service:               c.Config.HTTP.Auth.Bearer.Service,
    38  		PublicKeyPath:         c.Config.HTTP.Auth.Bearer.Cert,
    39  		AccessEntryType:       bearerAuthDefaultAccessEntryType,
    40  		EmptyDefaultNamespace: true,
    41  	})
    42  	if err != nil {
    43  		c.Log.Panic().Err(err).Msg("error creating bearer authorizer")
    44  	}
    45  
    46  	return func(next http.Handler) http.Handler {
    47  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    48  			vars := mux.Vars(r)
    49  			name := vars["name"]
    50  			header := r.Header.Get("Authorization")
    51  			action := auth.PullAction
    52  			if m := r.Method; m != http.MethodGet && m != http.MethodHead {
    53  				action = auth.PushAction
    54  			}
    55  			permissions, err := authorizer.Authorize(header, action, name)
    56  			if err != nil {
    57  				c.Log.Error().Err(err).Msg("issue parsing Authorization header")
    58  				w.Header().Set("Content-Type", "application/json")
    59  				WriteJSON(w, http.StatusInternalServerError, NewErrorList(NewError(UNSUPPORTED)))
    60  				return
    61  			}
    62  			if !permissions.Allowed {
    63  				authFail(w, permissions.WWWAuthenticateHeader, 0)
    64  				return
    65  			}
    66  			next.ServeHTTP(w, r)
    67  		})
    68  	}
    69  }
    70  
    71  // nolint:gocyclo  // we use closure making this a complex subroutine
    72  func basicAuthHandler(c *Controller) mux.MiddlewareFunc {
    73  	realm := c.Config.HTTP.Realm
    74  	if realm == "" {
    75  		realm = "Authorization Required"
    76  	}
    77  
    78  	realm = "Basic realm=" + strconv.Quote(realm)
    79  
    80  	// no password based authN, if neither LDAP nor HTTP BASIC is enabled
    81  	if c.Config.HTTP.Auth == nil || (c.Config.HTTP.Auth.HTPasswd.Path == "" && c.Config.HTTP.Auth.LDAP == nil) {
    82  		return func(next http.Handler) http.Handler {
    83  			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    84  				if c.Config.HTTP.AllowReadAccess &&
    85  					c.Config.HTTP.TLS.CACert != "" &&
    86  					r.TLS.VerifiedChains == nil &&
    87  					r.Method != http.MethodGet && r.Method != http.MethodHead {
    88  					authFail(w, realm, 5)
    89  					return
    90  				}
    91  
    92  				if (r.Method != http.MethodGet && r.Method != http.MethodHead) && c.Config.HTTP.ReadOnly {
    93  					// Reject modification requests in read-only mode
    94  					w.WriteHeader(http.StatusMethodNotAllowed)
    95  					return
    96  				}
    97  				// Process request
    98  				next.ServeHTTP(w, r)
    99  			})
   100  		}
   101  	}
   102  
   103  	credMap := make(map[string]string)
   104  
   105  	delay := c.Config.HTTP.Auth.FailDelay
   106  
   107  	var ldapClient *LDAPClient
   108  
   109  	if c.Config.HTTP.Auth != nil {
   110  		if c.Config.HTTP.Auth.LDAP != nil {
   111  			l := c.Config.HTTP.Auth.LDAP
   112  			ldapClient = &LDAPClient{
   113  				Host:               l.Address,
   114  				Port:               l.Port,
   115  				UseSSL:             !l.Insecure,
   116  				SkipTLS:            !l.StartTLS,
   117  				Base:               l.BaseDN,
   118  				BindDN:             l.BindDN,
   119  				BindPassword:       l.BindPassword,
   120  				UserFilter:         fmt.Sprintf("(%s=%%s)", l.UserAttribute),
   121  				InsecureSkipVerify: l.SkipVerify,
   122  				ServerName:         l.Address,
   123  				Log:                c.Log,
   124  				SubtreeSearch:      l.SubtreeSearch,
   125  			}
   126  
   127  			if c.Config.HTTP.Auth.LDAP.CACert != "" {
   128  				caCert, err := ioutil.ReadFile(c.Config.HTTP.Auth.LDAP.CACert)
   129  
   130  				if err != nil {
   131  					panic(err)
   132  				}
   133  
   134  				caCertPool := x509.NewCertPool()
   135  
   136  				if !caCertPool.AppendCertsFromPEM(caCert) {
   137  					panic(errors.ErrBadCACert)
   138  				}
   139  
   140  				ldapClient.ClientCAs = caCertPool
   141  			} else {
   142  				// default to system cert pool
   143  				caCertPool, err := x509.SystemCertPool()
   144  
   145  				if err != nil {
   146  					panic(errors.ErrBadCACert)
   147  				}
   148  
   149  				ldapClient.ClientCAs = caCertPool
   150  			}
   151  		}
   152  
   153  		if c.Config.HTTP.Auth.HTPasswd.Path != "" {
   154  			f, err := os.Open(c.Config.HTTP.Auth.HTPasswd.Path)
   155  
   156  			if err != nil {
   157  				panic(err)
   158  			}
   159  			defer f.Close()
   160  
   161  			scanner := bufio.NewScanner(f)
   162  
   163  			for scanner.Scan() {
   164  				line := scanner.Text()
   165  				if strings.Contains(line, ":") {
   166  					tokens := strings.Split(scanner.Text(), ":")
   167  					credMap[tokens[0]] = tokens[1]
   168  				}
   169  			}
   170  		}
   171  	}
   172  
   173  	return func(next http.Handler) http.Handler {
   174  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   175  			if (r.Method == http.MethodGet || r.Method == http.MethodHead) && c.Config.HTTP.AllowReadAccess {
   176  				// Process request
   177  				next.ServeHTTP(w, r)
   178  				return
   179  			}
   180  
   181  			if (r.Method != http.MethodGet && r.Method != http.MethodHead) && c.Config.HTTP.ReadOnly {
   182  				// Reject modification requests in read-only mode
   183  				w.WriteHeader(http.StatusMethodNotAllowed)
   184  				return
   185  			}
   186  
   187  			basicAuth := r.Header.Get("Authorization")
   188  			if basicAuth == "" {
   189  				authFail(w, realm, delay)
   190  				return
   191  			}
   192  
   193  			s := strings.SplitN(basicAuth, " ", 2)
   194  
   195  			if len(s) != 2 || strings.ToLower(s[0]) != "basic" {
   196  				authFail(w, realm, delay)
   197  				return
   198  			}
   199  
   200  			b, err := base64.StdEncoding.DecodeString(s[1])
   201  			if err != nil {
   202  				authFail(w, realm, delay)
   203  				return
   204  			}
   205  
   206  			pair := strings.SplitN(string(b), ":", 2)
   207  			// nolint:gomnd
   208  			if len(pair) != 2 {
   209  				authFail(w, realm, delay)
   210  				return
   211  			}
   212  
   213  			username := pair[0]
   214  			passphrase := pair[1]
   215  
   216  			// first, HTTPPassword authN (which is local)
   217  			passphraseHash, ok := credMap[username]
   218  			if ok {
   219  				if err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase)); err == nil {
   220  					// Process request
   221  					next.ServeHTTP(w, r)
   222  					return
   223  				}
   224  			}
   225  
   226  			// next, LDAP if configured (network-based which can lose connectivity)
   227  			if c.Config.HTTP.Auth != nil && c.Config.HTTP.Auth.LDAP != nil {
   228  				ok, _, err := ldapClient.Authenticate(username, passphrase)
   229  				if ok && err == nil {
   230  					// Process request
   231  					next.ServeHTTP(w, r)
   232  					return
   233  				}
   234  			}
   235  
   236  			authFail(w, realm, delay)
   237  		})
   238  	}
   239  }
   240  
   241  func isAuthnEnabled(config *config.Config) bool {
   242  	if config.HTTP.Auth != nil &&
   243  		(config.HTTP.Auth.HTPasswd.Path != "" || config.HTTP.Auth.LDAP != nil) {
   244  		return true
   245  	}
   246  
   247  	return false
   248  }
   249  
   250  func isBearerAuthEnabled(config *config.Config) bool {
   251  	if config.HTTP.Auth != nil &&
   252  		config.HTTP.Auth.Bearer != nil &&
   253  		config.HTTP.Auth.Bearer.Cert != "" &&
   254  		config.HTTP.Auth.Bearer.Realm != "" &&
   255  		config.HTTP.Auth.Bearer.Service != "" {
   256  		return true
   257  	}
   258  
   259  	return false
   260  }
   261  
   262  func authFail(w http.ResponseWriter, realm string, delay int) {
   263  	time.Sleep(time.Duration(delay) * time.Second)
   264  	w.Header().Set("WWW-Authenticate", realm)
   265  	w.Header().Set("Content-Type", "application/json")
   266  	WriteJSON(w, http.StatusUnauthorized, NewErrorList(NewError(UNAUTHORIZED)))
   267  }