mirror of https://github.com/perkeep/perkeep.git
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:
parent
6b7d73d757
commit
12213c058e
|
@ -61,7 +61,6 @@ func init() {
|
||||||
cmd := new(fileCmd)
|
cmd := new(fileCmd)
|
||||||
flags.BoolVar(&cmd.makePermanode, "permanode", false, "Create an associate a new permanode for the uploaded file or directory.")
|
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.")
|
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,
|
flags.BoolVar(&cmd.vivify, "vivify", false,
|
||||||
"If true, ask the server to create and sign permanode(s) associated with each uploaded"+
|
"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"+
|
" 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 {
|
if len(args) == 0 {
|
||||||
return UsageError("No files or directories given.")
|
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 {
|
if c.name != "" && !c.makePermanode {
|
||||||
return UsageError("Can't set name without using --permanode")
|
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}
|
up.fileOpts = &fileOptions{permanode: c.filePermanodes, tag: c.tag, vivify: c.vivify}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -178,6 +188,10 @@ func (c *fileCmd) RunCommand(up *Uploader, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if fi.IsDir() {
|
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 := up.NewTreeUpload(filename)
|
||||||
t.Start()
|
t.Start()
|
||||||
lastPut, err = t.Wait()
|
lastPut, err = t.Wait()
|
||||||
|
|
147
pkg/auth/auth.go
147
pkg/auth/auth.go
|
@ -29,6 +29,21 @@ import (
|
||||||
"camlistore.org/pkg/netutil"
|
"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 kBasicAuthPattern *regexp.Regexp = regexp.MustCompile(`^Basic ([a-zA-Z0-9\+/=]+)`)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -36,8 +51,9 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthMode interface {
|
type AuthMode interface {
|
||||||
// IsAuthorized checks the credentials in req.
|
// AllowedAccess returns a bitmask of all operations
|
||||||
IsAuthorized(req *http.Request) bool
|
// this user/request is allowed to do.
|
||||||
|
AllowedAccess(req *http.Request) Operation
|
||||||
// AddAuthHeader inserts in req the credentials needed
|
// AddAuthHeader inserts in req the credentials needed
|
||||||
// for a client to authenticate.
|
// for a client to authenticate.
|
||||||
AddAuthHeader(req *http.Request)
|
AddAuthHeader(req *http.Request)
|
||||||
|
@ -60,7 +76,8 @@ func FromConfig(authConfig string) (AuthMode, error) {
|
||||||
authType := pieces[0]
|
authType := pieces[0]
|
||||||
|
|
||||||
if pw := os.Getenv("CAMLI_ADVERTISED_PASSWORD"); pw != "" {
|
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
|
return mode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,9 +94,12 @@ func FromConfig(authConfig string) (AuthMode, error) {
|
||||||
password := pieces[2]
|
password := pieces[2]
|
||||||
mode = &UserPass{Username: username, Password: password}
|
mode = &UserPass{Username: username, Password: password}
|
||||||
for _, opt := range pieces[3:] {
|
for _, opt := range pieces[3:] {
|
||||||
switch opt {
|
switch {
|
||||||
case "+localhost":
|
case opt == "+localhost":
|
||||||
mode.(*UserPass).OrLocalhost = true
|
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:
|
default:
|
||||||
return nil, fmt.Errorf("Unknown userpass option %q", opt)
|
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
|
// UserPass is used when the auth string provided in the config
|
||||||
// is of the kind "userpass:username:pass"
|
// 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 {
|
type UserPass struct {
|
||||||
Username, Password string
|
Username, Password string
|
||||||
OrLocalhost bool // if true, allow localhost ident auth too
|
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) {
|
if up.OrLocalhost && localhostAuthorized(req) {
|
||||||
return true
|
return OpAll
|
||||||
}
|
}
|
||||||
|
|
||||||
user, pass, err := basicAuth(req)
|
user, pass, err := basicAuth(req)
|
||||||
if err != nil {
|
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) {
|
func (up *UserPass) AddAuthHeader(req *http.Request) {
|
||||||
|
@ -137,26 +172,55 @@ func (up *UserPass) AddAuthHeader(req *http.Request) {
|
||||||
|
|
||||||
type None struct{}
|
type None struct{}
|
||||||
|
|
||||||
func (None) IsAuthorized(req *http.Request) bool {
|
func (None) AllowedAccess(req *http.Request) Operation {
|
||||||
return true
|
return OpAll
|
||||||
}
|
|
||||||
|
|
||||||
type Localhost struct {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Localhost) IsAuthorized(req *http.Request) bool {
|
|
||||||
return localhostAuthorized(req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (None) AddAuthHeader(req *http.Request) {
|
func (None) AddAuthHeader(req *http.Request) {
|
||||||
// Nothing.
|
// 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
|
// DevAuth is used when the env var CAMLI_ADVERTISED_PASSWORD
|
||||||
// is defined
|
// is defined
|
||||||
type DevAuth struct {
|
type DevAuth struct {
|
||||||
Password string
|
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 {
|
func localhostAuthorized(req *http.Request) bool {
|
||||||
|
@ -196,30 +260,21 @@ func isLocalhost(addrPort net.IP) bool {
|
||||||
return addrPort.IsLoopback()
|
return addrPort.IsLoopback()
|
||||||
}
|
}
|
||||||
|
|
||||||
func LocalhostAuthorized(req *http.Request) bool {
|
func IsLocalhost(req *http.Request) bool {
|
||||||
return localhostAuthorized(req)
|
return localhostAuthorized(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (da *DevAuth) IsAuthorized(req *http.Request) bool {
|
// TODO(mpl): if/when we ever need it:
|
||||||
// First see if the local TCP port is owned by the same
|
// func AllowedWithAuth(am AuthMode, req *http.Request, op Operation) bool
|
||||||
// non-root user as this server.
|
|
||||||
if localhostAuthorized(req) {
|
// Allowed returns whether the given request
|
||||||
return true
|
// 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
|
||||||
}
|
}
|
||||||
|
return mode.AllowedAccess(req)&op == op
|
||||||
_, 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TriedAuthorization(req *http.Request) bool {
|
func TriedAuthorization(req *http.Request) bool {
|
||||||
|
@ -242,8 +297,14 @@ type Handler struct {
|
||||||
http.Handler
|
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) {
|
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)
|
h.Handler.ServeHTTP(w, r)
|
||||||
} else {
|
} else {
|
||||||
SendUnauthorized(w)
|
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
|
// requireAuth wraps a function with another function that enforces
|
||||||
// HTTP Basic Auth.
|
// HTTP Basic Auth and checks if the operations in op are all permitted.
|
||||||
func RequireAuth(handler func(conn http.ResponseWriter, req *http.Request)) func(conn http.ResponseWriter, req *http.Request) {
|
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) {
|
return func(conn http.ResponseWriter, req *http.Request) {
|
||||||
if mode.IsAuthorized(req) {
|
if Allowed(req, op) {
|
||||||
handler(conn, req)
|
handler(conn, req)
|
||||||
} else {
|
} else {
|
||||||
SendUnauthorized(conn)
|
SendUnauthorized(conn)
|
||||||
|
|
|
@ -69,7 +69,7 @@ func (h *Handler) ServeHTTP(conn http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case h.AllowGlobalAccess || auth.IsAuthorized(req):
|
case h.AllowGlobalAccess || auth.Allowed(req, auth.OpGet):
|
||||||
serveBlobRef(conn, req, blobRef, h.Fetcher)
|
serveBlobRef(conn, req, blobRef, h.Fetcher)
|
||||||
case auth.TriedAuthorization(req):
|
case auth.TriedAuthorization(req):
|
||||||
log.Printf("Attempted authorization failed on %s", req.URL)
|
log.Printf("Attempted authorization failed on %s", req.URL)
|
||||||
|
|
|
@ -54,7 +54,7 @@ func RequestEntityTooLargeError(conn http.ResponseWriter) {
|
||||||
|
|
||||||
func ServerError(conn http.ResponseWriter, req *http.Request, err error) {
|
func ServerError(conn http.ResponseWriter, req *http.Request, err error) {
|
||||||
conn.WriteHeader(http.StatusInternalServerError)
|
conn.WriteHeader(http.StatusInternalServerError)
|
||||||
if auth.LocalhostAuthorized(req) {
|
if auth.IsLocalhost(req) {
|
||||||
fmt.Fprintf(conn, "Server error: %s\n", err)
|
fmt.Fprintf(conn, "Server error: %s\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,9 @@ func (rh *RootHandler) registerUIHandler(h *UIHandler) {
|
||||||
|
|
||||||
func (rh *RootHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (rh *RootHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
if wantsDiscovery(req) {
|
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)
|
rh.serveDiscovery(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -104,7 +106,7 @@ func (rh *RootHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
configLink := ""
|
configLink := ""
|
||||||
if auth.LocalhostAuthorized(req) {
|
if auth.IsLocalhost(req) {
|
||||||
configLink = "<p>If you're coming from localhost, hit <a href='/setup'>/setup</a>.</p>"
|
configLink = "<p>If you're coming from localhost, hit <a href='/setup'>/setup</a>.</p>"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(rw, "<html><body>This is camlistored, a "+
|
fmt.Fprintf(rw, "<html><body>This is camlistored, a "+
|
||||||
|
|
|
@ -246,7 +246,7 @@ func handleSetupChange(rw http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sh *SetupHandler) ServeHTTP(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,
|
fmt.Fprintf(rw,
|
||||||
"<html><body>Setup only allowed from localhost"+
|
"<html><body>Setup only allowed from localhost"+
|
||||||
"<p><a href='/'>Back</a></p>"+
|
"<p><a href='/'>Back</a></p>"+
|
||||||
|
|
|
@ -113,23 +113,24 @@ func handleCamliUsingStorage(conn http.ResponseWriter, req *http.Request, action
|
||||||
case "GET":
|
case "GET":
|
||||||
switch action {
|
switch action {
|
||||||
case "enumerate-blobs":
|
case "enumerate-blobs":
|
||||||
handler = auth.RequireAuth(handlers.CreateEnumerateHandler(storage))
|
handler = auth.RequireAuth(handlers.CreateEnumerateHandler(storage), auth.OpGet)
|
||||||
case "stat":
|
case "stat":
|
||||||
handler = auth.RequireAuth(handlers.CreateStatHandler(storage))
|
handler = auth.RequireAuth(handlers.CreateStatHandler(storage), auth.OpAll)
|
||||||
default:
|
default:
|
||||||
handler = gethandler.CreateGetHandler(storage)
|
handler = gethandler.CreateGetHandler(storage)
|
||||||
}
|
}
|
||||||
case "POST":
|
case "POST":
|
||||||
switch action {
|
switch action {
|
||||||
case "stat":
|
case "stat":
|
||||||
handler = auth.RequireAuth(handlers.CreateStatHandler(storage))
|
handler = auth.RequireAuth(handlers.CreateStatHandler(storage), auth.OpStat)
|
||||||
case "upload":
|
case "upload":
|
||||||
handler = auth.RequireAuth(handlers.CreateUploadHandler(storage))
|
handler = auth.RequireAuth(handlers.CreateUploadHandler(storage), auth.OpUpload)
|
||||||
case "remove":
|
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
|
case "PUT": // no longer part of spec
|
||||||
handler = auth.RequireAuth(handlers.CreateNonStandardPutHandler(storage))
|
handler = auth.RequireAuth(handlers.CreateNonStandardPutHandler(storage), auth.OpAll)
|
||||||
}
|
}
|
||||||
handler(conn, req)
|
handler(conn, req)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue