diff --git a/client/src/components/menu.js b/client/src/components/menu.js index 63de9b7..e5dee1e 100644 --- a/client/src/components/menu.js +++ b/client/src/components/menu.js @@ -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 */} - - + + {canView(rules, api.node) && ( + + )} + + {canView(rules, api.namespace) && ( + + )} {/* Workloads */} - - - - - - + {canView(rules, api.deployment) && ( + + )} + + {canView(rules, api.service) && ( + + )} + + {canView(rules, api.replicaSet) && ( + + )} + + {canView(rules, api.pod) && ( + + )} + + {canView(rules, api.ingress) && ( + + )} + + {canView(rules, api.configMap) && ( + + )} {/* Storage */} - - - + {canView(rules, api.storageClass) && ( + + )} + + {canView(rules, api.persistentVolume) && ( + + )} + + {canView(rules, api.persistentVolumeClaim) && ( + + )} {/* Security */} - - - - + {canView(rules, api.serviceAccount) && ( + + )} + + {canView(rules, api.role, api.clusterRole) && ( + + )} + + {canView(rules, api.roleBinding, api.clusterRoleBinding) && ( + + )} + + {canView(rules, api.secret) && ( + + )} @@ -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 ( @@ -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)); diff --git a/client/src/router.js b/client/src/router.js index 9599bf3..a615653 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -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'; diff --git a/client/src/services/api.js b/client/src/services/api.js index 155df14..b2c0f6e 100644 --- a/client/src/services/api.js +++ b/client/src/services/api.js @@ -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; diff --git a/client/src/services/apiProxy.js b/client/src/services/apiProxy.js index 85cfb65..bcd9ef3 100644 --- a/client/src/services/apiProxy.js +++ b/client/src/services/apiProxy.js @@ -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; diff --git a/client/src/services/auth.js b/client/src/services/auth.js new file mode 100644 index 0000000..8dfcf82 --- /dev/null +++ b/client/src/services/auth.js @@ -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(); +} diff --git a/client/src/views/account.js b/client/src/views/account.js index 9698ed9..26c0312 100644 --- a/client/src/views/account.js +++ b/client/src/views/account.js @@ -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 = () => ( diff --git a/client/src/views/auth.js b/client/src/views/auth.js index ada47ad..ea0bc3b 100644 --- a/client/src/views/auth.js +++ b/client/src/views/auth.js @@ -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'; diff --git a/client/src/views/configMap.js b/client/src/views/configMap.js index d0f14b3..513d877 100644 --- a/client/src/views/configMap.js +++ b/client/src/views/configMap.js @@ -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 {
- {!item ? : Object.entries(item.data || {}).map(([key, value]) => ( + {!item ? : _.map(item.data, (value, key) => ( ))}