diff --git a/cmd/camput/files.go b/cmd/camput/files.go index feaab59a0..c0539f29b 100644 --- a/cmd/camput/files.go +++ b/cmd/camput/files.go @@ -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() diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 6a80ac061..64a03a9be 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -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) diff --git a/pkg/blobserver/gethandler/get.go b/pkg/blobserver/gethandler/get.go index 535a3f938..946099dbe 100644 --- a/pkg/blobserver/gethandler/get.go +++ b/pkg/blobserver/gethandler/get.go @@ -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) diff --git a/pkg/httputil/httputil.go b/pkg/httputil/httputil.go index 16f63dee8..4a9f67b54 100644 --- a/pkg/httputil/httputil.go +++ b/pkg/httputil/httputil.go @@ -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 } diff --git a/pkg/server/root.go b/pkg/server/root.go index b364a8794..21442dc2a 100644 --- a/pkg/server/root.go +++ b/pkg/server/root.go @@ -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 = "
If you're coming from localhost, hit /setup.
" } fmt.Fprintf(rw, "This is camlistored, a "+ diff --git a/pkg/server/wizard.go b/pkg/server/wizard.go index c95560234..19ec811f3 100644 --- a/pkg/server/wizard.go +++ b/pkg/server/wizard.go @@ -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, "Setup only allowed from localhost"+ ""+ diff --git a/pkg/serverconfig/serverconfig.go b/pkg/serverconfig/serverconfig.go index f2cbfa439..b6e76eb30 100644 --- a/pkg/serverconfig/serverconfig.go +++ b/pkg/serverconfig/serverconfig.go @@ -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) }