From 0f7545483e12e58ccb5283b874c1e30f577356bd Mon Sep 17 00:00:00 2001 From: Jean-Baptiste WATENBERG Date: Thu, 11 Mar 2021 17:59:54 +0100 Subject: [PATCH 1/4] shell-ui: Add ability to configure user group mapping --- shell-ui/src/NavBar.js | 6 ++- shell-ui/src/UserDataListener.js | 7 +++- shell-ui/src/auth/permissionUtils.js | 19 ++++++++- shell-ui/src/auth/permissionUtils.spec.js | 49 +++++++++++++++++++++++ shell-ui/src/index.js | 7 +++- ui/src/components/Navbar.js | 2 +- 6 files changed, 82 insertions(+), 8 deletions(-) diff --git a/shell-ui/src/NavBar.js b/shell-ui/src/NavBar.js index f71e6d761a..296fa988c3 100644 --- a/shell-ui/src/NavBar.js +++ b/shell-ui/src/NavBar.js @@ -6,11 +6,13 @@ import type { Options, SolutionsNavbarProps, TranslationAndGroups, + UserGroupsMapping } from './index'; import type { Node } from 'react'; import { logOut } from './auth/logout'; import { getAccessiblePathsFromOptions, + getUserGroups, isEntryAccessibleByTheUser, normalizePath, } from './auth/permissionUtils'; @@ -60,10 +62,10 @@ const translateOptionsToMenu = ( ); }; -export const Navbar = ({ options }: { options: Options }): Node => { +export const Navbar = ({ options, userGroupsMapping }: { options: Options, userGroupsMapping?: UserGroupsMapping }): Node => { const auth = useAuth(); - const userGroups: string[] = auth.userData?.profile?.groups || []; + const userGroups: string[] = getUserGroups(auth.userData, userGroupsMapping); const accessiblePaths = getAccessiblePathsFromOptions(options, userGroups); useLayoutEffect(() => { accessiblePaths.forEach((accessiblePath) => { diff --git a/shell-ui/src/UserDataListener.js b/shell-ui/src/UserDataListener.js index a0fea142b1..2805aefeb1 100644 --- a/shell-ui/src/UserDataListener.js +++ b/shell-ui/src/UserDataListener.js @@ -1,10 +1,13 @@ import { useAuth } from 'oidc-react'; import { useLayoutEffect } from 'react'; -import { AUTHENTICATED_EVENT, SolutionsNavbarProps } from './index'; +import { getUserGroups } from './auth/permissionUtils'; +import { AUTHENTICATED_EVENT, SolutionsNavbarProps, type UserGroupsMapping } from './index'; export const UserDataListener = ({ + userGroupsMapping, onAuthenticated, }: { + userGroupsMapping?: UserGroupsMapping, onAuthenticated?: $PropertyType, }) => { const auth = useAuth(); @@ -12,7 +15,7 @@ export const UserDataListener = ({ useLayoutEffect(() => { if (onAuthenticated) { onAuthenticated( - new CustomEvent(AUTHENTICATED_EVENT, { detail: auth.userData }), + new CustomEvent(AUTHENTICATED_EVENT, { detail: {...auth.userData, groups: getUserGroups(auth.userData, userGroupsMapping) } }), ); } }, [JSON.stringify(auth.userData), !!onAuthenticated]); diff --git a/shell-ui/src/auth/permissionUtils.js b/shell-ui/src/auth/permissionUtils.js index 4d152babcf..badb318452 100644 --- a/shell-ui/src/auth/permissionUtils.js +++ b/shell-ui/src/auth/permissionUtils.js @@ -1,5 +1,10 @@ //@flow -import type { Options, TranslationAndGroups } from '../index'; +import type { User } from 'oidc-react'; +import type { + Options, + TranslationAndGroups, + UserGroupsMapping, +} from '../index'; export const isEntryAccessibleByTheUser = ( [path, translationAndGroup]: [string, TranslationAndGroups], @@ -39,3 +44,15 @@ export const isPathAccessible = ( (accessiblePath) => normalizePath(accessiblePath) === normalizedPath, ); }; + +export const getUserGroups = ( + user?: User, + userGroupsMapping?: UserGroupsMapping, +): string[] => { + const userOIDCGroups: string[] = user?.profile?.groups || []; + const userMappedGroups = userGroupsMapping + ? userGroupsMapping[user?.profile?.email || ''] || [] + : []; + + return Array.from(new Set([...userOIDCGroups, ...userMappedGroups])); +}; diff --git a/shell-ui/src/auth/permissionUtils.spec.js b/shell-ui/src/auth/permissionUtils.spec.js index 8652e74660..29aebe3987 100644 --- a/shell-ui/src/auth/permissionUtils.spec.js +++ b/shell-ui/src/auth/permissionUtils.spec.js @@ -1,6 +1,7 @@ //@flow import { getAccessiblePathsFromOptions, + getUserGroups, isEntryAccessibleByTheUser, isPathAccessible, normalizePath, @@ -116,3 +117,51 @@ describe('permission utils - isPathAccessible', () => { expect(isAccessible).toBe(false); }); }); + +describe('permission utils - getUserGroups', () => { + it('should return an array of groups when defined in OIDC claims ', () => { + //E + const oidcGroups = ['oidcGroup']; + const groups = getUserGroups( + { profile: { email: 'test@test.com', groups: oidcGroups } }, + undefined, + ); + //V + expect(groups).toEqual(oidcGroups); + }); + + it('should return an array of groups when defined in static mapping', () => { + //S + const staticGroups = ['oidcGroup']; + //E + const groups = getUserGroups( + { profile: { email: 'test@test.com' } }, + { 'test@test.com': staticGroups }, + ); + //V + expect(groups).toEqual(staticGroups); + }); + + it('should return a merged array of groups when defined in OIDC claims and mapping ', () => { + //S + const oidcOnlyGroups = ['OIDCGroup']; + const staticOnlyGroups = ['StaticGroup']; + const oidcAndStaticGroups = ['group']; + //E + const groups = getUserGroups( + { + profile: { + email: 'test@test.com', + groups: [...oidcAndStaticGroups, ...oidcOnlyGroups], + }, + }, + { 'test@test.com': [...oidcAndStaticGroups, ...staticOnlyGroups] }, + ); + //V + expect(groups).toEqual([ + ...oidcAndStaticGroups, + ...oidcOnlyGroups, + ...staticOnlyGroups, + ]); + }); +}); diff --git a/shell-ui/src/index.js b/shell-ui/src/index.js index 1dbefbad03..403734c090 100644 --- a/shell-ui/src/index.js +++ b/shell-ui/src/index.js @@ -21,6 +21,8 @@ export type MenuItems = {[path: string]: TranslationAndGroups } export type Options = { main: MenuItems, subLogin: MenuItems }; +export type UserGroupsMapping = {[email: string]: string[]}; + export type SolutionsNavbarProps = { 'oidc-provider-url'?: string, scopes?: string, @@ -44,6 +46,7 @@ type Config = { scopes?: string, }, options?: Options, + userGroupsMapping?: UserGroupsMapping }; const SolutionsNavbar = ({ @@ -133,7 +136,7 @@ const SolutionsNavbar = ({ return ( - + - + ); diff --git a/ui/src/components/Navbar.js b/ui/src/components/Navbar.js index 7ded5d976e..04a4187f76 100644 --- a/ui/src/components/Navbar.js +++ b/ui/src/components/Navbar.js @@ -51,7 +51,7 @@ function useLoginEffect(navbarRef: { current: NavbarWebComponent | null }) { const onAuthenticated = (evt: Event) => { /// flow is not accepting CustomEvent type for listener arguments of {add,remove}EventListener https://github.com/facebook/flow/issues/7179 // $flow-disable-line - if (evt.detail) { + if (evt.detail && evt.detail.profile) { // $flow-disable-line dispatch(updateAPIConfigAction(evt.detail)); setIsAuthenticated(true); From a07beb4786ad7f43913de028942e39b9245f576d Mon Sep 17 00:00:00 2001 From: Jean-Baptiste WATENBERG Date: Mon, 22 Mar 2021 11:27:55 +0100 Subject: [PATCH 2/4] salt: Sync DEX static users with shell ui static group mapping --- docs/operation/cluster_and_service_configuration.rst | 1 - .../addons/ui/config/metalk8s-shell-ui-config.yaml.j2 | 11 +++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/operation/cluster_and_service_configuration.rst b/docs/operation/cluster_and_service_configuration.rst index ae624cc073..be5516faa3 100644 --- a/docs/operation/cluster_and_service_configuration.rst +++ b/docs/operation/cluster_and_service_configuration.rst @@ -128,7 +128,6 @@ Features exposed include: The default Shell UI configuration values are specified below: .. literalinclude:: ../../salt/metalk8s/addons/ui/config/metalk8s-shell-ui-config.yaml.j2 - :language: yaml :lines: 3- See :ref:`csc-shell-ui-config-customization` to override these defaults. diff --git a/salt/metalk8s/addons/ui/config/metalk8s-shell-ui-config.yaml.j2 b/salt/metalk8s/addons/ui/config/metalk8s-shell-ui-config.yaml.j2 index d9d6a26758..12d1803508 100644 --- a/salt/metalk8s/addons/ui/config/metalk8s-shell-ui-config.yaml.j2 +++ b/salt/metalk8s/addons/ui/config/metalk8s-shell-ui-config.yaml.j2 @@ -1,5 +1,8 @@ #!jinja|yaml +{%- set dex_defaults = salt.slsutil.renderer('salt://metalk8s/addons/dex/config/dex.yaml.j2', saltenv=saltenv) %} +{%- set dex = salt.metalk8s_service_configuration.get_service_conf('metalk8s-auth', 'metalk8s-dex-config', dex_defaults) %} + # Defaults for shell UI configuration apiVersion: addons.metalk8s.scality.com/v1alpha1 kind: ShellUIConfig @@ -11,16 +14,20 @@ spec: responseType: "id_token" scopes: "openid profile email groups offline_access audience:server:client_id:oidc-auth-client" userGroupsMapping: - "admin@metalk8s.invalid": - - admin +{%- for user in dex.spec.config.staticPasswords | map(attribute='email') %} + "{{ user }}": [metalk8s:admin] +{%- endfor %} options: main: "https://{{ grains.metalk8s.control_plane_ip }}:8443/": en: "Platform" fr: "Plateforme" + groups: [metalk8s:admin] + activeIfMatches: "https://{{ grains.metalk8s.control_plane_ip }}:8443/(?!alerts).*" "https://{{ grains.metalk8s.control_plane_ip }}:8443/alerts": en: "Alerts" fr: "Alertes" + groups: [metalk8s:admin] subLogin: "https://{{ grains.metalk8s.control_plane_ip }}:8443/docs": en: "Documentation" From 231ca9aebd783709618f287ee4cd4eaa3f031c66 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste WATENBERG Date: Mon, 22 Mar 2021 13:03:52 +0100 Subject: [PATCH 3/4] doc: Add steps to apply user modifications in Dex --- .../user_authentication_and_identity_management.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/operation/account_administration/user_authentication_and_identity_management.rst b/docs/operation/account_administration/user_authentication_and_identity_management.rst index a66b30b511..75436cf918 100644 --- a/docs/operation/account_administration/user_authentication_and_identity_management.rst +++ b/docs/operation/account_administration/user_authentication_and_identity_management.rst @@ -98,10 +98,12 @@ the following steps from the bootstrap node. .. parsed-literal:: + root\@bootstrap $ STATES=$(printf ",metalk8s.addons.%s.deployed" \\ + dex prometheus-operator ui) root\@bootstrap $ kubectl exec -n kube-system -c salt-master \\ - --kubeconfig /etc/kubernetes/admin.conf \\ - salt-master-bootstrap -- salt-run state.sls \\ - metalk8s.addons.dex.deployed saltenv=metalk8s-|version| + --kubeconfig /etc/kubernetes/admin.conf \\ + salt-master-bootstrap -- salt-run state.sls \\ + "${STATES:1}" saltenv=metalk8s-|version| #. Bind the user to an existing (Cluster) Role using :ref:`a ClusterRoleBlinding `. From 0c9d5d1ebcc6760b4e32983299b1f6979d02cb59 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste WATENBERG Date: Mon, 22 Mar 2021 13:06:03 +0100 Subject: [PATCH 4/4] doc: Fix cluster and service configuration formating --- docs/operation/cluster_and_service_configuration.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/operation/cluster_and_service_configuration.rst b/docs/operation/cluster_and_service_configuration.rst index be5516faa3..ec57182056 100644 --- a/docs/operation/cluster_and_service_configuration.rst +++ b/docs/operation/cluster_and_service_configuration.rst @@ -610,7 +610,7 @@ Cluster and Service ConfigMap ``metalk8s-ui-config`` in namespace metalk8s-ui-config Changing the MetalK8s UI Ingress Path -"""""""""""""""""""""""""""""""""""""""""""""""" +""""""""""""""""""""""""""""""""""""" In order to expose another UI at the root path of the control plane, in place of MetalK8s UI, you need to change the Ingress path from @@ -644,7 +644,7 @@ these steps: .. _csc-ui-theme-customization: MetalK8s UI Theme Customization -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Default configuration for MetalK8s UI Theme can be overridden by editing its Cluster and Service ConfigMap ``metalk8s-theme`` in namespace @@ -658,7 +658,6 @@ Cluster and Service ConfigMap ``metalk8s-theme`` in namespace Once the theme is edited, apply your changes by running: - .. parsed-literal:: root\@bootstrap $ kubectl exec -n kube-system -c salt-master \\