add authorizing fileId write access

need to secure upload/update/delete for benchmark/filer/mount
need to add secure grpc
This commit is contained in:
Chris Lu 2019-02-14 00:08:20 -08:00
parent 4ff4a147b2
commit 215cd27b37
8 changed files with 125 additions and 92 deletions

View file

@ -10,7 +10,7 @@ func init() {
} }
var cmdScaffold = &Command{ var cmdScaffold = &Command{
UsageLine: "scaffold [filer]", UsageLine: "scaffold -config=[filer|notification|replication|security]",
Short: "generate basic configuration files", Short: "generate basic configuration files",
Long: `Generate filer.toml with all possible configurations for you to customize. Long: `Generate filer.toml with all possible configurations for you to customize.
@ -244,10 +244,14 @@ directory = "/" # destination directory
` `
SECURITY_TOML_EXAMPLE = ` SECURITY_TOML_EXAMPLE = `
# Put this file to one of the location, with descending priority
# ./security.toml
# $HOME/.seaweedfs/security.toml
# /etc/seaweedfs/security.toml
# this file is read by master, volume server, and filer # this file is read by master, volume server, and filer
[jwt] [jwt.signing]
signing_key = "" key = ""
` `
) )

View file

@ -42,20 +42,20 @@ https://github.com/pkieltyka/jwtauth/blob/master/jwtauth.go
*/ */
type Guard struct { type Guard struct {
whiteList []string whiteList []string
SecretKey SigningKey SigningKey SigningKey
isActive bool isActive bool
} }
func NewGuard(whiteList []string, secretKey string) *Guard { func NewGuard(whiteList []string, signingKey string) *Guard {
g := &Guard{whiteList: whiteList, SecretKey: SigningKey(secretKey)} g := &Guard{whiteList: whiteList, SigningKey: SigningKey(signingKey)}
g.isActive = len(g.whiteList) != 0 || len(g.SecretKey) != 0 g.isActive = len(g.whiteList) != 0 || len(g.SigningKey) != 0
return g return g
} }
func (g *Guard) WhiteList(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { func (g *Guard) WhiteList(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
if !g.isActive { if !g.isActive {
//if no security needed, just skip all checkings //if no security needed, just skip all checking
return f return f
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
@ -67,20 +67,6 @@ func (g *Guard) WhiteList(f func(w http.ResponseWriter, r *http.Request)) func(w
} }
} }
func (g *Guard) Secure(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
if !g.isActive {
//if no security needed, just skip all checkings
return f
}
return func(w http.ResponseWriter, r *http.Request) {
if err := g.checkJwt(w, r); err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
f(w, r)
}
}
func GetActualRemoteHost(r *http.Request) (host string, err error) { func GetActualRemoteHost(r *http.Request) (host string, err error) {
host = r.Header.Get("HTTP_X_FORWARDED_FOR") host = r.Header.Get("HTTP_X_FORWARDED_FOR")
if host == "" { if host == "" {
@ -130,33 +116,3 @@ func (g *Guard) checkWhiteList(w http.ResponseWriter, r *http.Request) error {
glog.V(0).Infof("Not in whitelist: %s", r.RemoteAddr) glog.V(0).Infof("Not in whitelist: %s", r.RemoteAddr)
return fmt.Errorf("Not in whitelis: %s", r.RemoteAddr) return fmt.Errorf("Not in whitelis: %s", r.RemoteAddr)
} }
func (g *Guard) checkJwt(w http.ResponseWriter, r *http.Request) error {
if g.checkWhiteList(w, r) == nil {
return nil
}
if len(g.SecretKey) == 0 {
return nil
}
tokenStr := GetJwt(r)
if tokenStr == "" {
return ErrUnauthorized
}
// Verify the token
token, err := DecodeJwt(g.SecretKey, tokenStr)
if err != nil {
glog.V(1).Infof("Token verification error from %s: %v", r.RemoteAddr, err)
return ErrUnauthorized
}
if !token.Valid {
glog.V(1).Infof("Token invliad from %s: %v", r.RemoteAddr, tokenStr)
return ErrUnauthorized
}
glog.V(1).Infof("No permission from %s", r.RemoteAddr)
return fmt.Errorf("No write permission from %s", r.RemoteAddr)
}

View file

@ -1,6 +1,7 @@
package security package security
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
@ -11,21 +12,28 @@ import (
) )
type EncodedJwt string type EncodedJwt string
type SigningKey string type SigningKey []byte
type SeaweedFileIdClaims struct {
Fid string `json:"fid"`
jwt.StandardClaims
}
func GenJwt(signingKey SigningKey, fileId string) EncodedJwt { func GenJwt(signingKey SigningKey, fileId string) EncodedJwt {
if signingKey == "" { if len(signingKey) == 0 {
return "" return ""
} }
t := jwt.New(jwt.GetSigningMethod("HS256")) claims := SeaweedFileIdClaims{
t.Claims = &jwt.StandardClaims{ fileId,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Second * 10).Unix(), ExpiresAt: time.Now().Add(time.Second * 10).Unix(),
Subject: fileId, },
} }
encoded, e := t.SignedString(signingKey) t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
encoded, e := t.SignedString([]byte(signingKey))
if e != nil { if e != nil {
glog.V(0).Infof("Failed to sign claims: %v", t.Claims) glog.V(0).Infof("Failed to sign claims %+v: %v", t.Claims, e)
return "" return ""
} }
return EncodedJwt(encoded) return EncodedJwt(encoded)
@ -44,31 +52,15 @@ func GetJwt(r *http.Request) EncodedJwt {
} }
} }
// Get token from cookie
if tokenStr == "" {
cookie, err := r.Cookie("jwt")
if err == nil {
tokenStr = cookie.Value
}
}
return EncodedJwt(tokenStr) return EncodedJwt(tokenStr)
} }
func EncodeJwt(signingKey SigningKey, claims *jwt.StandardClaims) (EncodedJwt, error) {
if signingKey == "" {
return "", nil
}
t := jwt.New(jwt.GetSigningMethod("HS256"))
t.Claims = claims
encoded, e := t.SignedString(signingKey)
return EncodedJwt(encoded), e
}
func DecodeJwt(signingKey SigningKey, tokenString EncodedJwt) (token *jwt.Token, err error) { func DecodeJwt(signingKey SigningKey, tokenString EncodedJwt) (token *jwt.Token, err error) {
// check exp, nbf // check exp, nbf
return jwt.Parse(string(tokenString), func(token *jwt.Token) (interface{}, error) { return jwt.Parse(string(tokenString), func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unknown token method")
}
return signingKey, nil return signingKey, nil
}) })
} }

