Changed auth to take into account not only the credentials,

but the requested operation/action too.

This allows to restrict vivify credentials to only upload
(as well as get and stat, because they're needed) to the
blobserver.

Change-Id: Idaed60d1f0d679cb9795ba9a11f094f964774335
This commit is contained in:
mpl 2013-01-02 23:52:35 +01:00
parent 6b7d73d757
commit 12213c058e
7 changed files with 133 additions and 55 deletions

View File

@ -61,7 +61,6 @@ func init() {
cmd := new(fileCmd)
flags.BoolVar(&cmd.makePermanode, "permanode", false, "Create an associate a new permanode for the uploaded file or directory.")
flags.BoolVar(&cmd.filePermanodes, "filenodes", false, "Create (if necessary) content-based permanodes for each uploaded file.")
// TODO(mpl): check against possibly conflicting flags
flags.BoolVar(&cmd.vivify, "vivify", false,
"If true, ask the server to create and sign permanode(s) associated with each uploaded"+
" file. This permits the server to have your signing key. Used mostly with untrusted"+
@ -103,6 +102,11 @@ func (c *fileCmd) RunCommand(up *Uploader, args []string) error {
if len(args) == 0 {
return UsageError("No files or directories given.")
}
if c.vivify {
if c.makePermanode || c.filePermanodes || c.tag != "" || c.name != "" {
return UsageError("--vivify excludes any other option")
}
}
if c.name != "" && !c.makePermanode {
return UsageError("Can't set name without using --permanode")
}
@ -133,6 +137,12 @@ func (c *fileCmd) RunCommand(up *Uploader, args []string) error {
}
}
}
if c.makePermanode || c.filePermanodes {
testSigBlobRef := up.Client.SignerPublicKeyBlobref()
if testSigBlobRef == nil {
return UsageError("A gpg key is needed to create permanodes; configure one or use vivify mode.")
}
}
up.fileOpts = &fileOptions{permanode: c.filePermanodes, tag: c.tag, vivify: c.vivify}
var (
@ -178,6 +188,10 @@ func (c *fileCmd) RunCommand(up *Uploader, args []string) error {
return err
}
if fi.IsDir() {
if up.fileOpts.wantVivify() {
vlog.Printf("Directories not supported in vivify mode; skipping %v\n", filename)
continue
}
t := up.NewTreeUpload(filename)
t.Start()
lastPut, err = t.Wait()

View File

@ -29,6 +29,21 @@ import (
"camlistore.org/pkg/netutil"
)
// Operation represents a bitmask of operations. See the OpX constants.
type Operation int
const (
OpUpload Operation = 1 << iota
OpStat
OpGet
OpEnumerate
OpRemove
OpRead = OpEnumerate | OpStat | OpGet
OpRW = OpUpload | OpEnumerate | OpStat | OpGet // Not Remove
OpVivify = OpUpload | OpStat | OpGet
OpAll = OpUpload | OpEnumerate | OpStat | OpRemove | OpGet
)
var kBasicAuthPattern *regexp.Regexp = regexp.MustCompile(`^Basic ([a-zA-Z0-9\+/=]+)`)
var (
@ -36,8 +51,9 @@ var (
)
type AuthMode interface {
// IsAuthorized checks the credentials in req.
IsAuthorized(req *http.Request) bool
// AllowedAccess returns a bitmask of all operations
// this user/request is allowed to do.
AllowedAccess(req *http.Request) Operation
// AddAuthHeader inserts in req the credentials needed
// for a client to authenticate.
AddAuthHeader(req *http.Request)
@ -60,7 +76,8 @@ func FromConfig(authConfig string) (AuthMode, error) {
authType := pieces[0]
if pw := os.Getenv("CAMLI_ADVERTISED_PASSWORD"); pw != "" {
mode = &DevAuth{pw}
// the vivify mode password is automatically set to "vivi" + Password
mode = &DevAuth{pw, "vivi" + pw}
return mode, nil
}
@ -77,9 +94,12 @@ func FromConfig(authConfig string) (AuthMode, error) {
password := pieces[2]
mode = &UserPass{Username: username, Password: password}
for _, opt := range pieces[3:] {
switch opt {
case "+localhost":
switch {
case opt == "+localhost":
mode.(*UserPass).OrLocalhost = true
case strings.HasPrefix(opt, "vivify="):
// optional vivify mode password: "userpass:joe:ponies:vivify=rainbowdash"
mode.(*UserPass).VivifyPass = strings.Replace(opt, "vivify=", "", -1)
default:
return nil, fmt.Errorf("Unknown userpass option %q", opt)
}
@ -115,20 +135,35 @@ func basicAuth(req *http.Request) (string, string, error) {
// UserPass is used when the auth string provided in the config
// is of the kind "userpass:username:pass"
// Possible options appended to the config string are
// "+localhost" and "vivify=pass", where pass will be the
// alternative password which only allows the vivify operation.
type UserPass struct {
Username, Password string
OrLocalhost bool // if true, allow localhost ident auth too
// Alternative password used (only) for the vivify operation.
// It is checked when uploading, but Password takes precedence.
VivifyPass string
}
func (up *UserPass) IsAuthorized(req *http.Request) bool {
func (up *UserPass) AllowedAccess(req *http.Request) Operation {
if up.OrLocalhost && localhostAuthorized(req) {
return true
return OpAll
}
user, pass, err := basicAuth(req)
if err != nil {
return false
return 0
}
return user == up.Username && pass == up.Password
if user == up.Username {
if pass == up.Password {
return OpAll
}
if pass == up.VivifyPass {
return OpVivify
}
}
return 0
}
func (up *UserPass) AddAuthHeader(req *http.Request) {
@ -137,26 +172,55 @@ func (up *UserPass) AddAuthHeader(req *http.Request) {
type None struct{}
func (None) IsAuthorized(req *http.Request) bool {
return true
}
type Localhost struct {
None
}
func (Localhost) IsAuthorized(req *http.Request) bool {
return localhostAuthorized(req)
func (None) AllowedAccess(req *http.Request) Operation {
return OpAll
}
func (None) AddAuthHeader(req *http.Request) {
// Nothing.
}
type Localhost struct {
None
}
func (Localhost) AllowedAccess(req *http.Request) Operation {
if localhostAuthorized(req) {
return OpAll
}
return 0
}
// DevAuth is used when the env var CAMLI_ADVERTISED_PASSWORD
// is defined
type DevAuth struct {
Password string
// Password for the vivify mode, automatically set to "vivi" + Password
VivifyPass string
}
func (da *DevAuth) AllowedAccess(req *http.Request) Operation {
// First see if the local TCP port is owned by the same
// non-root user as this server.
if localhostAuthorized(req) {
return OpAll
}
_, pass, err := basicAuth(req)
if err != nil {
return 0
}
if pass == da.Password {
return OpAll
}
if pass == da.VivifyPass {
return OpVivify
}
return 0
}
func (da *DevAuth) AddAuthHeader(req *http.Request) {
req.SetBasicAuth("", da.Password)
}
func localhostAuthorized(req *http.Request) bool {
@ -196,30 +260,21 @@ func isLocalhost(addrPort net.IP) bool {
return addrPort.IsLoopback()
}
func LocalhostAuthorized(req *http.Request) bool {
func IsLocalhost(req *http.Request) bool {
return localhostAuthorized(req)
}
func (da *DevAuth) IsAuthorized(req *http.Request) bool {
// First see if the local TCP port is owned by the same
// non-root user as this server.
if localhostAuthorized(req) {
return true
// TODO(mpl): if/when we ever need it:
// func AllowedWithAuth(am AuthMode, req *http.Request, op Operation) bool
// Allowed returns whether the given request
// has access to perform all the operations in op.
func Allowed(req *http.Request, op Operation) bool {
if op|OpUpload != 0 {
// upload (at least from camput) requires stat and get too
op = op | OpVivify
}
_, pass, err := basicAuth(req)
if err != nil {
return false
}
return pass == da.Password
}
func (da *DevAuth) AddAuthHeader(req *http.Request) {
req.SetBasicAuth("", da.Password)
}
func IsAuthorized(req *http.Request) bool {
return mode.IsAuthorized(req)
return mode.AllowedAccess(req)&op == op
}
func TriedAuthorization(req *http.Request) bool {
@ -242,8 +297,14 @@ type Handler struct {
http.Handler
}
// ServeHTTP serves only if this request and auth mode are allowed all Operations.
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if mode.IsAuthorized(r) {
h.serveHTTPForOp(w, r, OpAll)
}
// serveHTTPForOp serves only if op is allowed for this request and auth mode.
func (h Handler) serveHTTPForOp(w http.ResponseWriter, r *http.Request, op Operation) {
if Allowed(r, op) {
h.Handler.ServeHTTP(w, r)
} else {
SendUnauthorized(w)
@ -251,10 +312,10 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// requireAuth wraps a function with another function that enforces
// HTTP Basic Auth.
func RequireAuth(handler func(conn http.ResponseWriter, req *http.Request)) func(conn http.ResponseWriter, req *http.Request) {
// HTTP Basic Auth and checks if the operations in op are all permitted.
func RequireAuth(handler func(conn http.ResponseWriter, req *http.Request), op Operation) func(conn http.ResponseWriter, req *http.Request) {
return func(conn http.ResponseWriter, req *http.Request) {
if mode.IsAuthorized(req) {
if Allowed(req, op) {
handler(conn, req)
} else {
SendUnauthorized(conn)

View File

@ -69,7 +69,7 @@ func (h *Handler) ServeHTTP(conn http.ResponseWriter, req *http.Request) {
}
switch {
case h.AllowGlobalAccess || auth.IsAuthorized(req):
case h.AllowGlobalAccess || auth.Allowed(req, auth.OpGet):
serveBlobRef(conn, req, blobRef, h.Fetcher)
case auth.TriedAuthorization(req):
log.Printf("Attempted authorization failed on %s", req.URL)

View File

@ -54,7 +54,7 @@ func RequestEntityTooLargeError(conn http.ResponseWriter) {
func ServerError(conn http.ResponseWriter, req *http.Request, err error) {
conn.WriteHeader(http.StatusInternalServerError)
if auth.LocalhostAuthorized(req) {
if auth.IsLocalhost(req) {
fmt.Fprintf(conn, "Server error: %s\n", err)
return
}

View File

@ -89,7 +89,9 @@ func (rh *RootHandler) registerUIHandler(h *UIHandler) {
func (rh *RootHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if wantsDiscovery(req) {
if auth.IsAuthorized(req) {
// TODO(mpl): an OpDiscovery would be more to the point,
// but OpGet is similar/good enough for now.
if auth.Allowed(req, auth.OpGet) {
rh.serveDiscovery(rw, req)
return
}
@ -104,7 +106,7 @@ func (rh *RootHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
configLink := ""
if auth.LocalhostAuthorized(req) {
if auth.IsLocalhost(req) {
configLink = "<p>If you're coming from localhost, hit <a href='/setup'>/setup</a>.</p>"
}
fmt.Fprintf(rw, "<html><body>This is camlistored, a "+

View File

@ -246,7 +246,7 @@ func handleSetupChange(rw http.ResponseWriter, req *http.Request) {
}
func (sh *SetupHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !auth.LocalhostAuthorized(req) {
if !auth.IsLocalhost(req) {
fmt.Fprintf(rw,
"<html><body>Setup only allowed from localhost"+
"<p><a href='/'>Back</a></p>"+

View File

@ -113,23 +113,24 @@ func handleCamliUsingStorage(conn http.ResponseWriter, req *http.Request, action
case "GET":
switch action {
case "enumerate-blobs":
handler = auth.RequireAuth(handlers.CreateEnumerateHandler(storage))
handler = auth.RequireAuth(handlers.CreateEnumerateHandler(storage), auth.OpGet)
case "stat":
handler = auth.RequireAuth(handlers.CreateStatHandler(storage))
handler = auth.RequireAuth(handlers.CreateStatHandler(storage), auth.OpAll)
default:
handler = gethandler.CreateGetHandler(storage)
}
case "POST":
switch action {
case "stat":
handler = auth.RequireAuth(handlers.CreateStatHandler(storage))
handler = auth.RequireAuth(handlers.CreateStatHandler(storage), auth.OpStat)
case "upload":
handler = auth.RequireAuth(handlers.CreateUploadHandler(storage))
handler = auth.RequireAuth(handlers.CreateUploadHandler(storage), auth.OpUpload)
case "remove":
handler = auth.RequireAuth(handlers.CreateRemoveHandler(storage))
handler = auth.RequireAuth(handlers.CreateRemoveHandler(storage), auth.OpAll)
}
// TODO: delete. Replaced with upload helper endpoint.
case "PUT": // no longer part of spec
handler = auth.RequireAuth(handlers.CreateNonStandardPutHandler(storage))
handler = auth.RequireAuth(handlers.CreateNonStandardPutHandler(storage), auth.OpAll)
}
handler(conn, req)
}