From 949feb18af55e3fdac6a09cdd320b4e49f223cf4 Mon Sep 17 00:00:00 2001 From: Yaodong Yu <2814461814@qq.com> Date: Tue, 25 Jul 2023 17:17:59 +0800 Subject: [PATCH] feat: add basic enforcer manager (#2130) * feat: add basic enforcer manager * chore: generate swagger --- controllers/enforcer.go | 145 ++++++++++++++++ object/adapter.go | 5 + object/casbin_adapter.go | 27 +++ object/check.go | 2 +- object/enforcer.go | 119 +++++++++++++ object/model.go | 9 + object/permission.go | 2 +- object/permission_enforcer.go | 16 +- routers/router.go | 6 + swagger/swagger.json | 214 +++++++++++++++++++++++- swagger/swagger.yml | 144 +++++++++++++++- web/src/App.js | 18 +- web/src/EnforcerEditPage.js | 257 +++++++++++++++++++++++++++++ web/src/EnforcerListPage.js | 218 ++++++++++++++++++++++++ web/src/backend/EnforcerBackend.js | 71 ++++++++ 15 files changed, 1223 insertions(+), 30 deletions(-) create mode 100644 controllers/enforcer.go create mode 100644 object/enforcer.go create mode 100644 web/src/EnforcerEditPage.js create mode 100644 web/src/EnforcerListPage.js create mode 100644 web/src/backend/EnforcerBackend.js diff --git a/controllers/enforcer.go b/controllers/enforcer.go new file mode 100644 index 00000000..6f929cc1 --- /dev/null +++ b/controllers/enforcer.go @@ -0,0 +1,145 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "encoding/json" + + "github.com/beego/beego/utils/pagination" + "github.com/casdoor/casdoor/object" + "github.com/casdoor/casdoor/util" +) + +// GetEnforcers +// @Title GetEnforcers +// @Tag Enforcer API +// @Description get enforcers +// @Param owner query string true "The owner of enforcers" +// @Success 200 {array} object.Enforcer +// @router /get-enforcers [get] +func (c *ApiController) GetEnforcers() { + owner := c.Input().Get("owner") + limit := c.Input().Get("pageSize") + page := c.Input().Get("p") + field := c.Input().Get("field") + value := c.Input().Get("value") + sortField := c.Input().Get("sortField") + sortOrder := c.Input().Get("sortOrder") + + if limit == "" || page == "" { + enforcers, err := object.GetEnforcers(owner) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.ResponseOk(enforcers) + } else { + limit := util.ParseInt(limit) + count, err := object.GetEnforcerCount(owner, field, value) + if err != nil { + c.ResponseError(err.Error()) + return + } + + paginator := pagination.SetPaginator(c.Ctx, limit, count) + enforcers, err := object.GetPaginationEnforcers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.ResponseOk(enforcers) + } +} + +// GetEnforcer +// @Title GetEnforcer +// @Tag Enforcer API +// @Description get enforcer +// @Param id query string true "The id ( owner/name ) of enforcer" +// @Success 200 {object} object +// @router /get-enforcer [get] +func (c *ApiController) GetEnforcer() { + id := c.Input().Get("id") + + enforcer, err := object.GetEnforcer(id) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.ResponseOk(enforcer) +} + +// UpdateEnforcer +// @Title UpdateEnforcer +// @Tag Enforcer API +// @Description update enforcer +// @Param id query string true "The id ( owner/name ) of enforcer" +// @Param enforcer body object true "The enforcer object" +// @Success 200 {object} object +// @router /update-enforcer [post] +func (c *ApiController) UpdateEnforcer() { + id := c.Input().Get("id") + + enforcer := object.Enforcer{} + err := json.Unmarshal(c.Ctx.Input.RequestBody, &enforcer) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.UpdateEnforcer(id, &enforcer)) + c.ServeJSON() +} + +// AddEnforcer +// @Title AddEnforcer +// @Tag Enforcer API +// @Description add enforcer +// @Param enforcer body object true "The enforcer object" +// @Success 200 {object} object +// @router /add-enforcer [post] +func (c *ApiController) AddEnforcer() { + enforcer := object.Enforcer{} + err := json.Unmarshal(c.Ctx.Input.RequestBody, &enforcer) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.AddEnforcer(&enforcer)) + c.ServeJSON() +} + +// DeleteEnforcer +// @Title DeleteEnforcer +// @Tag Enforcer API +// @Description delete enforcer +// @Param body body object.Enforce true "The enforcer object" +// @Success 200 {object} object +// @router /delete-enforcer [post] +func (c *ApiController) DeleteEnforcer() { + var enforcer object.Enforcer + err := json.Unmarshal(c.Ctx.Input.RequestBody, &enforcer) + if err != nil { + c.ResponseError(err.Error()) + return + } + + c.Data["json"] = wrapActionResponse(object.DeleteEnforcer(&enforcer)) + c.ServeJSON() +} diff --git a/object/adapter.go b/object/adapter.go index ffc982e8..5d992fda 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -197,6 +197,11 @@ func (a *Adapter) createTable() { panic(err) } + err = a.Engine.Sync2(new(Enforcer)) + if err != nil { + panic(err) + } + err = a.Engine.Sync2(new(Provider)) if err != nil { panic(err) diff --git a/object/casbin_adapter.go b/object/casbin_adapter.go index 8b4ddd08..13d95058 100644 --- a/object/casbin_adapter.go +++ b/object/casbin_adapter.go @@ -100,6 +100,13 @@ func UpdateCasbinAdapter(id string, casbinAdapter *CasbinAdapter) (bool, error) return false, err } + if name != casbinAdapter.Name { + err := casbinAdapterChangeTrigger(name, casbinAdapter.Name) + if err != nil { + return false, err + } + } + session := adapter.Engine.ID(core.PK{owner, name}).AllCols() if casbinAdapter.Password == "***" { session.Omit("password") @@ -180,6 +187,26 @@ func initEnforcer(modelObj *Model, casbinAdapter *CasbinAdapter) (*casbin.Enforc return enforcer, nil } +func casbinAdapterChangeTrigger(oldName string, newName string) error { + session := adapter.Engine.NewSession() + defer session.Close() + + err := session.Begin() + if err != nil { + return err + } + + enforcer := new(Enforcer) + enforcer.Adapter = newName + _, err = session.Where("adapter=?", oldName).Update(enforcer) + if err != nil { + session.Rollback() + return err + } + + return session.Commit() +} + func safeReturn(policy []string, i int) string { if len(policy) > i { return policy[i] diff --git a/object/check.go b/object/check.go index 12530394..365071f9 100644 --- a/object/check.go +++ b/object/check.go @@ -365,7 +365,7 @@ func CheckAccessPermission(userId string, application *Application) (bool, error if containsAsterisk { return true, err } - enforcer := getEnforcer(permission) + enforcer := getPermissionEnforcer(permission) if allowed, err = enforcer.Enforce(userId, application.Name, "read"); allowed { return allowed, err } diff --git a/object/enforcer.go b/object/enforcer.go new file mode 100644 index 00000000..d0beb6b5 --- /dev/null +++ b/object/enforcer.go @@ -0,0 +1,119 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "github.com/casbin/casbin/v2" + "github.com/casdoor/casdoor/util" + "github.com/xorm-io/core" +) + +type Enforcer struct { + Owner string `xorm:"varchar(100) notnull pk" json:"owner"` + Name string `xorm:"varchar(100) notnull pk" json:"name"` + CreatedTime string `xorm:"varchar(100)" json:"createdTime"` + UpdatedTime string `xorm:"varchar(100) updated" json:"updatedTime"` + DisplayName string `xorm:"varchar(100)" json:"displayName"` + Description string `xorm:"varchar(100)" json:"description"` + + Model string `xorm:"varchar(100)" json:"model"` + Adapter string `xorm:"varchar(100)" json:"adapter"` + IsEnabled bool `json:"isEnabled"` + + *casbin.Enforcer +} + +func GetEnforcerCount(owner, field, value string) (int64, error) { + session := GetSession(owner, -1, -1, field, value, "", "") + return session.Count(&Enforcer{}) +} + +func GetEnforcers(owner string) ([]*Enforcer, error) { + enforcers := []*Enforcer{} + err := adapter.Engine.Desc("created_time").Find(&enforcers, &Enforcer{Owner: owner}) + if err != nil { + return enforcers, err + } + + return enforcers, nil +} + +func GetPaginationEnforcers(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Enforcer, error) { + enforcers := []*Enforcer{} + session := GetSession(owner, offset, limit, field, value, sortField, sortOrder) + err := session.Find(&enforcers) + if err != nil { + return enforcers, err + } + + return enforcers, nil +} + +func getEnforcer(owner string, name string) (*Enforcer, error) { + if owner == "" || name == "" { + return nil, nil + } + + enforcer := Enforcer{Owner: owner, Name: name} + existed, err := adapter.Engine.Get(&enforcer) + if err != nil { + return &enforcer, err + } + + if existed { + return &enforcer, nil + } else { + return nil, nil + } +} + +func GetEnforcer(id string) (*Enforcer, error) { + owner, name := util.GetOwnerAndNameFromId(id) + return getEnforcer(owner, name) +} + +func UpdateEnforcer(id string, enforcer *Enforcer) (bool, error) { + owner, name := util.GetOwnerAndNameFromId(id) + if oldEnforcer, err := getEnforcer(owner, name); err != nil { + return false, err + } else if oldEnforcer == nil { + return false, nil + } + + affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(enforcer) + if err != nil { + return false, err + } + + return affected != 0, nil +} + +func AddEnforcer(enforcer *Enforcer) (bool, error) { + affected, err := adapter.Engine.Insert(enforcer) + if err != nil { + return false, err + } + + return affected != 0, nil +} + +func DeleteEnforcer(enforcer *Enforcer) (bool, error) { + affected, err := adapter.Engine.ID(core.PK{enforcer.Owner, enforcer.Name}).Delete(&Enforcer{}) + if err != nil { + return false, err + } + + return affected != 0, nil +} diff --git a/object/model.go b/object/model.go index fd3800d1..f051cb28 100644 --- a/object/model.go +++ b/object/model.go @@ -154,6 +154,15 @@ func modelChangeTrigger(oldName string, newName string) error { permission.Model = newName _, err = session.Where("model=?", oldName).Update(permission) if err != nil { + session.Rollback() + return err + } + + enforcer := new(Enforcer) + enforcer.Model = newName + _, err = session.Where("model=?", oldName).Update(enforcer) + if err != nil { + session.Rollback() return err } diff --git a/object/permission.go b/object/permission.go index 08d4100a..0e5d9236 100644 --- a/object/permission.go +++ b/object/permission.go @@ -118,7 +118,7 @@ func GetPermission(id string) (*Permission, error) { // checkPermissionValid verifies if the permission is valid func checkPermissionValid(permission *Permission) error { - enforcer := getEnforcer(permission) + enforcer := getPermissionEnforcer(permission) enforcer.EnableAutoSave(false) policies := getPolicies(permission) diff --git a/object/permission_enforcer.go b/object/permission_enforcer.go index 741c5f9c..998e36a7 100644 --- a/object/permission_enforcer.go +++ b/object/permission_enforcer.go @@ -26,7 +26,7 @@ import ( xormadapter "github.com/casdoor/xorm-adapter/v3" ) -func getEnforcer(p *Permission, permissionIDs ...string) *casbin.Enforcer { +func getPermissionEnforcer(p *Permission, permissionIDs ...string) *casbin.Enforcer { // Init an enforcer instance without specifying a model or adapter. // If you specify an adapter, it will load all policies, which is a // heavy process that can slow down the application. @@ -204,7 +204,7 @@ func getGroupingPolicies(permission *Permission) [][]string { } func addPolicies(permission *Permission) { - enforcer := getEnforcer(permission) + enforcer := getPermissionEnforcer(permission) policies := getPolicies(permission) _, err := enforcer.AddPolicies(policies) @@ -214,7 +214,7 @@ func addPolicies(permission *Permission) { } func addGroupingPolicies(permission *Permission) { - enforcer := getEnforcer(permission) + enforcer := getPermissionEnforcer(permission) groupingPolicies := getGroupingPolicies(permission) if len(groupingPolicies) > 0 { @@ -226,7 +226,7 @@ func addGroupingPolicies(permission *Permission) { } func removeGroupingPolicies(permission *Permission) { - enforcer := getEnforcer(permission) + enforcer := getPermissionEnforcer(permission) groupingPolicies := getGroupingPolicies(permission) if len(groupingPolicies) > 0 { @@ -238,7 +238,7 @@ func removeGroupingPolicies(permission *Permission) { } func removePolicies(permission *Permission) { - enforcer := getEnforcer(permission) + enforcer := getPermissionEnforcer(permission) policies := getPolicies(permission) _, err := enforcer.RemovePolicies(policies) @@ -250,12 +250,12 @@ func removePolicies(permission *Permission) { type CasbinRequest = []interface{} func Enforce(permission *Permission, request *CasbinRequest, permissionIds ...string) (bool, error) { - enforcer := getEnforcer(permission, permissionIds...) + enforcer := getPermissionEnforcer(permission, permissionIds...) return enforcer.Enforce(*request...) } func BatchEnforce(permission *Permission, requests *[]CasbinRequest, permissionIds ...string) ([]bool, error) { - enforcer := getEnforcer(permission, permissionIds...) + enforcer := getPermissionEnforcer(permission, permissionIds...) return enforcer.BatchEnforce(*requests) } @@ -276,7 +276,7 @@ func getAllValues(userId string, fn func(enforcer *casbin.Enforcer) []string) [] var values []string for _, permission := range permissions { - enforcer := getEnforcer(permission) + enforcer := getPermissionEnforcer(permission) values = append(values, fn(enforcer)...) } return values diff --git a/routers/router.go b/routers/router.go index 5b24cc34..04b12956 100644 --- a/routers/router.go +++ b/routers/router.go @@ -123,6 +123,12 @@ func initAPI() { beego.Router("/api/add-policy", &controllers.ApiController{}, "POST:AddPolicy") beego.Router("/api/remove-policy", &controllers.ApiController{}, "POST:RemovePolicy") + beego.Router("/api/get-enforcers", &controllers.ApiController{}, "GET:GetEnforcers") + beego.Router("/api/get-enforcer", &controllers.ApiController{}, "GET:GetEnforcer") + beego.Router("/api/update-enforcer", &controllers.ApiController{}, "POST:UpdateEnforcer") + beego.Router("/api/add-enforcer", &controllers.ApiController{}, "POST:AddEnforcer") + beego.Router("/api/delete-enforcer", &controllers.ApiController{}, "POST:DeleteEnforcer") + beego.Router("/api/set-password", &controllers.ApiController{}, "POST:SetPassword") beego.Router("/api/check-user-password", &controllers.ApiController{}, "POST:CheckUserPassword") beego.Router("/api/get-email-and-phone", &controllers.ApiController{}, "GET:GetEmailAndPhone") diff --git a/swagger/swagger.json b/swagger/swagger.json index cfd25bb4..5fa11b53 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -155,6 +155,34 @@ } } }, + "/api/add-enforcer": { + "post": { + "tags": [ + "Enforcer API" + ], + "description": "add enforcer", + "operationId": "ApiController.AddEnforcer", + "parameters": [ + { + "in": "body", + "name": "enforcer", + "description": "The enforcer object", + "required": true, + "schema": { + "$ref": "#/definitions/object" + } + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/object" + } + } + } + } + }, "/api/add-group": { "post": { "tags": [ @@ -1073,6 +1101,34 @@ } } }, + "/api/delete-enforcer": { + "post": { + "tags": [ + "Enforcer API" + ], + "description": "delete enforcer", + "operationId": "ApiController.DeleteEnforcer", + "parameters": [ + { + "in": "body", + "name": "body", + "description": "The enforcer object", + "required": true, + "schema": { + "$ref": "#/definitions/object.Enforce" + } + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/object" + } + } + } + } + }, "/api/delete-group": { "post": { "tags": [ @@ -2018,6 +2074,61 @@ } } }, + "/api/get-enforcer": { + "get": { + "tags": [ + "Enforcer API" + ], + "description": "get enforcer", + "operationId": "ApiController.GetEnforcer", + "parameters": [ + { + "in": "query", + "name": "id", + "description": "The id ( owner/name ) of enforcer", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/object" + } + } + } + } + }, + "/api/get-enforcers": { + "get": { + "tags": [ + "Enforcer API" + ], + "description": "get enforcers", + "operationId": "ApiController.GetEnforcers", + "parameters": [ + { + "in": "query", + "name": "owner", + "description": "The owner of enforcers", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/object.Enforcer" + } + } + } + } + } + }, "/api/get-global-providers": { "get": { "tags": [ @@ -4394,6 +4505,41 @@ } } }, + "/api/update-enforcer": { + "post": { + "tags": [ + "Enforcer API" + ], + "description": "update enforcer", + "operationId": "ApiController.UpdateEnforcer", + "parameters": [ + { + "in": "query", + "name": "id", + "description": "The id ( owner/name ) of enforcer", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "enforcer", + "description": "The enforcer object", + "required": true, + "schema": { + "$ref": "#/definitions/object" + } + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/object" + } + } + } + } + }, "/api/update-group": { "post": { "tags": [ @@ -5273,6 +5419,14 @@ } }, "definitions": { + "1225.0xc000333110.false": { + "title": "false", + "type": "object" + }, + "1260.0xc000333140.false": { + "title": "false", + "type": "object" + }, "LaravelResponse": { "title": "LaravelResponse", "type": "object" @@ -5289,6 +5443,10 @@ "title": "Response", "type": "object" }, + "casbin.Enforcer": { + "title": "Enforcer", + "type": "object" + }, "controllers.AuthForm": { "title": "AuthForm", "type": "object" @@ -5322,16 +5480,10 @@ "type": "object", "properties": { "data": { - "additionalProperties": { - "description": "support string, struct or []struct", - "type": "string" - } + "$ref": "#/definitions/1225.0xc000333110.false" }, "data2": { - "additionalProperties": { - "description": "support string, struct or []struct", - "type": "string" - } + "$ref": "#/definitions/1260.0xc000333140.false" }, "msg": { "type": "string" @@ -5373,6 +5525,10 @@ "title": "object", "type": "object" }, + "object.\u0026{197582 0xc000ace360 false}": { + "title": "\u0026{197582 0xc000ace360 false}", + "type": "object" + }, "object.AccountItem": { "title": "AccountItem", "type": "object", @@ -5566,7 +5722,7 @@ "title": "CasbinRequest", "type": "array", "items": { - "$ref": "#/definitions/object.CasbinRequest" + "$ref": "#/definitions/object.\u0026{197582 0xc000ace360 false}" } }, "object.Cert": { @@ -5662,6 +5818,43 @@ } } }, + "object.Enforce": { + "title": "Enforce", + "type": "object" + }, + "object.Enforcer": { + "title": "Enforcer", + "type": "object", + "properties": { + "adapter": { + "type": "string" + }, + "createdTime": { + "type": "string" + }, + "description": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "isEnabled": { + "type": "boolean" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "updatedTime": { + "type": "string" + } + } + }, "object.GaugeVecInfo": { "title": "GaugeVecInfo", "type": "object", @@ -7323,6 +7516,9 @@ "meetup": { "type": "string" }, + "metamask": { + "type": "string" + }, "mfaEmailEnabled": { "type": "boolean" }, diff --git a/swagger/swagger.yml b/swagger/swagger.yml index 6ad263f8..a29e3a47 100644 --- a/swagger/swagger.yml +++ b/swagger/swagger.yml @@ -100,6 +100,24 @@ paths: description: The Response object schema: $ref: '#/definitions/controllers.Response' + /api/add-enforcer: + post: + tags: + - Enforcer API + description: add enforcer + operationId: ApiController.AddEnforcer + parameters: + - in: body + name: enforcer + description: The enforcer object + required: true + schema: + $ref: '#/definitions/object' + responses: + "200": + description: "" + schema: + $ref: '#/definitions/object' /api/add-group: post: tags: @@ -693,6 +711,24 @@ paths: description: The Response object schema: $ref: '#/definitions/controllers.Response' + /api/delete-enforcer: + post: + tags: + - Enforcer API + description: delete enforcer + operationId: ApiController.DeleteEnforcer + parameters: + - in: body + name: body + description: The enforcer object + required: true + schema: + $ref: '#/definitions/object.Enforce' + responses: + "200": + description: "" + schema: + $ref: '#/definitions/object' /api/delete-group: post: tags: @@ -1307,6 +1343,42 @@ paths: description: The Response object schema: $ref: '#/definitions/controllers.Response' + /api/get-enforcer: + get: + tags: + - Enforcer API + description: get enforcer + operationId: ApiController.GetEnforcer + parameters: + - in: query + name: id + description: The id ( owner/name ) of enforcer + required: true + type: string + responses: + "200": + description: "" + schema: + $ref: '#/definitions/object' + /api/get-enforcers: + get: + tags: + - Enforcer API + description: get enforcers + operationId: ApiController.GetEnforcers + parameters: + - in: query + name: owner + description: The owner of enforcers + required: true + type: string + responses: + "200": + description: "" + schema: + type: array + items: + $ref: '#/definitions/object.Enforcer' /api/get-global-providers: get: tags: @@ -2869,6 +2941,29 @@ paths: description: The Response object schema: $ref: '#/definitions/controllers.Response' + /api/update-enforcer: + post: + tags: + - Enforcer API + description: update enforcer + operationId: ApiController.UpdateEnforcer + parameters: + - in: query + name: id + description: The id ( owner/name ) of enforcer + required: true + type: string + - in: body + name: enforcer + description: The enforcer object + required: true + schema: + $ref: '#/definitions/object' + responses: + "200": + description: "" + schema: + $ref: '#/definitions/object' /api/update-group: post: tags: @@ -3446,6 +3541,12 @@ paths: schema: $ref: '#/definitions/controllers.Response' definitions: + 1225.0xc000333110.false: + title: "false" + type: object + 1260.0xc000333140.false: + title: "false" + type: object LaravelResponse: title: LaravelResponse type: object @@ -3458,6 +3559,9 @@ definitions: Response: title: Response type: object + casbin.Enforcer: + title: Enforcer + type: object controllers.AuthForm: title: AuthForm type: object @@ -3482,13 +3586,9 @@ definitions: type: object properties: data: - additionalProperties: - description: support string, struct or []struct - type: string + $ref: '#/definitions/1225.0xc000333110.false' data2: - additionalProperties: - description: support string, struct or []struct - type: string + $ref: '#/definitions/1260.0xc000333140.false' msg: type: string name: @@ -3515,6 +3615,9 @@ definitions: object: title: object type: object + object.&{197582 0xc000ace360 false}: + title: '&{197582 0xc000ace360 false}' + type: object object.AccountItem: title: AccountItem type: object @@ -3646,7 +3749,7 @@ definitions: title: CasbinRequest type: array items: - $ref: '#/definitions/object.CasbinRequest' + $ref: '#/definitions/object.&{197582 0xc000ace360 false}' object.Cert: title: Cert type: object @@ -3710,6 +3813,31 @@ definitions: type: array items: type: string + object.Enforce: + title: Enforce + type: object + object.Enforcer: + title: Enforcer + type: object + properties: + adapter: + type: string + createdTime: + type: string + description: + type: string + displayName: + type: string + isEnabled: + type: boolean + model: + type: string + name: + type: string + owner: + type: string + updatedTime: + type: string object.GaugeVecInfo: title: GaugeVecInfo type: object @@ -4828,6 +4956,8 @@ definitions: $ref: '#/definitions/object.ManagedAccount' meetup: type: string + metamask: + type: string mfaEmailEnabled: type: boolean mfaPhoneEnabled: diff --git a/web/src/App.js b/web/src/App.js index 3e982eac..e6c3ca55 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -15,10 +15,6 @@ import React, {Component} from "react"; import "./App.less"; import {Helmet} from "react-helmet"; -import EnableMfaNotification from "./common/notifaction/EnableMfaNotification"; -import GroupTreePage from "./GroupTreePage"; -import GroupEditPage from "./GroupEdit"; -import GroupListPage from "./GroupList"; import {MfaRuleRequired} from "./Setting"; import * as Setting from "./Setting"; import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs"; @@ -33,6 +29,11 @@ import RoleListPage from "./RoleListPage"; import RoleEditPage from "./RoleEditPage"; import PermissionListPage from "./PermissionListPage"; import PermissionEditPage from "./PermissionEditPage"; +import EnforcerEditPage from "./EnforcerEditPage"; +import EnforcerListPage from "./EnforcerListPage"; +import GroupTreePage from "./GroupTreePage"; +import GroupEditPage from "./GroupEdit"; +import GroupListPage from "./GroupList"; import ProviderListPage from "./ProviderListPage"; import ProviderEditPage from "./ProviderEditPage"; import ApplicationListPage from "./ApplicationListPage"; @@ -86,6 +87,7 @@ import OdicDiscoveryPage from "./auth/OidcDiscoveryPage"; import SamlCallback from "./auth/SamlCallback"; import i18next from "i18next"; import {withTranslation} from "react-i18next"; +import EnableMfaNotification from "./common/notifaction/EnableMfaNotification"; import LanguageSelect from "./common/select/LanguageSelect"; import ThemeSelect from "./common/select/ThemeSelect"; import OrganizationSelect from "./common/select/OrganizationSelect"; @@ -164,6 +166,8 @@ class App extends Component { this.setState({selectedMenuKey: "/models"}); } else if (uri.includes("/adapters")) { this.setState({selectedMenuKey: "/adapters"}); + } else if (uri.includes("/enforcers")) { + this.setState({selectedMenuKey: "/enforcers"}); } else if (uri.includes("/providers")) { this.setState({selectedMenuKey: "/providers"}); } else if (uri.includes("/applications")) { @@ -481,6 +485,10 @@ class App extends Component { res.push(Setting.getItem({i18next.t("general:Adapters")}, "/adapters" )); + + res.push(Setting.getItem({i18next.t("general:Enforcers")}, + "/enforcers" + )); } if (Setting.isLocalAdminUser(this.state.account)) { @@ -599,6 +607,8 @@ class App extends Component { this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> + this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> this.renderLoginIfNotLoggedIn()} /> diff --git a/web/src/EnforcerEditPage.js b/web/src/EnforcerEditPage.js new file mode 100644 index 00000000..99dae1f9 --- /dev/null +++ b/web/src/EnforcerEditPage.js @@ -0,0 +1,257 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from "react"; +import {Button, Card, Col, Input, Row, Select, Switch} from "antd"; +import * as AdapterBackend from "./backend/AdapterBackend"; +import * as EnforcerBackend from "./backend/EnforcerBackend"; +import * as ModelBackend from "./backend/ModelBackend"; +import * as OrganizationBackend from "./backend/OrganizationBackend"; +import * as Setting from "./Setting"; +import i18next from "i18next"; + +class EnforcerEditPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName, + enforcerName: props.match.params.enforcerName, + enforcer: null, + organizations: [], + models: [], + adapters: [], + mode: props.location.mode !== undefined ? props.location.mode : "edit", + }; + } + + UNSAFE_componentWillMount() { + this.getEnforcer(); + this.getOrganizations(); + } + + getEnforcer() { + EnforcerBackend.getEnforcer(this.state.organizationName, this.state.enforcerName) + .then((res) => { + if (res.data === null) { + this.props.history.push("/404"); + return; + } + + if (res.status === "error") { + Setting.showMessage("error", res.msg); + return; + } + + this.setState({ + enforcer: res.data, + }); + + this.getModels(this.state.organizationName); + this.getAdapters(this.state.organizationName); + }); + } + + getOrganizations() { + OrganizationBackend.getOrganizations("admin") + .then((res) => { + this.setState({ + organizations: res.data || [], + }); + }); + } + + getModels(organizationName) { + ModelBackend.getModels(organizationName) + .then((res) => { + this.setState({ + models: res.data || [], + }); + }); + } + + getAdapters(organizationName) { + AdapterBackend.getAdapters(organizationName) + .then((res) => { + this.setState({ + adapters: res.data || [], + }); + }); + } + + parseEnforcerField(key, value) { + if ([""].includes(key)) { + value = Setting.myParseInt(value); + } + return value; + } + + updateEnforcerField(key, value) { + value = this.parseEnforcerField(key, value); + + const enforcer = this.state.enforcer; + enforcer[key] = value; + this.setState({ + enforcer: enforcer, + }); + } + + renderEnforcer() { + return ( + + {this.state.mode === "add" ? i18next.t("enforcer:New Enforcer") : i18next.t("enforcer:Edit Enforcer")}     + + + {this.state.mode === "add" ? : null} + + } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> + + + {Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} : + + + { + this.updateEnforcerField("name", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : + + + { + this.updateEnforcerField("displayName", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} : + + + { + this.updateEnforcerField("description", e.target.value); + }} /> + + + + + {Setting.getLabel(i18next.t("general:Model"), i18next.t("general:Model - Tooltip"))} : + + + { + this.updateEnforcerField("adapter", adapter); + })} + options={this.state.adapters.map((adapter) => Setting.getOption(adapter.name, adapter.name)) + } /> + + + + + {Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} : + + + { + this.updateEnforcerField("isEnabled", checked); + }} /> + + + + ); + } + + submitEnforcerEdit(willExist) { + const enforcer = Setting.deepCopy(this.state.enforcer); + EnforcerBackend.updateEnforcer(this.state.organizationName, this.state.enforcerName, enforcer) + .then((res) => { + if (res.status === "ok") { + Setting.showMessage("success", i18next.t("general:Successfully saved")); + this.setState({ + enforcerName: this.state.enforcer.name, + }); + + if (willExist) { + this.props.history.push("/enforcers"); + } else { + this.props.history.push(`/enforcers/${this.state.enforcer.owner}/${this.state.enforcer.name}`); + } + } else { + Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`); + this.updateEnforcerField("name", this.state.enforcerName); + } + }) + .catch(error => { + Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`); + }); + } + + deleteEnforcer() { + EnforcerBackend.deleteEnforcer(this.state.enforcer) + .then((res) => { + if (res.status === "ok") { + this.props.history.push("/enforcers"); + } else { + Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); + } + }) + .catch(error => { + Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`); + }); + } + + render() { + return ( +
+ { + this.state.enforcer !== null ? this.renderEnforcer() : null + } +
+ + + {this.state.mode === "add" ? : null} +
+
+ ); + } +} + +export default EnforcerEditPage; diff --git a/web/src/EnforcerListPage.js b/web/src/EnforcerListPage.js new file mode 100644 index 00000000..080ed966 --- /dev/null +++ b/web/src/EnforcerListPage.js @@ -0,0 +1,218 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from "react"; +import {Link} from "react-router-dom"; +import {Button, Switch, Table} from "antd"; +import moment from "moment"; +import * as Setting from "./Setting"; +import * as EnforcerBackend from "./backend/EnforcerBackend"; +import i18next from "i18next"; +import BaseListPage from "./BaseListPage"; +import PopconfirmModal from "./common/modal/PopconfirmModal"; + +class EnforcerListPage extends BaseListPage { + newEnforcer() { + const randomName = Setting.getRandomName(); + const owner = Setting.getRequestOrganization(this.props.account); + return { + owner: owner, + name: `enforcer_${randomName}`, + createdTime: moment().format(), + displayName: `New Enforcer - ${randomName}`, + isEnabled: true, + }; + } + + addEnforcer() { + const newEnforcer = this.newEnforcer(); + EnforcerBackend.addEnforcer(newEnforcer) + .then((res) => { + if (res.status === "ok") { + this.props.history.push({pathname: `/enforcers/${newEnforcer.owner}/${newEnforcer.name}`, mode: "add"}); + Setting.showMessage("success", i18next.t("general:Successfully added")); + } else { + Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`); + } + }) + .catch(error => { + Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`); + }); + } + + deleteEnforcer(i) { + EnforcerBackend.deleteEnforcer(this.state.data[i]) + .then((res) => { + if (res.status === "ok") { + Setting.showMessage("success", i18next.t("general:Successfully deleted")); + this.setState({ + data: Setting.deleteRow(this.state.data, i), + pagination: {total: this.state.pagination.total - 1}, + }); + } else { + Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); + } + }) + .catch(error => { + Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`); + }); + } + + renderTable(enforcers) { + const columns = [ + { + title: i18next.t("general:Name"), + dataIndex: "name", + key: "name", + width: "150px", + fixed: "left", + sorter: true, + ...this.getColumnSearchProps("name"), + render: (text, record, index) => { + return ( + + {text} + + ); + }, + }, + { + title: i18next.t("general:Organization"), + dataIndex: "owner", + key: "owner", + width: "120px", + sorter: true, + ...this.getColumnSearchProps("owner"), + render: (text, record, index) => { + return ( + + {text} + + ); + }, + }, + { + title: i18next.t("general:Created time"), + dataIndex: "createdTime", + key: "createdTime", + width: "160px", + sorter: true, + render: (text, record, index) => { + return Setting.getFormattedDate(text); + }, + }, + { + title: i18next.t("general:Display name"), + dataIndex: "displayName", + key: "displayName", + width: "200px", + sorter: true, + ...this.getColumnSearchProps("displayName"), + }, + { + title: i18next.t("general:Is enabled"), + dataIndex: "isEnabled", + key: "isEnabled", + width: "120px", + sorter: true, + render: (text, record, index) => { + return ( + + ); + }, + }, + { + title: i18next.t("general:Action"), + dataIndex: "", + key: "op", + width: "170px", + fixed: (Setting.isMobile()) ? "false" : "right", + render: (text, record, index) => { + return ( +
+ + this.deleteEnforcer(index)} + > + +
+ ); + }, + }, + ]; + + const paginationProps = { + total: this.state.pagination.total, + showQuickJumper: true, + showSizeChanger: true, + showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total), + }; + + return ( +
+ `${record.owner}/${record.name}`} size="middle" bordered + pagination={paginationProps} + title={() => ( +
+ {i18next.t("general:Enforcers")}     + +
+ )} + loading={this.state.loading} + onChange={this.handleTableChange} + /> + + ); + } + + fetch = (params = {}) => { + let field = params.searchedColumn, value = params.searchText; + const sortField = params.sortField, sortOrder = params.sortOrder; + if (params.type !== undefined && params.type !== null) { + field = "type"; + value = params.type; + } + this.setState({loading: true}); + EnforcerBackend.getEnforcers(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) + .then((res) => { + this.setState({ + loading: false, + }); + if (res.status === "ok") { + this.setState({ + data: res.data, + pagination: { + ...params.pagination, + total: res.data2, + }, + searchText: params.searchText, + searchedColumn: params.searchedColumn, + }); + } else { + if (Setting.isResponseDenied(res)) { + this.setState({ + isAuthorized: false, + }); + } else { + Setting.showMessage("error", res.msg); + } + } + }); + }; +} + +export default EnforcerListPage; diff --git a/web/src/backend/EnforcerBackend.js b/web/src/backend/EnforcerBackend.js new file mode 100644 index 00000000..9a79e31c --- /dev/null +++ b/web/src/backend/EnforcerBackend.js @@ -0,0 +1,71 @@ +// Copyright 2023 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as Setting from "../Setting"; + +export function getEnforcers(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") { + return fetch(`${Setting.ServerUrl}/api/get-enforcers?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, { + method: "GET", + credentials: "include", + headers: { + "Accept-Language": Setting.getAcceptLanguage(), + }, + }).then(res => res.json()); +} + +export function getEnforcer(owner, name) { + return fetch(`${Setting.ServerUrl}/api/get-enforcer?id=${owner}/${encodeURIComponent(name)}`, { + method: "GET", + credentials: "include", + headers: { + "Accept-Language": Setting.getAcceptLanguage(), + }, + }).then(res => res.json()); +} + +export function updateEnforcer(owner, name, enforcer) { + const newEnforcer = Setting.deepCopy(enforcer); + return fetch(`${Setting.ServerUrl}/api/update-enforcer?id=${owner}/${encodeURIComponent(name)}`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newEnforcer), + headers: { + "Accept-Language": Setting.getAcceptLanguage(), + }, + }).then(res => res.json()); +} + +export function addEnforcer(enforcer) { + const newEnforcer = Setting.deepCopy(enforcer); + return fetch(`${Setting.ServerUrl}/api/add-enforcer`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newEnforcer), + headers: { + "Accept-Language": Setting.getAcceptLanguage(), + }, + }).then(res => res.json()); +} + +export function deleteEnforcer(enforcer) { + const newEnforcer = Setting.deepCopy(enforcer); + return fetch(`${Setting.ServerUrl}/api/delete-enforcer`, { + method: "POST", + credentials: "include", + body: JSON.stringify(newEnforcer), + headers: { + "Accept-Language": Setting.getAcceptLanguage(), + }, + }).then(res => res.json()); +}