Skip to content

Commit 84cb7d5

Browse files
authoredJul 4, 2023
[feat]: org component role setting (#80)
Signed-off-by: kevin <kevin@lindb.io>
1 parent d838d45 commit 84cb7d5

11 files changed

+257
-9
lines changed
 

‎model/acl.go

+5
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ type ResourceACLParam struct {
3636
func (p *ResourceACLParam) ToParams() []any {
3737
return []any{p.Role.String(), fmt.Sprintf("%d", p.OrgID), p.Category.String(), p.Resource, p.Action.String()}
3838
}
39+
40+
// ToStringParams returns casbin params.
41+
func (p *ResourceACLParam) ToStringParams() []string {
42+
return []string{p.Role.String(), fmt.Sprintf("%d", p.OrgID), p.Category.String(), p.Resource, p.Action.String()}
43+
}

‎model/acl_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,12 @@ func Test_ResourceACLParam_ToParams(t *testing.T) {
3333
Resource: "abc",
3434
Action: accesscontrol.Write,
3535
}).ToParams())
36+
37+
assert.Equal(t, []string{"Admin", "123", "Dashboard", "abc", "write"}, (&ResourceACLParam{
38+
Role: accesscontrol.RoleAdmin,
39+
OrgID: 123,
40+
Category: accesscontrol.Dashboard,
41+
Resource: "abc",
42+
Action: accesscontrol.Write,
43+
}).ToStringParams())
3644
}

‎model/component.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ type Component struct {
7676
Role accesscontrol.RoleType `json:"role" gorm:"column:role"`
7777
Order int `json:"order" gorm:"column:order"`
7878

79-
ParentUID string `json:"parentUID" gorm:"column:parent_uid"`
79+
ParentUID string `json:"parentUID,omitempty" gorm:"column:parent_uid"`
8080
Children []*Component `json:"children,omitempty" gorm:"-"`
8181
}
8282

‎service/authorize.go

+10
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ type AuthorizeService interface {
5757
AddResourcePolicy(aclParam *modelpkg.ResourceACLParam) error
5858
// RemoveResourcePoliciesByCategory removes resource level acl policies by category.
5959
RemoveResourcePoliciesByCategory(orgID int64, category accesscontrol.ResourceCategory) error
60+
// UpdateResourceRole updates the acl role for resource.
61+
UpdateResourceRole(alcParam *modelpkg.ResourceACLParam) error
6062
// CheckResourceACL checks resource if can be accesed by given param.
6163
CheckResourceACL(aclParam *modelpkg.ResourceACLParam) bool
6264
// CheckResourcesACL checks resource list if can be accesed by given params.
@@ -139,6 +141,14 @@ func (srv *authorizeService) RemoveResourcePoliciesByCategory(orgID int64, categ
139141
return err
140142
}
141143

144+
// UpdateResourceRole updates the acl role for resource.
145+
func (srv *authorizeService) UpdateResourceRole(alcParam *modelpkg.ResourceACLParam) error {
146+
_, err := srv.resource.UpdateFilteredPolicies(
147+
[][]string{alcParam.ToStringParams()},
148+
1, fmt.Sprintf("%d", alcParam.OrgID), alcParam.Category.String(), alcParam.Resource, alcParam.Action.String())
149+
return err
150+
}
151+
142152
// CheckResourceACL checks resource if can be accesed by given param.
143153
func (srv *authorizeService) CheckResourceACL(aclParam *modelpkg.ResourceACLParam) bool {
144154
params := aclParam.ToParams()

‎service/authorize_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,25 @@ func TestAuthorizeService_AddResourcePolicy(t *testing.T) {
276276
assert.NoError(t, err)
277277
})
278278
}
279+
280+
func TestAuthorizeService_UpdateResourcesRole(t *testing.T) {
281+
ctrl := gomock.NewController(t)
282+
defer ctrl.Finish()
283+
284+
enforcer := casbinmock.NewMockIEnforcer(ctrl)
285+
srv := &authorizeService{
286+
resource: enforcer,
287+
logger: logger.GetLogger("Service", "AuthTest"),
288+
}
289+
enforcer.EXPECT().UpdateFilteredPolicies(
290+
[][]string{{"Admin", "123", "Component", "abc", "write"}},
291+
1, "123", accesscontrol.Component.String(), "abc", "write").Return(false, fmt.Errorf("err"))
292+
err := srv.UpdateResourceRole(&modelpkg.ResourceACLParam{
293+
OrgID: 123,
294+
Category: accesscontrol.Component,
295+
Role: accesscontrol.RoleAdmin,
296+
Resource: "abc",
297+
Action: accesscontrol.Write,
298+
})
299+
assert.Error(t, err)
300+
}

