diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2e402f5..5e848a8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { Resource, defaultTheme, localStorageStore, + usePermissions, } from 'react-admin'; import { SchemaList, @@ -51,7 +52,7 @@ import { httpClientProvider } from './providers/httpClientProvider'; import { K8SDeploymentList, K8SDeploymentShow } from './resources/k8s/k8s_deployment'; import { K8SPvcCreate, K8SPvcList, K8SPvcShow } from './resources/k8s/k8s_pvc'; import { K8SServiceList, K8SServiceShow } from './resources/k8s/k8s_service'; -import { K8SSecretCreate, K8SSecretList, K8SSecretShow } from './resources/k8s/k8s_secret'; +import { K8SSecretList, K8SSecretShow } from './resources/k8s/k8s_secret'; import { K8SJobList, K8SJobShow } from './resources/k8s/k8s_job'; console.log('Config', Config); @@ -110,7 +111,8 @@ function App() { function DynamicAdminUI() { const [views, setViews] = useState([]); const { crdIds } = useUpdateCrdIds(); - + const { permissions } = usePermissions(); + const canAccess = (res: string, op: string) => permissions && permissions.canAccess(res, op) const viewsContext = useContext(ViewsContext); @@ -160,42 +162,41 @@ function DynamicAdminUI() { } + create={canAccess('crs', 'write') ? SchemaCreate : <>} + show={canAccess('crs', 'read') ? SchemaShow : <>} icon={SettingsIcon} /> } + show={canAccess('k8s_service', 'read') ? K8SServiceShow : <>} icon={LinkIcon} /> } + show={canAccess('k8s_deployment', 'read') ? K8SDeploymentShow : <>} icon={AppIcon} /> } + show={canAccess('k8s_job', 'read') ? K8SJobShow : <>} icon={ModelTraininigIcon} /> } + list={canAccess('k8s_pvc', 'list') ? K8SPvcList : <>} + show={canAccess('k8s_pvc', 'read') ? K8SPvcShow : <>} icon={AlbumIcon} /> } + show={canAccess('k8s_secret', 'read') ? K8SSecretShow : <>} icon={KeyIcon} /> diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx index 21c93af..8231d33 100644 --- a/frontend/src/Layout.tsx +++ b/frontend/src/Layout.tsx @@ -4,12 +4,14 @@ import { Menu, MenuProps, ResourceDefinition, + usePermissions, useResourceDefinitions, } from 'react-admin'; import { Divider } from '@mui/material'; const MyMenu = (props: MenuProps) => { const resources = useResourceDefinitions(); + const { permissions } = usePermissions(); return ( @@ -19,7 +21,8 @@ const MyMenu = (props: MenuProps) => { resource.name !== 'crd' && resource.name !== 'crs' && !resource.name.startsWith('k8s') && - resource.hasList && ( + resource.hasList && + permissions && permissions.canAccess(resource.name, 'list') && ( { ) )} - - - - - + {permissions && permissions.canAccess('k8s_service', 'list') && } + {permissions && permissions.canAccess('k8s_deployment', 'list') && } + {permissions && permissions.canAccess('k8s_job', 'list') && } + {permissions && permissions.canAccess('k8s_pvc', 'list') && } + {permissions && permissions.canAccess('k8s_secret', 'list') && }
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 901aa35..51ff9b8 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -4,6 +4,7 @@ import { EmptyClasses, ResourceDefinition, useGetList, + usePermissions, useResourceDefinitions, useTranslate, } from 'react-admin'; @@ -58,6 +59,8 @@ const AppDashboard = () => { const translate = useTranslate(); const resources = useResourceDefinitions(); const navigate = useNavigate(); + const { permissions } = usePermissions(); + const hasListPermission = (resource: string) => permissions && permissions.canAccess(resource, 'list') const cards: any[] = []; @@ -66,7 +69,8 @@ const AppDashboard = () => { resource.name !== 'crd' && resource.name !== 'crs' && !resource.name.startsWith('k8s_') && - resource.hasList + resource.hasList && + hasListPermission(resource.name) ) { cards.push( diff --git a/frontend/src/providers/authProvider.ts b/frontend/src/providers/authProvider.ts index 885b466..9777853 100644 --- a/frontend/src/providers/authProvider.ts +++ b/frontend/src/providers/authProvider.ts @@ -21,7 +21,9 @@ export const NoneAuthProvider = (): AuthProvider => { logout: noop, checkAuth: noop, checkError: noop, - getPermissions: noop, + getPermissions: () => { + return Promise.resolve({canAccess: (resource: string, op: string) => true }); + }, getAuthorization: noop, }; }; @@ -39,13 +41,14 @@ export const buildAuthProvider = (config: any): AuthProvider => { ? config.application.apiUrl : ''; return BasicAuthProvider(baseUrl + '/api/crd'); - } else if ( - type === AUTH_TYPE_OAUTH2 && - 'oauth2' in config.authentication - ) { + } else if (type === AUTH_TYPE_OAUTH2 && 'oauth2' in config.authentication) { + const baseUrl = + 'application' in config && 'apiUrl' in config.application + ? config.application.apiUrl + : ''; const oauth2: [string, string, string, string] = config.authentication.oauth2; - return OAuth2AuthProvider(...oauth2); + return OAuth2AuthProvider(baseUrl, ...oauth2); } } return NoneAuthProvider(); diff --git a/frontend/src/providers/authProviderBasic.ts b/frontend/src/providers/authProviderBasic.ts index f798533..f0827bd 100644 --- a/frontend/src/providers/authProviderBasic.ts +++ b/frontend/src/providers/authProviderBasic.ts @@ -86,7 +86,7 @@ export const BasicAuthProvider = (loginUrl?: string): AuthProvider => { }, // get the user permissions (optional) getPermissions: () => { - return Promise.resolve(); + return Promise.resolve({canAccess: (resource: string, op: string) => true }); }, }; }; diff --git a/frontend/src/providers/authProviderOAuth2.ts b/frontend/src/providers/authProviderOAuth2.ts index 8426733..f2e398e 100644 --- a/frontend/src/providers/authProviderOAuth2.ts +++ b/frontend/src/providers/authProviderOAuth2.ts @@ -2,6 +2,7 @@ import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; import { AuthProvider } from './authProvider'; export const OAuth2AuthProvider = ( + baseUrl: string, authority: string, clientId: string, redirectUri: string, @@ -16,15 +17,17 @@ export const OAuth2AuthProvider = ( loadUserInfo: true, }); - return { - getAuthorization: async () => { - const user = await userManager.getUser(); - if (user) { - return Promise.resolve('Bearer ' + user.access_token); - } + const oauthGetAuthorization = async () => { + const user = await userManager.getUser(); + if (user) { + return Promise.resolve('Bearer ' + user.access_token); + } - return Promise.reject(); - }, + return Promise.reject(); + } + + return { + getAuthorization: oauthGetAuthorization, login: () => { return userManager.signinRedirect(); }, @@ -46,6 +49,7 @@ export const OAuth2AuthProvider = ( // remove local credentials and notify the auth server that the user logged out logout: () => { userManager.removeUser(); + sessionStorage.removeItem('user-permissions'); return Promise.resolve(); }, // get the user's profile @@ -58,8 +62,36 @@ export const OAuth2AuthProvider = ( }); }, // get the user permissions (optional) - getPermissions: () => { - return Promise.resolve(); + getPermissions: async () => { + if (sessionStorage.getItem('user-permissions')) { + const permissions = JSON.parse(sessionStorage.getItem('user-permissions')!); + permissions.canAccess = (resource: string, op: string) => (permissions[resource] && permissions[resource].indexOf(op) >= 0); + return Promise.resolve(permissions); + } + + const request = new Request(baseUrl +'/api/user', { + method: 'GET', + headers: new Headers({ + 'Content-Type': 'application/json', + 'Authorization': await oauthGetAuthorization(), + }), + }); + + return fetch(request) + .then(response => { + if (response.status < 200 || response.status >= 300) { + throw new Error(response.statusText); + } + return response.json(); + }) + .then(permissions => { + sessionStorage.setItem('user-permissions', JSON.stringify(permissions)); + permissions.canAccess = (resource: string, op: string) => (permissions[resource] && permissions[resource].indexOf(op) >= 0); + return permissions; + }) + .catch(() => { + throw new Error('Network error'); + }); }, handleCallback: async () => { // get an access token based on the query paramaters diff --git a/frontend/src/resources/cr.tsx b/frontend/src/resources/cr.tsx index 40df96e..42b5791 100644 --- a/frontend/src/resources/cr.tsx +++ b/frontend/src/resources/cr.tsx @@ -18,6 +18,7 @@ import { useTranslate, useShowController, useEditController, + usePermissions, } from 'react-admin'; import { Box, Typography } from '@mui/material'; import { ViewToolbar } from '../components/ViewToolbar'; @@ -110,11 +111,16 @@ export const CrEdit = () => { }; export const CrList = () => { + const crdId = useResourceContext(); + + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(crdId, op) + return ( <> - }> + }> { label={'resources.cr.fields.kind'} /> - - - + {hasPermission('write') && } + {hasPermission('read') && } + {hasPermission('write') && } @@ -139,13 +145,18 @@ export const CrList = () => { export const CrShow = () => { const { jsonSchema } = useGetCrdJsonSchema(); const { record } = useShowController(); + const crdId = useResourceContext(); + + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(crdId, op) + if (!record) return null; return ( <> - }> + }> { export const SchemaList = () => { const notify = useNotify(); const translate = useTranslate(); + const { permissions } = usePermissions(); + const canAccess = (op: string) => permissions && permissions.canAccess('crs', op) const { updateCrdIds } = useUpdateCrdIds(); @@ -170,15 +173,15 @@ export const SchemaList = () => { {translate('resources.crs.listSubtitle')} - }> + }> - - - + { canAccess('write') && } + { canAccess('read') && } + { canAccess('write') && } @@ -189,6 +192,9 @@ export const SchemaList = () => { export const SchemaShow = () => { const translate = useTranslate(); const { record } = useShowController(); + const { permissions } = usePermissions(); + const canAccess = (op: string) => permissions && permissions.canAccess('crs', op) + if (!record) return null; return ( @@ -200,7 +206,7 @@ export const SchemaShow = () => { recordRepresentation: record.id, })} - }> + }> { } const CrList = () => { + + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_APIGATEWAYS, op) + return ( <> @@ -255,8 +260,8 @@ const CrList = () => { - - + {hasPermission('write') && } + {hasPermission('read') && } @@ -268,13 +273,15 @@ const CrList = () => { const CrShow = () => { const translate = useTranslate(); const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_APIGATEWAYS, op) if (!record) return null; return ( <> - }> + }> diff --git a/frontend/src/resources/custom/cr.buckets.minio.scc-digitalhub.github.io.tsx b/frontend/src/resources/custom/cr.buckets.minio.scc-digitalhub.github.io.tsx index 7870115..cfc18e9 100644 --- a/frontend/src/resources/custom/cr.buckets.minio.scc-digitalhub.github.io.tsx +++ b/frontend/src/resources/custom/cr.buckets.minio.scc-digitalhub.github.io.tsx @@ -23,6 +23,7 @@ import { ReferenceArrayField, CreateButton, TopToolbar, + usePermissions, } from 'react-admin'; import { View } from '../index'; import { ViewToolbar } from '../../components/ViewToolbar'; @@ -155,6 +156,9 @@ const CrEdit = () => { const CrList = () => { const translate = useTranslate(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_MINIO_BUCKETS, op) + const [value, setValue] = useState(0); const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue); @@ -170,15 +174,15 @@ const CrList = () => { - }> + }> - - - + {hasPermission('write') && } + {hasPermission('read') && } + {hasPermission('write') && } @@ -228,6 +232,9 @@ const S3Users = () => { sort: sort, }); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_MINIO_USERS, op) + if (isLoading) return ; // if (!data) return null; @@ -238,7 +245,7 @@ const S3Users = () => { <> - + {hasPermission('write') && } }> { - - - } + {hasPermission('read') && } + {hasPermission('write') && + resource={CR_MINIO_USERS}/>} @@ -272,6 +279,9 @@ const S3Policies = () => { sort: sort, }); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_MINIO_POLICIES, op) + if (isLoading) return ; @@ -281,7 +291,7 @@ const S3Policies = () => { <> - + {hasPermission('write') && } }> { - - - } + {hasPermission('read') && } + {hasPermission('write') && + resource={CR_MINIO_POLICIES}/>} @@ -309,6 +319,9 @@ const S3Policies = () => { const CrShow = () => { const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_MINIO_BUCKETS, op) + if (!record) return null; return ( @@ -319,7 +332,7 @@ const CrShow = () => { crName={CR_MINIO_BUCKETS} crId={record.spec.name} /> - }> + }> diff --git a/frontend/src/resources/custom/cr.dremiorestservers.operator.dremiorestserver.com.tsx b/frontend/src/resources/custom/cr.dremiorestservers.operator.dremiorestserver.com.tsx index b15eb67..f7f517d 100644 --- a/frontend/src/resources/custom/cr.dremiorestservers.operator.dremiorestserver.com.tsx +++ b/frontend/src/resources/custom/cr.dremiorestservers.operator.dremiorestserver.com.tsx @@ -22,6 +22,7 @@ import { SingleFieldList, ChipField, NumberInput, + usePermissions, } from 'react-admin'; import { View } from '../index'; import { ViewToolbar } from '../../components/ViewToolbar'; @@ -296,6 +297,9 @@ const CrEdit = () => { }; const CrList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_DREMIOREST, op) + return ( <> @@ -306,9 +310,9 @@ const CrList = () => { - - - + {hasPermission('write') && } + {hasPermission('read') && } + {hasPermission('write') && } @@ -319,6 +323,9 @@ const CrList = () => { const CrShow = () => { const translate = useTranslate(); const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_DREMIOREST, op) + if (!record) return null; return ( @@ -329,7 +336,7 @@ const CrShow = () => { crName={CR_DREMIOREST} crId={record.spec.database} /> - }> + }> diff --git a/frontend/src/resources/custom/cr.nuclioapigateways.nuclio.io.tsx b/frontend/src/resources/custom/cr.nuclioapigateways.nuclio.io.tsx index 61871e7..32c5365 100644 --- a/frontend/src/resources/custom/cr.nuclioapigateways.nuclio.io.tsx +++ b/frontend/src/resources/custom/cr.nuclioapigateways.nuclio.io.tsx @@ -23,6 +23,7 @@ import { BooleanInput, useGetList, ReferenceInput, + usePermissions, } from 'react-admin'; import { ViewToolbar } from '../../components/ViewToolbar'; import { @@ -492,6 +493,9 @@ const CrEdit = () => { }; const CrList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_NUCLIO_APIGATEWAYS, op) + return ( <> @@ -504,9 +508,9 @@ const CrList = () => { - - - + {hasPermission('write') && } + {hasPermission('read') && } + {hasPermission('write') && } @@ -517,13 +521,15 @@ const CrList = () => { const CrShow = () => { const translate = useTranslate(); const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_NUCLIO_APIGATEWAYS, op) if (!record) return null; return ( <> - }> + }> diff --git a/frontend/src/resources/custom/cr.policies.minio.scc-digitalhub.github.io.tsx b/frontend/src/resources/custom/cr.policies.minio.scc-digitalhub.github.io.tsx index e18c76a..bf4c868 100644 --- a/frontend/src/resources/custom/cr.policies.minio.scc-digitalhub.github.io.tsx +++ b/frontend/src/resources/custom/cr.policies.minio.scc-digitalhub.github.io.tsx @@ -15,6 +15,7 @@ import { TopToolbar, ListButton, Labeled, + usePermissions, } from 'react-admin'; import { View } from '../index'; import { ViewToolbar } from '../../components/ViewToolbar'; @@ -135,6 +136,9 @@ const CrEdit = () => { const CrShow = () => { const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_MINIO_POLICIES, op) + if (!record) return null; return ( @@ -147,7 +151,7 @@ const CrShow = () => { - + {hasPermission('write') && } diff --git a/frontend/src/resources/custom/cr.postgres.db.movetokube.com.tsx b/frontend/src/resources/custom/cr.postgres.db.movetokube.com.tsx index b190ce1..1d22a94 100644 --- a/frontend/src/resources/custom/cr.postgres.db.movetokube.com.tsx +++ b/frontend/src/resources/custom/cr.postgres.db.movetokube.com.tsx @@ -22,6 +22,7 @@ import { Button, SortPayload, Link, + usePermissions, } from 'react-admin'; import { View } from '../index'; import { formatArray, parseArray } from '../../utils'; @@ -174,6 +175,9 @@ const CrEdit = () => { }; const CrList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_POSTGRES_DB, op) + return ( <> @@ -183,9 +187,9 @@ const CrList = () => { - - - + {hasPermission('write') && } + {hasPermission('read') && } + {hasPermission('write') && } @@ -195,6 +199,9 @@ const CrList = () => { const CrShow = () => { const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_POSTGRES_DB, op) + if (!record) return null; return ( @@ -205,7 +212,7 @@ const CrShow = () => { crName={CR_POSTGRES_DB} crId={record.spec.database} /> - }> + }> @@ -222,6 +229,8 @@ const CrShow = () => { const PostgresUsers = () => { const translate = useTranslate(); const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_POSTGRES_USERS, op) const sort : SortPayload = { field: 'id', order: 'ASC' }; const { data, total, isLoading } = useGetList(CR_POSTGRES_USERS, { @@ -265,15 +274,15 @@ const PostgresUsers = () => { label={`resources.${CR_POSTGRES_USERS}.fields.spec.secretName`} /> - - - } + {hasPermission('read') && } + {hasPermission('write') && + resource={CR_POSTGRES_USERS}/>} )} - + {hasPermission('write') && } ); }; diff --git a/frontend/src/resources/custom/cr.postgrests.operator.postgrest.org.tsx b/frontend/src/resources/custom/cr.postgrests.operator.postgrest.org.tsx index d1509db..621b527 100644 --- a/frontend/src/resources/custom/cr.postgrests.operator.postgrest.org.tsx +++ b/frontend/src/resources/custom/cr.postgrests.operator.postgrest.org.tsx @@ -23,6 +23,7 @@ import { SingleFieldList, ChipField, NumberInput, + usePermissions, } from 'react-admin'; import { View } from '../index'; import { ViewToolbar } from '../../components/ViewToolbar'; @@ -401,11 +402,14 @@ const CrEdit = () => { }; const CrList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_POSTGREST, op) + return ( <> - }> + }> @@ -415,9 +419,9 @@ const CrList = () => { - - - + {hasPermission('write') && } + {hasPermission('read') && } + {hasPermission('write') && } @@ -428,6 +432,8 @@ const CrList = () => { const CrShow = () => { const { record } = useShowController(); const translate = useTranslate(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_POSTGREST, op) if (!record) return null; return ( @@ -438,7 +444,7 @@ const CrShow = () => { crName={CR_POSTGREST} crId={record.spec.database} /> - }> + }> diff --git a/frontend/src/resources/custom/cr.postgresusers.db.movetokube.com.tsx b/frontend/src/resources/custom/cr.postgresusers.db.movetokube.com.tsx index b35e518..c7d3fa4 100644 --- a/frontend/src/resources/custom/cr.postgresusers.db.movetokube.com.tsx +++ b/frontend/src/resources/custom/cr.postgresusers.db.movetokube.com.tsx @@ -18,6 +18,7 @@ import { useRedirect, useTranslate, SelectInput, + usePermissions, } from 'react-admin'; import { View } from '../index'; import { ViewToolbar } from '../../components/ViewToolbar'; @@ -203,6 +204,9 @@ const CrList = () => { const CrShow = () => { const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_POSTGRES_USERS, op) + if (!record) return null; return ( @@ -218,9 +222,9 @@ const CrShow = () => { diff --git a/frontend/src/resources/custom/cr.users.minio.scc-digitalhub.github.io.tsx b/frontend/src/resources/custom/cr.users.minio.scc-digitalhub.github.io.tsx index f1c96ea..140b990 100644 --- a/frontend/src/resources/custom/cr.users.minio.scc-digitalhub.github.io.tsx +++ b/frontend/src/resources/custom/cr.users.minio.scc-digitalhub.github.io.tsx @@ -17,6 +17,7 @@ import { ReferenceArrayField, TopToolbar, ListButton, + usePermissions, } from 'react-admin'; import { View } from '../index'; import { ViewToolbar } from '../../components/ViewToolbar'; @@ -134,6 +135,9 @@ const CrEdit = () => { const CrShow = () => { const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess(CR_MINIO_USERS, op) + if (!record) return null; return ( @@ -146,7 +150,7 @@ const CrShow = () => { - + {hasPermission('write') && } diff --git a/frontend/src/resources/k8s/k8s_deployment.tsx b/frontend/src/resources/k8s/k8s_deployment.tsx index 24eee0e..d662bc6 100644 --- a/frontend/src/resources/k8s/k8s_deployment.tsx +++ b/frontend/src/resources/k8s/k8s_deployment.tsx @@ -10,6 +10,7 @@ import { useShowController, useTranslate, useRecordContext, + usePermissions, } from 'react-admin'; import { Box, Typography } from '@mui/material'; import { Breadcrumb } from '@dslab/ra-breadcrumb'; @@ -25,20 +26,23 @@ const StatusField = (props: any) => { ) }; -export const K8SDeploymentList = () => ( - <> +export const K8SDeploymentList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess('k8s_deployment', op) + + return <> - + {hasPermission('read') && } -); +}; export const K8SDeploymentShow = () => { const translate = useTranslate(); diff --git a/frontend/src/resources/k8s/k8s_job.tsx b/frontend/src/resources/k8s/k8s_job.tsx index 1202415..b717e35 100644 --- a/frontend/src/resources/k8s/k8s_job.tsx +++ b/frontend/src/resources/k8s/k8s_job.tsx @@ -10,6 +10,7 @@ import { useTranslate, useRecordContext, DeleteButton, + usePermissions, } from 'react-admin'; import { Box, Typography } from '@mui/material'; import { Breadcrumb } from '@dslab/ra-breadcrumb'; @@ -55,8 +56,11 @@ const CompletionField = (props: any) => { ) }; -export const K8SJobList = () => ( - <> +export const K8SJobList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess('k8s_job', op) + + return <> @@ -64,17 +68,20 @@ export const K8SJobList = () => ( - - + {hasPermission('read') && } + {hasPermission('write') && } -); +}; export const K8SJobShow = () => { const translate = useTranslate(); const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess('k8s_job', op) + if (!record) return null; return ( <> @@ -85,7 +92,7 @@ export const K8SJobShow = () => { recordRepresentation: record.id, })} - }> + }> diff --git a/frontend/src/resources/k8s/k8s_pvc.tsx b/frontend/src/resources/k8s/k8s_pvc.tsx index f7a2bd6..fffdc04 100644 --- a/frontend/src/resources/k8s/k8s_pvc.tsx +++ b/frontend/src/resources/k8s/k8s_pvc.tsx @@ -17,6 +17,7 @@ import { SelectInput, ReferenceInput, NumberInput, + usePermissions, } from 'react-admin'; import { Box, Grid, Typography } from '@mui/material'; import { Breadcrumb } from '@dslab/ra-breadcrumb'; @@ -127,28 +128,33 @@ export const K8SPvcCreate = () => { ); }; -export const K8SPvcList = () => ( - <> +export const K8SPvcList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess('k8s_pvc', op) + + return <> - }> - + }> + - - + {hasPermission('read') && } + {hasPermission('write') && } -); +}; export const K8SPvcShow = () => { const translate = useTranslate(); const { record } = useShowController(); + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess('k8s_pvc', op) if (!record) return null; return ( <> @@ -159,7 +165,7 @@ export const K8SPvcShow = () => { recordRepresentation: record.id, })} - }> + }> diff --git a/frontend/src/resources/k8s/k8s_secret.tsx b/frontend/src/resources/k8s/k8s_secret.tsx index 364e6d5..dfac863 100644 --- a/frontend/src/resources/k8s/k8s_secret.tsx +++ b/frontend/src/resources/k8s/k8s_secret.tsx @@ -18,6 +18,7 @@ import { ArrayInput, SimpleFormIterator, useNotify, + usePermissions, } from 'react-admin'; import { Box, Grid, Typography } from '@mui/material'; import { Breadcrumb } from '@dslab/ra-breadcrumb'; @@ -98,8 +99,11 @@ const DataNumField = (props: any) => { }; -export const K8SSecretList = () => ( - <> +export const K8SSecretList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess('k8s_secret', op) + +return <> @@ -107,12 +111,12 @@ export const K8SSecretList = () => ( - + {hasPermission('read') && } -); +}; const DecodeButton = (props: any) => { const record = useRecordContext(props); diff --git a/frontend/src/resources/k8s/k8s_service.tsx b/frontend/src/resources/k8s/k8s_service.tsx index bae373b..ed60acd 100644 --- a/frontend/src/resources/k8s/k8s_service.tsx +++ b/frontend/src/resources/k8s/k8s_service.tsx @@ -10,6 +10,7 @@ import { useShowController, useTranslate, useRecordContext, + usePermissions, } from 'react-admin'; import { Box, Typography } from '@mui/material'; import { Breadcrumb } from '@dslab/ra-breadcrumb'; @@ -35,8 +36,11 @@ const TypeField = (props: any) => { ) : (<>) }; -export const K8SServiceList = () => ( - <> +export const K8SServiceList = () => { + const { permissions } = usePermissions(); + const hasPermission = (op: string) => permissions && permissions.canAccess('k8s_service', op) + + return <> @@ -45,12 +49,12 @@ export const K8SServiceList = () => ( - + {hasPermission('read') && } -); +}; export const K8SServiceShow = () => { const translate = useTranslate(); diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/api/CustomResourceApi.java b/src/main/java/it/smartcommunitylab/dhub/rm/api/CustomResourceApi.java index 8cb2ba5..5edfe0f 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/api/CustomResourceApi.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/api/CustomResourceApi.java @@ -23,7 +23,6 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@PreAuthorize("hasAuthority('ROLE_USER')") @SecurityRequirement(name = "basicAuth") @SecurityRequirement(name = "jwtAuth") @RequestMapping(SystemKeys.API_PATH) @@ -36,6 +35,7 @@ public class CustomResourceApi { @Value("${kubernetes.namespace}") private String namespace; + @PreAuthorize("@authz.canAccess(#crdId, 'list')") @GetMapping("/{crdId}") public Page findAll( @PathVariable @Pattern(regexp = SystemKeys.REGEX_CRD_ID) String crdId, @@ -45,6 +45,7 @@ public Page findAll( return service.findAll(crdId, namespace, id, pageable); } + @PreAuthorize("@authz.canAccess(#crdId, 'read')") @GetMapping("/{crdId}/{id}") public IdAwareCustomResource findById( @PathVariable @Pattern(regexp = SystemKeys.REGEX_CRD_ID) String crdId, @@ -53,6 +54,7 @@ public IdAwareCustomResource findById( return service.findById(crdId, id, namespace); } + @PreAuthorize("@authz.canAccess(#crdId, 'write')") @PostMapping("/{crdId}") public IdAwareCustomResource add( @PathVariable @Pattern(regexp = SystemKeys.REGEX_CRD_ID) String crdId, @@ -61,6 +63,7 @@ public IdAwareCustomResource add( return service.add(crdId, request, namespace); } + @PreAuthorize("@authz.canAccess(#crdId, 'write')") @PutMapping("/{crdId}/{id}") public IdAwareCustomResource update( @PathVariable @Pattern(regexp = SystemKeys.REGEX_CRD_ID) String crdId, @@ -70,6 +73,7 @@ public IdAwareCustomResource update( return service.update(crdId, id, request, namespace); } + @PreAuthorize("@authz.canAccess(#crdId, 'write')") @DeleteMapping("/{crdId}/{id}") public void delete( @PathVariable @Pattern(regexp = SystemKeys.REGEX_CRD_ID) String crdId, diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/api/CustomResourceSchemaApi.java b/src/main/java/it/smartcommunitylab/dhub/rm/api/CustomResourceSchemaApi.java index 2f7861c..1ba76bd 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/api/CustomResourceSchemaApi.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/api/CustomResourceSchemaApi.java @@ -48,6 +48,7 @@ public CustomResourceSchemaDTO findById(@PathVariable @Pattern(regexp = SystemKe return service.findById(id); } + @PreAuthorize("@authz.canAccess('crs', 'write')") @PostMapping public CustomResourceSchemaDTO add( @RequestParam(required = false) String id, @@ -56,6 +57,7 @@ public CustomResourceSchemaDTO add( return service.add(id, request); } + @PreAuthorize("@authz.canAccess('crs', 'write')") @PutMapping("/{id}") public CustomResourceSchemaDTO update( @PathVariable @Pattern(regexp = SystemKeys.REGEX_SCHEMA_ID) String id, @@ -64,6 +66,7 @@ public CustomResourceSchemaDTO update( return service.update(id, request); } + @PreAuthorize("@authz.canAccess('crs', 'write')") @DeleteMapping("/{id}") public void delete(@PathVariable @Pattern(regexp = SystemKeys.REGEX_SCHEMA_ID) String id) { service.delete(id); diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SDeploymentApi.java b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SDeploymentApi.java index 8b56ec8..139a327 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SDeploymentApi.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SDeploymentApi.java @@ -23,7 +23,7 @@ import jakarta.validation.constraints.Pattern; @RestController -@PreAuthorize("hasAuthority('ROLE_USER')") +@PreAuthorize("@authz.canAccess('k8s_deployment', 'list')") @SecurityRequirement(name = "basicAuth") @SecurityRequirement(name = "jwtAuth") @RequestMapping(SystemKeys.API_PATH + "/k8s_deployment") @@ -36,6 +36,7 @@ public class K8SDeploymentApi { @Value("${kubernetes.namespace}") private String namespace; + @PreAuthorize("@authz.canAccess('k8s_deployment', 'list')") @GetMapping public Page> findAll( @RequestParam(required = false) Collection id, @@ -44,11 +45,13 @@ public Page> findAll( return service.findAll(namespace, id, pageable); } + @PreAuthorize("@authz.canAccess('k8s_deployment', 'read')") @GetMapping("/{deploymentId}") public IdAwareResource findById(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) String deploymentId) { return service.findById(namespace, deploymentId); } + @PreAuthorize("@authz.canAccess('k8s_deployment', 'read')") @GetMapping("/{deploymentId}/log") public List getLog(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) String deploymentId) { return service.getLog(namespace, deploymentId); diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SJobApi.java b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SJobApi.java index d7475c0..7670b4e 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SJobApi.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SJobApi.java @@ -24,7 +24,7 @@ import jakarta.validation.constraints.Pattern; @RestController -@PreAuthorize("hasAuthority('ROLE_USER')") +@PreAuthorize("@authz.canAccess('k8s_job', 'list')") @SecurityRequirement(name = "basicAuth") @SecurityRequirement(name = "jwtAuth") @RequestMapping(SystemKeys.API_PATH + "/k8s_job") @@ -37,6 +37,7 @@ public class K8SJobApi { @Value("${kubernetes.namespace}") private String namespace; + @PreAuthorize("@authz.canAccess('k8s_job', 'list')") @GetMapping public Page> findAll( @RequestParam(required = false) Collection id, @@ -45,16 +46,19 @@ public Page> findAll( return service.findAll(namespace, id, pageable); } + @PreAuthorize("@authz.canAccess('k8s_job', 'read')") @GetMapping("/{jobId}") public IdAwareResource findById(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) String jobId) { return service.findById(namespace, jobId); } + @PreAuthorize("@authz.canAccess('k8s_job', 'read')") @GetMapping("/{jobId}/log") public List getLog(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) String jobId) { return service.getLog(namespace, jobId); } + @PreAuthorize("@authz.canAccess('k8s_job', 'write')") @DeleteMapping("/{jobId}") public void delete(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) String jobId) { service.delete(namespace, jobId); diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SPVCApi.java b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SPVCApi.java index 8cc1cbb..8dc6d63 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SPVCApi.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SPVCApi.java @@ -29,7 +29,7 @@ import jakarta.validation.constraints.Pattern; @RestController -@PreAuthorize("hasAuthority('ROLE_USER')") +@PreAuthorize("@authz.canAccess('k8s_pvc', 'list')") @SecurityRequirement(name = "basicAuth") @SecurityRequirement(name = "jwtAuth") @RequestMapping(SystemKeys.API_PATH) @@ -42,6 +42,7 @@ public class K8SPVCApi { @Value("${kubernetes.namespace}") private String namespace; + @PreAuthorize("@authz.canAccess('k8s_pvc', 'list')") @GetMapping("/k8s_pvc") public Page> findAll( @RequestParam(required = false) Collection id, @@ -50,11 +51,13 @@ public Page> findAll( return service.findAll(namespace, id, pageable); } + @PreAuthorize("@authz.canAccess('k8s_pvc', 'read')") @GetMapping("/k8s_pvc/{pvcId}") public IdAwareResource findById(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) String pvcId) { return service.findById(namespace, pvcId); } + @PreAuthorize("@authz.canAccess('k8s_pvc', 'write')") @PostMapping("/k8s_pvc") public IdAwareResource add( @Valid @RequestBody PersistentVolumeClaimDTO request @@ -62,6 +65,7 @@ public IdAwareResource add( return service.add(namespace, request); } + @PreAuthorize("@authz.canAccess('k8s_pvc', 'write')") @DeleteMapping("/k8s_pvc/{pvcId}") public void delete(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) String pvcId) { service.delete(namespace, pvcId); @@ -69,6 +73,7 @@ public void delete(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) Strin @GetMapping("/k8s_storageclass") + @PreAuthorize("@authz.canAccess('k8s_pvc', 'list')") public Page> getStorageClasses( @RequestParam(required = false) Collection id, Pageable pageable diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SSecretApi.java b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SSecretApi.java index 3530b33..7dcff1d 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SSecretApi.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SSecretApi.java @@ -23,7 +23,7 @@ import it.smartcommunitylab.dhub.rm.service.K8SSecretService; @RestController -@PreAuthorize("hasAuthority('ROLE_USER')") +@PreAuthorize("@authz.canAccess('k8s_secret', 'list')") @SecurityRequirement(name = "basicAuth") @SecurityRequirement(name = "jwtAuth") @RequestMapping(SystemKeys.API_PATH + "/k8s_secret") @@ -36,6 +36,7 @@ public class K8SSecretApi { @Value("${kubernetes.namespace}") private String namespace; + @PreAuthorize("@authz.canAccess('k8s_secret', 'list')") @GetMapping public Page> findAll( @RequestParam(required = false) Collection id, @@ -44,11 +45,13 @@ public Page> findAll( return service.findAll(namespace, id, pageable); } + @PreAuthorize("@authz.canAccess('k8s_secret', 'read')") @GetMapping("/{secretId}") public IdAwareResource findById(@PathVariable String secretId) { return service.findById(namespace, secretId); } + @PreAuthorize("@authz.canAccess('k8s_secret', 'read')") @GetMapping("/{secretId}/decode/{key:.*}") public Map decodeSecret(@PathVariable String secretId, @PathVariable String key) { return Collections.singletonMap(key, service.decode(namespace, secretId, key)); diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SServiceApi.java b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SServiceApi.java index 01945fd..cac94af 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SServiceApi.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/api/K8SServiceApi.java @@ -21,7 +21,7 @@ import jakarta.validation.constraints.Pattern; @RestController -@PreAuthorize("hasAuthority('ROLE_USER')") +@PreAuthorize("@authz.canAccess('k8s_service', 'list')") @SecurityRequirement(name = "basicAuth") @SecurityRequirement(name = "jwtAuth") @RequestMapping(SystemKeys.API_PATH + "/k8s_service") @@ -34,6 +34,7 @@ public class K8SServiceApi { @Value("${kubernetes.namespace}") private String namespace; + @PreAuthorize("@authz.canAccess('k8s_service', 'list')") @GetMapping public Page> findAll( @RequestParam(required = false) Collection id, @@ -42,6 +43,7 @@ public Page> findAll( return service.findAll(namespace, id, pageable); } + @PreAuthorize("@authz.canAccess('k8s_service', 'read')") @GetMapping("/{serviceId}") public IdAwareResource findById(@PathVariable @Pattern(regexp = SystemKeys.REGEX_CR_ID) String serviceId) { return service.findById(namespace, serviceId); diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/api/UserApi.java b/src/main/java/it/smartcommunitylab/dhub/rm/api/UserApi.java new file mode 100644 index 0000000..68c8e22 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/dhub/rm/api/UserApi.java @@ -0,0 +1,44 @@ +package it.smartcommunitylab.dhub.rm.api; + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import it.smartcommunitylab.dhub.rm.SystemKeys; +import it.smartcommunitylab.dhub.rm.config.RoleProperties.RoleConfig; +import it.smartcommunitylab.dhub.rm.model.IdAwareCustomResourceDefinition; +import it.smartcommunitylab.dhub.rm.model.dto.CustomResourceSchemaDTO; +import it.smartcommunitylab.dhub.rm.service.AccessControlService; +import it.smartcommunitylab.dhub.rm.service.AccessControlService.RESOURCE_OP; +import it.smartcommunitylab.dhub.rm.service.CustomResourceDefinitionService; +import it.smartcommunitylab.dhub.rm.service.CustomResourceSchemaService; +import jakarta.validation.constraints.Pattern; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@PreAuthorize("hasAuthority('ROLE_USER')") +@SecurityRequirement(name = "basicAuth") +@SecurityRequirement(name = "jwtAuth") +@RequestMapping(SystemKeys.API_PATH + "/user") +@Validated +public class UserApi { + + @Autowired + private AccessControlService service; + + @GetMapping + public Map> getPermissions() { + Map> userPermissions = service.getUserPermissions(); + return userPermissions; + } +} diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/config/AuthenticationProperties.java b/src/main/java/it/smartcommunitylab/dhub/rm/config/AuthenticationProperties.java index 0ec9ec7..dc11ab3 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/config/AuthenticationProperties.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/config/AuthenticationProperties.java @@ -74,6 +74,7 @@ public static class OAuth2AuthenticationProperties { private String issuerUri; private String audience; private String[] scopes; + private String roleClaim; public String getIssuerUri() { return issuerUri; @@ -102,5 +103,14 @@ public String[] getScopes() { public void setScopes(String[] scopes) { this.scopes = scopes; } + + public String getRoleClaim() { + return roleClaim; + } + + public void setRoleClaim(String roleClaim) { + this.roleClaim = roleClaim; + } + } } diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/config/RoleProperties.java b/src/main/java/it/smartcommunitylab/dhub/rm/config/RoleProperties.java new file mode 100644 index 0000000..47827f2 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/dhub/rm/config/RoleProperties.java @@ -0,0 +1,39 @@ +package it.smartcommunitylab.dhub.rm.config; + +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "access", ignoreUnknownFields = true) +public class RoleProperties { + + private List roles; + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public static class RoleConfig { + private String role; + private List resources; + + public String getRole() { + return role; + } + public void setRole(String role) { + this.role = role; + } + public List getResources() { + return resources; + } + public void setResources(List resources) { + this.resources = resources; + } + } +} diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/config/SecurityConfig.java b/src/main/java/it/smartcommunitylab/dhub/rm/config/SecurityConfig.java index 197254b..0d7f758 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/config/SecurityConfig.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/config/SecurityConfig.java @@ -1,7 +1,5 @@ package it.smartcommunitylab.dhub.rm.config; -import static org.springframework.security.config.Customizer.withDefaults; - import it.smartcommunitylab.dhub.rm.SystemKeys; import java.util.ArrayList; import java.util.Arrays; @@ -34,12 +32,13 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtClaimValidator; import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsConfiguration; @@ -99,20 +98,30 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { if (authenticationProperties.isBasicAuthEnabled()) { logger.info("Enable basic authentication"); - http.httpBasic(withDefaults()).userDetailsService(userDetailsService()); + http + .httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) + .userDetailsService(userDetailsService()); + } if (authenticationProperties.isOAuth2Enabled()) { logger.info("Enable OAuth2 JWT authentication"); - http.oauth2ResourceServer(oauth2 -> - oauth2.jwt().decoder(jwtDecoder()).jwtAuthenticationConverter(jwtAuthenticationConverter()) - ); + http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(jwtDecoder()).jwtAuthenticationConverter(jwtAuthenticationConverter()))); } } else { logger.warn("Enable anonymous authentication"); - http.anonymous(anon -> anon.authorities("ROLE_USER")); + http.anonymous(anon -> { + anon.authorities("ROLE_USER", "ROLE_ADMIN"); + anon.principal("anonymous"); + }); } + http.exceptionHandling(handling -> { + handling + .authenticationEntryPoint(new Http403ForbiddenEntryPoint()) + .accessDeniedHandler(new AccessDeniedHandlerImpl()); // use 403 + }); + return http.build(); } @@ -131,33 +140,41 @@ private UserDetailsService userDetailsService() { } private JwtDecoder jwtDecoder() { - NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation( - authenticationProperties.getOauth2().getIssuerUri() - ); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder + .withIssuerLocation(authenticationProperties.getOauth2().getIssuerUri()) + .build(); - Predicate> testClaimValue = claimValue -> + Predicate> audClaimValue = claimValue -> (claimValue != null) && claimValue.contains(authenticationProperties.getOauth2().getAudience()); - OAuth2TokenValidator audienceValidator = new JwtClaimValidator<>(JwtClaimNames.AUD, testClaimValue); + OAuth2TokenValidator audienceValidator = new JwtClaimValidator<>(JwtClaimNames.AUD, audClaimValue); - OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer( - authenticationProperties.getOauth2().getIssuerUri() - ); + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(authenticationProperties.getOauth2().getIssuerUri()); OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); - jwtDecoder.setJwtValidator(withAudience); return jwtDecoder; } private Converter jwtAuthenticationConverter() { + String claim = authenticationProperties.getOauth2().getRoleClaim(); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter((Jwt source) -> { if (source == null) return null; - List roles = new ArrayList<>(); - roles.add(new SimpleGrantedAuthority("ROLE_USER")); + List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority("ROLE_USER")); + + if (StringUtils.hasText(claim) && source.hasClaim(claim)) { + List roles = source.getClaimAsStringList(claim); + if (roles != null) { + roles.forEach(r -> { + //use as is + authorities.add(new SimpleGrantedAuthority(r)); + }); + } + } - return roles; + return authorities; }); return converter; } @@ -179,4 +196,7 @@ private CorsConfigurationSource corsConfigurationSource(String origins) { source.registerCorsConfiguration("/**", config); return source; } + } + + diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/controller/MainController.java b/src/main/java/it/smartcommunitylab/dhub/rm/controller/MainController.java index 8abe0f6..7c16770 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/controller/MainController.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/controller/MainController.java @@ -3,9 +3,13 @@ import it.smartcommunitylab.dhub.rm.SystemKeys; import it.smartcommunitylab.dhub.rm.config.ApplicationProperties; import it.smartcommunitylab.dhub.rm.config.AuthenticationProperties; +import it.smartcommunitylab.dhub.rm.service.AccessControlService; import jakarta.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -14,15 +18,23 @@ import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + @Controller public class MainController { + private static final Logger logger = LoggerFactory.getLogger(MainController.class); + @Autowired AuthenticationProperties authenticationProperties; @Autowired ApplicationProperties applicationProperties; + @Autowired + AccessControlService acService; + private static final String CONSOLE_CONTEXT = SystemKeys.CONSOLE_PATH; @GetMapping("/") diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/AccessControlService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/AccessControlService.java new file mode 100644 index 0000000..53a04f0 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/AccessControlService.java @@ -0,0 +1,172 @@ +package it.smartcommunitylab.dhub.rm.service; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import it.smartcommunitylab.dhub.rm.config.AuthenticationProperties; +import it.smartcommunitylab.dhub.rm.config.RoleProperties; +import it.smartcommunitylab.dhub.rm.model.IdAwareCustomResourceDefinition; +import jakarta.annotation.PostConstruct; + +/** + * Role-Based Access Control (RBAC) Service for managing the user access to the + * different app APIs + */ +@Service("authz") +public class AccessControlService { + + private static final String ROLE_ADMIN = "ROLE_ADMIN"; + + public enum RESOURCE_OP {list, read, write, all} + + @Autowired + AuthenticationProperties authenticationProperties; + @Autowired + RoleProperties roleProperties; + @Autowired + private CustomResourceDefinitionService service; + + // role -> resource -> operations + private Map>> roleMap = new HashMap<>(); + + private static final String[] K8S_RESOURCES = new String[]{"k8s_job", "k8s_service", "k8s_deployment", "k8s_secret", "k8s_pvc"}; + + /** + * Pre-build role model. Map role to list of resources and their permissions. + */ + @PostConstruct + public void initRoles() { + if (roleProperties.getRoles() != null) { + // default admin role: all permitted + roleMap.put(ROLE_ADMIN, new HashMap<>()); + for (String r: getFullResourceList()) { + addRole(roleMap.get(ROLE_ADMIN), r, RESOURCE_OP.all); + } + + roleProperties.getRoles().forEach(role -> { + roleMap.put(role.getRole(), new HashMap<>()); + List resources = role.getResources(); + // each resource has :: or syntax with wildcards + resources.forEach(res -> { + RESOURCE_OP op = null; + String rName = null; + if (!res.contains("::")) { + op = RESOURCE_OP.all; + rName = res; + } else { + String[] arr = res.split("::"); + if (arr[1].equals("*")) op = RESOURCE_OP.all; + else op = RESOURCE_OP.valueOf(arr[1]); + rName = arr[0]; + } + if (rName.equals("*")) { + for (String r: getFullResourceList()) { + addRole(roleMap.get(role.getRole()), r, op); + } + } else { + addRole(roleMap.get(role.getRole()), rName, op); + } + }); + }); + } + } + + private Set getFullResourceList() { + Set set = new HashSet(); + set.addAll(Arrays.asList(K8S_RESOURCES)); + set.addAll(service.findAll(null, false, PageRequest.ofSize(1000)).getContent().stream().map(r -> r.getId()).toList()); + return set; + } + + private void addRole(Map> map, String id, RESOURCE_OP op) { + Set ops = new HashSet<>(); + map.put(id, ops); + switch (op) { + case all: ops.add(RESOURCE_OP.all); + case write: ops.add(RESOURCE_OP.write); + case read: ops.add(RESOURCE_OP.read); + case list: ops.add(RESOURCE_OP.list); + } +} + + /** + * Check if the current user can access the specified resource for the specific operation + * @param resource + * @param op + * @return + */ + public boolean canAccess(String resource, RESOURCE_OP op) { + // bypass for no auth scenario + if (!authenticationProperties.isBasicAuthEnabled() && !authenticationProperties.isOAuth2Enabled()) return true; + + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext == null || securityContext.getAuthentication() == null || securityContext.getAuthentication().getAuthorities() == null || securityContext.getAuthentication().getAuthorities().isEmpty()) return false; + + // check roles - associated resources and wildcards + Collection authorities = securityContext.getAuthentication().getAuthorities(); + for (GrantedAuthority a : authorities) { + if (roleMap.containsKey(a.getAuthority())) { + Map> opMap = roleMap.get(a.getAuthority()); + // first check explicit + if (opMap.containsKey(resource)) { + if (opMap.get(resource).contains(op)) return true; + } + else if (opMap.containsKey("*")) { + if (opMap.get("*").contains(op)) return true; + } + } + } + return false; + } + + /** + * Return map of resource - permissions for the current user given the list of roles of the user + * @return + */ + public Map> getUserPermissions() { + boolean all = !authenticationProperties.isBasicAuthEnabled() && !authenticationProperties.isOAuth2Enabled(); + if (!all) { + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext == null || securityContext.getAuthentication() == null || securityContext.getAuthentication().getAuthorities() == null || securityContext.getAuthentication().getAuthorities().isEmpty()) { + return Collections.emptyMap(); + } + Collection authorities = securityContext.getAuthentication().getAuthorities(); + Map> result = new HashMap<>(); + for (GrantedAuthority a : authorities) { + if (roleMap.containsKey(a.getAuthority())) { + for (Map.Entry> entry : roleMap.get(a.getAuthority()).entrySet()) { + if (!result.containsKey(entry.getKey())) result.put(entry.getKey(), new HashSet<>(entry.getValue())); + else result.put(entry.getKey(), mergeOps(result.get(entry.getKey()), entry.getValue())); + } + } + } + return result; + } else { + // all permissions for all resources + Map> result = new HashMap<>(); + for (String r: getFullResourceList()) { + addRole(result, r, RESOURCE_OP.all); + } + return result; + } + } + + private Set mergeOps(Set dest, Set src) { + dest.addAll(src); + return dest; + } +} diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceDefinitionService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceDefinitionService.java index f0aa80e..3df4892 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceDefinitionService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceDefinitionService.java @@ -48,7 +48,7 @@ public class CustomResourceDefinitionService { private static final Logger logger = LoggerFactory.getLogger(CustomResourceDefinitionService.class); private final KubernetesClient client; - private final AuthorizationService authService; + private final K8SAuthorizationService authService; private final CustomResourceSchemaRepository customResourceSchemaRepository; private ConcurrentHashMap crdMap = new ConcurrentHashMap<>(); @@ -74,7 +74,7 @@ public List load(String key) throws Exception { public CustomResourceDefinitionService( KubernetesClient client, - AuthorizationService authService, + K8SAuthorizationService authService, CustomResourceSchemaRepository customResourceSchemaRepository ) { this.client = client; diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceSchemaService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceSchemaService.java index ecda8a7..748fab6 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceSchemaService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceSchemaService.java @@ -50,12 +50,12 @@ public class CustomResourceSchemaService { private final DTOToSchemaConverter dtoToSchemaConverter; private final SchemaToDTOConverter schemaToDTOConverter; private final CustomResourceDefinitionService crdService; - private final AuthorizationService authService; + private final K8SAuthorizationService authService; public CustomResourceSchemaService( CustomResourceSchemaRepository customResourceSchemaRepository, CustomResourceDefinitionService crdService, - AuthorizationService authService + K8SAuthorizationService authService ) { this.customResourceSchemaRepository = customResourceSchemaRepository; this.dtoToSchemaConverter = new DTOToSchemaConverter(); diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceService.java index a1b699d..8dbb076 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/CustomResourceService.java @@ -44,13 +44,13 @@ public class CustomResourceService { private final KubernetesClient client; private final CustomResourceDefinitionService crdService; private final CustomResourceSchemaService schemaService; - private final AuthorizationService authService; + private final K8SAuthorizationService authService; public CustomResourceService( KubernetesClient client, CustomResourceDefinitionService crdService, CustomResourceSchemaService schemaService, - AuthorizationService authService + K8SAuthorizationService authService ) { Assert.notNull(client, "Client required"); this.client = client; diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/AuthorizationService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SAuthorizationService.java similarity index 87% rename from src/main/java/it/smartcommunitylab/dhub/rm/service/AuthorizationService.java rename to src/main/java/it/smartcommunitylab/dhub/rm/service/K8SAuthorizationService.java index d584f15..be0a661 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/AuthorizationService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SAuthorizationService.java @@ -5,10 +5,10 @@ import org.springframework.stereotype.Service; /** - * Resource access authorization service. Defines properties and methods to control the access to K8S resources + * K8S Resource access authorization service. Defines properties and methods to control the access to K8S resources */ @Service -public class AuthorizationService { +public class K8SAuthorizationService { @Value("${kubernetes.crd.allowed}") private List allowedCrds; diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SDeploymentService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SDeploymentService.java index 7d0e27f..433ad4f 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SDeploymentService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SDeploymentService.java @@ -20,7 +20,7 @@ @Service public class K8SDeploymentService extends K8SResourceService { - public K8SDeploymentService(KubernetesClient client, AuthorizationService authService) { + public K8SDeploymentService(KubernetesClient client, K8SAuthorizationService authService) { super(client, authService, 60); } diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SJobService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SJobService.java index fb6de28..a836b65 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SJobService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SJobService.java @@ -19,7 +19,7 @@ @Service public class K8SJobService extends K8SResourceService { - public K8SJobService(KubernetesClient client, AuthorizationService authService) { + public K8SJobService(KubernetesClient client, K8SAuthorizationService authService) { super(client, authService, 60); } diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SPVCService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SPVCService.java index 7c3eab1..2e13819 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SPVCService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SPVCService.java @@ -34,7 +34,7 @@ public class K8SPVCService extends K8SResourceService { @Value("${kubernetes.pvc.storage-classes}") private String acceptedStorageClasses; - public K8SPVCService(KubernetesClient client, AuthorizationService authService) { + public K8SPVCService(KubernetesClient client, K8SAuthorizationService authService) { super(client, authService, 60); } diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SResourceService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SResourceService.java index b1e25cd..1716a06 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SResourceService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SResourceService.java @@ -33,14 +33,14 @@ public abstract class K8SResourceService { public static final Logger logger = LoggerFactory.getLogger(K8SResourceService.class); private final KubernetesClient client; - private final AuthorizationService authService; + private final K8SAuthorizationService authService; private ConcurrentHashMap>> resourceMap = new ConcurrentHashMap<>(); private LoadingCache>> resourceCache; public K8SResourceService( KubernetesClient client, - AuthorizationService authService, + K8SAuthorizationService authService, int cacheExpirationSec ) { Assert.notNull(client, "Client required"); @@ -81,7 +81,7 @@ public java.util.Map> load(String key) throws Excepti * Reference to auth service * @return */ - protected AuthorizationService getAuthService() { + protected K8SAuthorizationService getAuthService() { return authService; } diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SSecretService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SSecretService.java index 66316a7..4e5e03e 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SSecretService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SSecretService.java @@ -49,7 +49,7 @@ public class K8SSecretService extends K8SResourceService { Set owners = new HashSet<>(); Set names = new HashSet<>(); - public K8SSecretService(KubernetesClient client, AuthorizationService authService) { + public K8SSecretService(KubernetesClient client, K8SAuthorizationService authService) { super(client, authService, 60); } diff --git a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SSvcService.java b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SSvcService.java index 357c6ef..eb8cc0a 100644 --- a/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SSvcService.java +++ b/src/main/java/it/smartcommunitylab/dhub/rm/service/K8SSvcService.java @@ -16,7 +16,7 @@ @Service public class K8SSvcService extends K8SResourceService { - public K8SSvcService(KubernetesClient client, AuthorizationService authService) { + public K8SSvcService(KubernetesClient client, K8SAuthorizationService authService) { super(client, authService, 60); }