mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2024-01-19 02:48:24 +00:00
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:
parent
4ff4a147b2
commit
215cd27b37
|
@ -10,7 +10,7 @@ func init() {
|
|||
}
|
||||
|
||||
var cmdScaffold = &Command{
|
||||
UsageLine: "scaffold [filer]",
|
||||
UsageLine: "scaffold -config=[filer|notification|replication|security]",
|
||||
Short: "generate basic configuration files",
|
||||
Long: `Generate filer.toml with all possible configurations for you to customize.
|
||||
|
||||
|
@ -244,10 +244,14 @@ directory = "/" # destination directory
|
|||
`
|
||||
|
||||
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
|
||||
|
||||
[jwt]
|
||||
signing_key = ""
|
||||
[jwt.signing]
|
||||
key = ""
|
||||
|
||||
`
|
||||
)
|
||||
|
|
|
@ -42,20 +42,20 @@ https://github.com/pkieltyka/jwtauth/blob/master/jwtauth.go
|
|||
*/
|
||||
type Guard struct {
|
||||
whiteList []string
|
||||
SecretKey SigningKey
|
||||
SigningKey SigningKey
|
||||
|
||||
isActive bool
|
||||
}
|
||||
|
||||
func NewGuard(whiteList []string, secretKey string) *Guard {
|
||||
g := &Guard{whiteList: whiteList, SecretKey: SigningKey(secretKey)}
|
||||
g.isActive = len(g.whiteList) != 0 || len(g.SecretKey) != 0
|
||||
func NewGuard(whiteList []string, signingKey string) *Guard {
|
||||
g := &Guard{whiteList: whiteList, SigningKey: SigningKey(signingKey)}
|
||||
g.isActive = len(g.whiteList) != 0 || len(g.SigningKey) != 0
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *Guard) WhiteList(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
|
||||
//if no security needed, just skip all checking
|
||||
return f
|
||||
}
|
||||
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) {
|
||||
host = r.Header.Get("HTTP_X_FORWARDED_FOR")
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
@ -11,21 +12,28 @@ import (
|
|||
)
|
||||
|
||||
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 {
|
||||
if signingKey == "" {
|
||||
if len(signingKey) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := jwt.New(jwt.GetSigningMethod("HS256"))
|
||||
t.Claims = &jwt.StandardClaims{
|
||||
claims := SeaweedFileIdClaims{
|
||||
fileId,
|
||||
jwt.StandardClaims{
|
||||
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 {
|
||||
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 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
// check exp, nbf
|
||||
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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/chrislusf/seaweedfs/weed/topology"
|
||||
"github.com/chrislusf/seaweedfs/weed/util"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type MasterServer struct {
|
||||
|
@ -47,6 +48,10 @@ func NewMasterServer(r *mux.Router, port int, metaFolder string,
|
|||
whiteList []string,
|
||||
) *MasterServer {
|
||||
|
||||
LoadConfiguration("security", false)
|
||||
v := viper.GetViper()
|
||||
signingKey := v.GetString("jwt.signing.key")
|
||||
|
||||
var preallocateSize int64
|
||||
if preallocate {
|
||||
preallocateSize = int64(volumeSizeLimitMB) * (1 << 20)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/chrislusf/seaweedfs/weed/operation"
|
||||
"github.com/chrislusf/seaweedfs/weed/security"
|
||||
"github.com/chrislusf/seaweedfs/weed/stats"
|
||||
"github.com/chrislusf/seaweedfs/weed/storage"
|
||||
)
|
||||
|
@ -40,13 +41,24 @@ func (ms *MasterServer) lookupVolumeId(vids []string, collection string) (volume
|
|||
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) {
|
||||
vid := r.FormValue("volumeId")
|
||||
if vid != "" {
|
||||
// backward compatible
|
||||
commaSep := strings.Index(vid, ",")
|
||||
if commaSep > 0 {
|
||||
vid = vid[0:commaSep]
|
||||
}
|
||||
}
|
||||
fileId := r.FormValue("fileId")
|
||||
if fileId != "" {
|
||||
commaSep := strings.Index(fileId, ",")
|
||||
if commaSep > 0 {
|
||||
vid = fileId[0:commaSep]
|
||||
}
|
||||
}
|
||||
vids := []string{vid}
|
||||
collection := r.FormValue("collection") //optional, but can be faster if too many collections
|
||||
volumeLocations := ms.lookupVolumeId(vids, collection)
|
||||
|
@ -54,6 +66,8 @@ func (ms *MasterServer) dirLookupHandler(w http.ResponseWriter, r *http.Request)
|
|||
httpStatus := http.StatusOK
|
||||
if location.Error != "" {
|
||||
httpStatus = http.StatusNotFound
|
||||
} else {
|
||||
ms.maybeAddJwtAuthorization(w, fileId)
|
||||
}
|
||||
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)
|
||||
if err == nil {
|
||||
ms.maybeAddJwtAuthorization(w, fid)
|
||||
writeJsonQuiet(w, r, http.StatusOK, operation.AssignResult{Fid: fid, Url: dn.Url(), PublicUrl: dn.PublicUrl, Count: count})
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/chrislusf/seaweedfs/weed/glog"
|
||||
"github.com/chrislusf/seaweedfs/weed/security"
|
||||
"github.com/chrislusf/seaweedfs/weed/storage"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type VolumeServer struct {
|
||||
|
@ -31,6 +32,12 @@ func NewVolumeServer(adminMux, publicMux *http.ServeMux, ip string,
|
|||
whiteList []string,
|
||||
fixJpgOrientation bool,
|
||||
readRedirect bool) *VolumeServer {
|
||||
|
||||
LoadConfiguration("security", false)
|
||||
v := viper.GetViper()
|
||||
signingKey := v.GetString("jwt.signing.key")
|
||||
enableUiAccess := v.GetBool("access.ui")
|
||||
|
||||
vs := &VolumeServer{
|
||||
pulseSeconds: pulseSeconds,
|
||||
dataCenter: dataCenter,
|
||||
|
@ -42,14 +49,17 @@ func NewVolumeServer(adminMux, publicMux *http.ServeMux, ip string,
|
|||
vs.MasterNodes = masterNodes
|
||||
vs.store = storage.NewStore(port, ip, publicUrl, folders, maxCounts, vs.needleMapKind)
|
||||
|
||||
vs.guard = security.NewGuard(whiteList, "")
|
||||
vs.guard = security.NewGuard(whiteList, signingKey)
|
||||
|
||||
handleStaticResources(adminMux)
|
||||
if signingKey == "" || enableUiAccess {
|
||||
// only expose the volume server details for safe environments
|
||||
adminMux.HandleFunc("/ui/index.html", vs.uiStatusHandler)
|
||||
adminMux.HandleFunc("/status", vs.guard.WhiteList(vs.statusHandler))
|
||||
adminMux.HandleFunc("/stats/counter", vs.guard.WhiteList(statsCounterHandler))
|
||||
adminMux.HandleFunc("/stats/memory", vs.guard.WhiteList(statsMemoryHandler))
|
||||
adminMux.HandleFunc("/stats/disk", vs.guard.WhiteList(vs.statsDiskHandler))
|
||||
}
|
||||
adminMux.HandleFunc("/", vs.privateStoreHandler)
|
||||
if publicMux != adminMux {
|
||||
// separated admin and public port
|
||||
|
@ -69,5 +79,5 @@ func (vs *VolumeServer) Shutdown() {
|
|||
}
|
||||
|
||||
func (vs *VolumeServer) jwt(fileId string) security.EncodedJwt {
|
||||
return security.GenJwt(vs.guard.SecretKey, fileId)
|
||||
return security.GenJwt(vs.guard.SigningKey, fileId)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package weed_server
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/chrislusf/seaweedfs/weed/glog"
|
||||
"github.com/chrislusf/seaweedfs/weed/security"
|
||||
"github.com/chrislusf/seaweedfs/weed/stats"
|
||||
)
|
||||
|
||||
|
@ -45,3 +47,32 @@ func (vs *VolumeServer) publicReadOnlyHandler(w http.ResponseWriter, r *http.Req
|
|||
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
|
||||
}
|
||||
|
|
|
@ -20,13 +20,20 @@ func (vs *VolumeServer) PostHandler(w http.ResponseWriter, r *http.Request) {
|
|||
writeJsonError(w, r, http.StatusBadRequest, e)
|
||||
return
|
||||
}
|
||||
vid, _, _, _, _ := parseURLPath(r.URL.Path)
|
||||
|
||||
vid, fid, _, _, _ := parseURLPath(r.URL.Path)
|
||||
volumeId, ve := storage.NewVolumeId(vid)
|
||||
if ve != nil {
|
||||
glog.V(0).Infoln("NewVolumeId error:", ve)
|
||||
writeJsonError(w, r, http.StatusBadRequest, ve)
|
||||
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)
|
||||
if ne != nil {
|
||||
writeJsonError(w, r, http.StatusBadRequest, ne)
|
||||
|
@ -56,6 +63,11 @@ func (vs *VolumeServer) DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|||
volumeId, _ := storage.NewVolumeId(vid)
|
||||
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)
|
||||
|
||||
cookie := n.Cookie
|
||||
|
|
Loading…
Reference in a new issue