‎service/component.go

+18-3
Original file line numberDiff line numberDiff line change
@@ -278,20 +278,35 @@ func (srv *componentService) SaveOrgComponents(ctx context.Context, orgUID strin
278278
// UpdateRolesOfOrgComponent updates roles for current org.
279279
func (srv *componentService) UpdateRolesOfOrgComponent(ctx context.Context, cmps []model.OrgComponentInfo) error {
280280
user := util.GetUser(ctx)
281-
return srv.db.Transaction(func(tx dbpkg.DB) error {
281+
if err := srv.db.Transaction(func(tx dbpkg.DB) error {
282282
for _, cmp := range cmps {
283283
cmpFromDB := &model.Component{}
284284
if err := tx.Get(cmpFromDB, "uid=?", cmp.ComponentUID); err != nil {
285285
return err
286286
}
287287
if err := tx.UpdateSingle(&model.OrgComponent{},
288288
"role", cmp.Role,
289-
"org_id and component_id=?", user.Org.ID, cmpFromDB.ID); err != nil {
289+
"org_id=? and component_id=?", user.Org.ID, cmpFromDB.ID); err != nil {
290290
return err
291291
}
292292
}
293293
return nil
294-
})
294+
}); err != nil {
295+
return err
296+
}
297+
// update acl role
298+
for _, cmp := range cmps {
299+
if err := srv.authorizeSrv.UpdateResourceRole(&model.ResourceACLParam{
300+
Role: cmp.Role,
301+
OrgID: user.Org.ID,
302+
Category: accesscontrol.Component,
303+
Resource: cmp.ComponentUID,
304+
Action: accesscontrol.Write,
305+
}); err != nil {
306+
return err
307+
}
308+
}
309+
return nil
295310
}
296311

297312
// saveComponentTree saves component tree.

‎service/component_test.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,19 @@ func TestComponentService_UpdateRolesOfOrgComponent(t *testing.T) {
482482
prepare: func() {
483483
mockDB.EXPECT().Get(gomock.Any(), "uid=?", "123").Return(nil)
484484
mockDB.EXPECT().UpdateSingle(gomock.Any(), "role",
485-
accesscontrol.RoleAdmin, gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("err"))
485+
accesscontrol.RoleAdmin, "org_id=? and component_id=?", int64(12),
486+
gomock.Any()).Return(fmt.Errorf("err"))
487+
},
488+
wantErr: true,
489+
},
490+
{
491+
name: "update acl role failure",
492+
prepare: func() {
493+
mockDB.EXPECT().Get(gomock.Any(), "uid=?", "123").Return(nil)
494+
mockDB.EXPECT().UpdateSingle(gomock.Any(), "role",
495+
accesscontrol.RoleAdmin, "org_id=? and component_id=?", int64(12),
496+
gomock.Any()).Return(nil)
497+
authorizeSrv.EXPECT().UpdateResourceRole(gomock.Any()).Return(fmt.Errorf("err"))
486498
},
487499
wantErr: true,
488500
},
@@ -491,7 +503,9 @@ func TestComponentService_UpdateRolesOfOrgComponent(t *testing.T) {
491503
prepare: func() {
492504
mockDB.EXPECT().Get(gomock.Any(), "uid=?", "123").Return(nil)
493505
mockDB.EXPECT().UpdateSingle(gomock.Any(), "role",
494-
accesscontrol.RoleAdmin, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
506+
accesscontrol.RoleAdmin, "org_id=? and component_id=?", int64(12),
507+
gomock.Any()).Return(nil)
508+
authorizeSrv.EXPECT().UpdateResourceRole(gomock.Any()).Return(nil)
495509
},
496510
wantErr: false,
497511
},

‎web/src/features/setting/Setting.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import ListDataSource from './datasource/ListDataSource';
4444
import OrgList from './org/OrgList';
4545
import UserList from './user/UserList';
4646
import ComponentSetting from './component/ComponentSetting';
47+
import OrgComponent from './component/OrgComponent';
4748
const { Meta } = Card;
4849
const { Text, Title } = Typography;
4950

