Home Download Docs Code Community
     1	/*
     2	Copyright 2011 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	// Package auth implements Camlistore authentication.
    18	package auth // import "perkeep.org/pkg/auth"
    19	
    20	import (
    21		"crypto/rand"
    22		"crypto/subtle"
    23		"errors"
    24		"fmt"
    25		"net/http"
    26		"os"
    27		"regexp"
    28		"strings"
    29		"sync"
    30	
    31		"perkeep.org/internal/httputil"
    32	)
    33	
    34	// Operation represents a bitmask of operations. See the OpX constants.
    35	type Operation int
    36	
    37	const (
    38		OpUpload Operation = 1 << iota
    39		OpStat
    40		OpGet
    41		OpEnumerate
    42		OpRemove
    43		OpSign
    44		// OpDiscovery note: since we disclose an OpAll token in the discovery
    45		// response, please bear in mind that granting OpDiscovery is in effect the
    46		// same as granting OpAll for now.
    47		OpDiscovery
    48		OpRead   = OpEnumerate | OpStat | OpGet | OpDiscovery
    49		OpRW     = OpUpload | OpEnumerate | OpStat | OpGet // Not Remove
    50		OpVivify = OpUpload | OpStat | OpGet | OpDiscovery
    51		OpAll    = OpUpload | OpEnumerate | OpStat | OpRemove | OpGet | OpSign | OpDiscovery
    52	)
    53	
    54	const OmitAuthToken = "OmitAuthToken"
    55	
    56	var (
    57		// Each mode defines an auth logic which depends on the chosen auth mechanism.
    58		// Access is allowed if any of the modes allows it.
    59		// No need to guard for now as all the writes are done sequentially during setup.
    60		modes []AuthMode
    61	)
    62	
    63	// An AuthMode is the interface implemented by diffent authentication
    64	// schemes.
    65	type AuthMode interface {
    66		// AllowedAccess returns a bitmask of all operations
    67		// this user/request is allowed to do.
    68		AllowedAccess(req *http.Request) Operation
    69		// AddAuthHeader inserts in req the credentials needed
    70		// for a client to authenticate.
    71		AddAuthHeader(req *http.Request)
    72	}
    73	
    74	// UnauthorizedSender may be implemented by AuthModes which want to
    75	// handle sending unauthorized.
    76	type UnauthorizedSender interface {
    77		// SendUnauthorized sends an unauthorized response,
    78		// and returns whether it handled it.
    79		SendUnauthorized(http.ResponseWriter, *http.Request) (handled bool)
    80	}
    81	
    82	func FromEnv() (AuthMode, error) {
    83		return FromConfig(os.Getenv("CAMLI_AUTH"))
    84	}
    85	
    86	// An AuthConfigParser parses a registered authentication type's option
    87	// and returns an AuthMode.
    88	type AuthConfigParser func(arg string) (AuthMode, error)
    89	
    90	var authConstructor = map[string]AuthConfigParser{
    91		"none":      newNoneAuth,
    92		"localhost": newLocalhostAuth,
    93		"userpass":  newUserPassAuth,
    94		"token":     NewTokenAuth,
    95		"devauth":   newDevAuth,
    96		"basic":     newBasicAuth,
    97	}
    98	
    99	// RegisterAuth registers a new authentication scheme.
   100	func RegisterAuth(name string, ctor AuthConfigParser) {
   101		if _, dup := authConstructor[name]; dup {
   102			panic("Dup registration of auth mode " + name)
   103		}
   104		authConstructor[name] = ctor
   105	}
   106	
   107	func newNoneAuth(string) (AuthMode, error) {
   108		return None{}, nil
   109	}
   110	
   111	func newLocalhostAuth(string) (AuthMode, error) {
   112		return Localhost{}, nil
   113	}
   114	
   115	func newDevAuth(pw string) (AuthMode, error) {
   116		// the vivify mode password is automatically set to "vivi" + Password
   117		vp := "vivi" + pw
   118		return &DevAuth{
   119			Password:   pw,
   120			VivifyPass: &vp,
   121		}, nil
   122	}
   123	
   124	func newUserPassAuth(arg string) (AuthMode, error) {
   125		pieces := strings.Split(arg, ":")
   126		if len(pieces) < 2 {
   127			return nil, fmt.Errorf("Wrong userpass auth string; needs to be \"user:password\"")
   128		}
   129		username := pieces[0]
   130		password := pieces[1]
   131		mode := &UserPass{Username: username, Password: password}
   132		for _, opt := range pieces[2:] {
   133			switch {
   134			case opt == "+localhost":
   135				mode.OrLocalhost = true
   136			case strings.HasPrefix(opt, "vivify="):
   137				// optional vivify mode password: "userpass:joe:ponies:vivify=rainbowdash"
   138				vp := strings.Replace(opt, "vivify=", "", -1)
   139				mode.VivifyPass = &vp
   140			default:
   141				return nil, fmt.Errorf("Unknown userpass option %q", opt)
   142			}
   143		}
   144		return mode, nil
   145	}
   146	
   147	func newBasicAuth(arg string) (AuthMode, error) {
   148		pieces := strings.Split(arg, ":")
   149		if len(pieces) != 2 {
   150			return nil, fmt.Errorf("invalid basic auth syntax. got %q, want \"username:password\"", arg)
   151		}
   152		return NewBasicAuth(pieces[0], pieces[1]), nil
   153	}
   154	
   155	// NewBasicAuth returns a UserPass Authmode, adequate to support HTTP
   156	// basic authentication.
   157	func NewBasicAuth(username, password string) AuthMode {
   158		return &UserPass{
   159			Username: username,
   160			Password: password,
   161		}
   162	}
   163	
   164	// NewTokenAuth returns an Authmode similar to HTTP Basic Auth, which is only
   165	// relying on token instead of a username and password.
   166	func NewTokenAuth(token string) (AuthMode, error) {
   167		return &tokenAuth{token: token}, nil
   168	}
   169	
   170	// ErrNoAuth is returned when there is no configured authentication.
   171	var ErrNoAuth = errors.New("auth: no configured authentication")
   172	
   173	// FromConfig parses authConfig and accordingly sets up the AuthMode
   174	// that will be used for all upcoming authentication exchanges. The
   175	// supported modes are UserPass and DevAuth. UserPass requires an authConfig
   176	// of the kind "userpass:joe:ponies".
   177	//
   178	// If the input string is empty, the error will be ErrNoAuth.
   179	func FromConfig(authConfig string) (AuthMode, error) {
   180		if authConfig == "" {
   181			return nil, ErrNoAuth
   182		}
   183		pieces := strings.SplitN(authConfig, ":", 2)
   184		if len(pieces) < 1 {
   185			return nil, fmt.Errorf("Invalid auth string: %q", authConfig)
   186		}
   187		authType := pieces[0]
   188	
   189		if fn, ok := authConstructor[authType]; ok {
   190			arg := ""
   191			if len(pieces) == 2 {
   192				arg = pieces[1]
   193			}
   194			return fn(arg)
   195		}
   196		return nil, fmt.Errorf("Unknown auth type: %q", authType)
   197	}
   198	
   199	// SetMode sets the given authentication mode as the only allowed one for
   200	// future requests. That is, it replaces all modes that were previously added.
   201	func SetMode(m AuthMode) {
   202		modes = []AuthMode{m}
   203	}
   204	
   205	// AddMode adds the given authentication mode to the list of modes that future
   206	// requests can authenticate against.
   207	func AddMode(am AuthMode) {
   208		modes = append(modes, am)
   209	}
   210	
   211	type tokenAuth struct {
   212		token string
   213	}
   214	
   215	func (t *tokenAuth) AllowedAccess(r *http.Request) Operation {
   216		if authTokenHeaderMatches(r) {
   217			return OpAll
   218		}
   219		if websocketTokenMatches(r) {
   220			return OpAll
   221		}
   222		return 0
   223	}
   224	
   225	func (t *tokenAuth) AddAuthHeader(r *http.Request) {
   226		r.Header.Set("Authorization", "Token "+t.token)
   227	}
   228	
   229	// UserPass is used when the auth string provided in the config
   230	// is of the kind "userpass:username:pass"
   231	// Possible options appended to the config string are
   232	// "+localhost" and "vivify=pass", where pass will be the
   233	// alternative password which only allows the vivify operation.
   234	type UserPass struct {
   235		Username, Password string
   236		OrLocalhost        bool // if true, allow localhost ident auth too
   237	
   238		// VivifyPass, if not nil, is the alternative password used (only) for the vivify operation.
   239		// It is checked when uploading, but Password takes precedence.
   240		VivifyPass *string
   241	}
   242	
   243	func (up *UserPass) AllowedAccess(req *http.Request) Operation {
   244		user, pass, err := httputil.BasicAuth(req)
   245		if err == nil {
   246			if subtle.ConstantTimeCompare([]byte(user), []byte(up.Username)) == 1 {
   247				if subtle.ConstantTimeCompare([]byte(pass), []byte(up.Password)) == 1 {
   248					return OpAll
   249				}
   250				if up.VivifyPass != nil && subtle.ConstantTimeCompare([]byte(pass), []byte(*up.VivifyPass)) == 1 {
   251					return OpVivify
   252				}
   253			}
   254		}
   255	
   256		if authTokenHeaderMatches(req) {
   257			return OpAll
   258		}
   259		if websocketTokenMatches(req) {
   260			return OpAll
   261		}
   262		if up.OrLocalhost && httputil.IsLocalhost(req) {
   263			return OpAll
   264		}
   265	
   266		return 0
   267	}
   268	
   269	func (up *UserPass) AddAuthHeader(req *http.Request) {
   270		req.SetBasicAuth(up.Username, up.Password)
   271	}
   272	
   273	type None struct{}
   274	
   275	func (None) AllowedAccess(req *http.Request) Operation {
   276		return OpAll
   277	}
   278	
   279	func (None) AddAuthHeader(req *http.Request) {
   280		// Nothing.
   281	}
   282	
   283	type Localhost struct {
   284		None
   285	}
   286	
   287	func (Localhost) AllowedAccess(req *http.Request) (out Operation) {
   288		if httputil.IsLocalhost(req) {
   289			return OpAll
   290		}
   291		return 0
   292	}
   293	
   294	// DevAuth is used for development.  It has one password and one vivify password, but
   295	// also accepts all passwords from localhost. Usernames are ignored.
   296	type DevAuth struct {
   297		Password string
   298		// Password for the vivify mode, automatically set to "vivi" + Password
   299		VivifyPass *string
   300	}
   301	
   302	func (da *DevAuth) AllowedAccess(req *http.Request) Operation {
   303		_, pass, err := httputil.BasicAuth(req)
   304		if err == nil {
   305			if pass == da.Password {
   306				return OpAll
   307			}
   308			if da.VivifyPass != nil && pass == *da.VivifyPass {
   309				return OpVivify
   310			}
   311		}
   312	
   313		if authTokenHeaderMatches(req) {
   314			return OpAll
   315		}
   316		if websocketTokenMatches(req) {
   317			return OpAll
   318		}
   319	
   320		// See if the local TCP port is owned by the same non-root user as this
   321		// server.  This check performed last as it may require reading from the
   322		// kernel or exec'ing a program.
   323		if httputil.IsLocalhost(req) {
   324			return OpAll
   325		}
   326	
   327		return 0
   328	}
   329	
   330	func (da *DevAuth) AddAuthHeader(req *http.Request) {
   331		req.SetBasicAuth("", da.Password)
   332	}
   333	
   334	func IsLocalhost(req *http.Request) bool {
   335		return httputil.IsLocalhost(req)
   336	}
   337	
   338	// AllowedWithAuth returns whether the given request
   339	// has access to perform all the operations in op
   340	// against am.
   341	func AllowedWithAuth(am AuthMode, req *http.Request, op Operation) bool {
   342		if op&OpUpload != 0 {
   343			// upload (at least from pk-put) requires stat and get too
   344			op = op | OpVivify
   345		}
   346		return am.AllowedAccess(req)&op == op
   347	}
   348	
   349	// Allowed returns whether the given request
   350	// has access to perform all the operations in op.
   351	func Allowed(req *http.Request, op Operation) bool {
   352		for _, m := range modes {
   353			if AllowedWithAuth(m, req, op) {
   354				return true
   355			}
   356		}
   357		return false
   358	}
   359	
   360	var uiTokenPattern = regexp.MustCompile(`^Token ([a-zA-Z0-9]+)`)
   361	
   362	func authTokenHeaderMatches(req *http.Request) bool {
   363		authHeader := req.Header.Get("Authorization")
   364		if authHeader == "" {
   365			return false
   366		}
   367		matches := uiTokenPattern.FindStringSubmatch(authHeader)
   368		if len(matches) != 2 {
   369			return false
   370		}
   371		return matches[1] == Token()
   372	}
   373	
   374	func websocketTokenMatches(req *http.Request) bool {
   375		return req.Method == "GET" &&
   376			req.Header.Get("Upgrade") == "websocket" &&
   377			req.FormValue("authtoken") == Token()
   378	}
   379	
   380	func TriedAuthorization(req *http.Request) bool {
   381		// Currently a simple test just using HTTP basic auth
   382		// (presumably over https); may expand.
   383		return req.Header.Get("Authorization") != ""
   384	}
   385	
   386	func SendUnauthorized(rw http.ResponseWriter, req *http.Request) {
   387		for _, m := range modes {
   388			if us, ok := m.(UnauthorizedSender); ok {
   389				if us.SendUnauthorized(rw, req) {
   390					return
   391				}
   392			}
   393		}
   394		var realm string
   395		hasDevAuth := func() (*DevAuth, bool) {
   396			for _, m := range modes {
   397				if devAuth, ok := m.(*DevAuth); ok {
   398					return devAuth, ok
   399				}
   400			}
   401			return nil, false
   402		}
   403		if devAuth, ok := hasDevAuth(); ok {
   404			realm = "Any username, password is: " + devAuth.Password
   405		}
   406		// From what I've tested, it looks like sending just "Basic" would be ok,
   407		// but RFC 2617 says realm is mandatory, so probably better to send an empty one.
   408		rw.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", realm))
   409		rw.WriteHeader(http.StatusUnauthorized)
   410		fmt.Fprintf(rw, "<html><body><h1>Unauthorized</h1>")
   411	}
   412	
   413	type Handler struct {
   414		http.Handler
   415	}
   416	
   417	// ServeHTTP serves only if this request and auth mode are allowed all Operations.
   418	func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   419		h.serveHTTPForOp(w, r, OpAll)
   420	}
   421	
   422	// serveHTTPForOp serves only if op is allowed for this request and auth mode.
   423	func (h Handler) serveHTTPForOp(w http.ResponseWriter, r *http.Request, op Operation) {
   424		if Allowed(r, op) {
   425			h.Handler.ServeHTTP(w, r)
   426		} else {
   427			SendUnauthorized(w, r)
   428		}
   429	}
   430	
   431	// RequireAuth wraps a function with another function that enforces
   432	// HTTP Basic Auth and checks if the operations in op are all permitted.
   433	func RequireAuth(h http.Handler, op Operation) http.Handler {
   434		return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
   435			if Allowed(req, op) {
   436				h.ServeHTTP(rw, req)
   437			} else {
   438				SendUnauthorized(rw, req)
   439			}
   440		})
   441	}
   442	
   443	var (
   444		processRand     string
   445		processRandOnce sync.Once
   446	)
   447	
   448	// Token returns a 20 byte token generated (only once, then cached) with RandToken.
   449	// This token is used for authentication by the Camlistore web UI and its
   450	// websockets, therefore it should be handled with care.
   451	func Token() string {
   452		processRandOnce.Do(genProcessRand)
   453		return processRand
   454	}
   455	
   456	// DiscoveryToken returns OmitAuthToken if the first registered auth mode is of
   457	// type None, and Token() otherwise.
   458	func DiscoveryToken() string {
   459		if len(modes) == 0 {
   460			return Token()
   461		}
   462		if _, ok := modes[0].(None); ok {
   463			return OmitAuthToken
   464		}
   465		return Token()
   466	}
   467	
   468	// TokenOrNone returns a token auth mode if token is not OmitAuthToken, and
   469	// otherwise a None auth mode.
   470	func TokenOrNone(token string) (AuthMode, error) {
   471		if token == OmitAuthToken {
   472			return None{}, nil
   473		}
   474		return NewTokenAuth(token)
   475	}
   476	
   477	func genProcessRand() {
   478		processRand = RandToken(20)
   479	}
   480	
   481	// RandToken genererates (with crypto/rand.Read) and returns a token
   482	// that is the hex version (2x size) of size bytes of randomness.
   483	func RandToken(size int) string {
   484		buf := make([]byte, size)
   485		if n, err := rand.Read(buf); err != nil || n != len(buf) {
   486			panic("failed to get random: " + err.Error())
   487		}
   488		return fmt.Sprintf("%x", buf)
   489	}
Website layout inspired by memcached.
Content by the authors.