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

ui: Fix configuration when changing the basePath and fix permissions management in the navbar #3254

Merged
merged 6 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@

- Bump Node.js version to 14.16.0 (PR[#3214](https://github.com/scality/metalk8s/pull/3214))

- Introduces a `shell-ui` project that groups various UI components to be reused by
gdemonet marked this conversation as resolved.
Show resolved Hide resolved
solutions UIs (PR[#3106](https://github.com/scality/metalk8s/pull/3106))

- Move the navbar component to `shell-ui` to enable its reuse by solutions UIs
(PR[#3110](https://github.com/scality/metalk8s/pull/3110))

- Add a static user/groups mapping configuration as part of `shell-ui` configuration to
allow solutions UIs displaying features according to some user groups
(PR[#3154](https://github.com/scality/metalk8s/pull/3154))

## Release 2.8.1 (in development)
### Enhancements

Expand Down
2 changes: 1 addition & 1 deletion docs/operation/cluster_and_service_configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The default configuration values for Dex are specified below:

.. literalinclude:: ../../salt/metalk8s/addons/dex/config/dex.yaml.j2
:language: yaml
:lines: 3-42,45-
:lines: 14-42,45-

See :ref:`csc-dex-customization` for Dex configuration customizations.

Expand Down
13 changes: 12 additions & 1 deletion salt/metalk8s/addons/dex/config/dex.yaml.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
#!jinja|yaml

{%- set metalk8s_ui_defaults = salt.slsutil.renderer(
'salt://metalk8s/addons/ui/config/metalk8s-ui-config.yaml', saltenv=saltenv
)
%}

{%- set metalk8s_ui_config = salt.metalk8s_service_configuration.get_service_conf(
'metalk8s-ui', 'metalk8s-ui-config', metalk8s_ui_defaults
)
%}


# Defaults for configuration of Dex (OIDC)
apiVersion: addons.metalk8s.scality.com/v1alpha2
kind: DexConfig
Expand Down Expand Up @@ -54,7 +65,7 @@ spec:
- id: metalk8s-ui
name: MetalK8s UI
redirectURIs:
- https://{{ grains.metalk8s.control_plane_ip }}:8443/
- https://{{ grains.metalk8s.control_plane_ip }}:8443/{{ metalk8s_ui_config.spec.basePath.lstrip('/') }}
secret: ybrMJpVMQxsiZw26MhJzCjA2ut
- id: grafana-ui
name: Grafana UI
Expand Down
25 changes: 19 additions & 6 deletions salt/metalk8s/addons/ui/config/metalk8s-shell-ui-config.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@

{%- 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) %}
{%- set metalk8s_ui_defaults = salt.slsutil.renderer(
'salt://metalk8s/addons/ui/config/metalk8s-ui-config.yaml', saltenv=saltenv
)
%}

{%- set metalk8s_ui_config = salt.metalk8s_service_configuration.get_service_conf(
'metalk8s-ui', 'metalk8s-ui-config', metalk8s_ui_defaults
)
%}

{%- set normalized_base_path = metalk8s_ui_config.spec.basePath.strip('/') %}

{%- set metalk8s_ui_url = "https://" ~ grains.metalk8s.control_plane_ip ~
":8443" ~ ('/' ~ normalized_base_path if normalized_base_path else '') ~ '/' %}

# 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/"
redirectUrl: "https://{{ grains.metalk8s.control_plane_ip }}:8443/{{ metalk8s_ui_config.spec.basePath.lstrip('/') }}"
clientId: "metalk8s-ui"
responseType: "id_token"
scopes: "openid profile email groups offline_access audience:server:client_id:oidc-auth-client"
Expand All @@ -25,17 +39,16 @@ spec:
canChangeTheme: false
options:
main:
"https://{{ grains.metalk8s.control_plane_ip }}:8443/":
"{{ metalk8s_ui_url }}":
en: "Platform"
fr: "Plateforme"
groups: [metalk8s:admin]
activeIfMatches: "https://{{ grains.metalk8s.control_plane_ip }}:8443/(?!alerts).*"
"https://{{ grains.metalk8s.control_plane_ip }}:8443/alerts":
activeIfMatches: "{{ metalk8s_ui_url }}(?!alerts|docs).*"
"{{ metalk8s_ui_url }}alerts":
en: "Alerts"
fr: "Alertes"
groups: [metalk8s:admin]
subLogin:
"https://{{ grains.metalk8s.control_plane_ip }}:8443/docs":
"{{ metalk8s_ui_url }}docs":
en: "Documentation"
fr: "Documentation"

30 changes: 13 additions & 17 deletions salt/metalk8s/addons/ui/deployed/ingress.sls
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
)
%}

{%- set stripped_base_path = metalk8s_ui_config.spec.basePath.strip('/') %}
{%- set normalized_base_path = '/' ~ stripped_base_path %}

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
Expand Down Expand Up @@ -92,23 +95,16 @@ spec:
rules:
- http:
paths:
- path: {{ metalk8s_ui_config.spec.basePath }}
backend:
serviceName: metalk8s-ui
servicePort: 80
- path: /config.json
backend:
serviceName: metalk8s-ui
servicePort: 80
- path: /brand
backend:
serviceName: metalk8s-ui
servicePort: 80
- path: /static
backend:
serviceName: metalk8s-ui
servicePort: 80
- path: /manifest.json
{% for path in [
"/brand",
"/config.json",
"/manifest.json",
"/shell",
"/static",
normalized_base_path
] %}
- path: {{ path }}
backend:
serviceName: metalk8s-ui
servicePort: 80
{% endfor %}
5 changes: 4 additions & 1 deletion salt/metalk8s/addons/ui/deployed/ui.sls.in
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ include:
)
%}

{%- set stripped_base_path = metalk8s_ui_config.spec.basePath.strip('/') %}
{%- set normalized_base_path = '/' ~ stripped_base_path %}

Create metalk8s-ui deployment:
metalk8s_kubernetes.object_present:
- name: salt://{{ slspath }}/files/metalk8s-ui-deployment.yaml.j2
Expand Down Expand Up @@ -69,7 +72,7 @@ Create metalk8s-ui ConfigMap:
"url_alertmanager": "/api/alertmanager",
"url_navbar": "/shell/solution-ui-navbar.@@ShellUIVersion.js",
"url_navbar_config": "/shell/config.json",
"ui_base_path": "{{ metalk8s_ui_config.spec.basePath }}"
"ui_base_path": "{{ normalized_base_path }}"
}

Create shell-ui ConfigMap:
Expand Down
44 changes: 32 additions & 12 deletions shell-ui/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
if (process.env.NODE_ENV === "test") {
if (process.env.NODE_ENV === 'test') {
module.exports = {
"presets": ["@babel/preset-env", "@babel/preset-flow", ["@babel/preset-react", {
"runtime": "automatic"
}]],
"plugins": ["@babel/plugin-transform-modules-commonjs"]
}
presets: [
'@babel/preset-env',
'@babel/preset-flow',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
],
plugins: ['@babel/plugin-transform-modules-commonjs'],
};
} else {
module.exports = {
"presets": ["@babel/preset-env", "@babel/preset-flow", ["@babel/preset-react", {
"runtime": "automatic"
}]]
}

plugins: [
[
'babel-plugin-styled-components',
{
namespace: 'shell-ui',
},
],
gdemonet marked this conversation as resolved.
Show resolved Hide resolved
],
presets: [
'@babel/preset-env',
'@babel/preset-flow',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
],
};
}

1 change: 1 addition & 0 deletions shell-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@testing-library/user-event": "^13.0.10",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-styled-components": "^1.12.0",
"css-loader": "^5.0.1",
"flow-bin": "^0.143.1",
"html-webpack-plugin": "^4.5.1",
Expand Down
66 changes: 63 additions & 3 deletions shell-ui/src/Navbar.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { screen, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'
import userEvent from '@testing-library/user-event';
import './index';
import { waitForLoadingToFinish } from './__TESTS__/utils';
import { jest } from '@jest/globals';

const server = setupServer(
rest.get(
Expand Down Expand Up @@ -43,6 +44,25 @@ const server = setupServer(
),
);

function mockOidcReact() {
const {jest} = require('@jest/globals');

const original = jest.requireActual('oidc-react');
return {
...original, //Pass down all the exported objects
useAuth: () => ({
userData: {
profile: {
groups: ['group1'],
email: 'test@test.invalid',
name: 'user',
},
},
})
}
}
jest.mock('oidc-react', () => mockOidcReact());

const mockOIDCProvider = () => {
// This is a hack to workarround the following issue : MSW return lower cased content-type header,
// oidc-client is internally using XMLHttpRequest to perform queries and retrieve response header Content-Type using 'XMLHttpRequest.prototype.getResponseHeader'.
Expand All @@ -62,6 +82,9 @@ describe('navbar', () => {
jest.useFakeTimers();

beforeAll(() => server.listen());
beforeEach(() => {
jest.resetModules();
});
afterEach(() => server.resetHandlers());

it('should display a loading state when resolving its configuration', () => {
Expand All @@ -87,7 +110,7 @@ describe('navbar', () => {
return res(ctx.status(500));
}),
);

render(
<solutions-navbar
oidc-provider-url="https://mocked.ingress/oidc"
Expand All @@ -109,6 +132,7 @@ describe('navbar', () => {
it('should display expected selected menu when it matches by exact loaction (default behavior)', async () => {
//S
mockOIDCProvider();

//E
render(
<solutions-navbar
Expand Down Expand Up @@ -138,6 +162,7 @@ describe('navbar', () => {
it('should display expected selected menu when it matches by regex', async () => {
//S
mockOIDCProvider();

//E
render(
<solutions-navbar
Expand Down Expand Up @@ -171,6 +196,7 @@ describe('navbar', () => {
it('should set the language of the navbar', async () => {
//S
mockOIDCProvider();

//E
render(
<solutions-navbar
Expand Down Expand Up @@ -208,7 +234,7 @@ describe('navbar', () => {
});
expect(platformEntry).toBeInTheDocument();
expect(localStorage.setItem).toBeCalledWith('lang', 'fr');

//C
userEvent.click(screen.getByText('en'));
});
Expand All @@ -224,6 +250,7 @@ describe('navbar', () => {

mockOIDCProvider();


render(
<solutions-navbar
oidc-provider-url="https://mocked.ingress/oidc"
Expand Down Expand Up @@ -252,6 +279,7 @@ describe('navbar', () => {

mockOIDCProvider();


render(
<solutions-navbar
oidc-provider-url="https://mocked.ingress/oidc"
Expand Down Expand Up @@ -279,5 +307,37 @@ describe('navbar', () => {
expect(screen.queryByText(/Test/i)).not.toBeInTheDocument();
});

it('should display a restrained menu when an user is authorized', async () => {
//S

mockOIDCProvider();

render(
<solutions-navbar
oidc-provider-url="https://mocked.ingress/oidc"
client-id="metalk8s-ui"
response-type="id_token"
redirect-url="http://localhost:8082"
scopes="openid profile email groups offline_access audience:server:client_id:oidc-auth-client"
options={JSON.stringify({
main: {
'http://localhost:8082/': { en: 'Platform', fr: 'Plateforme' },
'http://localhost:8082/test': {
en: 'Test',
fr: 'Test',
groups: ['group1', 'group2'],
},
},
subLogin: {},
})}
/>,
);
//E
await waitForLoadingToFinish();
//V
expect(screen.queryByText(/Platform/i)).toBeInTheDocument();
expect(screen.queryByText(/Test/i)).toBeInTheDocument();
});

afterAll(() => server.close());
});
2 changes: 1 addition & 1 deletion shell-ui/src/auth/permissionUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const isEntryAccessibleByTheUser = (
userGroups: string[],
): boolean => {
return (
pathDescription.groups?.every((group) => userGroups.includes(group)) ??
pathDescription.groups?.some((group) => userGroups.includes(group)) ??
gdemonet marked this conversation as resolved.
Show resolved Hide resolved
true
);
};
Expand Down