Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shell-ui : Add users group mapping in shell UI config #3196

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
shell-ui: Add ability to configure user group mapping
  • Loading branch information
JBWatenbergScality committed Mar 24, 2021
commit 0f7545483e12e58ccb5283b874c1e30f577356bd
6 changes: 4 additions & 2 deletions shell-ui/src/NavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand Down
7 changes: 5 additions & 2 deletions shell-ui/src/UserDataListener.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
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<SolutionsNavbarProps, 'onAuthenticated'>,
}) => {
const auth = useAuth();

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]);
Expand Down
19 changes: 18 additions & 1 deletion shell-ui/src/auth/permissionUtils.js
Original file line number Diff line number Diff line change
@@ -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],
Expand Down Expand Up @@ -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]));
};
49 changes: 49 additions & 0 deletions shell-ui/src/auth/permissionUtils.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//@flow
import {
getAccessiblePathsFromOptions,
getUserGroups,
isEntryAccessibleByTheUser,
isPathAccessible,
normalizePath,
Expand Down Expand Up @@ -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,
]);
});
});
7 changes: 5 additions & 2 deletions shell-ui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type MenuItems = {[path: string]: TranslationAndGroups }

export type Options = { main: MenuItems, subLogin: MenuItems };

export type UserGroupsMapping = {[email: string]: string[]};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static mapping is fine for now I guess, maybe adding support for regular expressions could be useful in the future.

Anyway. This mapping would need to be defined from Salt (could be merged with user-provided values in UI CSC) by reading the Dex CSC, so we can always get the "platform-admin" (or whatever it's called) group to Dex static users.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, I think this simply means updating the salt/metalk8s/addons/ui/config/shell-ui-config.yaml.j2 template to:

#!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
spec:
  oidc:
    providerUrl: "/oidc"
    redirectUrl: "https://{{ grains.metalk8s.control_plane_ip }}:8443/"
    clientId: "metalk8s-ui"
    responseType: "id_token"
    scopes: "openid profile email groups offline_access audience:server:client_id:oidc-auth-client"
  userGroupsMapping:
{%- for user in dex.spec.config.staticPasswords | map(attribute='email') %}
    {{ user }}: [admin]
{%- endfor %}
  options:
    main:
      "https://{{ grains.metalk8s.control_plane_ip }}:8443/":
        en: "Platform"
        fr: "Plateforme"
      "https://{{ grains.metalk8s.control_plane_ip }}:8443/alerts":
        en: "Alerts"
        fr: "Alertes"
    subLogin:
      "https://{{ grains.metalk8s.control_plane_ip }}:8443/docs":
        en: "Documentation"
        fr: "Documentation"


export type SolutionsNavbarProps = {
'oidc-provider-url'?: string,
scopes?: string,
Expand All @@ -44,6 +46,7 @@ type Config = {
scopes?: string,
},
options?: Options,
userGroupsMapping?: UserGroupsMapping
};

const SolutionsNavbar = ({
Expand Down Expand Up @@ -133,15 +136,15 @@ const SolutionsNavbar = ({

return (
<AuthProvider {...oidcConfig}>
<UserDataListener onAuthenticated={onAuthenticated} />
<UserDataListener userGroupsMapping={config.userGroupsMapping} onAuthenticated={onAuthenticated} />
<StyledComponentsProvider
theme={{
// todo manages theme https://github.com/scality/metalk8s/issues/2545
brand: defaultTheme.dark,
logo_path: '/brand/assets/branding-dark.svg',
}}
>
<Navbar options={computedMenuOptions} />
<Navbar options={computedMenuOptions} userGroupsMapping={config.userGroupsMapping} />
</StyledComponentsProvider>
</AuthProvider>
);
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/Navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down