View file

@ -15,6 +15,7 @@ import (
"github.com/chrislusf/seaweedfs/weed/topology" "github.com/chrislusf/seaweedfs/weed/topology"
"github.com/chrislusf/seaweedfs/weed/util" "github.com/chrislusf/seaweedfs/weed/util"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/spf13/viper"
) )
type MasterServer struct { type MasterServer struct {
@ -47,6 +48,10 @@ func NewMasterServer(r *mux.Router, port int, metaFolder string,
whiteList []string, whiteList []string,
) *MasterServer { ) *MasterServer {
LoadConfiguration("security", false)
v := viper.GetViper()
signingKey := v.GetString("jwt.signing.key")
var preallocateSize int64 var preallocateSize int64
if preallocate { if preallocate {
preallocateSize = int64(volumeSizeLimitMB) * (1 << 20) preallocateSize = int64(volumeSizeLimitMB) * (1 << 20)

View file

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/chrislusf/seaweedfs/weed/operation" "github.com/chrislusf/seaweedfs/weed/operation"
"github.com/chrislusf/seaweedfs/weed/security"
"github.com/chrislusf/seaweedfs/weed/stats" "github.com/chrislusf/seaweedfs/weed/stats"
"github.com/chrislusf/seaweedfs/weed/storage" "github.com/chrislusf/seaweedfs/weed/storage"
) )
@ -40,13 +41,24 @@ func (ms *MasterServer) lookupVolumeId(vids []string, collection string) (volume
return return
} }
// Takes one volumeId only, can not do batch lookup // If "fileId" is provided, this returns the fileId location and a JWT to update or delete the file.
// If "volumeId" is provided, this only returns the volumeId location
func (ms *MasterServer) dirLookupHandler(w http.ResponseWriter, r *http.Request) { func (ms *MasterServer) dirLookupHandler(w http.ResponseWriter, r *http.Request) {
vid := r.FormValue("volumeId") vid := r.FormValue("volumeId")
if vid != "" {
// backward compatible
commaSep := strings.Index(vid, ",") commaSep := strings.Index(vid, ",")
if commaSep > 0 { if commaSep > 0 {
vid = vid[0:commaSep] vid = vid[0:commaSep]
} }
}
fileId := r.FormValue("fileId")
if fileId != "" {
commaSep := strings.Index(fileId, ",")
if commaSep > 0 {
vid = fileId[0:commaSep]
}
}
vids := []string{vid} vids := []string{vid}
collection := r.FormValue("collection") //optional, but can be faster if too many collections collection := r.FormValue("collection") //optional, but can be faster if too many collections
volumeLocations := ms.lookupVolumeId(vids, collection) volumeLocations := ms.lookupVolumeId(vids, collection)
@ -54,6 +66,8 @@ func (ms *MasterServer) dirLookupHandler(w http.ResponseWriter, r *http.Request)
httpStatus := http.StatusOK httpStatus := http.StatusOK
if location.Error != "" { if location.Error != "" {
httpStatus = http.StatusNotFound httpStatus = http.StatusNotFound
} else {
ms.maybeAddJwtAuthorization(w, fileId)
} }
writeJsonQuiet(w, r, httpStatus, location) writeJsonQuiet(w, r, httpStatus, location)
} }
@ -88,8 +102,17 @@ func (ms *MasterServer) dirAssignHandler(w http.ResponseWriter, r *http.Request)
} }
fid, count, dn, err := ms.Topo.PickForWrite(requestedCount, option) fid, count, dn, err := ms.Topo.PickForWrite(requestedCount, option)
if err == nil { if err == nil {
ms.maybeAddJwtAuthorization(w, fid)
writeJsonQuiet(w, r, http.StatusOK, operation.AssignResult{Fid: fid, Url: dn.Url(), PublicUrl: dn.PublicUrl, Count: count}) writeJsonQuiet(w, r, http.StatusOK, operation.AssignResult{Fid: fid, Url: dn.Url(), PublicUrl: dn.PublicUrl, Count: count})
} else { } else {
writeJsonQuiet(w, r, http.StatusNotAcceptable, operation.AssignResult{Error: err.Error()}) writeJsonQuiet(w, r, http.StatusNotAcceptable, operation.AssignResult{Error: err.Error()})
} }
} }
func (ms *MasterServer) maybeAddJwtAuthorization(w http.ResponseWriter, fileId string) {
encodedJwt := security.GenJwt(ms.guard.SigningKey, fileId)
if encodedJwt == "" {
return
}
w.Header().Set("Authorization", "BEARER "+string(encodedJwt))
}