@@ -77,6 +78,7 @@ const Setting: React.FC = () => {
7778
'/setting/org/teams',
7879
'/setting/orgs',
7980
'/setting/components',
81+
'/setting/orgs/components',
8082
].indexOf(location.pathname) >= 0
8183
? location.pathname
8284
: '/setting/datasources'
@@ -104,6 +106,12 @@ const Setting: React.FC = () => {
104106
<TabPane itemKey="/setting/components" tab="Component" icon={<Icon icon="menu" style={{ marginRight: 8 }} />}>
105107
<ComponentSetting />
106108
</TabPane>
109+
<TabPane
110+
itemKey="/setting/orgs/components"
111+
tab="Org. Component"
112+
icon={<Icon icon="menu" style={{ marginRight: 8 }} />}>
113+
<OrgComponent />
114+
</TabPane>
107115
</Tabs>
108116
</Card>
109117
);

‎web/src/features/setting/component/ComponentSetting.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import * as IconFontsStyles from '@src/styles/icon-fonts/fonts.scss?inline';
3131
import { useRequest } from '@src/hooks';
3232
import { ComponentSrv } from '@src/services';
3333
import { ApiKit } from '@src/utils';
34-
import FormSlot from '@douyinfe/semi-ui/lib/es/form/slot';
3534
import EmptyImg from '@src/images/empty.svg';
3635
import { Component, Feature, FeatureRepositoryInst, RoleList } from '@src/types';
3736

