Skip to content

Commit 964c517

Browse files
Adding ACL management to frontend - DependencyTrack/dependency-track#140
1 parent e165052 commit 964c517

File tree

6 files changed

+348
-4
lines changed

6 files changed

+348
-4
lines changed

src/i18n/locales/en.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@
312312
"permissions": "Permissions",
313313
"base_url": "Base URL",
314314
"enable_svg_badge": "Enable SVG badge support (unauthenticated)",
315+
"enable_acl": "Enable portfolio access control (beta)",
315316
"enable_bom_cyclonedx": "Enable CycloneDX",
316317
"enable_bom_spdx": "Enable SPDX",
317318
"enable_email": "Enable email",
@@ -417,7 +418,10 @@
417418
"internal": "Internal",
418419
"delete_repository": "Delete Repository",
419420
"repository_created": "Repository created",
420-
"repository_deleted": "Repository deleted"
421+
"repository_deleted": "Repository deleted",
422+
"portfolio_access_control": "Portfolio Access Control",
423+
"project_access": "Project access",
424+
"select_project": "Select Project"
421425
},
422426
"condition": {
423427
"warning": "Warning",

src/shared/api.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,7 @@
4747
"URL_POLICY": "api/v1/policy",
4848
"URL_POLICY_VIOLATION": "api/v1/violation",
4949
"URL_POLICY_VIOLATION_ANALYSIS": "api/v1/violation/analysis",
50-
"URL_LICENSE_GROUP": "api/v1/licenseGroup"
50+
"URL_LICENSE_GROUP": "api/v1/licenseGroup",
51+
"URL_ACL_MAPPING": "api/v1/acl/mapping",
52+
"URL_ACL_TEAM": "api/v1/acl/team"
5153
}

src/views/administration/AdminMenu.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,12 @@
208208
component: "Permissions",
209209
name: this.$t('admin.permissions'),
210210
href: "#permissionsTab"
211-
}
211+
},
212+
{
213+
component: "PortfolioAccessControl",
214+
name: this.$t('admin.portfolio_access_control'),
215+
href: "#portfolioAclTab"
216+
},
212217
]
213218
}
214219
]