View file

@ -6,6 +6,7 @@ import (
"github.com/chrislusf/seaweedfs/weed/glog" "github.com/chrislusf/seaweedfs/weed/glog"
"github.com/chrislusf/seaweedfs/weed/security" "github.com/chrislusf/seaweedfs/weed/security"
"github.com/chrislusf/seaweedfs/weed/storage" "github.com/chrislusf/seaweedfs/weed/storage"
"github.com/spf13/viper"
) )
type VolumeServer struct { type VolumeServer struct {
@ -31,6 +32,12 @@ func NewVolumeServer(adminMux, publicMux *http.ServeMux, ip string,
whiteList []string, whiteList []string,
fixJpgOrientation bool, fixJpgOrientation bool,
readRedirect bool) *VolumeServer { readRedirect bool) *VolumeServer {
LoadConfiguration("security", false)
v := viper.GetViper()
signingKey := v.GetString("jwt.signing.key")
enableUiAccess := v.GetBool("access.ui")
vs := &VolumeServer{ vs := &VolumeServer{
pulseSeconds: pulseSeconds, pulseSeconds: pulseSeconds,
dataCenter: dataCenter, dataCenter: dataCenter,
@ -42,14 +49,17 @@ func NewVolumeServer(adminMux, publicMux *http.ServeMux, ip string,
vs.MasterNodes = masterNodes vs.MasterNodes = masterNodes
vs.store = storage.NewStore(port, ip, publicUrl, folders, maxCounts, vs.needleMapKind) vs.store = storage.NewStore(port, ip, publicUrl, folders, maxCounts, vs.needleMapKind)
vs.guard = security.NewGuard(whiteList, "") vs.guard = security.NewGuard(whiteList, signingKey)
handleStaticResources(adminMux) handleStaticResources(adminMux)
if signingKey == "" || enableUiAccess {
// only expose the volume server details for safe environments
adminMux.HandleFunc("/ui/index.html", vs.uiStatusHandler) adminMux.HandleFunc("/ui/index.html", vs.uiStatusHandler)
adminMux.HandleFunc("/status", vs.guard.WhiteList(vs.statusHandler)) adminMux.HandleFunc("/status", vs.guard.WhiteList(vs.statusHandler))
adminMux.HandleFunc("/stats/counter", vs.guard.WhiteList(statsCounterHandler)) adminMux.HandleFunc("/stats/counter", vs.guard.WhiteList(statsCounterHandler))
adminMux.HandleFunc("/stats/memory", vs.guard.WhiteList(statsMemoryHandler)) adminMux.HandleFunc("/stats/memory", vs.guard.WhiteList(statsMemoryHandler))
adminMux.HandleFunc("/stats/disk", vs.guard.WhiteList(vs.statsDiskHandler)) adminMux.HandleFunc("/stats/disk", vs.guard.WhiteList(vs.statsDiskHandler))
}
adminMux.HandleFunc("/", vs.privateStoreHandler) adminMux.HandleFunc("/", vs.privateStoreHandler)
if publicMux != adminMux { if publicMux != adminMux {
// separated admin and public port // separated admin and public port
@ -69,5 +79,5 @@ func (vs *VolumeServer) Shutdown() {
} }
func (vs *VolumeServer) jwt(fileId string) security.EncodedJwt { func (vs *VolumeServer) jwt(fileId string) security.EncodedJwt {
return security.GenJwt(vs.guard.SecretKey, fileId) return security.GenJwt(vs.guard.SigningKey, fileId)
} }

View file

@ -3,6 +3,8 @@ package weed_server
import ( import (
"net/http" "net/http"
"github.com/chrislusf/seaweedfs/weed/glog"
"github.com/chrislusf/seaweedfs/weed/security"
"github.com/chrislusf/seaweedfs/weed/stats" "github.com/chrislusf/seaweedfs/weed/stats"
) )
@ -45,3 +47,32 @@ func (vs *VolumeServer) publicReadOnlyHandler(w http.ResponseWriter, r *http.Req
vs.GetOrHeadHandler(w, r) vs.GetOrHeadHandler(w, r)
} }
} }
func (vs *VolumeServer) maybeCheckJwtAuthorization(r *http.Request, vid, fid string) bool {
if len(vs.guard.SigningKey) == 0 {
return true
}
tokenStr := security.GetJwt(r)
if tokenStr == "" {
glog.V(1).Infof("missing jwt from %s", r.RemoteAddr)
return false
}
token, err := security.DecodeJwt(vs.guard.SigningKey, tokenStr)
if err != nil {
glog.V(1).Infof("jwt verification error from %s: %v", r.RemoteAddr, err)
return false
}
if !token.Valid {
glog.V(1).Infof("jwt invalid from %s: %v", r.RemoteAddr, tokenStr)
return false
}
if sc, ok := token.Claims.(*security.SeaweedFileIdClaims); ok {
return sc.Fid == vid+","+fid
}
glog.V(1).Infof("unexpected jwt from %s: %v", r.RemoteAddr, tokenStr)
return false
}

View file

@ -20,13 +20,20 @@ func (vs *VolumeServer) PostHandler(w http.ResponseWriter, r *http.Request) {
writeJsonError(w, r, http.StatusBadRequest, e) writeJsonError(w, r, http.StatusBadRequest, e)
return return
} }
vid, _, _, _, _ := parseURLPath(r.URL.Path)
vid, fid, _, _, _ := parseURLPath(r.URL.Path)
volumeId, ve := storage.NewVolumeId(vid) volumeId, ve := storage.NewVolumeId(vid)
if ve != nil { if ve != nil {
glog.V(0).Infoln("NewVolumeId error:", ve) glog.V(0).Infoln("NewVolumeId error:", ve)
writeJsonError(w, r, http.StatusBadRequest, ve) writeJsonError(w, r, http.StatusBadRequest, ve)
return return
} }
if !vs.maybeCheckJwtAuthorization(r, vid, fid) {
writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
return
}
needle, originalSize, ne := storage.CreateNeedleFromRequest(r, vs.FixJpgOrientation) needle, originalSize, ne := storage.CreateNeedleFromRequest(r, vs.FixJpgOrientation)
if ne != nil { if ne != nil {
writeJsonError(w, r, http.StatusBadRequest, ne) writeJsonError(w, r, http.StatusBadRequest, ne)
@ -56,6 +63,11 @@ func (vs *VolumeServer) DeleteHandler(w http.ResponseWriter, r *http.Request) {
volumeId, _ := storage.NewVolumeId(vid) volumeId, _ := storage.NewVolumeId(vid)
n.ParsePath(fid) n.ParsePath(fid)
if !vs.maybeCheckJwtAuthorization(r, vid, fid) {
writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
return
}
// glog.V(2).Infof("volume %s deleting %s", vid, n) // glog.V(2).Infof("volume %s deleting %s", vid, n)
cookie := n.Cookie cookie := n.Cookie