github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/pkg/auth/auth.go (about)

     1  /*
     2  Copyright 2011 Google Inc.
     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
    19  
    20  import (
    21  	"crypto/rand"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  	"os"
    26  	"strings"
    27  	"sync"
    28  
    29  	"camlistore.org/pkg/httputil"
    30  )
    31  
    32  // Operation represents a bitmask of operations. See the OpX constants.
    33  type Operation int
    34  
    35  const (
    36  	OpUpload Operation = 1 << iota
    37  	OpStat
    38  	OpGet
    39  	OpEnumerate
    40  	OpRemove
    41  	OpSign
    42  	OpDiscovery
    43  	OpRead   = OpEnumerate | OpStat | OpGet | OpDiscovery
    44  	OpRW     = OpUpload | OpEnumerate | OpStat | OpGet // Not Remove
    45  	OpVivify = OpUpload | OpStat | OpGet | OpDiscovery
    46  	OpAll    = OpUpload | OpEnumerate | OpStat | OpRemove | OpGet | OpSign | OpDiscovery
    47  )
    48  
    49  var (
    50  	mode AuthMode // the auth logic depending on the choosen auth mechanism
    51  )
    52  
    53  // An AuthMode is the interface implemented by diffent authentication
    54  // schemes.
    55  type AuthMode interface {
    56  	// AllowedAccess returns a bitmask of all operations
    57  	// this user/request is allowed to do.
    58  	AllowedAccess(req *http.Request) Operation
    59  	// AddAuthHeader inserts in req the credentials needed
    60  	// for a client to authenticate.
    61  	AddAuthHeader(req *http.Request)
    62  }
    63  
    64  // UnauthorizedSender may be implemented by AuthModes which want to
    65  // handle sending unauthorized.
    66  type UnauthorizedSender interface {
    67  	// SendUnauthorized sends an unauthorized response,
    68  	// and returns whether it handled it.
    69  	SendUnauthorized(http.ResponseWriter, *http.Request) (handled bool)
    70  }
    71  
    72  func FromEnv() (AuthMode, error) {
    73  	return FromConfig(os.Getenv("CAMLI_AUTH"))
    74  }
    75  
    76  // An AuthConfigParser parses a registered authentication type's option
    77  // and returns an AuthMode.
    78  type AuthConfigParser func(arg string) (AuthMode, error)
    79  
    80  var authConstructor = map[string]AuthConfigParser{
    81  	"none":      newNoneAuth,
    82  	"localhost": newLocalhostAuth,
    83  	"userpass":  newUserPassAuth,
    84  	"devauth":   newDevAuth,
    85  	"basic":     newBasicAuth,
    86  }
    87  
    88  // RegisterAuth registers a new authentication scheme.
    89  func RegisterAuth(name string, ctor AuthConfigParser) {
    90  	if _, dup := authConstructor[name]; dup {
    91  		panic("Dup registration of auth mode " + name)
    92  	}
    93  	authConstructor[name] = ctor
    94  }
    95  
    96  func newNoneAuth(string) (AuthMode, error) {
    97  	return None{}, nil
    98  }
    99  
   100  func newLocalhostAuth(string) (AuthMode, error) {
   101  	return Localhost{}, nil
   102  }
   103  
   104  func newDevAuth(pw string) (AuthMode, error) {
   105  	// the vivify mode password is automatically set to "vivi" + Password
   106  	return &DevAuth{
   107  		Password:   pw,
   108  		VivifyPass: "vivi" + pw,
   109  	}, nil
   110  }
   111  
   112  func newUserPassAuth(arg string) (AuthMode, error) {
   113  	pieces := strings.Split(arg, ":")
   114  	if len(pieces) < 2 {
   115  		return nil, fmt.Errorf("Wrong userpass auth string; needs to be \"user:password\"")
   116  	}
   117  	username := pieces[0]
   118  	password := pieces[1]
   119  	mode := &UserPass{Username: username, Password: password}
   120  	for _, opt := range pieces[2:] {
   121  		switch {
   122  		case opt == "+localhost":
   123  			mode.OrLocalhost = true
   124  		case strings.HasPrefix(opt, "vivify="):
   125  			// optional vivify mode password: "userpass:joe:ponies:vivify=rainbowdash"
   126  			mode.VivifyPass = strings.Replace(opt, "vivify=", "", -1)
   127  		default:
   128  			return nil, fmt.Errorf("Unknown userpass option %q", opt)
   129  		}
   130  	}
   131  	return mode, nil
   132  }
   133  
   134  func newBasicAuth(arg string) (AuthMode, error) {
   135  	pieces := strings.Split(arg, ":")
   136  	if len(pieces) != 2 {
   137  		return nil, fmt.Errorf("invalid basic auth syntax. got %q, want \"username:password\"", arg)
   138  	}
   139  	return NewBasicAuth(pieces[0], pieces[1]), nil
   140  }
   141  
   142  // NewBasicAuth returns a UserPass Authmode, adequate to support HTTP
   143  // basic authentication.
   144  func NewBasicAuth(username, password string) AuthMode {
   145  	return &UserPass{
   146  		Username: username,
   147  		Password: password,
   148  	}
   149  }
   150  
   151  // ErrNoAuth is returned when there is no configured authentication.
   152  var ErrNoAuth = errors.New("auth: no configured authentication")
   153  
   154  // FromConfig parses authConfig and accordingly sets up the AuthMode
   155  // that will be used for all upcoming authentication exchanges. The
   156  // supported modes are UserPass and DevAuth. UserPass requires an authConfig
   157  // of the kind "userpass:joe:ponies".
   158  //
   159  // If the input string is empty, the error will be ErrNoAuth.
   160  func FromConfig(authConfig string) (AuthMode, error) {
   161  	if authConfig == "" {
   162  		return nil, ErrNoAuth
   163  	}
   164  	pieces := strings.SplitN(authConfig, ":", 2)
   165  	if len(pieces) < 1 {
   166  		return nil, fmt.Errorf("Invalid auth string: %q", authConfig)
   167  	}
   168  	authType := pieces[0]
   169  
   170  	if fn, ok := authConstructor[authType]; ok {
   171  		arg := ""
   172  		if len(pieces) == 2 {
   173  			arg = pieces[1]
   174  		}
   175  		return fn(arg)
   176  	}
   177  	return nil, fmt.Errorf("Unknown auth type: %q", authType)
   178  }
   179  
   180  // SetMode sets the authentication mode for future requests.
   181  func SetMode(m AuthMode) {
   182  	mode = m
   183  }
   184  
   185  // UserPass is used when the auth string provided in the config
   186  // is of the kind "userpass:username:pass"
   187  // Possible options appended to the config string are
   188  // "+localhost" and "vivify=pass", where pass will be the
   189  // alternative password which only allows the vivify operation.
   190  type UserPass struct {
   191  	Username, Password string
   192  	OrLocalhost        bool // if true, allow localhost ident auth too
   193  	// Alternative password used (only) for the vivify operation.
   194  	// It is checked when uploading, but Password takes precedence.
   195  	VivifyPass string
   196  }
   197  
   198  func (up *UserPass) AllowedAccess(req *http.Request) Operation {
   199  	user, pass, err := httputil.BasicAuth(req)
   200  	if err == nil {
   201  		if user == up.Username {
   202  			if pass == up.Password {
   203  				return OpAll
   204  			}
   205  			if pass == up.VivifyPass {
   206  				return OpVivify
   207  			}
   208  		}
   209  	}
   210  
   211  	if websocketTokenMatches(req) {
   212  		return OpAll
   213  	}
   214  	if up.OrLocalhost && httputil.IsLocalhost(req) {
   215  		return OpAll
   216  	}
   217  
   218  	return 0
   219  }
   220  
   221  func (up *UserPass) AddAuthHeader(req *http.Request) {
   222  	req.SetBasicAuth(up.Username, up.Password)
   223  }
   224  
   225  type None struct{}
   226  
   227  func (None) AllowedAccess(req *http.Request) Operation {
   228  	return OpAll
   229  }
   230  
   231  func (None) AddAuthHeader(req *http.Request) {
   232  	// Nothing.
   233  }
   234  
   235  type Localhost struct {
   236  	None
   237  }
   238  
   239  func (Localhost) AllowedAccess(req *http.Request) (out Operation) {
   240  	if httputil.IsLocalhost(req) {
   241  		return OpAll
   242  	}
   243  	return 0
   244  }
   245  
   246  // DevAuth is used for development.  It has one password and one vivify password, but
   247  // also accepts all passwords from localhost. Usernames are ignored.
   248  type DevAuth struct {
   249  	Password string
   250  	// Password for the vivify mode, automatically set to "vivi" + Password
   251  	VivifyPass string
   252  }
   253  
   254  func (da *DevAuth) AllowedAccess(req *http.Request) Operation {
   255  	_, pass, err := httputil.BasicAuth(req)
   256  	if err == nil {
   257  		if pass == da.Password {
   258  			return OpAll
   259  		}
   260  		if pass == da.VivifyPass {
   261  			return OpVivify
   262  		}
   263  	}
   264  
   265  	if websocketTokenMatches(req) {
   266  		return OpAll
   267  	}
   268  
   269  	// See if the local TCP port is owned by the same non-root user as this
   270  	// server.  This check performed last as it may require reading from the
   271  	// kernel or exec'ing a program.
   272  	if httputil.IsLocalhost(req) {
   273  		return OpAll
   274  	}
   275  
   276  	return 0
   277  }
   278  
   279  func (da *DevAuth) AddAuthHeader(req *http.Request) {
   280  	req.SetBasicAuth("", da.Password)
   281  }
   282  
   283  func IsLocalhost(req *http.Request) bool {
   284  	return httputil.IsLocalhost(req)
   285  }
   286  
   287  // TODO(mpl): if/when we ever need it:
   288  // func AllowedWithAuth(am AuthMode, req *http.Request, op Operation) bool
   289  
   290  // Allowed returns whether the given request
   291  // has access to perform all the operations in op.
   292  func Allowed(req *http.Request, op Operation) bool {
   293  	if op&OpUpload != 0 {
   294  		// upload (at least from camput) requires stat and get too
   295  		op = op | OpVivify
   296  	}
   297  	return mode.AllowedAccess(req)&op == op
   298  }
   299  
   300  func websocketTokenMatches(req *http.Request) bool {
   301  	return req.Method == "GET" &&
   302  		req.Header.Get("Upgrade") == "websocket" &&
   303  		req.FormValue("authtoken") == ProcessRandom()
   304  }
   305  
   306  func TriedAuthorization(req *http.Request) bool {
   307  	// Currently a simple test just using HTTP basic auth
   308  	// (presumably over https); may expand.
   309  	return req.Header.Get("Authorization") != ""
   310  }
   311  
   312  func SendUnauthorized(rw http.ResponseWriter, req *http.Request) {
   313  	if us, ok := mode.(UnauthorizedSender); ok {
   314  		if us.SendUnauthorized(rw, req) {
   315  			return
   316  		}
   317  	}
   318  	realm := "camlistored"
   319  	if devAuth, ok := mode.(*DevAuth); ok {
   320  		realm = "Any username, password is: " + devAuth.Password
   321  	}
   322  	rw.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", realm))
   323  	rw.WriteHeader(http.StatusUnauthorized)
   324  	fmt.Fprintf(rw, "<html><body><h1>Unauthorized</h1>")
   325  }
   326  
   327  type Handler struct {
   328  	http.Handler
   329  }
   330  
   331  // ServeHTTP serves only if this request and auth mode are allowed all Operations.
   332  func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   333  	h.serveHTTPForOp(w, r, OpAll)
   334  }
   335  
   336  // serveHTTPForOp serves only if op is allowed for this request and auth mode.
   337  func (h Handler) serveHTTPForOp(w http.ResponseWriter, r *http.Request, op Operation) {
   338  	if Allowed(r, op) {
   339  		h.Handler.ServeHTTP(w, r)
   340  	} else {
   341  		SendUnauthorized(w, r)
   342  	}
   343  }
   344  
   345  // RequireAuth wraps a function with another function that enforces
   346  // HTTP Basic Auth and checks if the operations in op are all permitted.
   347  func RequireAuth(h http.Handler, op Operation) http.Handler {
   348  	return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
   349  		if Allowed(req, op) {
   350  			h.ServeHTTP(rw, req)
   351  		} else {
   352  			SendUnauthorized(rw, req)
   353  		}
   354  	})
   355  }
   356  
   357  var (
   358  	processRand     string
   359  	processRandOnce sync.Once
   360  )
   361  
   362  func ProcessRandom() string {
   363  	processRandOnce.Do(genProcessRand)
   364  	return processRand
   365  }
   366  
   367  func genProcessRand() {
   368  	processRand = RandToken(20)
   369  }
   370  
   371  // RandToken genererates (with crypto/rand.Read) and returns a token
   372  // that is the hex version (2x size) of size bytes of randomness.
   373  func RandToken(size int) string {
   374  	buf := make([]byte, size)
   375  	if n, err := rand.Read(buf); err != nil || n != len(buf) {
   376  		panic("failed to get random: " + err.Error())
   377  	}
   378  	return fmt.Sprintf("%x", buf)
   379  }