mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
iam: roles rules tab (#55)
This adds the rules tab for IAM/roles. Fixes #45 Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
parent
521175967e
commit
cba4d6d567
4720
ui/package-lock.json
generated
4720
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -38,9 +38,9 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.8",
|
||||
"ant-design-vue": "~1.4.6",
|
||||
"ant-design-vue": "~1.4.8",
|
||||
"axios": "^0.19.0",
|
||||
"core-js": "^3.4.2",
|
||||
"core-js": "^3.4.7",
|
||||
"enquire.js": "^2.1.6",
|
||||
"js-cookie": "^2.2.1",
|
||||
"lodash.get": "^4.4.2",
|
||||
@ -48,40 +48,43 @@
|
||||
"md5": "^2.2.1",
|
||||
"moment": "^2.24.0",
|
||||
"node-emoji": "^1.10.0",
|
||||
"npm-check-updates": "^3.2.1",
|
||||
"npm-check-updates": "^3.2.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"viser-vue": "^2.4.7",
|
||||
"vue": "^2.6.10",
|
||||
"vue-clipboard2": "^0.3.1",
|
||||
"vue-cropper": "0.4.9",
|
||||
"vue-i18n": "^8.15.0",
|
||||
"vue-i18n": "^8.15.1",
|
||||
"vue-ls": "^3.2.1",
|
||||
"vue-router": "^3.1.3",
|
||||
"vue-svg-component-runtime": "^1.0.1",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuex": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kazupon/vue-i18n-loader": "^0.4.1",
|
||||
"@vue/cli": "^4.0.5",
|
||||
"@vue/cli-plugin-babel": "^4.0.5",
|
||||
"@vue/cli-plugin-eslint": "^4.0.5",
|
||||
"@vue/cli-plugin-unit-jest": "^4.0.5",
|
||||
"@vue/cli-service": "^4.0.5",
|
||||
"@vue/eslint-config-standard": "^5.0.0",
|
||||
"@vue/cli": "^4.1.1",
|
||||
"@vue/cli-plugin-babel": "^4.1.1",
|
||||
"@vue/cli-plugin-eslint": "^4.1.1",
|
||||
"@vue/cli-plugin-unit-jest": "^4.1.1",
|
||||
"@vue/cli-service": "^4.1.1",
|
||||
"@vue/eslint-config-standard": "^5.0.1",
|
||||
"@vue/test-utils": "^1.0.0-beta.20",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-plugin-import": "^1.12.2",
|
||||
"eslint": "^6.6.0",
|
||||
"babel-plugin-import": "^1.13.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-html": "^6.0.0",
|
||||
"eslint-plugin-vue": "^6.0.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-node": "^10.0.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.0.1",
|
||||
"less": "^3.10.3",
|
||||
"less-loader": "^5.0.0",
|
||||
"node-sass": "^4.13.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"vue-cli-plugin-i18n": "^0.6.0",
|
||||
"vue-svg-icon-loader": "^2.1.1",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
|
||||
@ -17,11 +17,25 @@
|
||||
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function api (command, args = {}) {
|
||||
export function api (command, args = {}, method = 'GET', data = {}) {
|
||||
let params = {}
|
||||
args.command = command
|
||||
args.response = 'json'
|
||||
return axios.get('/', {
|
||||
params: args
|
||||
|
||||
if (data) {
|
||||
params = new URLSearchParams()
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
params.append(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
return axios({
|
||||
params: {
|
||||
...args
|
||||
},
|
||||
url: '/',
|
||||
method,
|
||||
data: params || {}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -23,12 +23,14 @@
|
||||
<div v-show="showAddDetail">
|
||||
<a-auto-complete
|
||||
style="width: 100%"
|
||||
:filterOption="filterOption"
|
||||
:value="newKey"
|
||||
:dataSource="Object.keys(detailOptions)"
|
||||
placeholder="Name"
|
||||
@change="e => onAddInputChange(e, 'newKey')" />
|
||||
<a-auto-complete
|
||||
style="width: 100%"
|
||||
:filterOption="filterOption"
|
||||
:value="newValue"
|
||||
:dataSource="detailOptions[newKey]"
|
||||
placeholder="Value"
|
||||
@ -112,6 +114,11 @@ export default {
|
||||
this.updateResource(this.resource)
|
||||
},
|
||||
methods: {
|
||||
filterOption (input, option) {
|
||||
return (
|
||||
option.componentOptions.children[0].text.toUpperCase().indexOf(input.toUpperCase()) >= 0
|
||||
)
|
||||
},
|
||||
updateResource (resource) {
|
||||
if (!resource) {
|
||||
return
|
||||
|
||||
@ -224,7 +224,7 @@ export default {
|
||||
name: 'details',
|
||||
component: () => import('@/components/view/DetailsTab.vue')
|
||||
}, {
|
||||
name: 'rules',
|
||||
name: 'Rules',
|
||||
component: () => import('@/views/iam/RolePermissionTab.vue')
|
||||
}],
|
||||
actions: [
|
||||
|
||||
42
ui/src/views/iam/PermissionEditable.vue
Normal file
42
ui/src/views/iam/PermissionEditable.vue
Normal file
@ -0,0 +1,42 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you 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.
|
||||
|
||||
<template>
|
||||
<a-select
|
||||
:value="defaultValue"
|
||||
@change="handleChange">
|
||||
<a-select-option value="allow">Allow</a-select-option>
|
||||
<a-select-option value="deny">Deny</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange (e) {
|
||||
this.$emit('change', e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@ -16,37 +16,416 @@
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<div>
|
||||
Boilerplate for implementation
|
||||
{{ loading }}
|
||||
{{ resource }}
|
||||
<a-icon v-if="loadingTable" type="loading" class="main-loading-spinner"></a-icon>
|
||||
<div v-else>
|
||||
<div v-if="updateTable" class="loading-overlay">
|
||||
<a-icon type="loading" />
|
||||
</div>
|
||||
<div
|
||||
class="rules-list ant-list ant-list-bordered"
|
||||
:class="{'rules-list--overflow-hidden' : updateTable}" >
|
||||
|
||||
<div class="rules-table-item ant-list-item">
|
||||
<div class="rules-table__col rules-table__col--grab"></div>
|
||||
<div class="rules-table__col rules-table__col--rule rules-table__col--new">
|
||||
<a-auto-complete
|
||||
:autoFocus="true"
|
||||
:filterOption="filterOption"
|
||||
:dataSource="apis"
|
||||
:value="newRule"
|
||||
@change="val => newRule = val"
|
||||
placeholder="Rule"
|
||||
:class="{'rule-dropdown-error' : newRuleSelectError}" />
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--permission">
|
||||
<permission-editable
|
||||
:defaultValue="newRulePermission"
|
||||
@change="onPermissionChange(null, $event)" />
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--description">
|
||||
<a-input v-model="newRuleDescription" placeholder="Description"></a-input>
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--actions">
|
||||
<a-tooltip
|
||||
placement="bottom">
|
||||
<template slot="title">
|
||||
Save new Rule
|
||||
</template>
|
||||
<a-button
|
||||
icon="plus"
|
||||
type="primary"
|
||||
shape="circle"
|
||||
@click="onRuleSave"
|
||||
>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<draggable
|
||||
v-model="rules"
|
||||
@change="changeOrder"
|
||||
handle=".drag-handle"
|
||||
animation="200"
|
||||
ghostClass="drag-ghost">
|
||||
<transition-group type="transition">
|
||||
<div
|
||||
v-for="(record, index) in rules"
|
||||
:key="`item-${index}`"
|
||||
class="rules-table-item ant-list-item">
|
||||
<div class="rules-table__col rules-table__col--grab drag-handle">
|
||||
<a-icon type="drag"></a-icon>
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--rule">
|
||||
{{ record.rule }}
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--permission">
|
||||
<permission-editable
|
||||
:defaultValue="record.permission"
|
||||
@change="onPermissionChange(record, $event)" />
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--description">
|
||||
<template v-if="record.description">
|
||||
{{ record.description }}
|
||||
</template>
|
||||
<div v-else class="no-description">
|
||||
No description entered.
|
||||
</div>
|
||||
</div>
|
||||
<div class="rules-table__col rules-table__col--actions">
|
||||
<rule-delete
|
||||
:record="record"
|
||||
@delete="onRuleDelete(record.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import draggable from 'vuedraggable'
|
||||
import PermissionEditable from './PermissionEditable'
|
||||
import RuleDelete from './RuleDelete'
|
||||
|
||||
export default {
|
||||
name: 'RolePermissionTab',
|
||||
components: {
|
||||
RuleDelete,
|
||||
PermissionEditable,
|
||||
draggable
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loadingTable: true,
|
||||
updateTable: false,
|
||||
rules: null,
|
||||
newRule: '',
|
||||
newRulePermission: 'allow',
|
||||
newRuleDescription: '',
|
||||
newRuleSelectError: false,
|
||||
drag: false,
|
||||
apis: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mounted () {
|
||||
this.apis = Object.keys(this.$store.getters.apis).sort((a, b) => a.localeCompare(b))
|
||||
this.fetchData()
|
||||
},
|
||||
watch: {
|
||||
resource: function () {
|
||||
this.fetchData(() => {
|
||||
this.resetNewFields()
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filterOption (input, option) {
|
||||
return (
|
||||
option.componentOptions.children[0].text.toUpperCase().indexOf(input.toUpperCase()) >= 0
|
||||
)
|
||||
},
|
||||
resetNewFields () {
|
||||
this.newRule = ''
|
||||
this.newRulePermission = 'allow'
|
||||
this.newRuleDescription = ''
|
||||
this.newRuleSelectError = false
|
||||
},
|
||||
fetchData (callback = null) {
|
||||
if (!this.resource.id) return
|
||||
api('listRolePermissions', { roleid: this.resource.id }).then(response => {
|
||||
this.rules = response.listrolepermissionsresponse.rolepermission
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
}).finally(() => {
|
||||
this.loadingTable = false
|
||||
this.updateTable = false
|
||||
if (callback) callback()
|
||||
})
|
||||
},
|
||||
changeOrder () {
|
||||
api('updateRolePermission', {}, 'POST', {
|
||||
roleid: this.resource.id,
|
||||
ruleorder: this.rules.map(rule => rule.id)
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
}).finally(() => {
|
||||
this.fetchData()
|
||||
})
|
||||
},
|
||||
onRuleDelete (key) {
|
||||
this.updateTable = true
|
||||
api('deleteRolePermission', { id: key }).catch(error => {
|
||||
console.error(error)
|
||||
}).finally(() => {
|
||||
this.fetchData()
|
||||
})
|
||||
},
|
||||
onPermissionChange (record, value) {
|
||||
this.newRulePermission = value
|
||||
|
||||
if (!record) return
|
||||
|
||||
this.updateTable = true
|
||||
api('updateRolePermission', {
|
||||
roleid: this.resource.id,
|
||||
ruleid: record.id,
|
||||
permission: value
|
||||
}).then(() => {
|
||||
this.fetchData()
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
},
|
||||
onRuleSelect (value) {
|
||||
this.newRule = value
|
||||
},
|
||||
onRuleSave () {
|
||||
if (!this.newRule) {
|
||||
this.newRuleSelectError = true
|
||||
return
|
||||
}
|
||||
|
||||
this.updateTable = true
|
||||
api('createRolePermission', {
|
||||
rule: this.newRule,
|
||||
permission: this.newRulePermission,
|
||||
description: this.newRuleDescription,
|
||||
roleid: this.resource.id
|
||||
}).then(() => {
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
}).finally(() => {
|
||||
this.resetNewFields()
|
||||
this.fetchData()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
<style lang="scss" scoped>
|
||||
.main-loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30px;
|
||||
}
|
||||
.role-add-btn {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.new-role-controls {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.rules-list {
|
||||
max-height: 600px;
|
||||
overflow: scroll;
|
||||
|
||||
&--overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.rules-table {
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
flex-wrap: nowrap;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
padding: 15px 0;
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid #e8e8e8;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&--grab {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
position: relative;
|
||||
top: auto;
|
||||
width: 35px;
|
||||
padding-left: 25px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--rule,
|
||||
&--description {
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--rule {
|
||||
padding-left: 60px;
|
||||
background-color: rgba(#e6f7ff, 0.7);
|
||||
|
||||
@media (min-width: 760px) {
|
||||
padding-left: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--permission {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 760px) {
|
||||
width: auto;
|
||||
|
||||
.ant-select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--actions {
|
||||
max-width: 60px;
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
width: auto;
|
||||
max-width: 70px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--new {
|
||||
padding-left: 15px;
|
||||
background-color: transparent;
|
||||
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.no-description {
|
||||
opacity: 0.4;
|
||||
font-size: 0.7rem;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drag-ghost {
|
||||
opacity: 0.5;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: #39A7DE;
|
||||
background-color: rgba(#fff, 0.8);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.rules-table__col--new {
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.rule-dropdown-error {
|
||||
.ant-input {
|
||||
border-color: #ff0000
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
60
ui/src/views/iam/RuleDelete.vue
Normal file
60
ui/src/views/iam/RuleDelete.vue
Normal file
@ -0,0 +1,60 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you 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.
|
||||
|
||||
<template>
|
||||
<a-tooltip placement="bottom">
|
||||
<template slot="title">
|
||||
Delete rule
|
||||
</template>
|
||||
<a-popconfirm
|
||||
title="Delete Rule?"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<a-button type="danger" shape="circle">
|
||||
<a-icon type="delete" />
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RuleDelete',
|
||||
props: {
|
||||
record: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleDelete () {
|
||||
this.$emit('delete')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style
|
||||
scoped
|
||||
lang="scss"
|
||||
>
|
||||
.anticon-delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user