src/views/administration/Administration.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import OidcGroups from "./accessmanagement/OidcGroups";
5252
import Teams from "./accessmanagement/Teams";
5353
import Permissions from "./accessmanagement/Permissions";
54+
import PortfolioAccessControl from "./accessmanagement/PortfolioAccessControl";
5455
5556
export default {
5657
components: {
@@ -61,7 +62,7 @@
6162
Cargo, Composer, Gem, Hex, Maven, Npm, Nuget, Python,
6263
Alerts, Templates,
6364
FortifySsc, DefectDojo, KennaSecurity,
64-
LdapUsers, ManagedUsers, OidcUsers, OidcGroups, Teams, Permissions
65+
LdapUsers, ManagedUsers, OidcUsers, OidcGroups, Teams, Permissions, PortfolioAccessControl
6566
},
6667
created() {
6768
// Specifies the admin plugin metadata (Vue component, i18n name, and href) of the plugin to load
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<template>
2+
<b-card no-body :header="header">
3+
<b-card-body>
4+
<div id="customToolbar">
5+
<c-switch id="isAclEnabled" color="primary" v-model="isAclEnabled" label v-bind="labelIcon" />{{$t('admin.enable_acl')}}
6+
</div>
7+
<bootstrap-table
8+
ref="table"
9+
:columns="columns"
10+
:data="data"
11+
:options="options">
12+
</bootstrap-table>
13+
</b-card-body>
14+
</b-card>
15+
</template>
16+
17+
<script>
18+
import xssFilters from "xss-filters";
19+
import common from "../../../shared/common";
20+
import i18n from "../../../i18n";
21+
import bootstrapTableMixin from "../../../mixins/bootstrapTableMixin";
22+
import EventBus from "../../../shared/eventbus";
23+
import ActionableListGroupItem from "../../components/ActionableListGroupItem";
24+
import SelectProjectModal from "./SelectProjectModal";
25+
import permissionsMixin from "../../../mixins/permissionsMixin";
26+
import {Switch as cSwitch} from "@coreui/vue";
27+
import BInputGroupFormInput from "../../../forms/BInputGroupFormInput";
28+
import configPropertyMixin from "../mixins/configPropertyMixin";
29+
30+
export default {
31+
props: {
32+
header: String
33+
},
34+
mixins: [bootstrapTableMixin, configPropertyMixin],
35+
components: {
36+
cSwitch
37+
},
38+
data() {
39+
return {
40+
isAclEnabled: false,
41+
labelIcon: {
42+
dataOn: '\u2713',
43+
dataOff: '\u2715'
44+
},
45+
columns: [
46+
{
47+
title: this.$t('admin.team_name'),
48+
field: "name",
49+
sortable: false,
50+
formatter(value, row, index) {
51+
return xssFilters.inHTMLData(common.valueWithDefault(value, ""));
52+
}
53+
}
54+
],
55+
data: [],
56+
options: {
57+
search: true,
58+
showColumns: true,
59+
showRefresh: true,
60+
pagination: true,
61+
silentSort: false,
62+
sidePagination: 'client',
63+
queryParamsType: 'pageSize',
64+
pageList: '[10, 25, 50, 100]',
65+
pageSize: 10,
66+
icons: {
67+
refresh: 'fa-refresh'
68+
},
69+
detailView: true,
70+
detailViewIcon: false,
71+
detailViewByClick: true,
72+
detailFormatter: (index, row) => {
73+
return this.vueFormatter({
74+
i18n,
75+
template: `
76+
<b-row class="expanded-row">
77+
<b-col sm="6">
78+
<b-form-group :label="this.$t('admin.project_access')">
79+
<div class="list-group">
80+
<span v-for="project in projects">
81+
<actionable-list-group-item :value="projectLabel(project.name, project.version)" delete-icon="true" v-on:actionClicked="removeProjectMapping(project.uuid)"/>
82+
</span>
83+
<actionable-list-group-item add-icon="true" v-on:actionClicked="$root.$emit('bv::show::modal', 'selectProjectModal')"/>
84+
</div>
85+
</b-form-group>
86+
</b-col>
87+
<b-col sm="6">
88+
</b-col>
89+
<select-project-modal v-on:selection="updateProjectSelection" />
90+
</b-row>
91+
`,
92+
mixins: [permissionsMixin],
93+
components: {
94+
cSwitch,
95+
ActionableListGroupItem,
96+
SelectProjectModal,
97+
BInputGroupFormInput
98+
},
99+
data() {
100+
return {
101+
team: row,
102+
name: row.name,
103+
projects: row.mappedLdapGroups,
104+
labelIcon: {
105+
dataOn: '\u2713',
106+
dataOff: '\u2715'
107+
}
108+
}
109+
},
110+
methods: {
111+
projectLabel: function(name, version) {
112+
if (version) {
113+
return name + " " + version;
114+
} else {
115+
return name;
116+
}
117+
},
118+
updateProjectSelection: function(selections) {
119+
this.$root.$emit('bv::hide::modal', 'selectProjectModal');
120+
for (let i=0; i<selections.length; i++) {
121+
let selection = selections[i];
122+
let url = `${this.$api.BASE_URL}/${this.$api.URL_ACL_MAPPING}`;
123+
this.axios.put(url, {
124+
team: this.team.uuid,
125+
project: selection.uuid
126+
}).then((response) => {
127+
if (this.projects === undefined || this.projects === null) {
128+
this.projects = [];
129+
}
130+
this.projects.push({name:selection.name, version:selection.version, uuid:selection.uuid});
131+
this.projects.sort();
132+
this.$toastr.s(this.$t('message.updated'));
133+
}).catch((error) => {
134+
if (error.response.status === 304) {
135+
//this.$toastr.w(this.$t('condition.unsuccessful_action'));
136+
} else {
137+
this.$toastr.w(this.$t('condition.unsuccessful_action'));
138+
}
139+
});
140+
}
141+
},
142+
removeProjectMapping: function(projectUuid) {
143+
let url = `${this.$api.BASE_URL}/${this.$api.URL_ACL_MAPPING}/team/${this.team.uuid}/project/${projectUuid}`;
144+
this.axios.delete(url).then((response) => {
145+
let k = [];
146+
for (let i=0; i<this.projects.length; i++) {
147+
if (this.projects[i].uuid !== projectUuid) {
148+
k.push(this.projects[i]);
149+
}
150+
}
151+
this.projects = k;
152+
this.$toastr.s(this.$t('message.updated'));
153+
}).catch((error) => {
154+
this.$toastr.w(this.$t('condition.unsuccessful_action'));
155+
});
156+
}
157+
},
158+
mounted() {
159+
let url = `${this.$api.BASE_URL}/${this.$api.URL_ACL_TEAM}/${this.team.uuid}`;
160+
this.axios
161+
.get(url)
162+
.then(response => {
163+
this.projects = response.data;
164+
})
165+
.catch(error => {
166+
this.$toastr.w(this.$t("condition.unsuccessful_action"));
167+
});
168+
}
169+
})
170+
},
171+
onExpandRow: this.vueFormatterInit,
172+
toolbar: '#customToolbar',
173+
responseHandler: function (res, xhr) {
174+
res.total = xhr.getResponseHeader("X-Total-Count");
175+
return res;
176+
},
177+
url: `${this.$api.BASE_URL}/${this.$api.URL_TEAM}`
178+
}
179+
};
180+
},
181+
methods: {
182+
refreshTable: function() {
183+
this.$refs.table.refresh({
184+
silent: true
185+
});
186+
},
187+
updateProperties: function() {
188+
this.updateConfigProperties([
189+
{groupName: 'access-management', propertyName: 'acl.enabled', propertyValue: this.isAclEnabled}
190+
]);
191+
},
192+
updateConfigProperties: function(configProperties) {
193+
let props = [];
194+
for (let i=0; i<configProperties.length; i++) {
195+
let prop = configProperties[i];
196+
prop.propertyValue = common.trimToNull(prop.propertyValue);
197+
props.push(prop);
198+
}
199+
let url = `${this.$api.BASE_URL}/${this.$api.URL_CONFIG_PROPERTY}/aggregate`;
200+
this.axios.post(url, props).then((response) => {
201+
this.$toastr.s(this.$t('admin.configuration_saved'));
202+
}).catch((error) => {
203+
this.$toastr.w(this.$t('condition.unsuccessful_action'));
204+
});
205+
},
206+
},
207+
watch:{
208+
isAclEnabled() {
209+
this.updateProperties();
210+
}
211+
},
212+
created() {
213+
this.axios.get(this.configUrl).then((response) => {
214+
let configItems = response.data.filter(function (item) { return item.groupName === "access-management" });
215+
for (let i=0; i<configItems.length; i++) {
216+
let item = configItems[i];
217+
switch (item.propertyName) {
218+
case "acl.enabled":
219+
this.isAclEnabled = common.toBoolean(item.propertyValue); break;
220+
}
221+
}
222+
});
223+
}
224+
}
225+
</script>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<template>
2+
<b-modal id="selectProjectModal" size="lg" hide-header-close no-stacking :title="$t('admin.select_project')">
3+
<div id="projectsToolbar" class="bs-table-custom-toolbar">
4+
<c-switch style="margin-left:1rem; margin-right:.5rem" id="showInactiveProjects" color="primary" v-model="showInactiveProjects" label v-bind="labelIcon" /><span class="text-muted">{{ $t('message.show_inactive_projects') }}</span>
5+
</div>
6+
<bootstrap-table
7+
ref="table"
8+
:columns="columns"
9+
:data="data"
10+
:options="options">
11+
</bootstrap-table>
12+
<template v-slot:modal-footer="{ cancel }">
13+
<b-button size="md" variant="secondary" @click="cancel()">{{ $t('message.cancel') }}</b-button>
14+
<b-button size="md" variant="primary" @click="$emit('selection', $refs.table.getSelections())">{{ $t('message.select') }}</b-button>
15+
</template>
16+
</b-modal>
17+
</template>
18+
19+
<script>
20+
import xssFilters from "xss-filters";
21+
import permissionsMixin from "../../../mixins/permissionsMixin";
22+
import common from "../../../shared/common";
23+
import { Switch as cSwitch } from '@coreui/vue';
24+
25+
export default {
26+
mixins: [permissionsMixin],
27+
components: {
28+
cSwitch
29+
},
30+
data() {
31+
return {
32+
showInactiveProjects: false,
33+
labelIcon: {
34+
dataOn: '\u2713',
35+
dataOff: '\u2715'
36+
},
37+
columns: [
38+
{
39+
field: "state",
40+
checkbox: true,
41+
align: "center"
42+
},
43+
{
44+
title: this.$t('message.project_name'),
45+
field: "name",
46+
sortable: true,
47+
formatter(value, row, index) {
48+
let url = xssFilters.uriInUnQuotedAttr("../projects/" + row.uuid);
49+
return `<a href="${url}">${xssFilters.inHTMLData(value)}</a>`;
50+
}
51+
},
52+
{
53+
title: this.$t('message.version'),
54+
field: "version",
55+
sortable: true,
56+
formatter(value, row, index) {
57+
return xssFilters.inHTMLData(common.valueWithDefault(value, ""));
58+
}
59+
}
60+
],
61+
data: [],
62+
options: {
63+
search: true,
64+
showColumns: true,
65+
showRefresh: true,
66+
pagination: true,
67+
silentSort: false,
68+
sidePagination: 'server',
69+
queryParamsType: 'pageSize',
70+
pageList: '[10, 25, 50, 100]',
71+
pageSize: 10,
72+
icons: {
73+
refresh: 'fa-refresh'
74+
},
75+
toolbar: '#projectsToolbar',
76+
responseHandler: function (res, xhr) {
77+
res.total = xhr.getResponseHeader("X-Total-Count");
78+
return res;
79+
},
80+
url: this.apiUrl()
81+
}
82+
};
83+
},
84+
methods: {
85+
apiUrl: function () {
86+
let url = `${this.$api.BASE_URL}/${this.$api.URL_PROJECT}`;
87+
if (this.showInactiveProjects === undefined) {
88+
url += "?excludeInactive=true";
89+
} else {
90+
url += "?excludeInactive=" + !this.showInactiveProjects;
91+
}
92+
return url;
93+
},
94+
refreshTable: function() {
95+
this.$refs.table.refresh({
96+
url: this.apiUrl(),
97+
silent: true
98+
});
99+
}
100+
},
101+
watch:{
102+
showInactiveProjects() {
103+
this.refreshTable();
104+
}
105+
},
106+
}
107+
</script>

0 commit comments

Comments
 (0)