diff --git a/config/map.json b/config/map.json new file mode 100644 index 00000000..cb35bded --- /dev/null +++ b/config/map.json @@ -0,0 +1,274 @@ +{ + "DELETE-/_ingest/pipeline/:id": "ingest.delete_pipeline", + "DELETE-/_scripts/:id": "script.delete", + "DELETE-/_search/scroll": "scroll.delete", + "DELETE-/_search/scroll/:scroll_id":"scroll.delete", + "DELETE-/_snapshot/:repository": "snapshot.delete_repository", + "DELETE-/_snapshot/:repository/:snapshot": "snapshot.delete", + "DELETE-/_template/:name": "indices.delete_template", + "DELETE-/:index": "indices.delete", + "DELETE-/:index/_alias/:name": "indices.delete_alias", + "DELETE-/:index/_aliases/:name": "indices.delete_alias", + "DELETE-/:index/_doc/:id": "doc.delete", + + "GET-/": "cluster.info", + "GET-/_alias": "indices.get_alias", + "GET-/_alias/:name": "indices.get_alias", + "GET-/_analyze": "indices.analyze", + "GET-/_cat": "cat.help", + "GET-/_cat/aliases": "cat.aliases", + "GET-/_cat/aliases/:name": "cat.aliases", + "GET-/_cat/allocation": "cat.allocation", + "GET-/_cat/allocation/:node_id": "cat.allocation", + "GET-/_cat/count": "cat.count", + "GET-/_cat/count/:index": "cat.count", + "GET-/_cat/fielddata": "cat.fielddata", + "GET-/_cat/fielddata/:fields": "cat.fielddata", + "GET-/_cat/health": "cat.health", + "GET-/_cat/indices": "cat.indices", + "GET-/_cat/indices/:index": "cat.indices", + "GET-/_cat/master": "cat.master", + "GET-/_cat/nodeattrs": "cat.nodeattrs", + "GET-/_cat/nodes": "cat.nodes", + "GET-/_cat/pending_tasks": "cat.pending_tasks", + "GET-/_cat/plugins": "cat.plugins", + "GET-/_cat/recovery": "cat.recovery", + "GET-/_cat/recovery/:index": "cat.recovery", + "GET-/_cat/repositories": "cat.repositories", + "GET-/_cat/segments": "cat.segments", + "GET-/_cat/segments/:index": "cat.segments", + "GET-/_cat/shards": "cat.shards", + "GET-/_cat/shards/:index": "cat.shards", + "GET-/_cat/snapshots": "cat.snapshots", + "GET-/_cat/snapshots/:repository": "cat.snapshots", + "GET-/_cat/tasks": "cat.tasks", + "GET-/_cat/templates": "cat.templates", + "GET-/_cat/templates/:name": "cat.templates", + "GET-/_cat/thread_pool": "cat.thread_pool", + "GET-/_cat/thread_pool/:thread_pool_patterns": "cat.thread_pool", + "GET-/_cluster/allocation/explain": "cluster.allocation_explain", + "GET-/_cluster/health": "cluster.health", + "GET-/_cluster/health/:index": "cluster.health", + "GET-/_cluster/nodes/hot_threads": "nodes.hot_threads", + "GET-/_cluster/nodes/hotthreads": "nodes.hot_threads", + "GET-/_cluster/nodes/:node_id/hot_threads": "nodes.hot_threads", + "GET-/_cluster/nodes/:node_id/hotthreads": "nodes.hot_threads", + "GET-/_cluster/pending_tasks": "cluster.pending_tasks", + "GET-/_cluster/settings": "cluster.get_settings", + "GET-/_cluster/state": "cluster.state", + "GET-/_cluster/state/:metric": "cluster.state", + "GET-/_cluster/state/:metric/:index": "cluster.state", + "GET-/_cluster/stats": "cluster.stats", + "GET-/_cluster/stats/nodes/:node_id": "cluster.stats", + "GET-/_count": "cluster.count", + + "GET-/_flush": "indices.flush", + "GET-/_flush/synced": "indices.flush_synced", + "GET-/_ingest/pipeline": "ingest.get_pipeline", + "GET-/_ingest/pipeline/_simulate": "ingest.simulate", + "GET-/_ingest/pipeline/:id": "ingest.get_pipeline", + "GET-/_ingest/pipeline/:id/_simulate": "ingest.simulate", + "GET-/_ingest/processor/grok": "ingest.processor_grok", + "GET-/_mapping": "indices.get_mapping", + + "GET-/_mget": "cluster.mget", + "GET-/_msearch": "cluster.msearch", + "GET-/_msearch/template": "cluster.msearch_template", + "GET-/_mtermvectors": "cluster.mtermvectors", + "GET-/_nodes": "nodes.info", + "GET-/_nodes/hot_threads": "nodes.hot_threads", + "GET-/_nodes/hotthreads": "nodes.hot_threads", + "GET-/_nodes/stats": "nodes.stats", + "GET-/_nodes/stats/:metric": "nodes.stats", + "GET-/_nodes/stats/:metric/:index_metric": "nodes.stats", + "GET-/_nodes/usage": "nodes.usage", + "GET-/_nodes/usage/:metric": "nodes.usage", + "GET-/_nodes/:metric": "nodes.info", + "GET-/_nodes/:node_id": "nodes.info", + "GET-/_nodes/:node_id/hot_threads": "nodes.hot_threads", + "GET-/_nodes/:node_id/hotthreads": "nodes.hot_threads", + "GET-/_nodes/:node_id/stats": "nodes.stats", + "GET-/_nodes/:node_id/stats/:metric": "nodes.stats", + "GET-/_nodes/:node_id/stats/:metric/:index_metric": "nodes.stats", + "GET-/_nodes/:node_id/usage": "nodes.usage", + "GET-/_nodes/:node_id/usage/:metric": "nodes.usage", + "GET-/_nodes/:node_id/:metric": "nodes.info", + "GET-/_rank_eval": "cluster.rank_eval", + "GET-/_recovery": "indices.recovery", + "GET-/_refresh": "indices.refresh", + "GET-/_remote/info": "cluster.remote_info", + "GET-/_render/template": "render_search_template", + "GET-/_render/template/:id": "render_search_template", + "GET-/_scripts/painless/_execute": "scripts.painless_execute", + "GET-/_scripts/:id": "scripts.get", + "GET-/_search": "cluster.search", + "GET-/_search/scroll": "scroll.get", + "GET-/_search/scroll/:scroll_id": "scroll.get", + "GET-/_search/template": "search_template", + "GET-/_search_shards": "cluster.search_shards", + "GET-/_segments": "indices.segments", + "GET-/_settings": "indices.get_settings", + "GET-/_settings/:name": "indices.get_settings", + "GET-/_shard_stores": "indices.shard_stores", + "GET-/_snapshot": "snapshot.get_repository", + "GET-/_snapshot/_status": "snapshot.status", + "GET-/_snapshot/:repository": "snapshot.get_repository", + "GET-/_snapshot/:repository/_status": "snapshot.status", + "GET-/_snapshot/:repository/:snapshot": "snapshot.get", + "GET-/_snapshot/:repository/:snapshot/_status": "snapshot.status", + "GET-/_stats": "indices.stats", + "GET-/_stats/:metric": "indices.stats", + "GET-/_tasks": "tasks.list", + "GET-/_tasks/:task_id": "tasks.get", + "GET-/_template": "indices.get_template", + "GET-/_template/:name": "indices.get_template", + "GET-/_upgrade": "indices.get_upgrade", + "GET-/_validate/query": "indices.validate_query", + "GET-/:index": "indices.get", + "GET-/:index/_alias": "indices.get_alias", + "GET-/:index/_alias/:name": "indices.get_alias", + "GET-/:index/_analyze": "indices.analyze", + "GET-/:index/_count": "indices.count", + "GET-/:index/_doc/:id": "doc.get", + "GET-/:index/_field_caps": "indices.field_caps", + "GET-/:index/_flush": "indices.flush", + "GET-/:index/_flush/synced": "indices.flush_synced", + "GET-/:index/_mapping": "indices.get_mapping", + "GET-/:index/_mappings": "indices.get_mapping", + "GET-/:index/_mapping/field/:fields": "indices.get_field_mapping", + + "GET-/:index/_mget": "indices.mget", + "GET-/:index/_msearch": "indices.msearch", + "GET-/:index/_msearch/template": "indices.msearch_template", + "GET-/:index/_mtermvectors": "indices.mtermvectors", + "GET-/:index/_rank_eval": "indices.rank_eval", + "GET-/:index/_recovery": "indices.recovery", + "GET-/:index/_refresh": "indices.refresh", + "GET-/:index/_search": "indices.search", + "GET-/:index/_search/template": "indices.search_template", + "GET-/:index/_search_shards": "indices.search_shards", + "GET-/:index/_segments": "indices.segments", + "GET-/:index/_settings": "indices.get_settings", + "GET-/:index/_settings/:name": "indices.get_settings", + "GET-/:index/_shard_stores": "indices.shard_stores", + "GET-/:index/_stats": "indices.stats", + "GET-/:index/_stats/:metric": "indices.stats", + "GET-/:index/_upgrade": "indices.get_upgrade", + "GET-/:index/_validate/query": "indices.validate_query", + + "HEAD-/": "cluster.info", + "HEAD-/_alias/:name": "indices.exists_alias", + "HEAD-/_template/:name": "indices.exists_template", + "HEAD-/:index": "indices.exists", + "HEAD-/:index/_alias/:name": "indices.exists_alias", + "HEAD-/:index/_analyze": "indices.analyze", + "HEAD-/:index/_mapping/{type}": "indices.exists_type", + + "HEAD-/:index/_doc/{id}": "doc.exists", + "HEAD-/:index/_doc/{id}/_source": "doc.exists_source", + + "POST-/_aliases": "indices.update_aliases", + "POST-/_analyze": "indices.analyze", + "POST-/_bulk": "cluster.bulk", + "POST-/_cache/clear": "indices.clear_cache", + "POST-/_cluster/allocation/explain": "cluster.allocation_explain", + "POST-/_cluster/reroute": "cluster.reroute", + "POST-/_count": "cluster.count", + "POST-/_delete_by_query/:task_id/_rethrottle": "reindex_rethrottle", + "POST-/_field_caps": "cluster.field_caps", + "POST-/_flush": "indices.flush", + "POST-/_flush/synced": "indices.flush_synced", + "POST-/_forcemerge": "indices.forcemerge", + "POST-/_ingest/pipeline/_simulate": "ingest.simulate", + "POST-/_ingest/pipeline/:id/_simulate": "ingest.simulate", + + "POST-/_mget": "cluster.mget", + "POST-/_msearch": "cluster.msearch", + "POST-/_msearch/template": "cluster.msearch_template", + "POST-/_mtermvectors": "cluster.mtermvectors", + "POST-/_nodes/reload_secure_settings": "nodes.reload_secure_settings", + "POST-/_nodes/:node_id/reload_secure_settings": "nodes.reload_secure_settings", + "POST-/_rank_eval": "cluster.rank_eval", + "POST-/_refresh": "indices.refresh", + "POST-/_reindex": "reindex", + "POST-/_reindex/:task_id/_rethrottle": "reindex_rethrottle", + "POST-/_render/template": "render_search_template.create", + "POST-/_render/template/:id": "render_search_template.get", + "POST-/_scripts/painless/_execute": "scripts_painless_execute", + "POST-/_scripts/:id": "scripts.put", + "POST-/_scripts/:id/:context": "scripts.put", + + "POST-/_search/scroll": "scroll.create", + "POST-/_search/scroll/:scroll_id": "scroll.create", + "POST-/_search/template": "search_template", + + "POST-/_snapshot/:repository": "snapshot.create_repository", + "POST-/_snapshot/:repository/_verify": "snapshot.verify_repository", + "POST-/_snapshot/:repository/:snapshot": "snapshot.create", + "POST-/_snapshot/:repository/:snapshot/_restore": "snapshot.restore", + "POST-/_tasks/_cancel": "tasks.cancel", + "POST-/_tasks/:task_id/_cancel": "tasks.cancel", + "POST-/_template/:name": "indices.put_template", + "POST-/_update_by_query/:task_id/_rethrottle": "reindex_rethrottle", + "POST-/_upgrade": "indices.upgrade", + "POST-/_validate/query": "indices.validate_query", + "POST-/:alias/_rollover": "indices.rollover", + "POST-/:alias/_rollover/:new_index": "indices.rollover", + "POST-/:index/_alias/:name": "indices.put_alias", + "POST-/:index/_aliases/:name": "indices.put_alias", + "POST-/:index/_analyze": "indices.analyze", + "POST-/:index/_bulk": "indices.bulk", + "POST-/:index/_cache/clear": "indices.clear_cache", + "POST-/:index/_close": "indices.close", + "POST-/:index/_count": "indices.count", + "POST-/:index/_delete_by_query": "indices.delete_by_query", + "POST-/:index/_doc": "doc.create", + "POST-/:index/_doc/:id": "doc.update", + "POST-/:index/_doc/:id/_update": "doc.update", + "POST-/:index/_field_caps": "indices.field_caps", + "POST-/:index/_flush": "indices.flush", + "POST-/:index/_flush/synced": "indices.flush_synced", + "POST-/:index/_forcemerge": "indices.forcemerge", + + "POST-/:index/_mget": "indices.mget", + "POST-/:index/_msearch": "indices.msearch", + "POST-/:index/_msearch/template": "indices.msearch_template", + "POST-/:index/_mtermvectors": "indices.mtermvectors", + "POST-/:index/_open": "indices.open", + "POST-/:index/_rank_eval": "indices.rank_eval", + "POST-/:index/_refresh": "indices.refresh", + "POST-/:index/_search": "indices.search", + "POST-/:index/_search/template": "indices.search_template", + "POST-/:index/_search_shards": "indices.search_shards", + "POST-/:index/_shrink/:target": "indices.shrink", + "POST-/:index/_split/:target": "indices.split", + "POST-/:index/_update_by_query": "indices.update_by_query", + "POST-/:index/_upgrade": "indices.upgrade", + "POST-/:index/_validate/query": "indices.validate_query", + + "POST-/:index/_mapping": "indices.put_mapping", + "POST-/:index/_mappings": "indices.put_mapping", + "PUT-/_bulk": "cluster.bulk", + "PUT-/_cluster/settings": "cluster.put_settings", + "PUT-/_ingest/pipeline/:id": "ingest.put_pipeline", + + "PUT-/_scripts/:id": "scripts.put", + "PUT-/_scripts/:id/:context": "scripts.put", + "PUT-/_settings": "cluster.settings_put", + "PUT-/_snapshot/:repository": "snapshot.create_repository", + "PUT-/_snapshot/:repository/:snapshot": "snapshot.create", + "PUT-/_template/:name": "indices.put_template", + "PUT-/:index": "indices.put", + "PUT-/:index/_alias/:name": "indices.put_alias", + "PUT-/:index/_aliases/:name": "indices.put_alias", + "PUT-/:index/_bulk": "indices.put_bulk", + + "PUT-/:index/_doc/:id": "doc.put", + + "PUT-/:index/_settings": "indices.put_settings", + "PUT-/:index/_shrink/:target": "indices.shrink", + "PUT-/:index/_split/:target": "indices.split", + + "PUT-/:index/_mapping": "indices.put_mapping", + "PUT-/:index/_mappings": "indices.put_mapping" +} \ No newline at end of file diff --git a/config/permission.json b/config/permission.json new file mode 100644 index 00000000..c5d94519 --- /dev/null +++ b/config/permission.json @@ -0,0 +1,186 @@ +{ + + "cat": [ + "cat.*", + "cat.indices", + "cat.help", + "cat.repositories", + "cat.pending_tasks", + "cat.tasks", + "cat.allocation", + "cat.count", + "cat.shards", + "cat.aliases", + "cat.nodeattrs", + "cat.templates", + "cat.thread_pool", + "cat.health", + "cat.recovery", + "cat.fielddata", + "cat.nodes", + "cat.plugins", + "cat.segments", + "cat.snapshots", + "cat.master" + ], + "cluster": [ + "cluster.*", + "cluster.health", + "cluster.get_settings", + "cluster.pending_tasks", + "cluster.stats", + "cluster.remote_info", + "cluster.allocation_explain", + "cluster.put_settings", + "cluster.reroute", + "cluster.count", + "cluster.state", + "cluster.info", + "cluster.bulk", + "cluster.mget", + "cluster.ping", + "cluster.msearch", + "cluster.msearch_template", + "cluster.mtermvectors", + "cluster.rank_eval", + "cluster.search", + "cluster.search_shards" + ], + + "doc": [ + "doc.*", + "doc.update", + + "doc.create", + "doc.delete", + "doc.get", + "doc.exists", + "doc.count", + "doc.exists_source", + "doc.bulk", + "doc.explain", + "doc.mget", + "doc.msearch", + "doc.msearch_template", + "doc.mtermvectors", + "doc.search" + ], + + "indices": [ + "indices.*", + "indices.exists_alias", + "indices.get_alias", + "indices.recovery", + "indices.delete", + "indices.put", + "indices.clear_cache", + "indices.update_by_query", + "indices.shrink", + "indices.forcemerge", + "indices.put_alias", + "indices.create", + "indices.split", + "indices.flush", + "indices.get_mapping", + "indices.upgrade", + "indices.validate_query", + "indices.exists_template", + "indices.get_upgrade", + "indices.update_aliases", + "indices.analyze", + "indices.exists", + "indices.close", + "indices.delete_template", + "indices.get_field_mapping", + "indices.delete_alias", + "indices.exists_type", + "indices.get_template", + "indices.put_template", + "indices.refresh", + "indices.segments", + "indices.termvectors", + "indices.flush_synced", + "indices.put_mapping", + "indices.get", + "indices.get_settings", + "indices.open", + "indices.put_settings", + "indices.stats", + "indices.delete_by_query", + "indices.rollover", + "indices.count", + "indices.shard_stores", + "indices.bulk", + "indices.mget", + "indices.msearch", + "indices.msearch_template", + "indices.mtermvectors", + "indices.rank_eval", + "indices.search", + "indices.search_shards", + "indices.field_caps" + ], + + "ingest": [ + "ingest.*", + "ingest.delete_pipeline", + "ingest.put_pipeline", + "ingest.simulate", + "ingest.get_pipeline", + "ingest.processor_grok" + ], + + "nodes": [ + "nodes.*", + "nodes.info", + "nodes.stats", + "nodes.reload_secure_settings", + "nodes.usage", + "nodes.hot_threads" + ], + + "reindex": [ + "reindex.*", + "reindex.rethrottle" + ], + + "render_search_template": [ + "render_search_template.*", + "render_search_template.create", + "render_search_template.*" + ], + "scripts": [ + "scripts.*", + "scripts.get", + "scripts.put", + "scripts.delete", + "scripts.painless_execute" + ], + + "scroll": [ + "scroll.*", + "scroll.delete", + "scroll.get", + "scroll.create" + ], + + "snapshot": [ + "snapshot.*", + "snapshot.get_repository", + "snapshot.create_repository", + "snapshot.create", + "snapshot.restore", + "snapshot.status", + "snapshot.delete", + "snapshot.delete_repository", + "snapshot.verify_repository", + "snapshot.get" + ], + + "tasks": [ + "tasks.*", + "tasks.list", + "tasks.cancel", + "tasks.get" + ] +} \ No newline at end of file diff --git a/internal/biz/account.go b/internal/biz/account.go new file mode 100644 index 00000000..6513179f --- /dev/null +++ b/internal/biz/account.go @@ -0,0 +1,219 @@ +package biz + +import ( + "errors" + "github.com/golang-jwt/jwt" + "github.com/mitchellh/mapstructure" + "golang.org/x/crypto/bcrypt" + "infini.sh/console/internal/dto" + "infini.sh/console/model/rbac" + "infini.sh/framework/core/event" + "infini.sh/framework/core/global" + "infini.sh/framework/core/orm" + "infini.sh/framework/core/util" + "time" +) + +type UserClaims struct { + *jwt.RegisteredClaims + *User +} +type User struct { + Username string `json:"username"` + UserId string `json:"user_id"` + Roles []string `json:"roles"` +} +type Account struct { + ID string `json:"id,omitempty" ` + Created string `json:"created,omitempty" ` + Updated string `json:"updated,omitempty" ` + Username string `json:"username" elastic_mapping:"username:{type:keyword}"` + Password string `json:"password" elastic_mapping:"password:{type:text}"` + Name string `json:"name" elastic_mapping:"name:{type:keyword}"` + Phone string `json:"phone" elastic_mapping:"phone:{type:keyword}"` + Email string `json:"email" elastic_mapping:"email:{type:keyword}"` + Tags []string `json:"tags" elastic_mapping:"tags:{type:text}"` + Roles []rbac.UserRole `json:"roles"` +} + +const Secret = "console" + +func authenticateUser(username string, password string) (user Account, err error) { + + err, result := orm.GetBy("name", username, rbac.User{}) + if err != nil { + err = ErrNotFound + return + } + if result.Total == 0 { + err = errors.New("user not found") + return + } + + err = mapstructure.Decode(result.Result[0], &user) + if err != nil { + return + } + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + if err == bcrypt.ErrMismatchedHashAndPassword { + err = errors.New("password incorrect") + return + } + + return +} +func authenticateAdmin(username string, password string) (user Account, 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 + } + user.ID = username + user.Username = username + user.Roles = []rbac.UserRole{{ + ID: "admin", Name: "admin", + }} + return user, nil +} +func authorize(user Account) (m map[string]interface{}, err error) { + + var roles, privilege []string + for _, v := range user.Roles { + role := RoleMap[v.Name] + roles = append(roles, v.Name) + privilege = append(privilege, role.Privilege.Platform...) + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaims{ + User: &User{ + Username: user.Username, + UserId: user.ID, + Roles: roles, + }, + 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": user.Username, + "id": user.ID, + "expire_in": 86400, + "roles": roles, + "privilege": privilege, + } + return +} +func Login(username string, password string) (m map[string]interface{}, err error) { + var user Account + if username == "admin" { + user, err = authenticateAdmin(username, password) + if err != nil { + return nil, err + } + + } else { + user, err = authenticateUser(username, password) + if err != nil { + return nil, err + } + } + + m, err = authorize(user) + if err != nil { + return + } + TokenMap[user.ID] = Token{ExpireIn: time.Now().Unix() + 86400} + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "login", + Type: "create", + Labels: util.MapStr{ + "username": username, + "password": password, + }, + User: util.MapStr{ + "id": user.ID, + "name": user.Username, + }, + }, nil, nil)) + return +} +func UpdatePassword(localUser *User, req dto.UpdatePassword) (err error) { + user := rbac.User{} + user.ID = localUser.UserId + _, err = orm.Get(&user) + if err != nil { + + return + } + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)) + if err == bcrypt.ErrMismatchedHashAndPassword { + err = errors.New("old password is not correct") + return + } + hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) + if err != nil { + return + } + user.Password = string(hash) + err = orm.Save(&user) + if err != nil { + return + } + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "user", + Type: "update", + Labels: util.MapStr{ + "old_password": req.OldPassword, + "new_password": req.NewPassword, + }, + User: util.MapStr{ + "id": user.ID, + "name": user.Name, + }, + }, nil, nil)) + return +} +func UpdateProfile(localUser *User, req dto.UpdateProfile) (err error) { + user := rbac.User{} + user.ID = localUser.UserId + _, err = orm.Get(&user) + if err != nil { + return + } + user.Name = req.Name + user.Email = req.Email + user.Phone = req.Phone + err = orm.Save(&user) + if err != nil { + return + } + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "user", + Type: "update", + Labels: util.MapStr{ + "name": req.Name, + "email": req.Email, + "phone": req.Phone, + }, + User: util.MapStr{ + "id": user.ID, + "name": user.Name, + }, + }, nil, nil)) + return +} diff --git a/internal/biz/context.go b/internal/biz/context.go new file mode 100644 index 00000000..2d4a8e49 --- /dev/null +++ b/internal/biz/context.go @@ -0,0 +1,23 @@ +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 +} diff --git a/internal/biz/enum/const.go b/internal/biz/enum/const.go new file mode 100644 index 00000000..5a313392 --- /dev/null +++ b/internal/biz/enum/const.go @@ -0,0 +1,192 @@ +package enum + +import ( + "time" +) + +var PermissionMap = make(map[string][]string) + +const ( + UserRead = "system.user:read" + UserAll = "system.user:all" + RoleRead = "system.role:read" + RoleAll = "system.role:all" + ClusterAll = "system.cluster:all" + ClusterRead = "system.cluster:read" + CommandAll = "system.command:all" + CommandRead = "system.command:read" + + InstanceRead = "gateway.instance:read" + InstanceAll = "gateway.instance:all" + EntryAll = "gateway.entry:all" + EntryRead = "gateway.entry:read" + RouterRead = "gateway.router:read" + RouterAll = "gateway.router:all" + FlowRead = "gateway.flow:read" + FlowAll = "gateway.flow:all" + + IndexAll = "data.index:all" + IndexRead = "data.index:read" + ViewsAll = "data.view:all" + ViewsRead = "data.view:read" + DiscoverAll = "data.discover:all" + DiscoverRead = "data.discover:read" + + RuleRead = "alerting.rule:read" + RuleAll = "alerting.rule:all" + AlertRead = "alerting.alert:read" + AlertAll = "alerting.alert:all" + ChannelRead = "alerting.channel:read" + ChannelAll = "alerting.channel:all" + + ClusterOverviewRead = "cluster.overview:read" + ClusterOverviewAll = "cluster.overview:all" + MonitoringRead = "cluster.monitoring:read" + MonitoringAll = "cluster.monitoring:all" + ActivitiesRead = "cluster.activities:read" + ActivitiesAll = "cluster.activities:all" +) + +const ( + PermissionUserRead string = "user:read" + PermissionUserWrite = "user:write" + PermissionRoleRead = "role:read" + PermissionRoleWrite = "role:write" + PermissionCommandRead = "command:read" + PermissionCommandWrite = "command:write" + PermissionElasticsearchClusterRead = "es.cluster:read" + PermissionElasticsearchClusterWrite = "es.cluster:write" // es cluster + PermissionElasticsearchIndexRead = "es.index:read" + PermissionElasticsearchIndexWrite = "es.index:write" // es index metadata + PermissionElasticsearchNodeRead = "es.node:read" //es node metadata + PermissionActivityRead = "activity:read" + PermissionActivityWrite = "activity:write" + PermissionAlertRuleRead = "alert.rule:read" + PermissionAlertRuleWrite = "alert.rule:write" + PermissionAlertHistoryRead = "alert.history:read" + PermissionAlertHistoryWrite = "alert.history:write" + PermissionAlertChannelRead = "alert.channel:read" + PermissionAlertChannelWrite = "alert.channel:write" + PermissionViewRead = "view:read" + PermissionViewWrite = "view:write" + PermissionGatewayInstanceRead = "gateway.instance:read" + PermissionGatewayInstanceWrite = "gateway.instance:write" + PermissionGatewayEntryRead = "gateway.entry:read" + PermissionGatewayEntryWrite = "gateway.entry:write" + PermissionGatewayRouterRead = "gateway.router:read" + PermissionGatewayRouterWrite = "gateway.router:write" + PermissionGatewayFlowRead = "gateway.flow:read" + PermissionGatewayFlowWrite = "gateway.flow:write" + PermissionElasticsearchMetricRead = "es.metric:read" +) + +var ( + UserReadPermission = []string{PermissionUserRead} + UserAllPermission = []string{PermissionUserRead, PermissionUserWrite,PermissionRoleRead} + + RoleReadPermission = []string{PermissionRoleRead} + RoleAllPermission = []string{PermissionRoleRead, PermissionRoleWrite} + + ClusterReadPermission = []string{PermissionElasticsearchClusterRead} + ClusterAllPermission = []string{PermissionElasticsearchClusterRead, PermissionElasticsearchClusterWrite} + + CommandReadPermission = []string{PermissionCommandRead} + CommandAllPermission = []string{PermissionCommandRead, PermissionCommandWrite} + + InstanceReadPermission = []string{PermissionGatewayInstanceRead} + InstanceAllPermission = []string{PermissionGatewayInstanceRead,PermissionGatewayInstanceWrite} + + EntryReadPermission = []string{PermissionGatewayEntryRead} + EntryAllPermission = []string{PermissionGatewayEntryRead, PermissionGatewayEntryWrite} + + RouterReadPermission = []string{PermissionGatewayRouterRead} + RouterAllPermission = []string{PermissionGatewayRouterRead, PermissionGatewayRouterWrite} + + FlowReadPermission = []string{PermissionGatewayFlowRead} + FlowAllPermission = []string{PermissionGatewayFlowRead, PermissionGatewayFlowWrite} + + IndexAllPermission = []string{"index:read"} + IndexReadPermission = []string{"index:read", "index:write"} + ViewsAllPermission = []string{PermissionViewRead} + ViewsReadPermission = []string{PermissionViewRead, PermissionViewWrite} + DiscoverReadPermission = []string{PermissionViewRead} + DiscoverAllPermission = []string{PermissionViewRead} + + RuleReadPermission = []string{PermissionAlertRuleRead} + RuleAllPermission = []string{PermissionAlertRuleRead, PermissionAlertRuleWrite} + AlertReadPermission = []string{PermissionAlertHistoryRead} + AlertAllPermission = []string{PermissionAlertHistoryRead, PermissionAlertHistoryWrite} + ChannelReadPermission = []string{PermissionAlertChannelRead} + ChannelAllPermission = []string{PermissionAlertChannelRead, PermissionAlertChannelWrite} + + ClusterOverviewReadPermission = []string{PermissionElasticsearchClusterRead, PermissionElasticsearchIndexRead, PermissionElasticsearchNodeRead, PermissionElasticsearchMetricRead} + ClusterOverviewAllPermission = ClusterOverviewReadPermission + MonitoringReadPermission = ClusterOverviewAllPermission + + ActivitiesReadPermission = []string{PermissionActivityRead} + ActivitiesAllPermission = []string{PermissionActivityRead, PermissionActivityWrite} +) + +var AdminPrivilege = []string{ + UserAll, RoleAll, ClusterAll, CommandAll, + InstanceAll, EntryAll, RouterAll, FlowAll, + IndexAll, ViewsAll, DiscoverAll, + RuleAll, AlertAll, ChannelAll, + ClusterOverviewAll, MonitoringAll, ActivitiesAll, +} + +var BuildRoles = make(map[string]map[string]interface{}, 0) + +func init() { + + BuildRoles["admin"] = map[string]interface{}{ + "id": "admin", + "name": "管理员", + "type": "platform", + "platform": AdminPrivilege, + "builtin": true, + "description": "is admin", + "created": time.Now(), + } + PermissionMap = map[string][]string{ + UserRead: UserReadPermission, + UserAll: UserAllPermission, + RoleRead: RoleReadPermission, + RoleAll: RoleAllPermission, + ClusterRead: ClusterReadPermission, + ClusterAll: ClusterAllPermission, + CommandRead: CommandReadPermission, + CommandAll: CommandAllPermission, + + InstanceRead: InstanceReadPermission, + InstanceAll: InstanceAllPermission, + EntryRead: EntryReadPermission, + EntryAll: EntryAllPermission, + RouterRead: RouterReadPermission, + RouterAll: RouterAllPermission, + FlowRead: FlowReadPermission, + FlowAll: FlowAllPermission, + + IndexAll: IndexAllPermission, + IndexRead: IndexReadPermission, + ViewsAll: ViewsAllPermission, + ViewsRead: ViewsReadPermission, + DiscoverRead: DiscoverReadPermission, + DiscoverAll: DiscoverAllPermission, + + RuleRead: RuleReadPermission, + RuleAll: RuleAllPermission, + AlertRead: AlertReadPermission, + AlertAll: AlertAllPermission, + ChannelRead: ChannelReadPermission, + ChannelAll: ChannelAllPermission, + + ClusterOverviewRead: ClusterOverviewReadPermission, + ClusterOverviewAll: ClusterOverviewAllPermission, + MonitoringAll: MonitoringReadPermission, + MonitoringRead: MonitoringReadPermission, + ActivitiesAll: ActivitiesAllPermission, + ActivitiesRead: ActivitiesReadPermission, + } + +} diff --git a/internal/biz/event.go b/internal/biz/event.go new file mode 100644 index 00000000..224b82a5 --- /dev/null +++ b/internal/biz/event.go @@ -0,0 +1,18 @@ +package biz + +import ( + "infini.sh/framework/core/event" + "infini.sh/framework/core/util" + "time" +) + +func GenerateEvent(metadata event.ActivityMetadata, fields util.MapStr, changeLog interface{}) *event.Activity { + return &event.Activity{ + ID: util.GetUUID(), + Timestamp: time.Now(), + Metadata: metadata, + Fields: fields, + Changelog: changeLog, + } + +} diff --git a/internal/biz/permission.go b/internal/biz/permission.go new file mode 100644 index 00000000..cfd64286 --- /dev/null +++ b/internal/biz/permission.go @@ -0,0 +1,40 @@ +package biz + +import ( + "infini.sh/console/model/rbac" +) + +var ClusterApis = make(map[string][]string) +var IndexApis = make([]string, 50) + +var RoleMap = make(map[string]rbac.Role) + +type Token struct { + JwtStr string `json:"jwt_str"` + Value string `json:"value"` + ExpireIn int64 `json:"expire_in"` +} + +var TokenMap = make(map[string]Token) + + +type RolePermission struct { + Platform []string `json:"platform,omitempty"` + Cluster []string `json:"cluster"` + ClusterPrivilege []string `json:"cluster_privilege"` + + IndexPrivilege map[string][]string `json:"index_privilege"` +} + +func ListElasticsearchPermission() interface{} { + list := ElasticsearchPermission{ + ClusterPrivileges: ClusterApis, + IndexPrivileges: IndexApis, + } + return list +} + +type ElasticsearchPermission struct { + IndexPrivileges []string `json:"index_privileges"` + ClusterPrivileges map[string][]string `json:"cluster_privileges"` +} diff --git a/internal/biz/role.go b/internal/biz/role.go new file mode 100644 index 00000000..b5d0de28 --- /dev/null +++ b/internal/biz/role.go @@ -0,0 +1,186 @@ +package biz + +import ( + "errors" + "fmt" + "infini.sh/console/internal/biz/enum" + "infini.sh/console/model/rbac" + "infini.sh/framework/core/event" + "infini.sh/framework/core/orm" + "infini.sh/framework/core/util" + log "src/github.com/cihub/seelog" + "strings" + "time" +) + +type RoleType = string + +const ( + Platform RoleType = "platform" + Elastisearch RoleType = "elasticsearch" +) + +func UpdateRole(localUser *User, role *rbac.Role) (err error) { + model, err := GetRole(role.ID) + if err != nil { + return err + } + role.Type = model.Type + role.Created = model.Created + changeLog, _ := util.DiffTwoObject(model, role) + role.Updated = time.Now() + err = orm.Save(role) + if err != nil { + return + } + + RoleMap[model.Name] = model + + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "role", + Type: "update", + Labels: util.MapStr{ + "id": model.ID, + "description": model.Description, + "privilege": role.Privilege, + "updated": model.Updated, + }, + User: util.MapStr{ + "userid": localUser.UserId, + "username": localUser.Username, + }, + }, nil, changeLog)) + + return +} + +func CreateRole(localUser *User, role *rbac.Role) (id string, err error) { + if role.Name == "" { + err = errors.New("role name is require") + return + } + if _, ok := enum.BuildRoles[role.Name]; ok { + err = fmt.Errorf("role name %s already exists", role.Name) + return + } + q := orm.Query{Size: 1} + q.Conds = orm.And(orm.Eq("name", role.Name)) + + err, result := orm.Search(rbac.Role{}, &q) + if err != nil { + return + } + if result.Total > 0 { + err = fmt.Errorf("role name %s already exists", role.Name) + return + } + + role.ID = util.GetUUID() + role.Created = time.Now() + role.Updated = time.Now() + err = orm.Save(role) + if err != nil { + return + } + id = role.ID + RoleMap[role.Name] = *role + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "role", + Type: "create", + Labels: util.MapStr{ + "id": id, + "name": role.Name, + "description": role.Description, + "privilege": role.Privilege, + "type": role.Type, + }, + User: util.MapStr{ + "userid": localUser.UserId, + "username": localUser.Username, + }, + }, nil, nil)) + + if err != nil { + log.Error(err) + } + return + +} +func DeleteRole(localUser *User, id string) (err error) { + role := rbac.Role{} + role.ID = id + roleName := role.Name + _, err = orm.Get(&role) + if err != nil { + return + } + + err = orm.Delete(&role) + if err != nil { + return + } + + delete(RoleMap, roleName) + + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "role", + Type: "delete", + Labels: util.MapStr{ + "id": id, + }, + User: util.MapStr{ + "userid": localUser.UserId, + "username": localUser.Username, + }, + }, util.MapStr{ + "id": id, + "name": role.Name, + "description": role.Description, + "type": role.Type, + "created": role.Created.Format("2006-01-02 15:04:05"), + "updated": role.Updated.Format("2006-01-02 15:04:05"), + }, nil)) + + return +} + +func GetRole(id string) (role rbac.Role, err error) { + + role.ID = id + _, err = orm.Get(&role) + if err != nil { + return + } + return +} + +func SearchRole(keyword string, from, size int) (roles orm.Result, err error) { + + query := orm.Query{} + + queryDSL := `{"query":{"bool":{"must":[%s]}}, "from": %d,"size": %d}` + mustBuilder := &strings.Builder{} + + if keyword != "" { + mustBuilder.WriteString(fmt.Sprintf(`{"query_string":{"default_field":"*","query": "%s"}}`, keyword)) + } + queryDSL = fmt.Sprintf(queryDSL, mustBuilder.String(), from, size) + query.RawQuery = []byte(queryDSL) + + err, roles = orm.Search(rbac.Role{}, &query) + + return +} +func IsAllowRoleType(roleType string) (err error) { + if roleType != Platform && roleType != Elastisearch { + err = fmt.Errorf("invalid role type %s ", roleType) + return + } + return +} diff --git a/internal/biz/user.go b/internal/biz/user.go new file mode 100644 index 00000000..34a0e20c --- /dev/null +++ b/internal/biz/user.go @@ -0,0 +1,279 @@ +package biz + +import ( + "fmt" + "golang.org/x/crypto/bcrypt" + "infini.sh/console/internal/dto" + "infini.sh/console/model/rbac" + "infini.sh/framework/core/event" + + "infini.sh/framework/core/orm" + "infini.sh/framework/core/util" + "strings" + "time" +) + +var ErrNotFound = fmt.Errorf("not found") + +func DeleteUser(localUser *User, id string) (err error) { + + user := rbac.User{} + user.ID = id + _, err = orm.Get(&user) + if err != nil { + return + } + err = orm.Delete(user) + if err != nil { + return + } + + delete(TokenMap, id) + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "user", + Type: "delete", + Labels: util.MapStr{ + "id": id, + }, + User: util.MapStr{ + "userid": localUser.UserId, + "username": localUser.Username, + }, + }, util.MapStr{ + "id": id, + "name": user.Name, + "email": user.Email, + "phone": user.Phone, + "password": user.Password, + "nickname": user.NickName, + "tags": user.Tags, + "roles": user.Roles, + "created": user.Created, + "updated": user.Updated, + }, nil)) + return +} +func CreateUser(localUser *User, req dto.CreateUser) (id string, password string, err error) { + q := orm.Query{Size: 1000} + q.Conds = orm.And(orm.Eq("name", req.Name)) + + err, result := orm.Search(rbac.User{}, &q) + if err != nil { + return + } + if result.Total > 0 { + err = fmt.Errorf("user name %s already exists", req.Name) + return + } + + roles := make([]rbac.UserRole, 0) + for _, v := range req.Roles { + roles = append(roles, rbac.UserRole{ + ID: v.Id, + Name: v.Name, + }) + } + randStr := util.GenerateRandomString(8) + hash, err := bcrypt.GenerateFromPassword([]byte(randStr), bcrypt.DefaultCost) + if err != nil { + return + } + user := rbac.User{ + Name: req.Name, + NickName: req.NickName, + Password: string(hash), + Email: req.Email, + Phone: req.Phone, + Roles: roles, + Tags: req.Tags, + } + user.ID = util.GetUUID() + user.Created = time.Now() + user.Updated = time.Now() + err = orm.Save(&user) + if err != nil { + + return + } + id = user.ID + password = randStr + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "user", + Type: "create", + Labels: util.MapStr{ + "id": id, + "name": user.Name, + "email": user.Email, + "phone": user.Phone, + "password": user.Password, + "nick_name": user.NickName, + "tags": user.Tags, + "roles": user.Roles, + "created": user.Created, + "updated": user.Updated, + }, + User: util.MapStr{ + "userid": localUser.UserId, + "username": localUser.Username, + }, + }, nil, nil)) + return +} +func UpdateUser(localUser *User, id string, req dto.UpdateUser) (err error) { + user := rbac.User{} + user.ID = id + _, err = orm.Get(&user) + if err != nil { + return + } + roles := make([]rbac.UserRole, 0) + for _, v := range req.Roles { + roles = append(roles, rbac.UserRole{ + ID: v.Id, + Name: v.Name, + }) + } + changeLog, _ := util.DiffTwoObject(user, req) + user.Name = req.Name + user.Email = req.Email + user.Phone = req.Phone + user.Tags = req.Tags + user.Roles = roles + user.Updated = time.Now() + err = orm.Save(&user) + if err != nil { + return + } + delete(TokenMap, id) + + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "user", + Type: "update", + Labels: util.MapStr{ + "id": id, + "email": user.Email, + "phone": user.Phone, + "name": user.Name, + "tags": user.Tags, + "roles": roles, + "updated": user.Updated, + }, + User: util.MapStr{ + "userid": localUser.UserId, + "username": localUser.Username, + }, + }, nil, changeLog)) + return +} +func UpdateUserRole(localUser *User, id string, req dto.UpdateUserRole) (err error) { + user := rbac.User{} + user.ID = id + _, err = orm.Get(&user) + if err != nil { + + return + } + changeLog, _ := util.DiffTwoObject(user, req) + roles := make([]rbac.UserRole, 0) + for _, v := range req.Roles { + roles = append(roles, rbac.UserRole{ + ID: v.Id, + Name: v.Name, + }) + } + user.Roles = roles + user.Updated = time.Now() + err = orm.Save(&user) + if err != nil { + return + } + delete(TokenMap, id) + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "user", + Type: "update", + Labels: util.MapStr{ + "id": id, + "roles": user.Roles, + "updated": user.Updated, + }, + User: util.MapStr{ + "userid": localUser.UserId, + "username": localUser.Username, + }, + }, nil, changeLog)) + return + +} +func GetUser(id string) (user rbac.User, err error) { + + user.ID = id + _, err = orm.Get(&user) + if err != nil { + return + } + return + +} +func SearchUser(keyword string, from, size int) (users orm.Result, err error) { + query := orm.Query{} + + queryDSL := `{"query":{"bool":{"must":[%s]}}, "from": %d,"size": %d}` + mustBuilder := &strings.Builder{} + + if keyword != "" { + mustBuilder.WriteString(fmt.Sprintf(`{"query_string":{"default_field":"*","query": "%s"}}`, keyword)) + } + queryDSL = fmt.Sprintf(queryDSL, mustBuilder.String(), from, size) + query.RawQuery = []byte(queryDSL) + + err, users = orm.Search(rbac.User{}, &query) + + return + +} +func UpdateUserPassword(localUser *User, id string, password string) (err error) { + user := rbac.User{} + user.ID = id + _, err = orm.Get(&user) + if err != nil { + + return + } + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return + } + user.Password = string(hash) + user.Updated = time.Now() + err = orm.Save(&user) + if err != nil { + return + } + if localUser.UserId == id { + delete(TokenMap, localUser.UserId) + } + err = orm.Save(GenerateEvent(event.ActivityMetadata{ + Category: "platform", + Group: "rbac", + Name: "user", + Type: "update", + Labels: util.MapStr{ + "id": id, + "password": password, + "updated": user.Updated, + }, + User: util.MapStr{ + "userid": localUser.UserId, + "username": localUser.Username, + }, + }, nil, nil)) + return +} diff --git a/internal/biz/validate.go b/internal/biz/validate.go new file mode 100644 index 00000000..d67943f1 --- /dev/null +++ b/internal/biz/validate.go @@ -0,0 +1,273 @@ +package biz + +import ( + "errors" + "fmt" + "infini.sh/console/internal/biz/enum" + "infini.sh/console/model/rbac" + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/util" + "src/github.com/golang-jwt/jwt" + "strings" + "time" +) + +type EsRequest struct { + Doc string `json:"doc"` + Privilege string `json:"privilege"` + ClusterRequest + IndexRequest +} +type ClusterRequest struct { + Cluster []string `json:"cluster"` + Privilege []string `json:"privilege"` +} +type IndexRequest struct { + Cluster []string `json:"cluster"` + Index []string `json:"index"` + Privilege []string `json:"privilege"` +} + +func NewIndexRequest(ps httprouter.Params, privilege []string) IndexRequest { + index := ps.ByName("index") + clusterId := ps.ByName("id") + return IndexRequest{ + Cluster: []string{clusterId}, + Index: []string{index}, + Privilege: privilege, + } +} +func NewClusterRequest(ps httprouter.Params, privilege []string) ClusterRequest { + clusterId := ps.ByName("id") + return ClusterRequest{ + Cluster: []string{clusterId}, + Privilege: privilege, + } +} + +func ValidateElasticsearch(req rbac.ElasticsearchPrivilege, roleNames []string) (err error) { + + return nil +} + +func ValidateIndex(req IndexRequest, userRole RolePermission) (err error) { + + userClusterMap := make(map[string]struct{}) + for _, v := range userRole.Cluster { + userClusterMap[v] = struct{}{} + } + for _, v := range req.Cluster { + if _, ok := userClusterMap[v]; !ok { + err = errors.New("no cluster permission") + return + } + } + + for _, val := range req.Privilege { + position := strings.Index(val, ".") + if position == -1 { + err = errors.New("invalid privilege parameter") + return err + } + prefix := val[:position] + for _, v := range req.Index { + privilege, ok := userRole.IndexPrivilege[v] + if !ok { + err = errors.New("no index permission") + return err + } + if util.StringInArray(privilege, prefix+".*") { + continue + } + if util.StringInArray(privilege, val) { + continue + } + return fmt.Errorf("no index api permission: %s", val) + } + } + + return nil +} +func ValidateCluster(req ClusterRequest, roleNames []string) (err error) { + userClusterMap := GetRoleClusterMap(roleNames) + for _, v := range req.Cluster { + userClusterPermissions, ok := userClusterMap[v] + if !ok && userClusterMap["*"] == nil{ + err = fmt.Errorf("no cluster[%s] permission", v) + return + } + if util.StringInArray(userClusterPermissions, "*") { + continue + } + // if include api.* for example: cat.* , return nil + for _, privilege := range req.Privilege { + prefix := privilege[:strings.Index(privilege, ".")] + + if util.StringInArray(userClusterPermissions, prefix+".*") { + continue + } + if util.StringInArray(userClusterPermissions, privilege) { + continue + } + return fmt.Errorf("no cluster api permission: %s", privilege) + } + } + return nil + +} + +func CombineUserRoles(roleNames []string) RolePermission { + newRole := RolePermission{} + m := make(map[string][]string) + for _, val := range roleNames { + role := RoleMap[val] + for _, v := range role.Privilege.Elasticsearch.Cluster.Resources { + newRole.Cluster = append(newRole.Cluster, v.ID) + } + for _, v := range role.Privilege.Elasticsearch.Cluster.Permissions { + newRole.ClusterPrivilege = append(newRole.ClusterPrivilege, v) + } + for _, v := range role.Privilege.Platform { + newRole.Platform = append(newRole.Platform, v) + } + for _, v := range role.Privilege.Elasticsearch.Index { + + for _, name := range v.Name { + if _, ok := m[name]; ok { + m[name] = append(m[name], v.Permissions...) + } else { + m[name] = v.Permissions + } + + } + + } + } + newRole.IndexPrivilege = m + return newRole +} +func GetRoleClusterMap(roles []string) map[string][]string { + userClusterMap := make(map[string][]string, 0) + for _, roleName := range roles { + role, ok := RoleMap[roleName] + if ok { + for _, ic := range role.Privilege.Elasticsearch.Cluster.Resources { + userClusterMap[ic.ID] = append(userClusterMap[ic.ID], role.Privilege.Elasticsearch.Cluster.Permissions...) + } + } + } + return userClusterMap +} +func GetRoleCluster(roles []string) []string { + userClusterMap := GetRoleClusterMap(roles) + realCluster := make([]string, 0, len(userClusterMap)) + for k, _ := range userClusterMap { + realCluster = append(realCluster, k) + } + return realCluster +} +func GetRoleIndex(roles, clusterIDs []string) map[string][]string { + userClusterMap := make(map[string]struct{}, len(clusterIDs)) + for _, clusterID := range clusterIDs { + userClusterMap[clusterID] = struct{}{} + } + realIndex := map[string][]string{} + for _, roleName := range roles { + role, ok := RoleMap[roleName] + if ok { + for _, ic := range role.Privilege.Elasticsearch.Cluster.Resources { + if _, ok = userClusterMap[ic.ID]; !ok { + continue + } + for _, ip := range role.Privilege.Elasticsearch.Index { + realIndex[ic.ID] = append(realIndex[ic.ID], ip.Name...) + } + } + } + } + return realIndex +} +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 + } + clams, ok := token.Claims.(*UserClaims) + + if clams.UserId == "" { + err = errors.New("user id is empty") + return + } + //fmt.Println("user token", clams.UserId, TokenMap[clams.UserId]) + tokenVal, ok := TokenMap[clams.UserId] + if !ok { + err = errors.New("token is invalid") + return + } + if tokenVal.ExpireIn < time.Now().Unix() { + err = errors.New("token is expire in") + delete(TokenMap, clams.UserId) + return + } + if ok && token.Valid { + return clams, nil + } + return + +} +func ValidatePermission(claims *UserClaims, permissions []string) (err error) { + + user := claims.User + + if user.UserId == "" { + err = errors.New("user id is empty") + return + } + if user.Roles == nil { + err = errors.New("api permission is empty") + return + } + + // 权限校验 + userPermissions := make([]string, 0) + for _, role := range user.Roles { + if _, ok := RoleMap[role]; ok { + for _, v := range RoleMap[role].Privilege.Platform { + userPermissions = append(userPermissions, v) + } + } + } + userPermissionMap := make(map[string]struct{}) + for _, val := range userPermissions { + for _, v := range enum.PermissionMap[val] { + userPermissionMap[v] = struct{}{} + } + + } + + for _, v := range permissions { + if _, ok := userPermissionMap[v]; !ok { + err = errors.New("permission denied") + return + } + } + return nil + +} diff --git a/internal/biz/validate_test.go b/internal/biz/validate_test.go new file mode 100644 index 00000000..345e12f4 --- /dev/null +++ b/internal/biz/validate_test.go @@ -0,0 +1,147 @@ +package biz + +import ( + "github.com/stretchr/testify/assert" + "infini.sh/framework/core/util" + "testing" +) + +func Test_validateIndex(t *testing.T) { + type args struct { + req IndexRequest + userRole RolePermission + } + tests := []struct { + name string + args args + want string + }{ + {"no index permission", + args{ + req: IndexRequest{ + + Cluster: []string{"cluster1"}, + Index: []string{"index2"}, + Privilege: []string{"indices.mapping"}, + }, + userRole: RolePermission{ + Cluster: []string{ + "cluster1", + }, + + ClusterPrivilege: []string{ + "cat.*", + }, + IndexPrivilege: map[string][]string{ + "index1": []string{"indices.delete"}, + }, + }, + }, "no index permission", + }, + {"no index api permission", + args{ + req: IndexRequest{ + + Cluster: []string{"cluster1"}, + Index: []string{"index1"}, + Privilege: []string{"indices.mapping"}, + }, + userRole: RolePermission{ + Cluster: []string{ + "cluster1", + }, + + ClusterPrivilege: []string{ + "cat.*", + }, + IndexPrivilege: map[string][]string{ + "index1": []string{"indices.delete"}, + }, + }, + }, + "no index api permission", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + got := ValidateIndex(tt.args.req, tt.args.userRole) + + assert.EqualError(t, got, tt.want) + }) + } +} +func Test_validateCluster(t *testing.T) { + type args struct { + req ClusterRequest + userRole RolePermission + } + tests := []struct { + name string + args args + want string + }{ + {"no cluster", + args{ + req: ClusterRequest{ + + Cluster: []string{"cluster1"}, + Privilege: []string{"indices.get_mapping"}, + }, + userRole: RolePermission{ + Cluster: []string{ + "cluster2", + }, + + ClusterPrivilege: []string{ + "cat.*", + }, + }, + }, "no cluster permission", + }, + {"no cluster", + args{ + req: ClusterRequest{ + Cluster: []string{"cluster1"}, + Privilege: []string{"indices.get_mapping"}, + }, + userRole: RolePermission{ + Cluster: []string{}, + ClusterPrivilege: []string{}, + }, + }, "no cluster permission", + }, + {"no cluster api", + args{ + req: ClusterRequest{ + + Cluster: []string{"cluster1"}, + Privilege: []string{"indices.get_mapping"}, + }, + userRole: RolePermission{ + Cluster: []string{ + "cluster1", + }, + + ClusterPrivilege: []string{ + "cat.*", + }, + }, + }, + "no cluster api permission", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ValidateCluster(tt.args.req, tt.args.userRole) + assert.EqualError(t, got, tt.want) + }) + } +} +func TestStringInArray(t *testing.T) { + array := []string{"a", "b", "c", "d", "e"} + assert.Equal(t, true, util.StringInArray(array, "c")) + assert.Equal(t, false, util.StringInArray(array, "h")) +} diff --git a/internal/core/response.go b/internal/core/response.go new file mode 100644 index 00000000..0804e91c --- /dev/null +++ b/internal/core/response.go @@ -0,0 +1,45 @@ +package core + +type Response struct { + Total int64 `json:"total,omitempty"` + Hit interface{} `json:"hit,omitempty"` + Id string `json:"_id,omitempty"` + Result string `json:"result,omitempty"` +} +type FoundResp struct { + Found bool `json:"found"` + Id string `json:"_id,omitempty"` + Source interface{} `json:"_source,omitempty"` +} + +func CreateResponse(id string) Response { + return Response{ + Id: id, + Result: "created", + } +} +func UpdateResponse(id string) Response { + return Response{ + Id: id, + Result: "updated", + } +} +func DeleteResponse(id string) Response { + return Response{ + Id: id, + Result: "deleted", + } +} +func NotFoundResponse(id string) FoundResp { + return FoundResp{ + Id: id, + Found: false, + } +} +func FoundResponse(id string, data interface{}) FoundResp { + return FoundResp{ + Id: id, + Found: true, + Source: data, + } +} diff --git a/internal/dto/role.go b/internal/dto/role.go new file mode 100644 index 00000000..806dba23 --- /dev/null +++ b/internal/dto/role.go @@ -0,0 +1,28 @@ +package dto + +type CreateUser struct { + NickName string `json:"nick_name"` + + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + Roles []Role `json:"roles"` + Tags []string `json:"tags"` +} +type Role struct { + Id string `json:"id"` + Name string `json:"name"` +} +type UpdateUser struct { + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + Tags []string `json:"tags"` + Roles []Role `json:"roles"` +} +type UpdateUserRole struct { + Roles []Role `json:"roles"` +} +type UpdateUserPassword struct { + Password string `json:"password"` +} diff --git a/internal/dto/user.go b/internal/dto/user.go new file mode 100644 index 00000000..68d7ed83 --- /dev/null +++ b/internal/dto/user.go @@ -0,0 +1,15 @@ +package dto + +type Login struct { + Username string `json:"username"` + Password string `json:"password"` +} +type UpdatePassword struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} +type UpdateProfile struct { + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email"` +} diff --git a/internal/middleware/es.go b/internal/middleware/es.go new file mode 100644 index 00000000..c8aa78db --- /dev/null +++ b/internal/middleware/es.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "infini.sh/console/internal/biz" + httprouter "infini.sh/framework/core/api/router" + "net/http" +) + +func IndexRequired(h httprouter.Handle, route ...string) 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, http.StatusUnauthorized, err) + return + } + newRole := biz.CombineUserRoles(claims.Roles) + + indexReq := biz.NewIndexRequest(ps, route) + + err = biz.ValidateIndex(indexReq, newRole) + if err != nil { + w = handleError(w, http.StatusForbidden, err) + return + } + h(w, r, ps) + } +} +func ClusterRequired(h httprouter.Handle, route ...string) 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, http.StatusUnauthorized, err) + return + } + //newRole := biz.CombineUserRoles(claims.Roles) + clusterReq := biz.NewClusterRequest(ps, route) + + err = biz.ValidateCluster(clusterReq, claims.Roles) + if err != nil { + w = handleError(w, http.StatusForbidden, err) + return + } + h(w, r, ps) + } +} diff --git a/internal/middleware/user.go b/internal/middleware/user.go new file mode 100644 index 00000000..3f13b419 --- /dev/null +++ b/internal/middleware/user.go @@ -0,0 +1,52 @@ +package middleware + +import ( + "infini.sh/console/internal/biz" + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/util" + "net/http" +) + +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, http.StatusUnauthorized, 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) { + claims, err := biz.ValidateLogin(r.Header.Get("Authorization")) + if err != nil { + + w = handleError(w, http.StatusUnauthorized, err) + return + } + err = biz.ValidatePermission(claims, permissions) + if err != nil { + w = handleError(w, http.StatusForbidden, err) + return + } + r = r.WithContext(biz.NewUserContext(r.Context(), claims)) + h(w, r, ps) + } +} +func handleError(w http.ResponseWriter, statusCode int, err error) http.ResponseWriter { + w.Header().Set("Content-type", util.ContentTypeJson) + w.WriteHeader(statusCode) + json := util.ToJson(util.MapStr{ + "error": util.MapStr{ + "status": statusCode, + "reason": err.Error(), + }, + }, true) + w.Write([]byte(json)) + + return w +} diff --git a/main.go b/main.go index dbd69cd1..ce11dd6f 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,9 @@ import ( "infini.sh/console/model" "infini.sh/console/model/alerting" "infini.sh/console/model/gateway" + "infini.sh/console/model/rbac" _ "infini.sh/console/plugin" + rbacApi "infini.sh/console/plugin/api/rbac" alerting2 "infini.sh/console/service/alerting" "infini.sh/framework" "infini.sh/framework/core/elastic" @@ -51,7 +53,7 @@ func main() { terminalFooter := "" app := framework.NewApp("console", "INFINI Cloud Console, The easiest way to operate your own elasticsearch platform.", - config.Version,config.BuildNumber, config.LastCommitLog, config.BuildDate, config.EOLDate, terminalHeader, terminalFooter) + config.Version, config.BuildNumber, config.LastCommitLog, config.BuildDate, config.EOLDate, terminalHeader, terminalFooter) app.Init(nil) defer app.Shutdown() @@ -60,11 +62,10 @@ func main() { if app.Setup(func() { err := bootstrapRequirementCheck() - if err !=nil{ + if err != nil { panic(err) } - //load core modules first module.RegisterSystemModule(&elastic2.ElasticModule{}) module.RegisterSystemModule(&filter.FilterModule{}) @@ -119,17 +120,17 @@ func main() { module.Start() - orm.RegisterSchemaWithIndexName(model.Dict{}, "dict") orm.RegisterSchemaWithIndexName(model.Reindex{}, "reindex") orm.RegisterSchemaWithIndexName(elastic.View{}, "view") orm.RegisterSchemaWithIndexName(alerting.Alert{}, "alerting-alerts") orm.RegisterSchemaWithIndexName(elastic.CommonCommand{}, "commands") orm.RegisterSchemaWithIndexName(elastic.TraceTemplate{}, "trace-template") - orm.RegisterSchemaWithIndexName(gateway.Instance{} , "gateway-instance") - orm.RegisterSchemaWithIndexName(alerting.Rule{} , "alert-rule") - orm.RegisterSchemaWithIndexName(alerting.Alert{} , "alert-history") - + orm.RegisterSchemaWithIndexName(gateway.Instance{}, "gateway-instance") + orm.RegisterSchemaWithIndexName(alerting.Rule{}, "alert-rule") + orm.RegisterSchemaWithIndexName(alerting.Alert{}, "alert-history") + orm.RegisterSchemaWithIndexName(rbac.Role{}, "rbac-role") + orm.RegisterSchemaWithIndexName(rbac.User{}, "rbac-user") api.RegisterSchema() go func() { @@ -138,6 +139,8 @@ func main() { log.Errorf("init alerting task error: %v", err) } }() + go rbacApi.Init() + }, nil) { app.Run() } diff --git a/model/rbac/role.go b/model/rbac/role.go new file mode 100644 index 00000000..6e1e6fb4 --- /dev/null +++ b/model/rbac/role.go @@ -0,0 +1,40 @@ +package rbac + +import ( + "time" +) + +type Role struct { + ID string `json:"id,omitempty" elastic_meta:"_id" elastic_mapping:"id: { type: keyword }"` + Created time.Time `json:"created,omitempty" elastic_mapping:"created: { type: date }"` + Updated time.Time `json:"updated,omitempty" elastic_mapping:"updated: { type: date }"` + Name string `json:"name" elastic_mapping:"name: { type: keyword }"` + Type string `json:"type" elastic_mapping:"type: { type: keyword }"` + Description string `json:"description" elastic_mapping:"description: { type: text }"` + Builtin bool `json:"builtin" elastic_mapping:"builtin: { type: boolean }"` + Privilege RolePrivilege `json:"privilege" elastic_mapping:"privilege: { type: object }"` +} + +type RolePrivilege struct { + Platform []string `json:"platform,omitempty" elastic_mapping:"platform: { type: keyword }"` + Elasticsearch ElasticsearchPrivilege `json:"elasticsearch,omitempty" elastic_mapping:"elasticsearch: { type: object }"` +} + +type ElasticsearchPrivilege struct { + Cluster ClusterPrivilege `json:"cluster,omitempty" elastic_mapping:"cluster: { type: object }"` + Index []IndexPrivilege `json:"index,omitempty" elastic_mapping:"index: { type: object }"` +} + +type InnerCluster struct { + ID string `json:"id" elastic_mapping:"id: { type: keyword }"` + Name string `json:"name" elastic_mapping:"name: { type: keyword }"` +} +type ClusterPrivilege struct { + Resources []InnerCluster `json:"resources,omitempty" elastic_mapping:"resources: { type: object }"` + Permissions []string `json:"permissions,omitempty" elastic_mapping:"permissions: { type: keyword }"` +} + +type IndexPrivilege struct { + Name []string `json:"name,omitempty" elastic_mapping:"name: { type: keyword }"` + Permissions []string `json:"permissions,omitempty" elastic_mapping:"permissions: { type: keyword }"` +} \ No newline at end of file diff --git a/model/rbac/user.go b/model/rbac/user.go new file mode 100644 index 00000000..c5f626a4 --- /dev/null +++ b/model/rbac/user.go @@ -0,0 +1,23 @@ +package rbac + +import ( + "time" +) + +type User struct { + ID string `json:"id,omitempty" elastic_meta:"_id" elastic_mapping:"id: { type: keyword }"` + Created time.Time `json:"created,omitempty" elastic_mapping:"created: { type: date }"` + Updated time.Time `json:"updated,omitempty" elastic_mapping:"updated: { type: date }"` + Name string `json:"name" elastic_mapping:"name: { type: keyword }"` + NickName string `json:"nick_name" elastic_mapping:"nick_name: { type: keyword }"` + Password string `json:"password" elastic_mapping:"password: { type: keyword }"` + Email string `json:"email" elastic_mapping:"email: { type: keyword }"` + Phone string `json:"phone" elastic_mapping:"phone: { type: keyword }"` + Tags []string `json:"tags" elastic_mapping:"mobile: { type: keyword }"` + Roles []UserRole `json:"roles" elastic_mapping:"roles: { type: object }"` +} + +type UserRole struct { + ID string `json:"id" elastic_mapping:"id: { type: keyword }"` + Name string `json:"name" elastic_mapping:"name: { type: keyword }"` +} \ No newline at end of file diff --git a/plugin/api/account/account.go b/plugin/api/account/account.go index 7ed4da78..e82c627d 100644 --- a/plugin/api/account/account.go +++ b/plugin/api/account/account.go @@ -1,9 +1,12 @@ package account import ( + "infini.sh/console/internal/biz" + "infini.sh/console/internal/core" + "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 +16,161 @@ 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)) + api.HandleAPIMethod(api.PUT, "/account/password", m.LoginRequired(account.UpdatePassword)) } -var userInSession string="user_in_session" +const userInSession = "user_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.ErrorInternalServer(w, err.Error()) 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.ErrorInternalServer(w, err.Error()) + return } + data["status"] = "ok" + + //api.SetSession(w, r, userInSession+req.Username, req.Username) + h.WriteOKJSON(w, data) } -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) { + reqUser, err := biz.FromUserContext(r.Context()) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + + delete(biz.TokenMap, reqUser.UserId) + h.WriteOKJSON(w, util.MapStr{ + "status": "ok", + }) +} +func (h Account) Profile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + reqUser, err := biz.FromUserContext(r.Context()) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + + if reqUser.UserId == "admin" { + + u := util.MapStr{ + "user_id": "admin", + "name": "admin", + "email": "admin@infini.ltd", + "nick_name": "admin", + "phone": "13011111111", + } + h.WriteOKJSON(w, core.FoundResponse(reqUser.UserId, u)) + } else { + user, err := biz.GetUser(reqUser.UserId) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + u := util.MapStr{ + "user_id": user.ID, + "name": user.Name, + "email": user.Email, + "nick_name": user.NickName, + "phone": user.Phone, + } + h.WriteOKJSON(w, core.FoundResponse(reqUser.UserId, u)) + } + + return +} +func (h Account) UpdatePassword(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + reqUser, err := biz.FromUserContext(r.Context()) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + var req dto.UpdatePassword + err = h.DecodeJSON(r, &req) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + err = biz.UpdatePassword(reqUser, req) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + h.WriteOKJSON(w, core.UpdateResponse(reqUser.UserId)) + return +} +func (h Account) UpdateProfile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + reqUser, err := biz.FromUserContext(r.Context()) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + var req dto.UpdateProfile + err = h.DecodeJSON(r, &req) + err = biz.UpdateProfile(reqUser, req) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + h.WriteOKJSON(w, core.UpdateResponse(reqUser.UserId)) + return +} diff --git a/plugin/api/index_management/index.go b/plugin/api/index_management/index.go index 50e48c97..6c659afd 100644 --- a/plugin/api/index_management/index.go +++ b/plugin/api/index_management/index.go @@ -1,17 +1,19 @@ package index_management import ( + "fmt" + "infini.sh/framework/core/elastic" "net/http" "strconv" "strings" "time" + "infini.sh/console/config" + model2 "infini.sh/console/model" "infini.sh/framework/core/api" httprouter "infini.sh/framework/core/api/router" "infini.sh/framework/core/orm" "infini.sh/framework/core/util" - "infini.sh/console/config" - model2 "infini.sh/console/model" ) type APIHandler struct { @@ -116,3 +118,73 @@ func (handler APIHandler) UpdateDictItemAction(w http.ResponseWriter, req *http. handler.WriteJSON(w, resp, http.StatusOK) } +func (h APIHandler) ListIndex(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { + clusterIds := h.GetParameterOrDefault(req, "ids", "") + keyword := h.GetParameterOrDefault(req, "keyword", "") + ids := strings.Split(clusterIds, ",") + for i := range ids { + if i < len(ids)-1 { + ids[i] = `"` + ids[i] + `",` + } else { + ids[i] = `"` + ids[i] + `"` + } + + } + if len(ids) == 0 { + h.Error400(w, "id is required") + return + } + var dsl = `{ + "_source": ["metadata.index_name"], + "collapse": { + "field": "metadata.index_name" + }, + "size": 100, + "query": { + "bool": { + "must": [ + { + "terms": { + "metadata.cluster_id": %s + } + }%s + ], + "must_not": [ + { + "term": { + "metadata.labels.state": { + "value": "delete" + } + } + } + ] + } + } + }` + + str := &strings.Builder{} + + if keyword != "" { + str.WriteString(fmt.Sprintf(`,{"wildcard":{"metadata.index_name":{"value":"*%s*"}}}`, keyword)) + } + dsl = fmt.Sprintf(dsl, ids, str) + + esClient := elastic.GetClient(h.Config.Elasticsearch) + resp, err := esClient.SearchWithRawQueryDSL(".infini_index", []byte(dsl)) + if err != nil { + + return + } + list := resp.Hits.Hits + var indexNames []string + for _, v := range list { + m := v.Source["metadata"].(map[string]interface{}) + indexNames = append(indexNames, m["index_name"].(string)) + + } + m := make(map[string]interface{}) + m["indexnames"] = indexNames + h.WriteOKJSON(w, m) + + return +} diff --git a/plugin/api/init.go b/plugin/api/init.go index c7f323aa..0d66b36b 100644 --- a/plugin/api/init.go +++ b/plugin/api/init.go @@ -2,6 +2,7 @@ package api import ( "infini.sh/console/config" + m "infini.sh/console/internal/middleware" "infini.sh/console/plugin/api/alerting" "infini.sh/console/plugin/api/index_management" "infini.sh/framework/core/api" @@ -23,28 +24,29 @@ func Init(cfg *config.AppConfig) { //api.HandleAPIMethod(api.GET, "/api/dict/:id",handler.GetDictItemAction) api.HandleAPIMethod(api.DELETE, path.Join(pathPrefix, "dict/:id"), handler.DeleteDictItemAction) api.HandleAPIMethod(api.PUT, path.Join(pathPrefix, "dict/:id"), handler.UpdateDictItemAction) - api.HandleAPIMethod(api.POST, path.Join(esPrefix, "doc/:index/_search"), handler.HandleSearchDocumentAction) - api.HandleAPIMethod(api.POST, path.Join(esPrefix, "doc/:index"), handler.HandleAddDocumentAction) - api.HandleAPIMethod(api.PUT, path.Join(esPrefix, "doc/:index/:docId"), handler.HandleUpdateDocumentAction) - api.HandleAPIMethod(api.DELETE, path.Join(esPrefix, "doc/:index/:docId"), handler.HandleDeleteDocumentAction) + + api.HandleAPIMethod(api.POST, path.Join(esPrefix, "doc/:index/_search"), m.IndexRequired(handler.HandleSearchDocumentAction, "doc.search")) + api.HandleAPIMethod(api.POST, path.Join(esPrefix, "doc/:index"), m.IndexRequired(handler.HandleAddDocumentAction, "doc.create")) + api.HandleAPIMethod(api.PUT, path.Join(esPrefix, "doc/:index/:docId"), m.IndexRequired(handler.HandleUpdateDocumentAction, "doc.update")) + api.HandleAPIMethod(api.DELETE, path.Join(esPrefix, "doc/:index/:docId"), m.IndexRequired(handler.HandleDeleteDocumentAction, "doc.delete")) api.HandleAPIMethod(api.GET, path.Join(esPrefix, "doc/_validate"), handler.ValidateDocIDAction) api.HandleAPIMethod(api.POST, path.Join(pathPrefix, "rebuild/*id"), handler.HandleReindexAction) api.HandleAPIMethod(api.GET, path.Join(pathPrefix, "rebuild/_search"), handler.HandleGetRebuildListAction) api.HandleAPIMethod(api.DELETE, path.Join(pathPrefix, "rebuild/:id"), handler.HandleDeleteRebuildAction) - api.HandleAPIMethod(api.GET, path.Join(esPrefix, "_cat/indices"), handler.HandleGetIndicesAction) - api.HandleAPIMethod(api.GET, path.Join(esPrefix, "index/:index/_mappings"), handler.HandleGetMappingsAction) - api.HandleAPIMethod(api.GET, path.Join(esPrefix, "index/:index/_settings"), handler.HandleGetSettingsAction) - api.HandleAPIMethod(api.PUT, path.Join(esPrefix, "index/:index/_settings"), handler.HandleUpdateSettingsAction) - api.HandleAPIMethod(api.DELETE, path.Join(esPrefix, "index/:index"), handler.HandleDeleteIndexAction) - api.HandleAPIMethod(api.POST, path.Join(esPrefix, "index/:index"), handler.HandleCreateIndexAction) + api.HandleAPIMethod(api.GET, path.Join(esPrefix, "_cat/indices"), m.ClusterRequired(handler.HandleGetIndicesAction, "cat.indices")) + api.HandleAPIMethod(api.GET, path.Join(esPrefix, "index/:index/_mappings"), m.IndexRequired(handler.HandleGetMappingsAction, "indices.get_mapping")) + api.HandleAPIMethod(api.GET, path.Join(esPrefix, "index/:index/_settings"), m.IndexRequired(handler.HandleGetSettingsAction, "indices.get_settings")) + api.HandleAPIMethod(api.PUT, path.Join(esPrefix, "index/:index/_settings"), m.IndexRequired(handler.HandleUpdateSettingsAction, "indices.put_mapping")) + api.HandleAPIMethod(api.DELETE, path.Join(esPrefix, "index/:index"), m.IndexRequired(handler.HandleDeleteIndexAction, "indices.delete")) + api.HandleAPIMethod(api.POST, path.Join(esPrefix, "index/:index"), m.IndexRequired(handler.HandleCreateIndexAction, "indices.create")) - api.HandleAPIMethod(api.POST, path.Join(pathPrefix, "elasticsearch/command"), handler.HandleAddCommonCommandAction) - api.HandleAPIMethod(api.PUT, path.Join(pathPrefix, "elasticsearch/command/:cid"), handler.HandleSaveCommonCommandAction) + api.HandleAPIMethod(api.POST, path.Join(pathPrefix, "elasticsearch/command"), handler.HandleAddCommonCommandAction) + api.HandleAPIMethod(api.PUT, path.Join(pathPrefix, "elasticsearch/command/:cid"), handler.HandleSaveCommonCommandAction) api.HandleAPIMethod(api.GET, path.Join(pathPrefix, "elasticsearch/command"), handler.HandleQueryCommonCommandAction) api.HandleAPIMethod(api.DELETE, path.Join(pathPrefix, "elasticsearch/command/:cid"), handler.HandleDeleteCommonCommandAction) - + api.HandleAPIMethod(api.GET, path.Join(pathPrefix, "elasticsearch/indices"), handler.ListIndex) //task.RegisterScheduleTask(task.ScheduleTask{ // Description: "sync reindex task result", // Task: func() { diff --git a/plugin/api/rbac/api.go b/plugin/api/rbac/api.go new file mode 100644 index 00000000..dccd061b --- /dev/null +++ b/plugin/api/rbac/api.go @@ -0,0 +1,106 @@ +package rbac + +import ( + "encoding/json" + "github.com/mitchellh/mapstructure" + "infini.sh/console/internal/biz" + "infini.sh/console/internal/biz/enum" + m "infini.sh/console/internal/middleware" + "infini.sh/console/model/rbac" + "infini.sh/framework/core/api" + "infini.sh/framework/core/elastic" + "infini.sh/framework/core/util" + "os" + "path" + log "github.com/cihub/seelog" +) + +type Rbac struct { + api.Handler +} + +func init() { + + r := Rbac{} + api.HandleAPIMethod(api.GET, "/permission/:type", r.ListPermission) + api.HandleAPIMethod(api.POST, "/role/:type", m.PermissionRequired(r.CreateRole, enum.RoleAllPermission...)) + api.HandleAPIMethod(api.GET, "/role/:id", m.PermissionRequired(r.GetRole, enum.RoleReadPermission...)) + api.HandleAPIMethod(api.DELETE, "/role/:id", m.PermissionRequired(r.DeleteRole, enum.RoleAllPermission...)) + api.HandleAPIMethod(api.PUT, "/role/:id", m.PermissionRequired(r.UpdateRole, enum.RoleAllPermission...)) + api.HandleAPIMethod(api.GET, "/role/_search", m.PermissionRequired(r.SearchRole, enum.RoleReadPermission...)) + + api.HandleAPIMethod(api.POST, "/user", m.PermissionRequired(r.CreateUser, enum.UserAllPermission...)) + api.HandleAPIMethod(api.GET, "/user/:id", m.PermissionRequired(r.GetUser, enum.UserReadPermission...)) + api.HandleAPIMethod(api.DELETE, "/user/:id", m.PermissionRequired(r.DeleteUser, enum.UserAllPermission...)) + api.HandleAPIMethod(api.PUT, "/user/:id", m.PermissionRequired(r.UpdateUser, enum.UserAllPermission...)) + api.HandleAPIMethod(api.PUT, "/user/:id/role", m.PermissionRequired(r.UpdateUserRole, enum.UserAllPermission...)) + api.HandleAPIMethod(api.GET, "/user/_search", m.PermissionRequired(r.SearchUser, enum.UserReadPermission...)) + api.HandleAPIMethod(api.PUT, "/user/:id/password", m.PermissionRequired(r.UpdateUserPassword, enum.UserAllPermission...)) +} + +func loadJsonConfig() { + pwd, _ := os.Getwd() + + bytes, err := util.FileGetContent(path.Join(pwd, "/config/permission.json")) + if err != nil { + panic("load json file err " + err.Error()) + + } + apis := make(map[string][]string) + err = json.Unmarshal(bytes, &apis) + if err != nil { + panic("json config unmarshal err " + err.Error()) + } + biz.IndexApis = apis["indices"] + delete(apis, "indices") + biz.ClusterApis = apis + + //bytes, err = util.FileGetContent(path.Join(pwd, "/config/map.json")) + //if err != nil { + // panic("load json file err " + err.Error()) + //} + //esapiMap := make(map[string]string) + //err = json.Unmarshal(bytes, &esapiMap) + //if err != nil { + // panic("json config unmarshal err " + err.Error()) + //} + //for k, v := range esapiMap { + // s := strings.Split(k, "-") + // biz.EsApiRoutes.AddRoute(s[0], s[1], v) + //} + +} +func loadRolePermission() { + biz.RoleMap = make(map[string]rbac.Role) + + biz.RoleMap["admin"] = rbac.Role{ + Privilege: rbac.RolePrivilege{ + Platform: enum.AdminPrivilege, + }, + } + + res, err := biz.SearchRole("", 0, 1000) + if err != nil { + log.Error(err) + return + } + response := elastic.SearchResponse{} + util.FromJSONBytes(res.Raw, &response) + + for _, v := range response.Hits.Hits { + var role rbac.Role + delete(v.Source, "created") + delete(v.Source, "updated") + err = mapstructure.Decode(v.Source, &role) + if err != nil { + log.Error(err) + return + } + biz.RoleMap[role.Name] = role + } + +} +func Init() { + loadJsonConfig() + loadRolePermission() +} diff --git a/plugin/api/rbac/permission.go b/plugin/api/rbac/permission.go new file mode 100644 index 00000000..fc31eb02 --- /dev/null +++ b/plugin/api/rbac/permission.go @@ -0,0 +1,24 @@ +package rbac + +import ( + log "github.com/cihub/seelog" + "infini.sh/console/internal/biz" + httprouter "infini.sh/framework/core/api/router" + "net/http" +) + +func (h Rbac) ListPermission(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + typ := ps.MustGetParameter("type") + err := biz.IsAllowRoleType(typ) + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + var permissions interface{} + if typ == biz.Elastisearch { + permissions = biz.ListElasticsearchPermission() + } + h.WriteOKJSON(w, permissions) + return +} diff --git a/plugin/api/rbac/role.go b/plugin/api/rbac/role.go new file mode 100644 index 00000000..ffa6097b --- /dev/null +++ b/plugin/api/rbac/role.go @@ -0,0 +1,150 @@ +package rbac + +import ( + log "github.com/cihub/seelog" + "infini.sh/console/internal/biz" + "infini.sh/console/internal/biz/enum" + "infini.sh/console/internal/core" + "infini.sh/console/model/rbac" + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/elastic" + "infini.sh/framework/core/util" + "net/http" +) + +func (h Rbac) CreateRole(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + roleType := ps.MustGetParameter("type") + + localUser, err := biz.FromUserContext(r.Context()) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + err = biz.IsAllowRoleType(roleType) + if err != nil { + h.ErrorInternalServer(w, err.Error()) + return + } + role := &rbac.Role{ + Type: roleType, + } + err = h.DecodeJSON(r, role) + if err != nil { + h.Error400(w, err.Error()) + return + } + + var id string + id, err = biz.CreateRole(localUser, role) + + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + _ = h.WriteOKJSON(w, core.CreateResponse(id)) + return + +} + +func (h Rbac) SearchRole(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + + var ( + keyword = h.GetParameterOrDefault(r, "keyword", "") + from = h.GetIntOrDefault(r, "from", 0) + size = h.GetIntOrDefault(r, "size", 20) + ) + + res, err := biz.SearchRole(keyword, from, size) + if err != nil { + log.Error(err) + h.ErrorInternalServer(w, err.Error()) + return + } + response := elastic.SearchResponse{} + util.FromJSONBytes(res.Raw, &response) + + hits := response.Hits.Hits + list := make([]elastic.IndexDocument, 0) + total := response.GetTotal() + var index string + for _, v := range hits { + index = v.Index + } + for k, v := range enum.BuildRoles { + list = append(list, elastic.IndexDocument{ + ID: k, + Index: index, + Type: "_doc", + Source: v, + }) + total++ + } + list = append(list, hits...) + response.Hits.Hits = list + response.Hits.Total = total + + h.WriteOKJSON(w, response) + return + +} + +func (h Rbac) GetRole(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.MustGetParameter("id") + role, err := biz.GetRole(id) + + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + h.WriteOKJSON(w, core.Response{Hit: role}) + return +} + +func (h Rbac) DeleteRole(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.MustGetParameter("id") + + localUser, err := biz.FromUserContext(r.Context()) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + err = biz.DeleteRole(localUser, id) + + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + _ = h.WriteOKJSON(w, core.DeleteResponse(id)) + return +} + +func (h Rbac) UpdateRole(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.MustGetParameter("id") + localUser, err := biz.FromUserContext(r.Context()) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + role := &rbac.Role{} + err = h.DecodeJSON(r, role) + if err != nil { + h.Error400(w, err.Error()) + return + } + role.ID = id + err = biz.UpdateRole(localUser, role) + + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + _ = h.WriteOKJSON(w, core.UpdateResponse(id)) + return +} diff --git a/plugin/api/rbac/user.go b/plugin/api/rbac/user.go new file mode 100644 index 00000000..b940db12 --- /dev/null +++ b/plugin/api/rbac/user.go @@ -0,0 +1,192 @@ +package rbac + +import ( + "errors" + "infini.sh/console/internal/biz" + "infini.sh/console/internal/core" + "infini.sh/console/internal/dto" + httprouter "infini.sh/framework/core/api/router" + "infini.sh/framework/core/util" + "infini.sh/framework/modules/elastic" + "net/http" + log "src/github.com/cihub/seelog" +) + +type CreateUserReq struct { + Username string `json:"username" ` + Password string `json:"password" ` + Name string `json:"name" ` + Phone string `json:"phone" ` + Email string `json:"email" ` + Tags []string `json:"tags"` +} + +func (h Rbac) CreateUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + + var req dto.CreateUser + err := h.DecodeJSON(r, &req) + if err != nil { + h.Error400(w, err.Error()) + return + } + if req.Name == "" { + + h.Error400(w, "username and phone and email is require") + return + } + localUser, err := biz.FromUserContext(r.Context()) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + id, pass, err := biz.CreateUser(localUser, req) + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + _ = h.WriteOKJSON(w, util.MapStr{ + "_id": id, + "password": pass, + "result": "created", + }) + return + +} + +func (h Rbac) GetUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.MustGetParameter("id") + user, err := biz.GetUser(id) + if errors.Is(err, elastic.ErrNotFound) { + h.WriteJSON(w, core.NotFoundResponse(id), http.StatusNotFound) + return + } + + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + h.WriteOKJSON(w, core.FoundResponse(id, user)) + return +} + +func (h Rbac) UpdateUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.MustGetParameter("id") + var req dto.UpdateUser + err := h.DecodeJSON(r, &req) + if err != nil { + _ = log.Error(err.Error()) + h.Error400(w, err.Error()) + return + } + localUser, err := biz.FromUserContext(r.Context()) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + err = biz.UpdateUser(localUser, id, req) + + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + _ = h.WriteOKJSON(w, core.UpdateResponse(id)) + return +} + +func (h Rbac) UpdateUserRole(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.MustGetParameter("id") + var req dto.UpdateUserRole + err := h.DecodeJSON(r, &req) + if err != nil { + _ = log.Error(err.Error()) + h.Error400(w, err.Error()) + return + } + localUser, err := biz.FromUserContext(r.Context()) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + err = biz.UpdateUserRole(localUser, id, req) + + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + _ = h.WriteOKJSON(w, core.UpdateResponse(id)) + return +} + +func (h Rbac) DeleteUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.MustGetParameter("id") + localUser, err := biz.FromUserContext(r.Context()) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + err = biz.DeleteUser(localUser, id) + if errors.Is(err, elastic.ErrNotFound) { + h.WriteJSON(w, core.NotFoundResponse(id), http.StatusNotFound) + return + } + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + _ = h.WriteOKJSON(w, core.DeleteResponse(id)) + return +} + +func (h Rbac) SearchUser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + var ( + keyword = h.GetParameterOrDefault(r, "keyword", "") + from = h.GetIntOrDefault(r, "from", 0) + size = h.GetIntOrDefault(r, "size", 20) + ) + + res, err := biz.SearchUser(keyword, from, size) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + + h.Write(w, res.Raw) + return + +} +func (h Rbac) UpdateUserPassword(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + id := ps.MustGetParameter("id") + var req dto.UpdateUserPassword + err := h.DecodeJSON(r, &req) + if err != nil { + _ = log.Error(err.Error()) + h.Error400(w, err.Error()) + return + } + localUser, err := biz.FromUserContext(r.Context()) + if err != nil { + log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + err = biz.UpdateUserPassword(localUser, id, req.Password) + if err != nil { + _ = log.Error(err.Error()) + h.ErrorInternalServer(w, err.Error()) + return + } + + _ = h.WriteOKJSON(w, core.UpdateResponse(id)) + return + +}