Added route restrictions based on user privilege
This commit allows us to add 'exclusiveToRoles' field to
route config files, with the list of roles that can access
this resource, if needed. In this case, only Administrator
can access Virtual-Media page and SOL console, and it is blocked for other
users.
Signed-off-by: Sivaprabu Ganesan <sivaprabug@ami.com>
Change-Id: Ibcee18bd92d97c34414ecaf2caf6af28070c5538
diff --git a/src/components/AppHeader/AppHeader.vue b/src/components/AppHeader/AppHeader.vue
index 84e4588..a198495 100644
--- a/src/components/AppHeader/AppHeader.vue
+++ b/src/components/AppHeader/AppHeader.vue
@@ -155,6 +155,9 @@
isAuthorized() {
return this.$store.getters['global/isAuthorized'];
},
+ userPrivilege() {
+ return this.$store.getters['global/userPrivilege'];
+ },
serverStatus() {
return this.$store.getters['global/serverStatus'];
},
diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue
index acfabe7..a5f8105 100644
--- a/src/components/AppNavigation/AppNavigation.vue
+++ b/src/components/AppNavigation/AppNavigation.vue
@@ -29,7 +29,7 @@
<b-collapse :id="navItem.id" tag="ul" class="nav-item__nav">
<li class="nav-item">
<router-link
- v-for="(subNavItem, i) of navItem.children"
+ v-for="(subNavItem, i) of filteredNavItem(navItem.children)"
:key="i"
:to="subNavItem.route"
:data-test-id="`nav-item-${subNavItem.id}`"
@@ -67,6 +67,7 @@
data() {
return {
isNavigationOpen: false,
+ currentUserRole: null,
};
},
watch: {
@@ -78,12 +79,24 @@
},
},
mounted() {
+ this.getPrivilege();
this.$root.$on('toggle-navigation', () => this.toggleIsOpen());
},
methods: {
toggleIsOpen() {
this.isNavigationOpen = !this.isNavigationOpen;
},
+ getPrivilege() {
+ this.currentUserRole = this.$store?.getters['global/userPrivilege'];
+ },
+ filteredNavItem(navItem) {
+ if (this.currentUserRole) {
+ return navItem.filter(({ exclusiveToRoles }) => {
+ if (!exclusiveToRoles?.length) return true;
+ return exclusiveToRoles.includes(this.currentUserRole);
+ });
+ } else return navItem;
+ },
},
};
</script>
diff --git a/src/components/AppNavigation/AppNavigationMixin.js b/src/components/AppNavigation/AppNavigationMixin.js
index bbbbb1e..6123098 100644
--- a/src/components/AppNavigation/AppNavigationMixin.js
+++ b/src/components/AppNavigation/AppNavigationMixin.js
@@ -6,6 +6,12 @@
import IconSecurity from '@carbon/icons-vue/es/security/16';
import IconChevronUp from '@carbon/icons-vue/es/chevron--up/16';
import IconDataBase from '@carbon/icons-vue/es/data--base--alt/16';
+const roles = {
+ administrator: 'Administrator',
+ operator: 'Operator',
+ readonly: 'ReadOnly',
+ noaccess: 'NoAccess',
+};
const AppNavigationMixin = {
components: {
@@ -95,6 +101,7 @@
id: 'serial-over-lan',
label: this.$t('appNavigation.serialOverLan'),
route: '/operations/serial-over-lan',
+ exclusiveToRoles: [roles.administrator],
},
{
id: 'server-power-operations',
@@ -105,6 +112,7 @@
id: 'virtual-media',
label: this.$t('appNavigation.virtualMedia'),
route: '/operations/virtual-media',
+ exclusiveToRoles: [roles.administrator],
},
],
},
diff --git a/src/env/components/AppNavigation/intel.js b/src/env/components/AppNavigation/intel.js
index 3fe0ad1..0688a05 100644
--- a/src/env/components/AppNavigation/intel.js
+++ b/src/env/components/AppNavigation/intel.js
@@ -7,6 +7,13 @@
import IconChevronUp from '@carbon/icons-vue/es/chevron--up/16';
import IconDataBase from '@carbon/icons-vue/es/data--base--alt/16';
+const roles = {
+ administrator: 'Administrator',
+ operator: 'Operator',
+ readonly: 'ReadOnly',
+ noaccess: 'NoAccess',
+};
+
const AppNavigationMixin = {
components: {
iconOverview: IconDashboard,
@@ -85,6 +92,7 @@
id: 'serial-over-lan',
label: this.$t('appNavigation.serialOverLan'),
route: '/operations/serial-over-lan',
+ exclusiveToRoles: [roles.administrator],
},
{
id: 'server-power-operations',
@@ -95,6 +103,7 @@
id: 'virtual-media',
label: this.$t('appNavigation.virtualMedia'),
route: '/operations/virtual-media',
+ exclusiveToRoles: [roles.administrator],
},
],
},
diff --git a/src/env/router/intel.js b/src/env/router/intel.js
index fd8ed77..5f3ee6e 100644
--- a/src/env/router/intel.js
+++ b/src/env/router/intel.js
@@ -27,6 +27,13 @@
import Power from '@/views/ResourceManagement/Power';
import i18n from '@/i18n';
+const roles = {
+ administrator: 'Administrator',
+ operator: 'Operator',
+ readonly: 'ReadOnly',
+ noaccess: 'NoAccess',
+};
+
const routes = [
{
path: '/login',
@@ -217,6 +224,7 @@
component: SerialOverLan,
meta: {
title: i18n.t('appPageTitle.serialOverLan'),
+ exclusiveToRoles: [roles.administrator],
},
},
{
@@ -233,6 +241,7 @@
component: VirtualMedia,
meta: {
title: i18n.t('appPageTitle.virtualMedia'),
+ exclusiveToRoles: [roles.administrator],
},
},
{
diff --git a/src/router/index.js b/src/router/index.js
index 3cd5226..bcb2c7a 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -8,16 +8,25 @@
import routes from './routes';
Vue.use(VueRouter);
-
const router = new VueRouter({
base: process.env.BASE_URL,
routes,
linkExactActiveClass: 'nav-link--current',
});
-router.beforeEach((to, from, next) => {
+function allowRouterToNavigate(to, next, currentUserRole) {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (store.getters['authentication/isLoggedIn']) {
+ if (to.meta.exclusiveToRoles) {
+ // The privilege for the specific router was verified using the
+ // exclusiveToRoles roles in the router.
+ if (to.meta.exclusiveToRoles.includes(currentUserRole)) {
+ next();
+ } else {
+ next('*');
+ }
+ return;
+ }
next();
return;
}
@@ -25,6 +34,25 @@
} else {
next();
}
+}
+
+router.beforeEach((to, from, next) => {
+ let currentUserRole = store.getters['global/userPrivilege'];
+ // condition will get satisfied if user refreshed after login
+ if (!currentUserRole && store.getters['authentication/isLoggedIn']) {
+ // invoke API call to get the role ID
+ let username = localStorage.getItem('storedUsername');
+ store.dispatch('authentication/getUserInfo', username).then((response) => {
+ if (response?.RoleId) {
+ // set role ID
+ store.commit('global/setPrivilege', response.RoleId);
+ // allow the route to continue
+ allowRouterToNavigate(to, next, response.RoleId);
+ }
+ });
+ } else {
+ allowRouterToNavigate(to, next, currentUserRole);
+ }
});
export default router;
diff --git a/src/router/routes.js b/src/router/routes.js
index 3cbdabc..1404da5 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -31,6 +31,13 @@
import Power from '@/views/ResourceManagement/Power';
import i18n from '@/i18n';
+const roles = {
+ administrator: 'Administrator',
+ operator: 'Operator',
+ readonly: 'ReadOnly',
+ noaccess: 'NoAccess',
+};
+
const routes = [
{
path: '/login',
@@ -253,6 +260,7 @@
component: SerialOverLan,
meta: {
title: i18n.t('appPageTitle.serialOverLan'),
+ exclusiveToRoles: [roles.administrator],
},
},
{
@@ -269,6 +277,7 @@
component: VirtualMedia,
meta: {
title: i18n.t('appPageTitle.virtualMedia'),
+ exclusiveToRoles: [roles.administrator],
},
},
{
diff --git a/src/store/modules/Authentication/AuthenticanStore.js b/src/store/modules/Authentication/AuthenticanStore.js
index 88fb54b..02d6e63 100644
--- a/src/store/modules/Authentication/AuthenticanStore.js
+++ b/src/store/modules/Authentication/AuthenticanStore.js
@@ -58,10 +58,10 @@
.then(() => router.go('/login'))
.catch((error) => console.log(error));
},
- checkPasswordChangeRequired(_, username) {
- api
+ getUserInfo(_, username) {
+ return api
.get(`/redfish/v1/AccountService/Accounts/${username}`)
- .then(({ data: { PasswordChangeRequired } }) => PasswordChangeRequired)
+ .then(({ data }) => data)
.catch((error) => console.log(error));
},
resetStoreState({ state }) {
diff --git a/src/store/modules/GlobalStore.js b/src/store/modules/GlobalStore.js
index 95d7a08..74eb1e8 100644
--- a/src/store/modules/GlobalStore.js
+++ b/src/store/modules/GlobalStore.js
@@ -40,6 +40,7 @@
: true,
username: localStorage.getItem('storedUsername'),
isAuthorized: true,
+ userPrivilege: null,
},
getters: {
assetTag: (state) => state.assetTag,
@@ -51,6 +52,7 @@
isUtcDisplay: (state) => state.isUtcDisplay,
username: (state) => state.username,
isAuthorized: (state) => state.isAuthorized,
+ userPrivilege: (state) => state.userPrivilege,
},
mutations: {
setAssetTag: (state, assetTag) => (state.assetTag = assetTag),
@@ -70,6 +72,9 @@
state.isAuthorized = true;
}, 100);
},
+ setPrivilege: (state, privilege) => {
+ state.userPrivilege = privilege;
+ },
},
actions: {
async getBmcTime({ commit }) {
diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue
index 8d96573..88d1c7d 100644
--- a/src/views/Login/Login.vue
+++ b/src/views/Login/Login.vue
@@ -126,17 +126,17 @@
localStorage.setItem('storedUsername', username);
this.$store.commit('global/setUsername', username);
this.$store.commit('global/setLanguagePreference', i18n.locale);
- return this.$store.dispatch(
- 'authentication/checkPasswordChangeRequired',
- username
- );
+ return this.$store.dispatch('authentication/getUserInfo', username);
})
- .then((passwordChangeRequired) => {
- if (passwordChangeRequired) {
+ .then(({ PasswordChangeRequired, RoleId }) => {
+ if (PasswordChangeRequired) {
this.$router.push('/change-password');
} else {
this.$router.push('/');
}
+ if (RoleId) {
+ this.$store.commit('global/setPrivilege', RoleId);
+ }
})
.catch((error) => console.log(error))
.finally(() => (this.disableSubmitButton = false));
diff --git a/tests/unit/AppNavigation.spec.js b/tests/unit/AppNavigation.spec.js
index b37c1e4..ce410c8 100644
--- a/tests/unit/AppNavigation.spec.js
+++ b/tests/unit/AppNavigation.spec.js
@@ -1,16 +1,25 @@
-import { mount, createWrapper } from '@vue/test-utils';
+import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import AppNavigation from '@/components/AppNavigation';
import Vue from 'vue';
+import Vuex from 'vuex';
import VueRouter from 'vue-router';
import { BootstrapVue } from 'bootstrap-vue';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('AppNavigation.vue', () => {
let wrapper;
+ const router = new VueRouter();
+ const actions = {
+ 'global/userPrivilege': jest.fn(),
+ };
+ const store = new Vuex.Store({ actions });
Vue.use(BootstrapVue);
Vue.use(VueRouter);
- const router = new VueRouter();
wrapper = mount(AppNavigation, {
+ store,
router,
mocks: {
$t: (key) => key,