Limit menu options based on accounts permissions

This commit is contained in:
Eric Herbrandson
2019-06-22 23:07:19 -05:00
parent ce80f6beda
commit 5608c35c66
8 changed files with 235 additions and 147 deletions
+72 -24
View File
@@ -9,10 +9,8 @@ import AddSvg from '../art/addSvg';
export default class Menu extends Base {
async componentDidMount() {
const rules = await api.getRules('default');
this.setState({rules});
console.log(rules);
const {status} = await api.getRules('default');
this.setState({rules: status.resourceRules});
}
render() {
@@ -28,33 +26,75 @@ export default class Menu extends Base {
{/* Cluster */}
<Group>
<MenuItem title='Cluster' path='' resource='Logo' onClick={onClick} />
<MenuItem title='Nodes' path='node' resource='Node' onClick={onClick} />
<MenuItem title='Namespaces' path='namespace' resource='Namespace' onClick={onClick} />
{canView(rules, api.node) && (
<MenuItem title='Nodes' path='node' resource='Node' onClick={onClick} />
)}
{canView(rules, api.namespace) && (
<MenuItem title='Namespaces' path='namespace' resource='Namespace' onClick={onClick} />
)}
</Group>
{/* Workloads */}
<Group>
<MenuItem title='Workloads' path='workload' resource='Deployment' onClick={onClick} />
<MenuItem title='Services' path='service' resource='Service' onClick={onClick} />
<MenuItem title='Replicas' path='replicaset' resource='ReplicaSet' onClick={onClick} />
<MenuItem title='Pods' path='pod' resource='Pod' onClick={onClick} />
<MenuItem title='Ingresses' path='ingress' resource='Ingress' onClick={onClick} />
<MenuItem title='Config' path='configmap' resource='ConfigMap' onClick={onClick} />
{canView(rules, api.deployment) && (
<MenuItem title='Workloads' path='workload' resource='Deployment' onClick={onClick} />
)}
{canView(rules, api.service) && (
<MenuItem title='Services' path='service' resource='Service' onClick={onClick} />
)}
{canView(rules, api.replicaSet) && (
<MenuItem title='Replicas' path='replicaset' resource='ReplicaSet' onClick={onClick} />
)}
{canView(rules, api.pod) && (
<MenuItem title='Pods' path='pod' resource='Pod' onClick={onClick} />
)}
{canView(rules, api.ingress) && (
<MenuItem title='Ingresses' path='ingress' resource='Ingress' onClick={onClick} />
)}
{canView(rules, api.configMap) && (
<MenuItem title='Config' path='configmap' resource='ConfigMap' onClick={onClick} />
)}
</Group>
{/* Storage */}
<Group>
<MenuItem title='Storage' path='storageclass' resource='StorageClass' onClick={onClick} />
<MenuItem title='Volumes' path='persistentvolume' resource='PersistentVolume' onClick={onClick} />
<MenuItem title='Claims' path='persistentvolumeclaim' resource='PersistentVolumeClaim' onClick={onClick} />
{canView(rules, api.storageClass) && (
<MenuItem title='Storage' path='storageclass' resource='StorageClass' onClick={onClick} />
)}
{canView(rules, api.persistentVolume) && (
<MenuItem title='Volumes' path='persistentvolume' resource='PersistentVolume' onClick={onClick} />
)}
{canView(rules, api.persistentVolumeClaim) && (
<MenuItem title='Claims' path='persistentvolumeclaim' resource='PersistentVolumeClaim' onClick={onClick} />
)}
</Group>
{/* Security */}
<Group>
<MenuItem title='Accounts' path='serviceaccount' resource='ServiceAccount' onClick={onClick} />
<MenuItem title='Roles' path='role' resource='Role' additionalPaths={['clusterrole']} onClick={onClick} />
<MenuItem title='Bindings' path='rolebinding' resource='Role' additionalPaths={['clusterrolebinding']} onClick={onClick} />
<MenuItem title='Secrets' path='secret' resource='Secret' onClick={onClick} />
{canView(rules, api.serviceAccount) && (
<MenuItem title='Accounts' path='serviceaccount' resource='ServiceAccount' onClick={onClick} />
)}
{canView(rules, api.role, api.clusterRole) && (
<MenuItem title='Roles' path='role' resource='Role' additionalPaths={['clusterrole']} onClick={onClick} />
)}
{canView(rules, api.roleBinding, api.clusterRoleBinding) && (
<MenuItem title='Bindings' path='rolebinding' resource='Role' additionalPaths={['clusterrolebinding']} onClick={onClick} />
)}
{canView(rules, api.secret) && (
<MenuItem title='Secrets' path='secret' resource='Secret' onClick={onClick} />
)}
</Group>
<Group>
@@ -83,8 +123,14 @@ export default class Menu extends Base {
}
}
function canView(resources, group, resource) {
resources.some((x) => {
function canView(resourcesRules, ...args) {
if (!resourcesRules) return false;
return args.some(x => canViewResource(resourcesRules, x.resource));
}
function canViewResource(resourcesRules, {group, resource}) {
return resourcesRules.some((x) => {
const hasVerb = (x.verbs.includes('list') || x.verbs.includes('*'));
const hasGroup = (x.apiGroups.includes(group) || x.apiGroups.includes('*'));
const hasResource = (x.resources.includes(resource)) || x.resources.includes('*');
@@ -92,11 +138,11 @@ function canView(resources, group, resource) {
});
}
function MenuItem(item) {
function MenuItem(props) {
const currentPath = getRootPath();
const paths = getPaths(item);
const paths = getPaths(props);
const className = paths.includes(currentPath) ? 'menu_item menu_itemSelected' : 'menu_item';
const {path, title, resource, onClick} = item;
const {path, title, resource, onClick} = props;
return (
<a href={`#!${path}`} title={title} className={className} onClick={onClick}>
@@ -108,6 +154,8 @@ function MenuItem(item) {
function Group({children = []}) {
if (!Array.isArray(children)) children = [children]; // eslint-disable-line no-param-reassign
children = children.filter(Boolean); // eslint-disable-line no-param-reassign
if (children.length === 0) return null;
const paths = children.flatMap(x => getPaths(x.props));
+1 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import {hasToken} from './services/apiProxy';
import {hasToken} from './services/auth';
import Account from './views/account';
import Auth from './views/auth';
import ClusterRole from './views/clusterRole';
+50 -86
View File
@@ -1,9 +1,35 @@
import _ from 'lodash';
import {Base64} from 'js-base64';
import {request, stream, streamResult, streamResults} from './apiProxy';
import {request, post, stream, apiFactory, apiFactoryWithNamespace} from './apiProxy';
import log from '../utils/log';
const JSON_HEADERS = {Accept: 'application/json', 'Content-Type': 'application/json'};
const configMap = apiFactoryWithNamespace('', 'v1', 'configmaps');
const event = apiFactoryWithNamespace('', 'v1', 'events');
const namespaceService = apiFactory('', 'v1', 'namespaces');
const node = apiFactory('', 'v1', 'nodes');
const persistentVolume = apiFactory('', 'v1', 'persistentvolumes');
const persistentVolumeClaim = apiFactoryWithNamespace('', 'v1', 'persistentvolumeclaims');
const pod = apiFactoryWithNamespace('', 'v1', 'pods');
const secret = apiFactoryWithNamespace('', 'v1', 'secrets');
const serviceAccount = apiFactoryWithNamespace('', 'v1', 'serviceaccounts');
const serviceService = apiFactoryWithNamespace('', 'v1', 'services');
const clusterRole = apiFactory('rbac.authorization.k8s.io', 'v1', 'clusterroles');
const clusterRoleBinding = apiFactory('rbac.authorization.k8s.io', 'v1', 'clusterrolebindings');
const role = apiFactoryWithNamespace('rbac.authorization.k8s.io', 'v1', 'roles');
const roleBinding = apiFactoryWithNamespace('rbac.authorization.k8s.io', 'v1', 'rolebindings');
const daemonSet = apiFactoryWithNamespace('apps', 'v1', 'daemonsets');
const deployment = apiFactoryWithNamespace('apps', 'v1', 'deployments', true);
const replicaSet = apiFactoryWithNamespace('apps', 'v1', 'replicasets', true);
const statefulSet = apiFactoryWithNamespace('apps', 'v1', 'statefulsets', true);
const cronJob = apiFactoryWithNamespace('batch', 'v1beta1', 'cronjobs');
const job = apiFactoryWithNamespace('batch', 'v1', 'jobs');
const ingress = apiFactoryWithNamespace('extensions', 'v1beta1', 'ingresses');
const storageClass = apiFactory('storage.k8s.io', 'v1', 'storageclasses');
const apis = {
apply,
@@ -15,31 +41,28 @@ const apis = {
metrics: metricsFactory(),
oidc: oidcFactory(),
// Non-namespaced apis
clusterRole: apiFactory('/apis/rbac.authorization.k8s.io/v1', 'clusterroles'),
namespace: apiFactory('/api/v1', 'namespaces'),
node: apiFactory('/api/v1', 'nodes'),
persistentVolume: apiFactory('/api/v1', 'persistentvolumes'),
storageClass: apiFactory('/apis/storage.k8s.io/v1', 'storageclasses'),
clusterRoleBinding: apiFactory('/apis/rbac.authorization.k8s.io/v1', 'clusterrolebindings'),
// Namespaced apis
configMap: apiFactoryWithNamespace('/api/v1', 'configmaps'),
cronJob: apiFactoryWithNamespace('/apis/batch/v1beta1', 'cronjobs'),
daemonSet: apiFactoryWithNamespace('/apis/apps/v1', 'daemonsets'),
deployment: apiFactoryWithNamespace('/apis/apps/v1', 'deployments', true),
event: apiFactoryWithNamespace('/api/v1', 'events'),
ingress: apiFactoryWithNamespace('/apis/extensions/v1beta1', 'ingresses'),
job: apiFactoryWithNamespace('/apis/batch/v1', 'jobs'),
persistentVolumeClaim: apiFactoryWithNamespace('/api/v1', 'persistentvolumeclaims'),
pod: apiFactoryWithNamespace('/api/v1', 'pods'),
replicaSet: apiFactoryWithNamespace('/apis/apps/v1', 'replicasets', true),
role: apiFactoryWithNamespace('/apis/rbac.authorization.k8s.io/v1', 'roles'),
secret: apiFactoryWithNamespace('/api/v1', 'secrets'),
service: apiFactoryWithNamespace('/api/v1', 'services'),
serviceAccount: apiFactoryWithNamespace('/api/v1', 'serviceaccounts'),
statefulSet: apiFactoryWithNamespace('/apis/apps/v1', 'statefulsets', true),
roleBinding: apiFactoryWithNamespace('/apis/rbac.authorization.k8s.io/v1', 'rolebindings'),
clusterRole,
namespace: namespaceService,
node,
persistentVolume,
storageClass,
clusterRoleBinding,
configMap,
cronJob,
daemonSet,
deployment,
event,
ingress,
job,
persistentVolumeClaim,
pod,
replicaSet,
role,
secret,
service: serviceService,
serviceAccount,
statefulSet,
roleBinding,
};
async function testAuth() {
@@ -110,48 +133,6 @@ function metrics(url, cb) {
}
}
function apiFactory(apiType, kind) {
const url = `${apiType}/${kind}`;
return {
list: (cb, errCb) => streamResults(url, cb, errCb),
get: (name, cb, errCb) => streamResult(url, name, cb, errCb),
post: body => post(url, body),
put: body => put(`${url}/${body.metadata.name}`, body),
delete: name => del(`${url}/${name}`),
};
}
function apiFactoryWithNamespace(apiType, kind, includeScale) {
const results = {
list: (namespace, cb, errCb) => streamResults(url(namespace), cb, errCb),
get: (namespace, name, cb, errCb) => streamResult(url(namespace), name, cb, errCb),
post: body => post(url(body.metadata.namespace), body),
put: body => put(`${url(body.metadata.namespace)}/${body.metadata.name}`, body),
delete: (namespace, name) => del(`${url(namespace)}/${name}`),
};
if (includeScale) {
results.scale = apiScaleFactory(apiType, kind);
}
return results;
function url(namespace) {
return namespace ? `${apiType}/namespaces/${namespace}/${kind}` : `${apiType}/${kind}`;
}
}
function apiScaleFactory(apiType, kind) {
return {
get: (namespace, name) => request(url(namespace, name)),
put: body => put(url(body.metadata.namespace, body.metadata.name), body),
};
function url(namespace, name) {
return `${apiType}/namespaces/${namespace}/${kind}/${name}/scale`;
}
}
function swagger() {
return request('/openapi/v2');
}
@@ -181,21 +162,4 @@ function logs(namespace, name, container, tailLines, showPrevious, cb) {
}
}
function post(url, json, autoLogoutOnAuthError = true) {
const body = JSON.stringify(json);
const opts = {method: 'POST', body, headers: JSON_HEADERS};
return request(url, opts, autoLogoutOnAuthError);
}
function put(url, json, autoLogoutOnAuthError = true) {
const body = JSON.stringify(json);
const opts = {method: 'PUT', body, headers: JSON_HEADERS};
return request(url, opts, autoLogoutOnAuthError);
}
function del(url) {
const opts = {method: 'DELETE', headers: JSON_HEADERS};
return request(url, opts);
}
export default apis;
+69 -33
View File
@@ -1,4 +1,4 @@
import * as cookie from 'js-cookie';
import {getToken, logout} from './auth';
import log from '../utils/log';
const {host, href, hash, search} = window.location;
@@ -6,38 +6,7 @@ const nonHashedUrl = href.replace(hash, '').replace(search, '');
const isDev = process.env.NODE_ENV !== 'production';
const BASE_HTTP_URL = isDev && host === 'localhost:4653' ? 'http://localhost:4654' : nonHashedUrl;
const BASE_WS_URL = BASE_HTTP_URL.replace('http', 'ws');
const authorizationCookie = cookie.get('Authorization');
if (authorizationCookie) {
setToken(authorizationCookie);
cookie.remove('Authorization');
}
export function getToken() {
return localStorage.authToken;
}
export function getUserInfo() {
const user = getToken().split('.')[1];
return JSON.parse(atob(user));
}
export function hasToken() {
return !!getToken();
}
export function setToken(token) {
localStorage.authToken = token;
}
export function deleteToken() {
delete localStorage.authToken;
}
export function logout() {
deleteToken();
window.location.reload();
}
const JSON_HEADERS = {Accept: 'application/json', 'Content-Type': 'application/json'};
export async function request(path, params, autoLogoutOnAuthError = true) {
const opts = Object.assign({headers: {}}, params);
@@ -71,6 +40,73 @@ export async function request(path, params, autoLogoutOnAuthError = true) {
return response.json();
}
export function apiFactory(group, version, resource) {
const apiRoot = getApiRoot(group, version);
const url = `${apiRoot}/${resource}`;
return {
resource: {group, resource},
list: (cb, errCb) => streamResults(url, cb, errCb),
get: (name, cb, errCb) => streamResult(url, name, cb, errCb),
post: body => post(url, body),
put: body => put(`${url}/${body.metadata.name}`, body),
delete: name => remove(`${url}/${name}`),
};
}
export function apiFactoryWithNamespace(group, version, resource, includeScale) {
const apiRoot = getApiRoot(group, version);
const results = {
resource: {group, resource},
list: (namespace, cb, errCb) => streamResults(url(namespace), cb, errCb),
get: (namespace, name, cb, errCb) => streamResult(url(namespace), name, cb, errCb),
post: body => post(url(body.metadata.namespace), body),
put: body => put(`${url(body.metadata.namespace)}/${body.metadata.name}`, body),
delete: (namespace, name) => remove(`${url(namespace)}/${name}`),
};
if (includeScale) {
results.scale = apiScaleFactory(apiRoot, resource);
}
return results;
function url(namespace) {
return namespace ? `${apiRoot}/namespaces/${namespace}/${resource}` : `${apiRoot}/${resource}`;
}
}
function getApiRoot(group, version) {
return group ? `/apis/${group}/${version}` : `api/${version}`;
}
function apiScaleFactory(apiRoot, resource) {
return {
get: (namespace, name) => request(url(namespace, name)),
put: body => put(url(body.metadata.namespace, body.metadata.name), body),
};
function url(namespace, name) {
return `${apiRoot}/namespaces/${namespace}/${resource}/${name}/scale`;
}
}
export function post(url, json, autoLogoutOnAuthError = true) {
const body = JSON.stringify(json);
const opts = {method: 'POST', body, headers: JSON_HEADERS};
return request(url, opts, autoLogoutOnAuthError);
}
export function put(url, json, autoLogoutOnAuthError = true) {
const body = JSON.stringify(json);
const opts = {method: 'PUT', body, headers: JSON_HEADERS};
return request(url, opts, autoLogoutOnAuthError);
}
export function remove(url) {
const opts = {method: 'DELETE', headers: JSON_HEADERS};
return request(url, opts);
}
export async function streamResult(url, name, cb, errCb) {
let isCancelled = false;
let socket;
+39
View File
@@ -0,0 +1,39 @@
import * as cookie from 'js-cookie';
// If we have an "Authorization" cookie, use that as the token for future api calls
const authorizationCookie = cookie.get('Authorization');
if (authorizationCookie) {
setToken(authorizationCookie);
cookie.remove('Authorization');
}
export function getToken() {
// This line deals with backwards compatability from when we used to only store the actual jwt
if (localStorage.authToken && !localStorage.authToken.startsWith('Bearer ')) {
localStorage.authToken = `Bearer ${localStorage.authToken}`;
}
return localStorage.authToken;
}
export function getUserInfo() {
const user = getToken().split('.')[1];
return JSON.parse(atob(user));
}
export function hasToken() {
return !!getToken();
}
export function setToken(token) {
localStorage.authToken = token;
}
export function deleteToken() {
delete localStorage.authToken;
}
export function logout() {
deleteToken();
window.location.reload();
}
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react';
import Button from '../components/button';
import ItemHeader from '../components/itemHeader';
import {getUserInfo, logout} from '../services/apiProxy';
import {getUserInfo, logout} from '../services/auth';
import LogoutSvg from '../art/logoutSvg';
const Account = () => (
+1 -1
View File
@@ -1,7 +1,7 @@
import './auth.scss';
import React from 'react';
import {addUserNotification} from '../components/notifier';
import {setToken, deleteToken} from '../services/apiProxy';
import {setToken, deleteToken} from '../services/auth';
import api from '../services/api';
import Base from '../components/base';
import Button from '../components/button';
+2 -1
View File
@@ -1,3 +1,4 @@
import _ from 'lodash';
import React from 'react';
import Base from '../components/base';
import api from '../services/api';
@@ -48,7 +49,7 @@ export default class ConfigMap extends Base {
</div>
<div className='contentPanel'>
{!item ? <Loading /> : Object.entries(item.data || {}).map(([key, value]) => (
{!item ? <Loading /> : _.map(item.data, (value, key) => (
<Field key={key} name={key} value={value} />
))}
</div>