From 47d51dcb1dfe684e01efb950b5df9c1d83e05ac5 Mon Sep 17 00:00:00 2001 From: nmirasch Date: Thu, 1 Oct 2020 09:47:06 +0200 Subject: [PATCH] KOGITO-3291: Implement security at Quarkus level for Management Console (#468) * KOGITO-3291: Implement security at Quarkus level for Management Console * KOGITO-3291: updated kogito-realm.json to map the user groups and remove comments * KOGITO-3291: Removed keycloak integration from react * KOGITO-3291: updated security commons testing --- config/kogito-realm.json | 16 +++ management-console/README.md | 2 +- management-console/pom.xml | 4 + .../java/org/kie/kogito/mgmt/VertxRouter.java | 19 +-- .../src/main/resources/application.properties | 19 ++- pom.xml | 1 + security-commons/pom.xml | 127 ++++++++++++++++ .../org/kie/kogito/security/UserResource.java | 82 +++++++++++ .../src/main/resources/META-INF/beans.xml | 0 .../src/main/resources/application.properties | 17 +++ .../KeycloakSecurityCommonsServiceIT.java | 78 ++++++++++ .../kie/kogito/security/UserResourceTest.java | 62 ++++++++ .../src/test/resources/application.properties | 21 +++ ui-packages/package.json | 1 - .../PageToolbar/tests/PageToolbar.test.tsx | 14 +- .../tests/KogitoPageLayout.test.tsx | 4 + .../common/src/utils/KeycloakClient.ts | 136 +++++++++--------- .../src/utils/tests/KeycloakClient.test.ts | 98 ++++++------- .../packages/management-console/package.json | 3 - .../management-console/src/index.html | 3 - .../management-console/webpack.common.js | 4 - .../packages/task-console/package.json | 3 - .../src/tests/indexKeycloak.test.tsx | 61 -------- .../packages/task-console/webpack.common.js | 4 - ui-packages/yarn.lock | 8 -- 25 files changed, 557 insertions(+), 230 deletions(-) create mode 100644 security-commons/pom.xml create mode 100644 security-commons/src/main/java/org/kie/kogito/security/UserResource.java create mode 100644 security-commons/src/main/resources/META-INF/beans.xml create mode 100644 security-commons/src/main/resources/application.properties create mode 100644 security-commons/src/test/java/org/kie/kogito/security/KeycloakSecurityCommonsServiceIT.java create mode 100644 security-commons/src/test/java/org/kie/kogito/security/UserResourceTest.java create mode 100644 security-commons/src/test/resources/application.properties delete mode 100644 ui-packages/packages/task-console/src/tests/indexKeycloak.test.tsx diff --git a/config/kogito-realm.json b/config/kogito-realm.json index d1b9c05174..5e2b2761e2 100644 --- a/config/kogito-realm.json +++ b/config/kogito-realm.json @@ -843,6 +843,22 @@ "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ], "defaultClientScopes": [ "web-origins", "role_list", diff --git a/management-console/README.md b/management-console/README.md index 677cc990a2..2fa22e5570 100644 --- a/management-console/README.md +++ b/management-console/README.md @@ -255,6 +255,6 @@ Start the management console at port 8380, (the keycloak client 'kogito-console- and enabling auth: ``` -mvn clean compile quarkus:dev -Dquarkus.http.port=8380 -Dkogito.auth.enabled=true +mvn clean compile quarkus:dev -Dquarkus.http.port=8380 -Dquarkus.profile=keycloak ``` diff --git a/management-console/pom.xml b/management-console/pom.xml index a1be4f3416..00b6f4e13b 100644 --- a/management-console/pom.xml +++ b/management-console/pom.xml @@ -35,6 +35,10 @@ + + org.kie.kogito + security-commons + io.quarkus quarkus-vertx-web diff --git a/management-console/src/main/java/org/kie/kogito/mgmt/VertxRouter.java b/management-console/src/main/java/org/kie/kogito/mgmt/VertxRouter.java index f28d895df9..0050a672f4 100644 --- a/management-console/src/main/java/org/kie/kogito/mgmt/VertxRouter.java +++ b/management-console/src/main/java/org/kie/kogito/mgmt/VertxRouter.java @@ -42,21 +42,9 @@ public class VertxRouter { String dataIndexHttpURL; @Inject - @ConfigProperty(name = "kogito.auth.enabled", defaultValue = "false") + @ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "false") String authEnabled; - @Inject - @ConfigProperty(name = "kogito.auth.keycloak.realm", defaultValue = "kogito") - String authKeycloakRealm; - - @Inject - @ConfigProperty(name = "kogito.auth.keycloak.url", defaultValue = "http://localhost:8280") - String authKeycloakUrl; - - @Inject - @ConfigProperty(name = "kogito.auth.keycloak.client.id", defaultValue = "kogito-console-quarkus") - String authKeycloakClientId; - @Inject Vertx vertx; @@ -68,10 +56,7 @@ public void init() { .readFileBlocking("META-INF/resources/index.html") .toString(UTF_8) .replace("__DATA_INDEX_ENDPOINT__", "\"" + dataIndexHttpURL + "/graphql\"") - .replace("__KOGITO_AUTH_ENABLED__", authEnabled) - .replace("__KOGITO_AUTH_KEYCLOAK_REALM__", "\"" + authKeycloakRealm + "\"") - .replace("__KOGITO_AUTH_KEYCLOAK_URL__", "\"" + authKeycloakUrl + "\"") - .replace("__KOGITO_AUTH_KEYCLOAK_CLIENT_ID__", "\"" + authKeycloakClientId + "\""); + .replace("__KOGITO_AUTH_ENABLED__", authEnabled); } void setupRouter(@Observes Router router) { diff --git a/management-console/src/main/resources/application.properties b/management-console/src/main/resources/application.properties index aa8d0000a7..70b1f9f43b 100644 --- a/management-console/src/main/resources/application.properties +++ b/management-console/src/main/resources/application.properties @@ -1 +1,18 @@ -quarkus.http.cors=true \ No newline at end of file +quarkus.http.cors=true + +quarkus.oidc.enabled=true +quarkus.oidc.tenant-enabled=false +quarkus.http.auth.permission.roles1.paths=/* +quarkus.http.auth.permission.roles1.policy=permit + +#enabled with the profile: 'keycloak' (-Dquarkus.profile=keycloak) +%keycloak.quarkus.oidc.enabled=true +%keycloak.quarkus.oidc.tenant-enabled=true +%keycloak.quarkus.oidc.auth-server-url=http://localhost:8280/auth/realms/kogito +%keycloak.quarkus.oidc.client-id=kogito-console-quarkus +%keycloak.quarkus.oidc.credentials.secret=secret +%keycloak.quarkus.oidc.application-type=web-app +%keycloak.quarkus.oidc.logout.path=/logout +%keycloak.quarkus.oidc.logout.post-logout-path=/ +%keycloak.quarkus.http.auth.permission.roles1.paths=/* +%keycloak.quarkus.http.auth.permission.roles1.policy=authenticated \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9af9036fa7..b7756aea6d 100644 --- a/pom.xml +++ b/pom.xml @@ -97,6 +97,7 @@ jobs-service data-index ui-packages + security-commons management-console trusty-ui task-console diff --git a/security-commons/pom.xml b/security-commons/pom.xml new file mode 100644 index 0000000000..4b5acd2143 --- /dev/null +++ b/security-commons/pom.xml @@ -0,0 +1,127 @@ + + + + kogito-apps + org.kie.kogito + 1.0.0-SNAPSHOT + + 4.0.0 + + Kogito :: Security Commons + security-commons + + + + + org.kie.kogito + kogito-bom + ${project.version} + pom + import + + + io.quarkus + quarkus-bom + ${version.io.quarkus} + pom + import + + + + + + io.quarkus + quarkus-resteasy-jsonb + + + io.quarkus + quarkus-oidc + + + + + org.kie.kogito + kogito-test-utils + test + + + org.keycloak + keycloak-core + test + + + io.quarkus + quarkus-junit5 + test + + + org.mockito + mockito-junit-jupiter + test + + + net.javacrumbs.json-unit + json-unit-assertj + test + + + io.rest-assured + rest-assured + test + + + + + + io.quarkus + quarkus-maven-plugin + + true + + + + + build + + + + + + maven-failsafe-plugin + + + org.jboss.logmanager.LogManager + ${container.image.keycloak} + + + + + default + + integration-test + + + + verify + + verify + + + + + + maven-surefire-plugin + + + + test + + + + + + + + \ No newline at end of file diff --git a/security-commons/src/main/java/org/kie/kogito/security/UserResource.java b/security-commons/src/main/java/org/kie/kogito/security/UserResource.java new file mode 100644 index 0000000000..0d0c411a8c --- /dev/null +++ b/security-commons/src/main/java/org/kie/kogito/security/UserResource.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.kie.kogito.security; + +import java.util.Collections; +import java.util.Set; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.credential.TokenCredential; +import io.quarkus.security.identity.SecurityIdentity; +import org.jboss.resteasy.annotations.cache.NoCache; + +@Path(UserResource.USER_PATH) +@Authenticated +public class UserResource { + + public static final String USER_PATH = "/api/user"; + + @Inject + SecurityIdentity identity; + + @GET + @Path("/me") + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public User me() { + return new User(identity); + } + + protected void setSecurityIdentity(SecurityIdentity securityIdentity) { + this.identity = securityIdentity; + } + + public static class User { + + private String userName = "Anonymous"; + private Set roles = Collections.emptySet(); + private String token = ""; + + User(SecurityIdentity identity) { + if (identity != null && + identity.getPrincipal() != null && + identity.getCredential(TokenCredential.class) != null) { + this.userName = identity.getPrincipal().getName(); + this.roles = identity.getRoles(); + this.token = identity.getCredential(TokenCredential.class).getToken(); + } + } + + public String getUserName() { + return userName; + } + + public Set getRoles() { + return roles; + } + + public String getToken() { + return token; + } + } +} + diff --git a/security-commons/src/main/resources/META-INF/beans.xml b/security-commons/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/security-commons/src/main/resources/application.properties b/security-commons/src/main/resources/application.properties new file mode 100644 index 0000000000..c286114364 --- /dev/null +++ b/security-commons/src/main/resources/application.properties @@ -0,0 +1,17 @@ +quarkus.http.cors=true + +quarkus.oidc.enabled=true +quarkus.oidc.tenant-enabled=false +quarkus.http.auth.permission.roles1.paths=/* +quarkus.http.auth.permission.roles1.policy=permit + +#enabled with the profile: 'keycloak' (-Dquarkus.profile=keycloak) +%keycloak.quarkus.oidc.enabled=true +%keycloak.quarkus.oidc.tenant-enabled=true +%keycloak.quarkus.oidc.auth-server-url=http://localhost:8280/auth/realms/kogito +%keycloak.quarkus.oidc.client-id=kogito-console-quarkus +%keycloak.quarkus.oidc.credentials.secret=secret +%keycloak.quarkus.oidc.application-type=web-app +%keycloak.quarkus.oidc.logout.path=/logout +%keycloak.quarkus.oidc.logout.post-logout-path=/api/user/me + diff --git a/security-commons/src/test/java/org/kie/kogito/security/KeycloakSecurityCommonsServiceIT.java b/security-commons/src/test/java/org/kie/kogito/security/KeycloakSecurityCommonsServiceIT.java new file mode 100644 index 0000000000..cdcc490bd0 --- /dev/null +++ b/security-commons/src/test/java/org/kie/kogito/security/KeycloakSecurityCommonsServiceIT.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kie.kogito.security; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; + +import org.keycloak.representations.AccessTokenResponse; +import org.kie.kogito.testcontainers.KogitoKeycloakContainer; + +import org.kie.kogito.testcontainers.quarkus.KeycloakQuarkusTestResource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasSize; + +@QuarkusTest +@QuarkusTestResource(KeycloakQuarkusTestResource.class) +class KeycloakSecurityCommonsServiceIT { + + public static final int OK_CODE = 200; + public static final int FORBIDDEN_CODE = 401; + + @ConfigProperty(name = KeycloakQuarkusTestResource.KOGITO_KEYCLOAK_PROPERTY) + String keycloakURL; + + @Test + void meTest() throws Exception { + given().auth().oauth2(getAccessToken("jdoe")) + .when() + .get(UserResource.USER_PATH + "/me") + .then() + .statusCode(OK_CODE) + .body("userName", is("jdoe")) + .body("roles", hasSize(2)); + + given().auth().oauth2(getAccessToken("alice")) + .when() + .get(UserResource.USER_PATH + "/me") + .then() + .statusCode(OK_CODE) + .body("userName", is("alice")) + .body("roles", hasSize(1)); + + given().when() + .get(UserResource.USER_PATH + "/me") + .then() + .statusCode(FORBIDDEN_CODE); + } + + private String getAccessToken(String userName) { + return given() + .param("grant_type", "password") + .param("username", userName) + .param("password", userName) + .param("client_id", KogitoKeycloakContainer.CLIENT_ID) + .param("client_secret", KogitoKeycloakContainer.CLIENT_SECRET) + .when() + .post(keycloakURL + "/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } +} \ No newline at end of file diff --git a/security-commons/src/test/java/org/kie/kogito/security/UserResourceTest.java b/security-commons/src/test/java/org/kie/kogito/security/UserResourceTest.java new file mode 100644 index 0000000000..1e1df9bb80 --- /dev/null +++ b/security-commons/src/test/java/org/kie/kogito/security/UserResourceTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kie.kogito.security; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import io.quarkus.security.credential.Credential; +import io.quarkus.security.credential.TokenCredential; +import io.quarkus.security.identity.SecurityIdentity; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static java.util.Arrays.asList; + +class UserResourceTest { + + @Test + void meTest() { + UserResource userResourceTest = new UserResource(); + + String userName= "testName"; + String testToken= "testToken"; + Set roles = new HashSet(); + roles.add("role1"); + Principal mockPrincipal = mock(Principal.class); + TokenCredential mockCredential = mock(TokenCredential.class); + SecurityIdentity securityIdentity = mock(SecurityIdentity.class); + userResourceTest.setSecurityIdentity(securityIdentity); + + when(mockPrincipal.getName()).thenReturn(userName); + when(securityIdentity.getPrincipal()).thenReturn(mockPrincipal); + when(securityIdentity.getRoles()).thenReturn(roles); + when(securityIdentity.getCredential(TokenCredential.class)).thenReturn(mockCredential); + when(mockCredential.getToken()).thenReturn(testToken); + + UserResource.User u = userResourceTest.me(); + assertEquals(userName, u.getUserName()); + assertEquals(roles, u.getRoles()); + assertEquals(testToken, u.getToken()); + } + +} \ No newline at end of file diff --git a/security-commons/src/test/resources/application.properties b/security-commons/src/test/resources/application.properties new file mode 100644 index 0000000000..11b370cda5 --- /dev/null +++ b/security-commons/src/test/resources/application.properties @@ -0,0 +1,21 @@ +# +# Copyright 2020 Red Hat, Inc. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Keycloak oidc +quarkus.oidc.enabled=true +quarkus.oidc.tenant-enabled=true +quarkus.oidc.client-id=kogito-app +quarkus.oidc.credentials.secret=secret \ No newline at end of file diff --git a/ui-packages/package.json b/ui-packages/package.json index 33f8d0fd92..ac2941b4d0 100644 --- a/ui-packages/package.json +++ b/ui-packages/package.json @@ -38,7 +38,6 @@ "camel-case": "^3.0.0", "graphql": "^14.6.0", "jsonpath": "^1.0.2", - "keycloak-js": "^8.0.0", "lodash": "^4.17.15", "lower-case": "^1.1.4", "no-case": "^2.3.2", diff --git a/ui-packages/packages/common/src/components/Molecules/PageToolbar/tests/PageToolbar.test.tsx b/ui-packages/packages/common/src/components/Molecules/PageToolbar/tests/PageToolbar.test.tsx index 3a9b3fdea3..478fd95195 100644 --- a/ui-packages/packages/common/src/components/Molecules/PageToolbar/tests/PageToolbar.test.tsx +++ b/ui-packages/packages/common/src/components/Molecules/PageToolbar/tests/PageToolbar.test.tsx @@ -6,6 +6,7 @@ import * as Keycloak from '../../../../utils/KeycloakClient'; describe('PageToolbar component tests', () => { const getUserName = jest.spyOn(Keycloak, 'getUserName'); const currentEnv = process.env; + const isAuthEnabledMock = jest.spyOn(Keycloak, 'isAuthEnabled'); getUserName.mockReturnValue('Ajay'); afterEach(() => { @@ -13,19 +14,20 @@ describe('PageToolbar component tests', () => { }); it('snapshot testing with kogito_auth_enabled param', () => { - process.env = { KOGITO_AUTH_ENABLED: 'true' }; + isAuthEnabledMock.mockReturnValue(true); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it('snapshot testing with kogito-auth_enabled as null', () => { - process.env = { KOGITO_AUTH_ENABLED: null }; + isAuthEnabledMock.mockReturnValue(undefined); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it('onDropdownSelect test', () => { - process.env = { KOGITO_AUTH_ENABLED: 'true' }; + isAuthEnabledMock.mockReturnValue(true); + const wrapper = shallow(); const event = { target: {} @@ -35,7 +37,8 @@ describe('PageToolbar component tests', () => { /* tslint:disable */ it('isDropDownToggle test', () => { - process.env = { KOGITO_AUTH_ENABLED: 'true' }; + isAuthEnabledMock.mockReturnValue(true); + const wrapper = shallow(); wrapper .find('Dropdown') @@ -44,7 +47,8 @@ describe('PageToolbar component tests', () => { }); it('handleModalToggleProp test', () => { - process.env = { KOGITO_AUTH_ENABLED: 'true' }; + isAuthEnabledMock.mockReturnValue(true); + const wrapper = shallow(); expect(wrapper.find('AboutModalBox').props()['isOpenProp']).toBeFalsy(); wrapper diff --git a/ui-packages/packages/common/src/components/Templates/KogitoPageLayout/tests/KogitoPageLayout.test.tsx b/ui-packages/packages/common/src/components/Templates/KogitoPageLayout/tests/KogitoPageLayout.test.tsx index 79dff455f8..00d496545c 100644 --- a/ui-packages/packages/common/src/components/Templates/KogitoPageLayout/tests/KogitoPageLayout.test.tsx +++ b/ui-packages/packages/common/src/components/Templates/KogitoPageLayout/tests/KogitoPageLayout.test.tsx @@ -2,6 +2,8 @@ import React from 'react'; import KogitoPageLayout from '../KogitoPageLayout'; import { getWrapper } from '../../../../utils/OuiaUtils'; import { act } from 'react-dom/test-utils'; +import * as Keycloak from "../../../../utils/KeycloakClient"; + const props = { children: children rendered, @@ -12,6 +14,8 @@ const props = { }; describe('KogitoPageLayout component tests', () => { + const isAuthEnabledMock = jest.spyOn(Keycloak, 'isAuthEnabled'); + isAuthEnabledMock.mockReturnValue(false); it('snapshot tests', () => { const wrapper = getWrapper( , diff --git a/ui-packages/packages/common/src/utils/KeycloakClient.ts b/ui-packages/packages/common/src/utils/KeycloakClient.ts index 50d7e36729..ea1b5a92b3 100644 --- a/ui-packages/packages/common/src/utils/KeycloakClient.ts +++ b/ui-packages/packages/common/src/utils/KeycloakClient.ts @@ -1,71 +1,97 @@ import axios from 'axios'; -import Keycloak, { KeycloakInstance } from 'keycloak-js'; +export interface UserContext { + userName: string; + roles: string[]; + token: string; +} export const isAuthEnabled = (): boolean => { // @ts-ignore - return window.KOGITO_AUTH_ENABLED || process.env.KOGITO_AUTH_ENABLED; + return window.KOGITO_AUTH_ENABLED; }; -export const createKeycloakInstance = (): KeycloakInstance => { - const authKeycloakRealm = - // @ts-ignore - window.KOGITO_AUTH_KEYCLOAK_REALM || process.env.KOGITO_KEYCLOAK_REALM; - const authKeycloakUrl = - // @ts-ignore - window.KOGITO_AUTH_KEYCLOAK_URL || process.env.KOGITO_KEYCLOAK_URL; - const authKeycloakClientId = - // @ts-ignore - window.KOGITO_AUTH_KEYCLOAK_CLIENT_ID || - process.env.KOGITO_KEYCLOAK_CLIENT_ID; - return Keycloak({ - realm: authKeycloakRealm, - url: authKeycloakUrl + '/auth', - clientId: authKeycloakClientId - }); -}; - -const keycloakInstance = createKeycloakInstance(); +let currentSecurityContext; +export const getLoadedSecurityContext = (): UserContext => { + if (!currentSecurityContext) { + return { + userName: 'Anonymous', + roles: [], + token: '' + }; + } + return currentSecurityContext; +} -export const getKeycloakInstance = (): KeycloakInstance => { - return keycloakInstance; -}; -export const getUserName = (): string => { - let username = 'Anonymous'; +export const loadSecurityContext = async ( + onloadSuccess: () => void +) => { if (isAuthEnabled()) { - // @ts-ignore - username = getKeycloakInstance().tokenParsed.preferred_username; + try { + const response = await axios.get(`/api/user/me`, { + headers: {'Access-Control-Allow-Origin': '*'} + }); + currentSecurityContext = response.data; + onloadSuccess(); + } catch (error) { + currentSecurityContext = { + userName: error.message, + roles: [], + token: '' + }; + } + } else { + currentSecurityContext = { + userName: 'Anonymous', + roles: [], + token: '' + }; + onloadSuccess(); } - return username; +}; + +export const getUserName = (): string => { + return getLoadedSecurityContext().userName; }; export const getToken = (): string => { - // @ts-ignore - return getKeycloakInstance().token; + return getLoadedSecurityContext().token; +}; + +export const getRoles = (): string[] => { + return getLoadedSecurityContext().roles; }; export const appRenderWithAxiosInterceptorConfig = ( appRender: () => void ): void => { + loadSecurityContext(() => { + appRender(); + }); if (isAuthEnabled()) { - getKeycloakInstance() - .init({ onLoad: 'login-required' }) - .success(authenticated => { - if (authenticated) { - appRender(); + axios.interceptors.response.use(response => response, + (error) => { + if (error.response.status === 401) { + loadSecurityContext(() => { + /* tslint:disable:no-string-literal */ + axios.defaults.headers.common['Authorization'] = + 'Bearer ' + getToken(); + /* tslint:enable:no-string-literal */ + return axios(error.config); + }); } + return Promise.reject(error); }); - axios.interceptors.request.use( config => { - const token = getToken(); - if (token !== undefined) { + if (currentSecurityContext) { + const t = getToken(); /* tslint:disable:no-string-literal */ - config.headers['Authorization'] = 'Bearer ' + token; + config.headers['Authorization'] = 'Bearer ' + t; /* tslint:enable:no-string-literal */ + return config; } - return config; }, error => { /* tslint:disable:no-floating-promises */ @@ -73,32 +99,10 @@ export const appRenderWithAxiosInterceptorConfig = ( /* tslint:enable:no-floating-promises */ } ); - - axios.interceptors.response.use( - response => { - return response; - }, - error => { - const originalRequest = error.config; - if (error.response.status === 401) { - getKeycloakInstance() - .updateToken(5) - .success(() => { - /* tslint:disable:no-string-literal */ - axios.defaults.headers.common['Authorization'] = - 'Bearer ' + getToken(); - /* tslint:enable:no-string-literal */ - return axios(originalRequest); - }); - } - return Promise.reject(error); - } - ); - } else { - appRender(); } -}; +} export const handleLogout = (): void => { - getKeycloakInstance().logout(); + currentSecurityContext = undefined; + window.location.replace(`/logout`); }; diff --git a/ui-packages/packages/common/src/utils/tests/KeycloakClient.test.ts b/ui-packages/packages/common/src/utils/tests/KeycloakClient.test.ts index 21de9edbb8..552297369f 100644 --- a/ui-packages/packages/common/src/utils/tests/KeycloakClient.test.ts +++ b/ui-packages/packages/common/src/utils/tests/KeycloakClient.test.ts @@ -1,62 +1,55 @@ import * as KeycloakClient from '../KeycloakClient'; -import Keycloak from 'keycloak-js'; import axios from 'axios'; -jest.mock('keycloak-js'); -const mockedKeycloak = Keycloak as jest.Mocked; -const MockKeycloakInstance = jest.fn(() => ({ - init: KeycloakPromiseMock, - logout: jest.fn(), - tokenParsed: { - preferred_username: 'jdoe' - }, - token: 'testToken', - clearToken: jest.fn(), - updateToken: jest.fn() -})); - -const KeycloakPromiseMock = jest.fn(() => ({ - success: jest.fn() -})); describe('Tests for keycloak client functions', () => { - const instance = new MockKeycloakInstance(); - const getKeycloakInstanceMock = jest.spyOn( - KeycloakClient, - 'getKeycloakInstance' - ); - // @ts-ignore - getKeycloakInstanceMock.mockReturnValue(instance); + const mockUserContext = { + userName: 'jdoe', + roles: ['role1'], + token: 'testToken' + }; it('Test isAuthEnabled with processEnv function', () => { - process.env.KOGITO_AUTH_ENABLED = 'true'; - expect(KeycloakClient.isAuthEnabled()).toEqual('true'); - - process.env.KOGITO_AUTH_ENABLED = 'false'; - expect(KeycloakClient.isAuthEnabled()).toEqual('false'); - // @ts-ignore window.KOGITO_AUTH_ENABLED = 'true'; expect(KeycloakClient.isAuthEnabled()).toEqual('true'); }); - it('Test getUserName function', () => { + it('Test getLoadedSecurityContext function', () => { const isAuthEnabledMock = jest.spyOn(KeycloakClient, 'isAuthEnabled'); + isAuthEnabledMock.mockReturnValue(true); - expect(KeycloakClient.getUserName()).toEqual('jdoe'); + expect(KeycloakClient.getLoadedSecurityContext().userName).toEqual('Anonymous'); + KeycloakClient.loadSecurityContext(()=> { + expect(KeycloakClient.getLoadedSecurityContext().userName).toEqual('jdoe'); + }) + }); - isAuthEnabledMock.mockReturnValue(false); - expect(KeycloakClient.getUserName()).toEqual('Anonymous'); + + it('Test getUserName function', () => { + const getLoadedSecurityContextMock = jest.spyOn(KeycloakClient, 'getLoadedSecurityContext'); + getLoadedSecurityContextMock.mockReturnValue(mockUserContext); + + expect(KeycloakClient.getUserName()).toEqual('jdoe'); }); it('Test getToken function', () => { + const getLoadedSecurityContextMock = jest.spyOn(KeycloakClient, 'getLoadedSecurityContext'); + getLoadedSecurityContextMock.mockReturnValue(mockUserContext); + expect(KeycloakClient.getToken()).toEqual('testToken'); }); + it('Test getRoles function', () => { + const getLoadedSecurityContextMock = jest.spyOn(KeycloakClient, 'getLoadedSecurityContext'); + getLoadedSecurityContextMock.mockReturnValue(mockUserContext); + + expect(KeycloakClient.getRoles()).toEqual(['role1']); + }); + it('Test handleLogout function', () => { KeycloakClient.handleLogout(); - expect(instance.logout.mock.calls.length).toBe(1); }); it('Test appRenderWithoutAuthenticationEnabled function', () => { @@ -67,28 +60,14 @@ describe('Tests for keycloak client functions', () => { expect(renderMock).toBeCalled(); }); - it('Test appRenderWithAuthenticationEnabled function', () => { + it('Test appRenderWithQuarkusAuthenticationEnabled function', () => { const isAuthEnabledMock = jest.spyOn(KeycloakClient, 'isAuthEnabled'); - const getTokenMock = jest.spyOn(KeycloakClient, 'getToken'); const renderMock = jest.fn(); - const mockInitPromise = new KeycloakPromiseMock(); - instance.init.mockReturnValue(mockInitPromise); - const mockUpdateTokenPromise = new KeycloakPromiseMock(); - instance.updateToken.mockReturnValue(mockUpdateTokenPromise); - - // @ts-ignore - mockedKeycloak.mockReturnValueOnce(instance); + const getTokenMock = jest.spyOn(KeycloakClient, 'getToken'); - isAuthEnabledMock.mockReturnValue(true); getTokenMock.mockReturnValue('testToken'); + isAuthEnabledMock.mockReturnValue(true); KeycloakClient.appRenderWithAxiosInterceptorConfig(renderMock); - expect(mockInitPromise.success.mock.calls.length).toBe(1); - - const successCallback = mockInitPromise.success.mock.calls[0][0]; - successCallback(false); - expect(renderMock).not.toBeCalled(); - successCallback(true); - expect(renderMock).toBeCalled(); expect( // @ts-ignore @@ -114,4 +93,17 @@ describe('Tests for keycloak client functions', () => { }); expect(getTokenMock.mock.calls.length).toBe(1); }); -}); + + it('Test loadUserContext function', () => { + const isAuthEnabledMock = jest.spyOn(KeycloakClient, 'isAuthEnabled'); + isAuthEnabledMock.mockReturnValue(true); + + const getMock = jest.spyOn(axios, 'get'); + getMock.mockResolvedValue({data: mockUserContext}); + + KeycloakClient.loadSecurityContext(() => { + const axiosCallback = getMock.mock.calls[0]; + expect(axiosCallback[0]).toBe("/api/user/me"); + }); + }); +}); \ No newline at end of file diff --git a/ui-packages/packages/management-console/package.json b/ui-packages/packages/management-console/package.json index 2f57aa850f..47c37bebe1 100755 --- a/ui-packages/packages/management-console/package.json +++ b/ui-packages/packages/management-console/package.json @@ -12,7 +12,6 @@ "precommit": "lint-staged", "build:prod": "yarn run lint && webpack --config webpack.prod.js", "start": "webpack-dev-server --hot --color --progress --info=true --config webpack.dev.js", - "start-auth": "webpack-dev-server --hot --color --progress --info=true --config webpack.dev.js --define process.env.KOGITO_AUTH_ENABLED=true", "test:report": "yarn test --ci --reporters=jest-junit", "test": "jest --runInBand --ci --reporters=default --reporters=jest-junit", "update-snapshot": "jest --updateSnapshot", @@ -20,9 +19,7 @@ "dev:restServer": "nodemon ./server/restServer.js", "dev:server": "nodemon ./server/app.js", "dev": "concurrently 'yarn start' 'yarn run dev:server'", - "dev-auth": "concurrently 'yarn start-auth' 'yarn run dev:server'", "dev-remote-dataindex": "yarn start --define process.env.KOGITO_DATAINDEX_HTTP_URL='\"http://localhost:8180/graphql\"'", - "dev-remote-dataindex-auth": "yarn start-auth --define process.env.KOGITO_DATAINDEX_HTTP_URL='\"http://localhost:8180/graphql\"'", "lint": "tslint -c ./tslint.json --project . './src/**/*.ts{,x}'", "format": "prettier --config ../../.prettierrc --check --write './src/**/*.{tsx,ts,js}'", "build:bundle-profile": "webpack --config webpack.prod.js --profile --json > stats.json", diff --git a/ui-packages/packages/management-console/src/index.html b/ui-packages/packages/management-console/src/index.html index 3fcf6141a1..16070341a8 100755 --- a/ui-packages/packages/management-console/src/index.html +++ b/ui-packages/packages/management-console/src/index.html @@ -10,9 +10,6 @@ diff --git a/ui-packages/packages/management-console/webpack.common.js b/ui-packages/packages/management-console/webpack.common.js index eb6b2314fe..fd8d284a58 100755 --- a/ui-packages/packages/management-console/webpack.common.js +++ b/ui-packages/packages/management-console/webpack.common.js @@ -14,10 +14,6 @@ module.exports = { favicon: 'src/favicon.ico' }), new webpack.EnvironmentPlugin({ - KOGITO_AUTH_ENABLED: false, - KOGITO_KEYCLOAK_REALM: 'kogito', - KOGITO_KEYCLOAK_URL: 'http://localhost:8280', - KOGITO_KEYCLOAK_CLIENT_ID: 'kogito-console-react', KOGITO_DATAINDEX_HTTP_URL: 'http://localhost:4000/graphql', KOGITO_APP_VERSION: 'DEV', KOGITO_APP_NAME: 'Management Console' diff --git a/ui-packages/packages/task-console/package.json b/ui-packages/packages/task-console/package.json index 1e77d75210..78fbc74765 100755 --- a/ui-packages/packages/task-console/package.json +++ b/ui-packages/packages/task-console/package.json @@ -12,7 +12,6 @@ "precommit": "lint-staged", "build:prod": "yarn run lint && webpack --config webpack.prod.js", "start": "webpack-dev-server --hot --color --progress --info=true --config webpack.dev.js", - "start-auth": "webpack-dev-server --hot --color --progress --info=true --config webpack.dev.js --define process.env.KOGITO_AUTH_ENABLED=true", "test:report": "yarn test --ci --reporters=jest-junit", "test": "jest --runInBand --ci --reporters=default --reporters=jest-junit", "update-snapshot": "jest --updateSnapshot", @@ -20,8 +19,6 @@ "lint": "tslint -c ./tslint.json --project . './src/**/*.ts{,x}'", "dev": "concurrently 'yarn run start' 'yarn run dev:server'", "dev-remote-dataindex": "yarn start --define process.env.KOGITO_DATAINDEX_HTTP_URL='\"http://localhost:8180/graphql\"'", - "dev-auth": "concurrently 'yarn start-auth' 'yarn run dev:server'", - "dev-remote-dataindex-auth": "yarn start-auth --define process.env.KOGITO_DATAINDEX_HTTP_URL='\"http://localhost:8180/graphql\"'", "dev:server": "nodemon ./server/app.js", "format": "prettier --config ../../.prettierrc --check --write './src/**/*.{tsx,ts,js}'", "build:bundle-profile": "webpack --config webpack.prod.js --profile --json > stats.json", diff --git a/ui-packages/packages/task-console/src/tests/indexKeycloak.test.tsx b/ui-packages/packages/task-console/src/tests/indexKeycloak.test.tsx deleted file mode 100644 index 1380020060..0000000000 --- a/ui-packages/packages/task-console/src/tests/indexKeycloak.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import Keycloak from 'keycloak-js'; -import { ApolloProvider } from 'react-apollo-hooks'; - -jest.mock('apollo-link-http'); -jest.mock('keycloak-js'); -const mockedKeycloak = Keycloak as jest.Mocked; - -const MockKeycloakInstance = jest.fn(() => ({ - init: jest.fn() -})); - -const MockKeycloakInstancePromise = jest.fn(() => ({ - success: jest.fn() -})); - -const renderMock = jest.fn(); -jest.mock('react-dom', () => ({ render: renderMock })); - -const rootDiv = document.createElement('div'); -global.document.getElementById = id => id === 'root' && rootDiv; -process.env.KOGITO_DATAINDEX_HTTP_URL = 'http://localhost:8180'; - -describe('Index test with Keycloak', () => { - it('rendering with keycloak', () => { - process.env.KOGITO_AUTH_ENABLED = 'true'; - process.env.KOGITO_KEYCLOAK_URL = 'http://localhost/keycloak'; - process.env.KOGITO_KEYCLOAK_CLIENT_ID = 'clientId'; - - const instance = new MockKeycloakInstance(); - - const mockPromise = new MockKeycloakInstancePromise(); - - instance.init.mockReturnValue(mockPromise); - - // @ts-ignore - mockedKeycloak.mockReturnValueOnce(instance); - - require('../index.tsx'); - - expect(renderMock).not.toBeCalled(); - expect(renderMock.mock.calls.length).toBe(0); - - expect(mockPromise.success.mock.calls.length).toBe(1); - - const successCallback = mockPromise.success.mock.calls[0][0]; - - successCallback(true); - - expect(renderMock).toBeCalled(); - expect(renderMock.mock.calls.length).toBe(1); - - const callArguments = renderMock.mock.calls[0]; - - const context = callArguments[0]; - - expect(context).not.toBeNull(); - expect(context).not.toBeInstanceOf(ApolloProvider); - - expect(callArguments[1]).toBe(rootDiv); - }); -}); diff --git a/ui-packages/packages/task-console/webpack.common.js b/ui-packages/packages/task-console/webpack.common.js index b41e1d626f..a0013f1917 100755 --- a/ui-packages/packages/task-console/webpack.common.js +++ b/ui-packages/packages/task-console/webpack.common.js @@ -14,10 +14,6 @@ module.exports = { favicon: 'src/favicon.ico' }), new webpack.EnvironmentPlugin({ - KOGITO_AUTH_ENABLED: false, - KOGITO_KEYCLOAK_REALM: 'kogito', - KOGITO_KEYCLOAK_URL: 'http://localhost:8280', - KOGITO_KEYCLOAK_CLIENT_ID: 'kogito-console-react', KOGITO_DATAINDEX_HTTP_URL: 'http://localhost:4000/graphql', KOGITO_APP_VERSION: 'DEV', KOGITO_APP_NAME: 'Task Console' diff --git a/ui-packages/yarn.lock b/ui-packages/yarn.lock index d3d3b56088..aaf32b07e6 100644 --- a/ui-packages/yarn.lock +++ b/ui-packages/yarn.lock @@ -11881,14 +11881,6 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -keycloak-js@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-8.0.2.tgz#2af5868aa4313c111601115e990ba317e5f14489" - integrity sha512-5Jy5rHx0oURTtj2T6LSMLiqyHzDzPr6ZnaY1fwCBlMsiMx7RVsIBeysYZ5yy29ojH9IMzNJzlZvuLKbEOCmgDQ== - dependencies: - base64-js "1.3.1" - js-sha256 "0.9.0" - keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"