@@ -257,9 +256,9 @@ const ComponentSetting: React.FC = () => {
257256
getFormApi={(api: any) => {
258257
formApi.current = api;
259258
}}>
260-
<FormSlot label="Parent">
259+
<Form.Slot label="Parent">
261260
<Tag size="large">{currentParent ? currentParent.label : 'Root'}</Tag>
262-
</FormSlot>
261+
</Form.Slot>
263262
<Form.Input field="label" label="Label" rules={[{ required: true, message: 'Label is required' }]} />
264263
<Form.Select
265264
label="Role"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
Licensed to LinDB under one or more contributor
3+
license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright
5+
ownership. LinDB licenses this file to you under
6+
the Apache License, Version 2.0 (the "License"); you may
7+
not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing,
12+
software distributed under the License is distributed on an
13+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
KIND, either express or implied. See the License for the
15+
specific language governing permissions and limitations
16+
under the License.
17+
*/
18+
import React, { useCallback, useEffect, useRef, useState } from 'react';
19+
import { useRequest } from '@src/hooks';
20+
import { ComponentSrv } from '@src/services';
21+
import { Component, RoleList } from '@src/types';
22+
import { isEmpty } from 'lodash-es';
23+
import { Button, Col, Divider, Empty, Form, Row, Tree, Typography } from '@douyinfe/semi-ui';
24+
import { Icon, Notification } from '@src/components';
25+
import EmptyImg from '@src/images/empty.svg';
26+
import './component.scss';
27+
import { IconRefresh, IconSaveStroked } from '@douyinfe/semi-icons';
28+
import { ApiKit } from '@src/utils';
29+
30+
const OrgComponent: React.FC = () => {
31+
const { result: navTree, refetch } = useRequest(['load_org_component_tree'], () => {
32+
return ComponentSrv.getOrgComponentTree();
33+
});
34+
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
35+
const [treeData, setTreeData] = useState<any[]>([]);
36+
const [editing, setEditing] = useState(false);
37+
const [submitting, setSubmitting] = useState(false);
38+
const [selectItem, setSelectItem] = useState<any>({});
39+
const formApi = useRef<any>();
40+
41+
const buildTree = useCallback((navTree: Component[], parent: Component | null): any[] => {
42+
return navTree.map((nav: Component) => {
43+
const item = {
44+
label: nav.label,
45+
uid: nav.uid,
46+
key: nav.uid,
47+
value: nav.uid,
48+
icon: <Icon icon={nav.icon} style={{ marginRight: 8 }} />,
49+
raw: nav,
50+
parent: parent,
51+
children: !isEmpty(nav.children) ? buildTree(nav.children, nav) : [],
52+
};
53+
return item;
54+
});
55+
}, []);
56+
57+
useEffect(() => {
58+
setTreeData(buildTree(navTree || [], null));
59+
}, [navTree, buildTree]);
60+
61+
return (
62+
<Row className="menu-setting">
63+
<Col span={10} className="menu-tree">
64+
<div className="buttons">
65+
<Button
66+
icon={<IconRefresh />}
67+
type="tertiary"
68+
onClick={() => {
69+
refetch();
70+
}}
71+
/>
72+
</div>
73+
<Divider />
74+
<Tree
75+
style={{ marginRight: 4 }}
76+
motion={false}
77+
draggable
78+
onExpand={(keys: string[]) => setExpandedKeys(keys)}
79+
expandedKeys={expandedKeys}
80+
treeData={treeData}
81+
renderLabel={(label: any, item: any) => (
82+
<div style={{ display: 'flex', alignItems: 'center' }}>
83+
<Typography.Text
84+
ellipsis={{ showTooltip: true }}
85+
style={{ flex: 1 }}
86+
onClick={() => {
87+
setSelectItem(item.raw);
88+
formApi.current.setValues(item.raw, { isOverride: true });
89+
setEditing(true);
90+
}}>
91+
{label}
92+
</Typography.Text>
93+
</div>
94+
)}
95+
/>
96+
</Col>
97+
<Col span={14} className="menu-form">
98+
{!editing && (
99+
<Empty
100+
title="Oops! No edit menu"
101+
style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', marginTop: 50 }}
102+
image={<img src={EmptyImg} style={{ width: 150, height: 150 }} />}
103+
darkModeImage={<img src={EmptyImg} style={{ width: 150, height: 150 }} />}
104+
layout="horizontal"
105+
description="Please select edit menu"
106+
/>
107+
)}
108+
109+
<Form
110+
style={{ display: editing ? 'block' : 'none', paddingLeft: 24 }}
111+
className="linsight-form"
112+
labelPosition="left"
113+
labelAlign="right"
114+
labelWidth={120}
115+
allowEmpty
116+
onSubmit={async (values: Component) => {
117+
try {
118+
setSubmitting(true);
119+
await ComponentSrv.updateRoleOfOrgComponent([{ componentUid: selectItem.uid, role: values.role }]);
120+
refetch();
121+
Notification.success('Component save successfully!');
122+
} catch (err) {
123+
Notification.error(ApiKit.getErrorMsg(err));
124+
} finally {
125+
setSubmitting(false);
126+
}
127+
}}
128+
getFormApi={(api: any) => {
129+
formApi.current = api;
130+
}}>
131+
<Form.Slot label="Label">
132+
<Icon icon={selectItem.icon} />
133+
<Typography.Text style={{ marginLeft: 4 }}>{selectItem.label}</Typography.Text>
134+
</Form.Slot>
135+
<Form.Select
136+
label="Role"
137+
field="role"
138+
optionList={RoleList}
139+
rules={[{ required: true, message: 'Label is required' }]}
140+
/>
141+
<Form.Slot>
142+
<Button
143+
icon={<IconSaveStroked />}
144+
loading={submitting}
145+
onClick={() => {
146+
formApi.current.submitForm();
147+
}}>
148+
Save
149+
</Button>
150+
</Form.Slot>
151+
</Form>
152+
</Col>
153+
</Row>
154+
);
155+
};
156+
157+
export default OrgComponent;

‎web/src/services/component.service.ts

+10
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ const getOrgComponents = (orgUID: string): Promise<OrgComponent[]> => {
3131
return ApiKit.GET<OrgComponent[]>(`${ApiPath.Org}/${orgUID}${ApiPath.Component}`);
3232
};
3333

34+
const getOrgComponentTree = (): Promise<Component[]> => {
35+
return ApiKit.GET<Component[]>(`${ApiPath.Org}/${ApiPath.Component}`);
36+
};
37+
38+
const updateRoleOfOrgComponent = (cmps: OrgComponent[]): Promise<string> => {
39+
return ApiKit.PUT<string>(`${ApiPath.Org}/${ApiPath.Component}/roles`, cmps);
40+
};
41+
3442
const createComponent = (cmp: Component): Promise<string> => {
3543
return ApiKit.POST<string>(ApiPath.Component, cmp);
3644
};
@@ -49,6 +57,8 @@ const sortComponents = (cmps: Component[]): Promise<string> => {
4957

5058
export default {
5159
getComponentTree,
60+
getOrgComponentTree,
61+
updateRoleOfOrgComponent,
5262
saveOrgComponents,
5363
getOrgComponents,
5464
createComponent,

0 commit comments

Comments
 (0)
Please sign in to comment.