Skip to content

Commit

Permalink
Merge pull request #77 from kstekovi/EAP7-1796
Browse files Browse the repository at this point in the history
EAP7-1796 - Add the ability to secure the management console with OIDC
  • Loading branch information
OndrejKotek authored Aug 8, 2023
2 parents 3680331 + 2bb5e89 commit bf28f9c
Show file tree
Hide file tree
Showing 9 changed files with 2,533 additions and 47 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Following is a table of supported environment properties that can be used when r
| :--------------- | :---------------------------------------------- | :-------------------------------------------------------------------------------------------- |
| `HAL_IMAGE` | `quay.io/halconsole/hal-development:latest` | [HAL standalone image](https://hal.github.io/documentation/get-started/#container) to be used |
| `WILDFLY_IMAGE` | `quay.io/halconsole/wildfly-development:latest` | WildFly/JBoss EAP image to be used |
| `KEYCLOAK_IMAGE` | `quay.io/keycloak/keycloak:latest` | Keycloak/RH-SSO image to be used for OIDC tests |
| `POSTGRES_IMAGE` | `docker.io/library/postgres:latest` | PostgreSQL image to be used for datasource tests |
| `MYSQL_IMAGE` | `docker.io/library/mysql:latest` | MySQL image to be used for datasource tests |
| `MARIADB_IMAGE` | `docker.io/library/mariadb:latest` | MariaDB image to be used for datasource tests |
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"clean": "npx del \"packages/testsuite/cypress/fixtures/**\" \"packages/testsuite/results\" \"mochawesome*\"",
"clean": "npx del \"packages/testsuite/cypress/fixtures/*/\" \"packages/testsuite/results\" \"mochawesome*\"",
"preformat": "npm run clean",
"format": "npx prettier --write .",
"lint": "npx eslint .",
Expand All @@ -27,6 +27,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/portscanner": "2.1.1",
"@typescript-eslint/eslint-plugin": "5.55.0",
"@typescript-eslint/parser": "5.55.0",
"axios": "1.3.4",
Expand All @@ -41,5 +42,8 @@
"testcontainers": "9.2.1",
"typedoc": "0.23.28",
"typescript": "5.0.2"
},
"dependencies": {
"portscanner": "2.1.1"
}
}
194 changes: 153 additions & 41 deletions packages/testsuite/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import axios from "axios";
import { defineConfig } from "cypress";
import { AlwaysPullPolicy, GenericContainer, StartedTestContainer, StoppedTestContainer, Wait } from "testcontainers";
import { Environment } from "testcontainers/dist/src/docker/types";
import { findAPortNotInUse } from "portscanner";

