From 8c013a613b4a502863486a3567d20fb6e41e689a Mon Sep 17 00:00:00 2001 From: xushuhui Date: Mon, 18 Apr 2022 16:02:14 +0800 Subject: [PATCH] feat: (rbac) permission validate / user tags --- internal/biz/account.go | 117 ++++++++++++++++++++++++++++++++++ internal/biz/context.go | 29 +++++++++ internal/middleware/user.go | 36 ++++++++++- model/rbac/role.go | 30 ++++++++- model/rbac/user.go | 1 + plugin/api/account/account.go | 109 +++++++++++++++---------------- plugin/api/rbac/init.go | 5 +- 7 files changed, 264 insertions(+), 63 deletions(-) create mode 100644 internal/biz/account.go create mode 100644 internal/biz/context.go diff --git a/internal/biz/account.go b/internal/biz/account.go new file mode 100644 index 00000000..44941811 --- /dev/null +++ b/internal/biz/account.go @@ -0,0 +1,117 @@ +package biz + +import ( + "errors" + "fmt" + "infini.sh/framework/core/global" + "infini.sh/framework/core/util" + "net/http" + "src/github.com/golang-jwt/jwt" + "strings" + "time" +) + +type UserClaims struct { + *jwt.RegisteredClaims + *User +} +type User struct { + Username string `json:"username"` + UserId string `json:"user_id"` + ApiPermission map[string]struct{} `json:"api_permission"` +} + +const Secret = "console" + +func Login(username string, password string) (m map[string]interface{}, err error) { + + u, _ := global.Env().GetConfig("bootstrap.username", "admin") + p, _ := global.Env().GetConfig("bootstrap.password", "admin") + + if u != username || p != password { + err = errors.New("invalid username or password") + return + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaims{ + User: &User{ + Username: u, + UserId: "admin", + ApiPermission: map[string]struct{}{ + "account.profile": struct{}{}, + }, + }, + RegisteredClaims: &jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + }, + }) + + tokenString, err := token.SignedString([]byte(Secret)) + if err != nil { + + return + } + m = util.MapStr{ + "access_token": tokenString, + "username": u, + "userid": "admin", + } + + return + +} + +func ValidateLogin(authorizationHeader string) (clams *UserClaims, err error) { + + if authorizationHeader == "" { + err = errors.New("authorization header is empty") + return + } + fields := strings.Fields(authorizationHeader) + if fields[0] != "Bearer" || len(fields) != 2 { + err = errors.New("authorization header is invalid") + return + } + tokenString := fields[1] + token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(Secret), nil + }) + if err != nil { + return + } + if clams, ok := token.Claims.(*UserClaims); ok && token.Valid { + return clams, nil + } + if clams.UserId == "" { + err = errors.New("user id is empty") + return + } + return + +} +func ValidatePermission(r *http.Request, permissions string) (err error) { + reqUser, err := FromUserContext(r.Context()) + if err != nil { + + return + } + if reqUser.UserId == "" { + err = errors.New("user id is empty") + return + } + if reqUser.ApiPermission == nil { + err = errors.New("api permission is empty") + return + } + + if _, ok := reqUser.ApiPermission[permissions]; !ok { + err = errors.New("permission denied") + return + } + + return + +} diff --git a/internal/biz/context.go b/internal/biz/context.go new file mode 100644 index 00000000..d4953794 --- /dev/null +++ b/internal/biz/context.go @@ -0,0 +1,29 @@ +package biz + +import ( + "context" + "errors" +) + +const ctxUserKey = "user" + +func NewUserContext(ctx context.Context, clam *UserClaims) context.Context { + return context.WithValue(ctx, ctxUserKey, clam) +} +func FromUserContext(ctx context.Context) (*User, error) { + ctxUser := ctx.Value(ctxUserKey) + if ctxUser == nil { + return nil, errors.New("user not found") + } + reqUser, ok := ctxUser.(*UserClaims) + if !ok { + return nil, errors.New("invalid context user") + } + return reqUser.User, nil +} +func NewPermissionContext(ctx context.Context) { + +} +func FromPermissionContext(ctx context.Context) { + +} diff --git a/internal/middleware/user.go b/internal/middleware/user.go index c8808e79..8c3f9b11 100644 --- a/internal/middleware/user.go +++ b/internal/middleware/user.go @@ -1,8 +1,38 @@ package middleware -func LoginRequired() { +import ( + "infini.sh/console/internal/biz" + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/util" + "net/http" +) -} -func PermissionRequired() { +func LoginRequired(h httprouter.Handle) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + claims, err := biz.ValidateLogin(r.Header.Get("Authorization")) + if err != nil { + w = handleError(w, err) + return + } + r = r.WithContext(biz.NewUserContext(r.Context(), claims)) + h(w, r, ps) + } +} + +func PermissionRequired(h httprouter.Handle, permissions string) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + err := biz.ValidatePermission(r, permissions) + if err != nil { + w = handleError(w, err) + return + } + h(w, r, ps) + } +} +func handleError(w http.ResponseWriter, err error) http.ResponseWriter { + w.Header().Set("Content-type", util.ContentTypeJson) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"error":"` + err.Error() + `"}`)) + return w } diff --git a/model/rbac/role.go b/model/rbac/role.go index 09f407a4..a174a590 100644 --- a/model/rbac/role.go +++ b/model/rbac/role.go @@ -1,6 +1,9 @@ package rbac -import "infini.sh/framework/core/orm" +import ( + "infini.sh/framework/core/orm" + "time" +) type Role struct { orm.ORMObjectBase @@ -20,3 +23,28 @@ type ElasticsearchPermission struct { ClusterPrivilege []string `json:"cluster_privilege" elastic_mapping:"cluster_privilege:{type:object}"` IndexPrivilege []string `json:"index_privilege" elastic_mapping:"index_privilege:{type:object}"` } + +type ConsoleOperate struct { + UserId string `json:"user_id" elastic_mapping:"user_id:{type:keyword}"` +} +type Operation struct { + Id string `json:"id"` + Timestamp time.Time `json:"timestamp"` + Metadata struct { + Labels struct { + Userid string `json:"userid"` + Username string `json:"username"` + } `json:"labels"` + Category string `json:"category"` + Group string `json:"group"` + Name string `json:"name"` + Type string `json:"type"` + } `json:"metadata"` + Changelog []struct { + From string `json:"from"` + Path []string `json:"path"` + To string `json:"to"` + Type string `json:"type"` + } `json:"changelog"` + Payload interface{} `json:"payload"` +} diff --git a/model/rbac/user.go b/model/rbac/user.go index d9883043..d9ebcf32 100644 --- a/model/rbac/user.go +++ b/model/rbac/user.go @@ -10,6 +10,7 @@ type User struct { Phone string `json:"phone" elastic_mapping:"phone:{type:keyword}"` Email string `json:"email" elastic_mapping:"email:{type:keyword}"` Roles []UserRole `json:"roles" elastic_mapping:"roles:{type:text}"` + Tags []string `json:"tags" elastic_mapping:"tags:{type:text}"` } type UserRole struct { Id string `json:"id"` diff --git a/plugin/api/account/account.go b/plugin/api/account/account.go index 7ed4da78..62cf8793 100644 --- a/plugin/api/account/account.go +++ b/plugin/api/account/account.go @@ -1,9 +1,11 @@ package account import ( + "infini.sh/console/internal/biz" + "infini.sh/console/internal/dto" + m "infini.sh/console/internal/middleware" "infini.sh/framework/core/api" - "infini.sh/framework/core/api/router" - "infini.sh/framework/core/global" + "infini.sh/framework/core/api/router" "infini.sh/framework/core/util" "net/http" ) @@ -13,94 +15,87 @@ type Account struct { } func init() { - account:=Account{} - api.HandleAPIMethod(api.POST, "/account/login", account.AccountLogin) + account := Account{} + api.HandleAPIMethod(api.POST, "/account/login", account.Login) + api.HandleAPIMethod(api.GET, "/account/current_user", account.CurrentUser) + + api.HandleAPIMethod(api.DELETE, "/account/logout", account.Logout) + api.HandleAPIMethod(api.GET, "/account/profile", + m.LoginRequired(account.Profile)) } -var userInSession string="user_in_session" +const userInSession = "user_in_session" -func (handler Account)AccountLogin(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { +func (h Account) Login(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - //{"userName":"admin","password":"111111","type":"account"} - - json,err:=handler.GetJSON(req) - if err!=nil{ - handler.Error(w,err) - return - } - userName,err:=json.String("userName") - if err!=nil{ - handler.Error(w,err) - return - } - password,err:=json.String("password") - if err!=nil{ - handler.Error(w,err) + var req dto.Login + err := h.DecodeJSON(r, &req) + if err != nil { + h.Error(w, err) return } - u,_:=global.Env().GetConfig("bootstrap.username","admin") - p,_:=global.Env().GetConfig("bootstrap.password","admin") - if u==userName&&p==password{ - data := util.MapStr{ - "status": "ok", - "type": "account", - "currentAuthority": "admin", - "userid": "10001", - } - api.SetSession(w,req, userInSession,userName) - handler.WriteJSON(w, data, http.StatusOK) - }else{ - data := util.MapStr{ - "status": "error", - "type": "account", - "currentAuthority": "guest", - } - handler.WriteJSON(w, data, http.StatusOK) + data, err := biz.Login(req.Username, req.Password) + if err != nil { + h.Error(w, err) + return } + h.WriteJSON(w, data, http.StatusOK) } -func (handler Account)CurrentUser(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { +func (h Account) CurrentUser(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { - exists,user:=api.GetSession(w,req, userInSession) - if exists{ - data:=util.MapStr{ - "name": user, - "avatar": "", - "userid": "10001", - "email": "hello@infini.ltd", + exists, user := api.GetSession(w, req, userInSession) + if exists { + data := util.MapStr{ + "name": user, + "avatar": "", + "userid": "10001", + "email": "hello@infini.ltd", "signature": "极限科技 - 专业的开源搜索与实时数据分析整体解决方案提供商。", - "title": "首席设计师", - "group": "INFINI Labs", + "title": "首席设计师", + "group": "INFINI Labs", "tags": []util.MapStr{ { - "key": "0", + "key": "0", "label": "很有想法的", }}, "notifyCount": 12, - "country": "China", + "country": "China", "geographic": util.MapStr{ "province": util.MapStr{ "label": "湖南省", - "key": "330000", + "key": "330000", }, "city": util.MapStr{ "label": "长沙市", - "key": "330100", + "key": "330100", }, }, "address": "岳麓区湘江金融中心", - "phone": "4001399200", + "phone": "4001399200", } - handler.WriteJSON(w, data,200) - }else{ + h.WriteJSON(w, data, 200) + } else { data := util.MapStr{ "status": "error", "type": "account", "currentAuthority": "guest", } - handler.WriteJSON(w, data, 403) + h.WriteJSON(w, data, 403) } } +func (h Account) Logout(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + +} +func (h Account) Profile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + reqUser, err := biz.FromUserContext(r.Context()) + if err != nil { + h.Error(w, err) + return + } + h.WriteJSON(w, reqUser, 200) + return +} diff --git a/plugin/api/rbac/init.go b/plugin/api/rbac/init.go index babcf327..283c9b1d 100644 --- a/plugin/api/rbac/init.go +++ b/plugin/api/rbac/init.go @@ -3,6 +3,7 @@ package rbac import ( "encoding/json" "infini.sh/console/internal/biz" + m "infini.sh/console/internal/middleware" "infini.sh/framework/core/api" "infini.sh/framework/core/util" @@ -18,10 +19,10 @@ func registerRouter() { r := Rbac{} api.HandleAPIMethod(api.GET, "/permission/:type", r.ListPermission) api.HandleAPIMethod(api.POST, "/role/:type", r.CreateRole) - api.HandleAPIMethod(api.GET, "/role/:id", r.GetRole) + api.HandleAPIMethod(api.GET, "/role/:id", m.LoginRequired(r.GetRole)) api.HandleAPIMethod(api.DELETE, "/role/:id", r.DeleteRole) api.HandleAPIMethod(api.PUT, "/role/:id", r.UpdateRole) - api.HandleAPIMethod(api.GET, "/role/_search", r.SearchRole) + api.HandleAPIMethod(api.GET, "/role/_search", m.LoginRequired(m.PermissionRequired(r.SearchRole, "search.role"))) api.HandleAPIMethod(api.POST, "/user", r.CreateUser) api.HandleAPIMethod(api.GET, "/user/:id", r.GetUser)