From 34151c00953a97790bff1db8b9a031254b2315b9 Mon Sep 17 00:00:00 2001
From: leoil <31148569+leoil@users.noreply.github.com>
Date: Sun, 28 May 2023 11:29:43 +0800
Subject: [PATCH] feat: Support uploading roles and permssions via xlsx files.
(#1899)
* Support uploading roles and permissions via xlsx file.
* Template xlsx file for uploading users and permissions.
* reformat according to gofumpt.
* fix typo.
---
controllers/permission_upload.go | 50 ++++++++++++++++++++
controllers/role_upload.go | 50 ++++++++++++++++++++
object/permission.go | 51 ++++++++++++++++++++
object/permission_upload.go | 77 +++++++++++++++++++++++++++++++
object/role.go | 42 +++++++++++++++++
object/role_upload.go | 63 +++++++++++++++++++++++++
object/user_upload.go | 23 +++++++++
routers/router.go | 2 +
web/src/PermissionListPage.js | 42 ++++++++++++++++-
web/src/RoleListPage.js | 44 +++++++++++++++++-
xlsx/permission_test.xlsx | Bin 0 -> 10595 bytes
xlsx/role_test.xlsx | Bin 0 -> 10134 bytes
12 files changed, 440 insertions(+), 4 deletions(-)
create mode 100644 controllers/permission_upload.go
create mode 100644 controllers/role_upload.go
create mode 100644 object/permission_upload.go
create mode 100644 object/role_upload.go
create mode 100644 xlsx/permission_test.xlsx
create mode 100644 xlsx/role_test.xlsx
diff --git a/controllers/permission_upload.go b/controllers/permission_upload.go
new file mode 100644
index 00000000..5770f383
--- /dev/null
+++ b/controllers/permission_upload.go
@@ -0,0 +1,50 @@
+// 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 (
+ "fmt"
+
+ "github.com/casdoor/casdoor/object"
+ "github.com/casdoor/casdoor/util"
+)
+
+func (c *ApiController) UploadPermissions() {
+ userId := c.GetSessionUsername()
+ owner, user := util.GetOwnerAndNameFromId(userId)
+
+ file, header, err := c.Ctx.Request.FormFile("file")
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
+
+ path := util.GetUploadXlsxPath(fileId)
+ util.EnsureFileFolderExists(path)
+ err = saveFile(path, &file)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ affected := object.UploadPermissions(owner, fileId)
+ if affected {
+ c.ResponseOk()
+ } else {
+ c.ResponseError(c.T("user_upload:Failed to import users"))
+ }
+}
diff --git a/controllers/role_upload.go b/controllers/role_upload.go
new file mode 100644
index 00000000..8b53c28c
--- /dev/null
+++ b/controllers/role_upload.go
@@ -0,0 +1,50 @@
+// 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 (
+ "fmt"
+
+ "github.com/casdoor/casdoor/object"
+ "github.com/casdoor/casdoor/util"
+)
+
+func (c *ApiController) UploadRoles() {
+ userId := c.GetSessionUsername()
+ owner, user := util.GetOwnerAndNameFromId(userId)
+
+ file, header, err := c.Ctx.Request.FormFile("file")
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ fileId := fmt.Sprintf("%s_%s_%s", owner, user, util.RemoveExt(header.Filename))
+
+ path := util.GetUploadXlsxPath(fileId)
+ util.EnsureFileFolderExists(path)
+ err = saveFile(path, &file)
+ if err != nil {
+ c.ResponseError(err.Error())
+ return
+ }
+
+ affected := object.UploadRoles(owner, fileId)
+ if affected {
+ c.ResponseOk()
+ } else {
+ c.ResponseError(c.T("user_upload:Failed to import users"))
+ }
+}
diff --git a/object/permission.go b/object/permission.go
index cf5e268e..006f9d55 100644
--- a/object/permission.go
+++ b/object/permission.go
@@ -15,6 +15,9 @@
package object
import (
+ "strings"
+
+ "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
@@ -188,6 +191,54 @@ func AddPermission(permission *Permission) bool {
return affected != 0
}
+func AddPermissions(permissions []*Permission) bool {
+ if len(permissions) == 0 {
+ return false
+ }
+
+ affected, err := adapter.Engine.Insert(permissions)
+ if err != nil {
+ if !strings.Contains(err.Error(), "Duplicate entry") {
+ panic(err)
+ }
+ }
+
+ for _, permission := range permissions {
+ // add using for loop
+ if affected != 0 {
+ addGroupingPolicies(permission)
+ addPolicies(permission)
+ }
+ }
+ return affected != 0
+}
+
+func AddPermissionsInBatch(permissions []*Permission) bool {
+ batchSize := conf.GetConfigBatchSize()
+
+ if len(permissions) == 0 {
+ return false
+ }
+
+ affected := false
+ for i := 0; i < (len(permissions)-1)/batchSize+1; i++ {
+ start := i * batchSize
+ end := (i + 1) * batchSize
+ if end > len(permissions) {
+ end = len(permissions)
+ }
+
+ tmp := permissions[start:end]
+ // TODO: save to log instead of standard output
+ // fmt.Printf("Add Permissions: [%d - %d].\n", start, end)
+ if AddPermissions(tmp) {
+ affected = true
+ }
+ }
+
+ return affected
+}
+
func DeletePermission(permission *Permission) bool {
affected, err := adapter.Engine.ID(core.PK{permission.Owner, permission.Name}).Delete(&Permission{})
if err != nil {
diff --git a/object/permission_upload.go b/object/permission_upload.go
new file mode 100644
index 00000000..15e94801
--- /dev/null
+++ b/object/permission_upload.go
@@ -0,0 +1,77 @@
+// 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/casdoor/casdoor/xlsx"
+)
+
+func getPermissionMap(owner string) map[string]*Permission {
+ m := map[string]*Permission{}
+
+ permissions := GetPermissions(owner)
+ for _, permission := range permissions {
+ m[permission.GetId()] = permission
+ }
+
+ return m
+}
+
+func UploadPermissions(owner string, fileId string) bool {
+ table := xlsx.ReadXlsxFile(fileId)
+
+ oldUserMap := getPermissionMap(owner)
+ newPermissions := []*Permission{}
+ for index, line := range table {
+ if index == 0 || parseLineItem(&line, 0) == "" {
+ continue
+ }
+
+ permission := &Permission{
+ Owner: parseLineItem(&line, 0),
+ Name: parseLineItem(&line, 1),
+ CreatedTime: parseLineItem(&line, 2),
+ DisplayName: parseLineItem(&line, 3),
+
+ Users: parseListItem(&line, 4),
+ Roles: parseListItem(&line, 5),
+ Domains: parseListItem(&line, 6),
+
+ Model: parseLineItem(&line, 7),
+ Adapter: parseLineItem(&line, 8),
+ ResourceType: parseLineItem(&line, 9),
+
+ Resources: parseListItem(&line, 10),
+ Actions: parseListItem(&line, 11),
+
+ Effect: parseLineItem(&line, 12),
+ IsEnabled: parseLineItemBool(&line, 13),
+
+ Submitter: parseLineItem(&line, 14),
+ Approver: parseLineItem(&line, 15),
+ ApproveTime: parseLineItem(&line, 16),
+ State: parseLineItem(&line, 17),
+ }
+
+ if _, ok := oldUserMap[permission.GetId()]; !ok {
+ newPermissions = append(newPermissions, permission)
+ }
+ }
+
+ if len(newPermissions) == 0 {
+ return false
+ }
+ return AddPermissionsInBatch(newPermissions)
+}
diff --git a/object/role.go b/object/role.go
index 5d744166..ec1c9493 100644
--- a/object/role.go
+++ b/object/role.go
@@ -16,6 +16,9 @@ package object
import (
"fmt"
+ "strings"
+
+ "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
@@ -160,6 +163,45 @@ func AddRole(role *Role) bool {
return affected != 0
}
+func AddRoles(roles []*Role) bool {
+ if len(roles) == 0 {
+ return false
+ }
+ affected, err := adapter.Engine.Insert(roles)
+ if err != nil {
+ if !strings.Contains(err.Error(), "Duplicate entry") {
+ panic(err)
+ }
+ }
+ return affected != 0
+}
+
+func AddRolesInBatch(roles []*Role) bool {
+ batchSize := conf.GetConfigBatchSize()
+
+ if len(roles) == 0 {
+ return false
+ }
+
+ affected := false
+ for i := 0; i < (len(roles)-1)/batchSize+1; i++ {
+ start := i * batchSize
+ end := (i + 1) * batchSize
+ if end > len(roles) {
+ end = len(roles)
+ }
+
+ tmp := roles[start:end]
+ // TODO: save to log instead of standard output
+ // fmt.Printf("Add users: [%d - %d].\n", start, end)
+ if AddRoles(tmp) {
+ affected = true
+ }
+ }
+
+ return affected
+}
+
func DeleteRole(role *Role) bool {
roleId := role.GetId()
permissions := GetPermissionsByRole(roleId)
diff --git a/object/role_upload.go b/object/role_upload.go
new file mode 100644
index 00000000..a91908ab
--- /dev/null
+++ b/object/role_upload.go
@@ -0,0 +1,63 @@
+// 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/casdoor/casdoor/xlsx"
+)
+
+func getRoleMap(owner string) map[string]*Role {
+ m := map[string]*Role{}
+
+ roles := GetRoles(owner)
+ for _, role := range roles {
+ m[role.GetId()] = role
+ }
+
+ return m
+}
+
+func UploadRoles(owner string, fileId string) bool {
+ table := xlsx.ReadXlsxFile(fileId)
+
+ oldUserMap := getRoleMap(owner)
+ newRoles := []*Role{}
+ for index, line := range table {
+ if index == 0 || parseLineItem(&line, 0) == "" {
+ continue
+ }
+
+ role := &Role{
+ Owner: parseLineItem(&line, 0),
+ Name: parseLineItem(&line, 1),
+ CreatedTime: parseLineItem(&line, 2),
+ DisplayName: parseLineItem(&line, 3),
+
+ Users: parseListItem(&line, 4),
+ Roles: parseListItem(&line, 5),
+ Domains: parseListItem(&line, 6),
+ IsEnabled: parseLineItemBool(&line, 7),
+ }
+
+ if _, ok := oldUserMap[role.GetId()]; !ok {
+ newRoles = append(newRoles, role)
+ }
+ }
+
+ if len(newRoles) == 0 {
+ return false
+ }
+ return AddRolesInBatch(newRoles)
+}
diff --git a/object/user_upload.go b/object/user_upload.go
index cdefb34d..e19ef701 100644
--- a/object/user_upload.go
+++ b/object/user_upload.go
@@ -15,6 +15,9 @@
package object
import (
+ "sort"
+ "strings"
+
"github.com/casdoor/casdoor/util"
"github.com/casdoor/casdoor/xlsx"
)
@@ -47,6 +50,26 @@ func parseLineItemBool(line *[]string, i int) bool {
return parseLineItemInt(line, i) != 0
}
+func parseListItem(lines *[]string, i int) []string {
+ if i >= len(*lines) {
+ return nil
+ }
+ line := (*lines)[i]
+ items := strings.Split(line, ";")
+ trimmedItems := make([]string, 0, len(items))
+
+ for _, item := range items {
+ trimmedItem := strings.TrimSpace(item)
+ if trimmedItem != "" {
+ trimmedItems = append(trimmedItems, trimmedItem)
+ }
+ }
+
+ sort.Strings(trimmedItems)
+
+ return trimmedItems
+}
+
func UploadUsers(owner string, fileId string) bool {
table := xlsx.ReadXlsxFile(fileId)
diff --git a/routers/router.go b/routers/router.go
index 264da792..3b30c295 100644
--- a/routers/router.go
+++ b/routers/router.go
@@ -82,6 +82,7 @@ func initAPI() {
beego.Router("/api/update-role", &controllers.ApiController{}, "POST:UpdateRole")
beego.Router("/api/add-role", &controllers.ApiController{}, "POST:AddRole")
beego.Router("/api/delete-role", &controllers.ApiController{}, "POST:DeleteRole")
+ beego.Router("/api/upload-roles", &controllers.ApiController{}, "POST:UploadRoles")
beego.Router("/api/get-permissions", &controllers.ApiController{}, "GET:GetPermissions")
beego.Router("/api/get-permissions-by-submitter", &controllers.ApiController{}, "GET:GetPermissionsBySubmitter")
@@ -90,6 +91,7 @@ func initAPI() {
beego.Router("/api/update-permission", &controllers.ApiController{}, "POST:UpdatePermission")
beego.Router("/api/add-permission", &controllers.ApiController{}, "POST:AddPermission")
beego.Router("/api/delete-permission", &controllers.ApiController{}, "POST:DeletePermission")
+ beego.Router("/api/upload-permissions", &controllers.ApiController{}, "POST:UploadPermissions")
beego.Router("/api/enforce", &controllers.ApiController{}, "POST:Enforce")
beego.Router("/api/batch-enforce", &controllers.ApiController{}, "POST:BatchEnforce")
diff --git a/web/src/PermissionListPage.js b/web/src/PermissionListPage.js
index fb817ac9..e12c8315 100644
--- a/web/src/PermissionListPage.js
+++ b/web/src/PermissionListPage.js
@@ -14,13 +14,14 @@
import React from "react";
import {Link} from "react-router-dom";
-import {Button, Switch, Table} from "antd";
+import {Button, Switch, Table, Upload} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as PermissionBackend from "./backend/PermissionBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
+import {UploadOutlined} from "@ant-design/icons";
class PermissionListPage extends BaseListPage {
newPermission() {
@@ -79,6 +80,40 @@ class PermissionListPage extends BaseListPage {
});
}
+ uploadPermissionFile(info) {
+ const {status, response: res} = info.file;
+ if (status === "done") {
+ if (res.status === "ok") {
+ Setting.showMessage("success", "Users uploaded successfully, refreshing the page");
+
+ const {pagination} = this.state;
+ this.fetch({pagination});
+ } else {
+ Setting.showMessage("error", `Users failed to upload: ${res.msg}`);
+ }
+ } else if (status === "error") {
+ Setting.showMessage("error", "File failed to upload");
+ }
+ }
+ renderPermissionUpload() {
+ const props = {
+ name: "file",
+ accept: ".xlsx",
+ method: "post",
+ action: `${Setting.ServerUrl}/api/upload-permissions`,
+ withCredentials: true,
+ onChange: (info) => {
+ this.uploadPermissionFile(info);
+ },
+ };
+
+ return (
+
n6Apz_N>PZP$lOz8STx&AB1*%u!#
zqpw3GKC17aKoKGPWug*q=3>ATsbEA>+gZCBLDRnf)OQvAN_ b0(T{0Cj1cPv+)S~L5Q_&=uBwzP^YuhsvKOgsa4
z2PSWMd8%J;vx0Ry&6Bp6b5(yplDLgCa}Xp>*k-Z9!|*ZG`l4zQdSY`+lDKMZec@HS
zgt49Mixt5jhdn#~u1a&4RjW1d3DJ^cSDF!fkrwBM0D6s4lAcurn!orVQvl!}Il*J`zkBY#ifcls
c@qglf`f(*$I0yy-094421_EPRnxA+74=prNs{jB1
literal 0
HcmV?d00001
diff --git a/xlsx/role_test.xlsx b/xlsx/role_test.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..87c748392002206cfb5c4da439c8e5913a505a0a
GIT binary patch
literal 10134
zcmeHtg;yNu^7h~k0}R14xDW0SoM6F&g`mOREjR=xIDz02++9PE;2InP2^I(p?iRjD
zcJD8{?0$d2y*=l2pFZ7HXS%AMs^_hGRTK~qK>%a`DgXeW1c>tmj@iKh0EkEc06qW}
zURToI&c)Qu#o(!@gQ>F~tB0)(MJ^&dLpA^&_Wu8l|6&W2Jsq&?Vh6TsZHr31*9^}I
zEh#}IYa!F6*+NP2h}WiR*dS)O@u4?U)u=}8q*b*U)1^BWCMsW`(~%Hcvthl`QKm7(
zD2y(5I(&31n