export default defineConfig({
defaultCommandTimeout: 16000,
Expand All @@ -13,15 +14,16 @@ export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
const startedContainers: Map<string, StartedTestContainer> = new Map<string, StartedTestContainer>();
const startedContainersManagementPorts: Map<string, number> = new Map<string, number>();
on("task", {
"start:wildfly:container": ({ name, configuration }) => {
"start:wildfly:container": ({ name, configuration, useNetworkHostMode }) => {
return new Promise((resolve, reject) => {
new GenericContainer(process.env.WILDFLY_IMAGE || "quay.io/halconsole/wildfly-development:latest")
let portOffset = 0;
const wildfly = new GenericContainer(
process.env.WILDFLY_IMAGE || "quay.io/halconsole/wildfly-development:latest"
)
.withPullPolicy(new AlwaysPullPolicy())
.withName(name as string)
.withNetworkMode(config.env.NETWORK_NAME as string)
.withNetworkAliases("wildfly")
.withExposedPorts(9990)
.withBindMounts([
{
source: __dirname + "/cypress/fixtures",
Expand All @@ -30,48 +32,149 @@ export default defineConfig({
},
])
.withWaitStrategy(Wait.forLogMessage(new RegExp(".*(WildFly Full.*|JBoss EAP.*)started in.*")))
.withStartupTimeout(333000)
.withCommand(["-c", configuration || "standalone-insecure.xml"] as string[])
.withStartupTimeout(333000);
if (useNetworkHostMode === true) {
console.log("host mode");
findAPortNotInUse(8080, 8180)
.then((freePort) => {
portOffset = freePort - 8080;
wildfly
.withNetworkMode("host")
.withCommand([
"-c",
configuration || "standalone-insecure.xml",
`-Djboss.socket.binding.port-offset=${portOffset.toString()}`,
] as string[]);
})
.catch((error) => {
console.log(error);
});
} else {
console.log(`default network mode, network name: ${config.env.NETWORK_NAME as string}`);
wildfly
.withNetworkMode(config.env.NETWORK_NAME as string)
.withNetworkAliases("wildfly")
.withExposedPorts(9990)
.withCommand(["-c", configuration || "standalone-insecure.xml"] as string[]);
}
wildfly
.start()
.then((wildflyContainer) => {
const managementPortWithOffset = portOffset + 9990;
startedContainers.set(name as string, wildflyContainer);
const managementApi = `http://localhost:${wildflyContainer.getMappedPort(9990)}/management`;
return axios
.post(managementApi, {
operation: "list-add",
address: ["core-service", "management", "management-interface", "http-interface"],
name: "allowed-origins",
value: `http://localhost:${config.env.HAL_CONTAINER_PORT as string}`,
})
.then(() => {
return axios.post(managementApi, {
operation: "reload",
});
})
.then(() => {
const startTime = new Date().getTime();
const interval = setInterval(() => {
if (new Date().getTime() - startTime > 10000) {
clearInterval(interval);
reject();
}
axios
.post(managementApi, {
operation: "read-attribute",
name: "server-state",
})
if (useNetworkHostMode === true) {
startedContainersManagementPorts.set(name as string, portOffset + 9990);
return wildflyContainer
.exec([
`/bin/bash`,
`-c`,
`$JBOSS_HOME/bin/jboss-cli.sh --connect --controller=localhost:${managementPortWithOffset} --command="/core-service=management/management-interface=http-interface:list-add(name=allowed-origins,value=http://localhost:${
config.env.HAL_CONTAINER_PORT as string
}"`,
])
.then((result) => {
console.log(result.output);
return wildflyContainer.exec([
`/bin/bash`,
`-c`,
`$JBOSS_HOME/bin/jboss-cli.sh --connect --controller=localhost:${managementPortWithOffset} --command="reload"`,
]);
})
.then((result) => {
console.log(result.output);
wildflyContainer
.exec([
`/bin/bash`,
`-c`,
`$JBOSS_HOME/bin/jboss-cli.sh --connect --controller=localhost:${managementPortWithOffset} --command="read-attribute server-state"`,
])
.then((response) => {
if ((response as { data: { result: string } }).data.result == "running") {
clearInterval(interval);
resolve(`http://localhost:${wildflyContainer.getMappedPort(9990)}`);
console.log(response.output);
if (response.output.includes("running")) {
resolve(`http://localhost:${managementPortWithOffset}`);
}
})
/* eslint @typescript-eslint/no-empty-function: off */
.catch(() => {});
}, 500);
});
.catch((error) => {
console.log(error);
});
});
} else {
startedContainersManagementPorts.set(name as string, wildflyContainer.getMappedPort(9990));
const managementApi = `http://localhost:${wildflyContainer.getMappedPort(9990)}/management`;
return axios
.post(managementApi, {
operation: "list-add",
address: ["core-service", "management", "management-interface", "http-interface"],
name: "allowed-origins",
value: `http://localhost:${config.env.HAL_CONTAINER_PORT as string}`,
})
.then(() => {
return axios.post(managementApi, {
operation: "reload",
});
})
.then(() => {
const startTime = new Date().getTime();
const interval = setInterval(() => {
if (new Date().getTime() - startTime > 10000) {
clearInterval(interval);
reject();
}
axios
.post(managementApi, {
operation: "read-attribute",
name: "server-state",
})
.then((response) => {
if ((response as { data: { result: string } }).data.result == "running") {
clearInterval(interval);
const wildflyServer = `http://localhost:${wildflyContainer.getMappedPort(9990)}`;
resolve(wildflyServer);
}
})
.catch((error) => {
console.log(error);
});
}, 500);
});
}
})
.catch((err) => reject(err));
.catch((err) => {
console.log(err);
reject(err);
});
});
},
"start:keycloak:container": ({ name }) => {
return findAPortNotInUse(8888, 8988).then((freePort: number) => {
const keycloak = new GenericContainer(process.env.KEYCLOAK_IMAGE || "quay.io/keycloak/keycloak:latest")
.withName(name as string)
.withNetworkMode("host")
.withWaitStrategy(Wait.forLogMessage(new RegExp(".*(Keycloak.*) started in.*")))
.withEnvironment({
KEYCLOAK_ADMIN: "admin",
KEYCLOAK_ADMIN_PASSWORD: "admin",
})
.withBindMounts([
{
source: __dirname + "/cypress/fixtures/realm-configuration.json",
target: "/opt/keycloak/data/import/realm-configuration.json",
mode: "z",
},
])
.withCommand(["start-dev", `--http-port=${freePort.toString()}`, "--import-realm"] as string[]);
return new Promise((resolve, reject) => {
keycloak
.start()
.then((keycloakContainer) => {
startedContainers.set(name as string, keycloakContainer);
resolve(`http://localhost:${freePort ?? "unknown port"}`);
})
.catch((err) => {
console.log(err);
reject(err);
});
});
});
},
"start:postgres:container": ({ name, environmentProperties }) => {
Expand Down Expand Up @@ -173,8 +276,16 @@ export default defineConfig({
"execute:in:container": ({ containerName, command }) => {
return new Promise((resolve, reject) => {
const containerToExec = startedContainers.get(containerName as string);
let managementPort = startedContainersManagementPorts.get(containerName as string);
managementPort = managementPort ?? 9990;
containerToExec
?.exec(["/bin/bash", "-c", `$JBOSS_HOME/bin/jboss-cli.sh -c --command=${command as string}`])
?.exec([
"/bin/bash",
"-c",
`$JBOSS_HOME/bin/jboss-cli.sh --connect --controller=localhost:${managementPort} --commands=${
command as string
}`,
])
.then((value) => {
console.log(value.output);
resolve(value.output);
Expand Down Expand Up @@ -204,6 +315,7 @@ export default defineConfig({
startedContainers.forEach((container, key) => {
console.log("Stopping container for test " + key);
startedContainers.delete(key);
startedContainersManagementPorts.delete(key);
promises.push(container.stop());
});
return Promise.all(promises);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
describe("TESTS: Access secured by Elytron OIDC client with RBAC", () => {
let wildfly: string;
let keycloak: string;

before(() => {
cy.startWildflyContainerSecured()
.then((result) => {
wildfly = result as string;
console.log(wildfly);
})
.then(() => {
cy.startKeycloakContainer().then((result) => {
keycloak = result as string;
// the following CLI commands secure the web-console by OIDC, with RBAC enabled
cy.executeInWildflyContainer(
`"/subsystem=elytron-oidc-client/provider=keycloak:add(provider-url=${keycloak}/realms/wildfly-infra),
/subsystem=elytron-oidc-client/secure-deployment=wildfly-management:add(provider=keycloak,client-id=wildfly-management,principal-attribute=preferred_username,bearer-only=true,ssl-required=EXTERNAL),
/subsystem=elytron-oidc-client/secure-server=wildfly-console:add(provider=keycloak,client-id=wildfly-console,public-client=true),
/core-service=management/access=authorization:write-attribute(name=provider,value=rbac),
/core-service=management/access=authorization:write-attribute(name=use-identity-roles,value=true),
reload"`
);
});
});
});

after(() => {
cy.task("stop:containers");
});

it("Logs in successfully and logs out by user with role", () => {
cy.visit(`/?connect=${wildfly}#home`);
cy.get("#username").type("userwithrole");
cy.get("#password").type("password");
cy.get("#kc-login").click();
cy.verifyUserName("userwithrole");
cy.verifyUserRole("Administrator");
cy.logoutFromWebConsole();
verifyNotLoggedIn(keycloak);
});

it("Returns 403 Forbidden for a user without role", () => {
cy.visit(`/?connect=${wildfly}#home`);
cy.get("#username").type("userwithoutrole");
cy.get("#password").type("password");
cy.get("#kc-login").click();
cy.verifyErrorMessage("Status 403 - Forbidden.");
});

function verifyNotLoggedIn(keycloak: string): void {
cy.url().should(`include`, keycloak);
cy.get("#username").should("exist");
cy.get("#password").should("exist");
cy.get("#kc-login").should("exist");
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
describe("TESTS: Access secured by Elytron OIDC client", () => {
let wildfly: string;
let keycloak: string;

before(() => {
cy.startWildflyContainerSecured()
.then((result) => {
wildfly = result as string;
})
.then(() => {
cy.startKeycloakContainer().then((result) => {
keycloak = result as string;
// the following CLI commands setup the OIDC configuration
cy.executeInWildflyContainer(
`"/subsystem=elytron-oidc-client/provider=keycloak:add(provider-url=${keycloak}/realms/wildfly-infra),
/subsystem=elytron-oidc-client/secure-deployment=wildfly-management:add(provider=keycloak,client-id=wildfly-management,principal-attribute=preferred_username,bearer-only=true,ssl-required=EXTERNAL),
/subsystem=elytron-oidc-client/secure-server=wildfly-console:add(provider=keycloak,client-id=wildfly-console,public-client=true),
reload"`
);
});
});
});

after(() => {
cy.task("stop:containers");
});

it("Logs in successfully and logs out", () => {
cy.visit(`?connect=${wildfly}#home`);
cy.get("#username").type("userwithoutrole");
cy.get("#password").type("password");
cy.get("#kc-login").click();
cy.url().should(`include`, `localhost:${Cypress.env("HAL_CONTAINER_PORT") as string}`);
cy.verifyUserName("userwithoutrole");
cy.logoutFromWebConsole();
verifyNotLoggedIn(keycloak);
});

it("Fails to log in with bad credentials", () => {
cy.visit(`?connect=${wildfly}#home`);
cy.get("#username").type("userwithoutrole");
cy.get("#password").type("wrongPassword");
cy.get("#kc-login").click();
verifyNotLoggedIn(keycloak);
});

function verifyNotLoggedIn(keycloak: string): void {
cy.url().should(`include`, keycloak);
cy.get("#username").should("exist");
cy.get("#password").should("exist");
cy.get("#kc-login").should("exist");
}
});
Loading

0 comments on commit bf28f9c

Please sign in to comment.