diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/internal/services/UserService.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/internal/services/UserService.java index d4f138f0a..3b6f98ae4 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/internal/services/UserService.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/internal/services/UserService.java @@ -24,6 +24,12 @@ @Produces(MediaType.APPLICATION_JSON) @Tag(name = "user", description = "Manage users") public interface UserService { + + @GET + @Path("roles") + @Blocking + List getRoles(); + @GET @Path("search") @Blocking diff --git a/horreum-backend/pom.xml b/horreum-backend/pom.xml index 846445a94..7d09dff38 100644 --- a/horreum-backend/pom.xml +++ b/horreum-backend/pom.xml @@ -148,6 +148,10 @@ io.quarkus quarkus-keycloak-admin-client + + io.quarkus + quarkus-elytron-security-properties-file + io.quarkus quarkus-elasticsearch-rest-client diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/UserServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/UserServiceImpl.java index b4932d6bc..39f58110d 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/UserServiceImpl.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/UserServiceImpl.java @@ -64,6 +64,18 @@ private static UserData toUserInfo(UserRepresentation rep) { return new UserData(rep.getId(), rep.getUsername(), rep.getFirstName(), rep.getLastName(), rep.getEmail()); } + @Override public List getRoles() { + if (identity.isAnonymous()) { + throw ServiceException.forbidden("Please log in and try again"); + } + var representations = keycloak.realm(realm).users().search(identity.getPrincipal().getName(), true); + if (representations.isEmpty()) { + throw ServiceException.notFound("Username not found"); + } + var roles = keycloak.realm(realm).users().get(representations.get(0).getId()).roles().getAll().getRealmMappings(); + return roles.stream().map(RoleRepresentation::getName).collect(Collectors.toList()); + } + @Override public List searchUsers(String query) { if (identity.isAnonymous()) { diff --git a/horreum-backend/src/main/resources/application.properties b/horreum-backend/src/main/resources/application.properties index f71af277a..28c0d6d63 100644 --- a/horreum-backend/src/main/resources/application.properties +++ b/horreum-backend/src/main/resources/application.properties @@ -199,3 +199,10 @@ horreum.dev-services.enabled=true quarkus.datasource.devservices.enabled=false quarkus.datasource.migration.devservices.enabled=false quarkus.keycloak.devservices.enabled=false + +## Add a dummy administrator in dev mode with name "user" and password "secret" with Basic HTTP authentication +%dev.quarkus.http.auth.basic=true +%dev.quarkus.security.users.embedded.enabled=true +%dev.quarkus.security.users.embedded.plain-text=true +%dev.quarkus.security.users.embedded.users.user=secret +%dev.quarkus.security.users.embedded.roles.user=admin diff --git a/horreum-web/src/Login.tsx b/horreum-web/src/Login.tsx index dff349ef7..34c51f818 100644 --- a/horreum-web/src/Login.tsx +++ b/horreum-web/src/Login.tsx @@ -4,7 +4,7 @@ import {AppContextType} from "./context/@types/appContextTypes"; import {Button, Form, FormGroup, Modal, Spinner, TextInput} from "@patternfly/react-core"; import {userApi} from "./api"; import store from "./store"; -import {BASIC_AUTH, UPDATE_ROLES, AFTER_LOGOUT} from "./auth"; +import {BASIC_AUTH, UPDATE_ROLES, AFTER_LOGOUT, STORE_PROFILE, UPDATE_DEFAULT_TEAM} from "./auth"; type LoginModalProps = { username: string @@ -38,12 +38,18 @@ export default function LoginModal(props: LoginModalProps) { onClick={() => { setCreating(true) store.dispatch({type: BASIC_AUTH, username, password}); - - // TODO: instead of fetching userdata, should be fetching the user roles instead - userApi.info([username || '']) - .then(userdata => { - alerting.dispatchInfo("LOGIN", "Log in successful", "Successful log in of user " + userdata[0].username, 3000) - store.dispatch({type: UPDATE_ROLES, authenticated: true, roles: [/* TODO: the roles fetched */]}); + + userApi.getRoles() + .then(roles => { + alerting.dispatchInfo("LOGIN", "Log in successful", "Successful log in of user " + username, 3000) + store.dispatch({type: UPDATE_ROLES, authenticated: true, roles: roles}); + userApi.searchUsers(username!).then( + userData => store.dispatch({type: STORE_PROFILE, profile: userData.filter(u => u.username == username).at(0)}), + error => alerting.dispatchInfo("LOGIN", "Unable to get user profile", error, 30000)) + userApi.defaultTeam().then( + response => store.dispatch({ type: UPDATE_DEFAULT_TEAM, team: response }), + error => alerting.dispatchInfo("LOGIN", "Cannot retrieve default team", error, 30000) + ) }, error => { alerting.dispatchInfo("LOGIN", "Failed to authenticate", error, 30000) diff --git a/horreum-web/src/api.tsx b/horreum-web/src/api.tsx index e54c3750c..84aa5f124 100644 --- a/horreum-web/src/api.tsx +++ b/horreum-web/src/api.tsx @@ -65,6 +65,7 @@ const authMiddleware: Middleware = { url: ctx.url, init: { ...ctx.init, + credentials: "omit", // this prevents the browser from showing the native auth dialog headers: {...ctx.init.headers, Authorization: "Basic " + basicAuthToken}, }, }) diff --git a/horreum-web/src/auth.tsx b/horreum-web/src/auth.tsx index da034ebfc..dd5cb208f 100644 --- a/horreum-web/src/auth.tsx +++ b/horreum-web/src/auth.tsx @@ -29,7 +29,7 @@ export class AuthState { interface InitAction { type: typeof INIT - keycloak: Keycloak + keycloak?: Keycloak initPromise?: Promise } @@ -46,7 +46,7 @@ interface UpdateRolesAction { interface StoreProfileAction { type: typeof STORE_PROFILE - profile: KeycloakProfile + profile?: KeycloakProfile } interface BasicAuthAction { diff --git a/horreum-web/src/keycloak.ts b/horreum-web/src/keycloak.ts index 0618b7948..cea57e788 100644 --- a/horreum-web/src/keycloak.ts +++ b/horreum-web/src/keycloak.ts @@ -64,7 +64,7 @@ export function initKeycloak(state: State) { } }) } - store.dispatch({ type: INIT, keycloak: keycloak, initPromise: initPromise }) + store.dispatch({ type: INIT, keycloak: oidc ? keycloak : undefined, initPromise: initPromise }) }) .catch(noop) }