This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
The following commit(s) were added to refs/heads/main by this push: new 3b17180f Basic authentication 3b17180f is described below commit 3b17180f9838c00695f21aa6a24db17cfe4b2293 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Tue Mar 5 21:32:05 2024 -0500 Basic authentication --- .../org/apache/camel/karavan/api/AuthResource.java | 50 ++++++++++++++---- .../apache/camel/karavan/api/UsersResource.java | 2 +- karavan-app/src/main/webui/src/api/KaravanApi.tsx | 44 +++++++++++++++- karavan-app/src/main/webui/src/api/LogWatchApi.tsx | 59 ++++++++++++---------- .../src/main/webui/src/api/NotificationApi.tsx | 20 +++++--- karavan-app/src/main/webui/src/api/ProjectStore.ts | 6 +++ 6 files changed, 137 insertions(+), 44 deletions(-) diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/AuthResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/AuthResource.java index 00751702..a277360d 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/AuthResource.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/AuthResource.java @@ -17,20 +17,18 @@ package org.apache.camel.karavan.api; import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.apache.camel.karavan.service.KaravanCacheService; import org.apache.camel.karavan.kubernetes.KubernetesService; import org.apache.camel.karavan.service.AuthService; import org.apache.camel.karavan.service.ProjectService; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.health.HealthCheckResponse; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; @Path("/public") public class AuthResource { @@ -44,8 +42,42 @@ public class AuthResource { @Inject KubernetesService kubernetesService; - @Inject - KaravanCacheService karavanCacheService; + @ConfigProperty(name = "quarkus.security.users.embedded.realm-name", defaultValue = "") + Optional<String> realm; + + @ConfigProperty(name = "quarkus.security.users.embedded.users") + Optional<Map<String,String>> users; + + public static String getMd5Hash(String input) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input.getBytes()); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + @Path("/auth") + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response authenticateUser(@FormParam("username") String username, @FormParam("password") String password) { + try { + if (users.isPresent() && users.get().containsKey(username)) { + var pwdStored = users.get().get(username); + var pwdReceived = new String(Base64.getDecoder().decode(password)); + var pwdString = username + ":" + realm.orElse("") + ":" + pwdReceived; + String pwdToCheck = getMd5Hash(pwdString); + if (Objects.equals(pwdToCheck, pwdStored)) { + return Response.ok().build(); + } + } + return Response.status(Response.Status.FORBIDDEN).entity("Incorrect Username and/or Password!").build(); + } catch (Exception e) { + return Response.status(Response.Status.FORBIDDEN).entity(e.getMessage()).build(); + } + } @GET @Path("/auth") diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/UsersResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/UsersResource.java index f63c1eb0..36d0a12f 100644 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/UsersResource.java +++ b/karavan-app/src/main/java/org/apache/camel/karavan/api/UsersResource.java @@ -53,7 +53,7 @@ public class UsersResource { UserInfo userInfo = (UserInfo) securityIdentity.getAttributes().get("userinfo"); this.userName = securityIdentity.getPrincipal().getName(); this.roles = securityIdentity.getRoles(); - this.displayName = userInfo.getName(); + this.displayName = userInfo != null ? userInfo.getName() : userName; } public String getDisplayName() { diff --git a/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-app/src/main/webui/src/api/KaravanApi.tsx index 8ef409dd..1738290f 100644 --- a/karavan-app/src/main/webui/src/api/KaravanApi.tsx +++ b/karavan-app/src/main/webui/src/api/KaravanApi.tsx @@ -27,6 +27,7 @@ import { import {Buffer} from 'buffer'; import {SsoApi} from "./SsoApi"; import {v4 as uuidv4} from "uuid"; +import {useAppConfigStore} from "./ProjectStore"; const USER_ID_KEY = 'KARAVAN_USER_ID'; axios.defaults.headers.common['Accept'] = 'application/json'; @@ -38,6 +39,7 @@ export class KaravanApi { static me?: any; static authType?: string = undefined; static isAuthorized: boolean = false; + static basicToken: string = ''; static getInstance() { return instance; @@ -59,6 +61,7 @@ export class KaravanApi { } static setAuthType(authType: string) { + console.log("setAuthType", authType) KaravanApi.authType = authType; switch (authType){ case "public": { @@ -69,12 +72,26 @@ export class KaravanApi { KaravanApi.setOidcAuthentication(); break; } + case "basic": { + KaravanApi.setBasicAuthentication(); + break; + } } } static setPublicAuthentication() { } + static setBasicAuthentication() { + instance.interceptors.request.use(async config => { + config.headers.Authorization = 'Basic ' + KaravanApi.basicToken; + return config; + }, + error => { + Promise.reject(error) + }); + } + static setOidcAuthentication() { instance.interceptors.request.use(async config => { config.headers.Authorization = 'Bearer ' + SsoApi.keycloak?.token; @@ -106,6 +123,31 @@ export class KaravanApi { }); } + static async auth(username: string, password: string, after: (ok: boolean, res: any) => void) { + instance.post('/public/auth', + {username: username, password: Buffer.from(password).toString('base64')}, + {headers: {'content-type': 'application/x-www-form-urlencoded'}}) + .then(res => { + if (res.status === 200) { + KaravanApi.isAuthorized = true; + KaravanApi.basicToken = Buffer.from(username + ":" + password).toString('base64'); + KaravanApi.setBasicAuthentication(); + KaravanApi.getMe(user => { + after(true, res); + useAppConfigStore.setState({isAuthorized: true}) + }) + } else if (res.status === 401) { + useAppConfigStore.setState({isAuthorized: false}) + KaravanApi.basicToken = ''; + after(false, res); + } + }).catch(err => { + KaravanApi.basicToken = ''; + useAppConfigStore.setState({isAuthorized: false}) + after(false, err); + }); + } + static async getReadiness(after: (readiness: any) => void) { axios.get('/public/readiness', {headers: {'Accept': 'application/json'}}) .then(res => { @@ -291,7 +333,7 @@ export class KaravanApi { .then(res => { after(res); }).catch(err => { - after(err); + after(err); }); } diff --git a/karavan-app/src/main/webui/src/api/LogWatchApi.tsx b/karavan-app/src/main/webui/src/api/LogWatchApi.tsx index 49f4b134..f761f7b3 100644 --- a/karavan-app/src/main/webui/src/api/LogWatchApi.tsx +++ b/karavan-app/src/main/webui/src/api/LogWatchApi.tsx @@ -25,33 +25,40 @@ export class LogWatchApi { static async fetchData(type: 'container' | 'build' | 'none', podName: string, controller: AbortController) { const fetchData = async () => { const headers: any = { Accept: "text/event-stream" }; - if (KaravanApi.authType === 'oidc') { - headers.Authorization = "Bearer " + SsoApi.keycloak?.token + let ready = false; + if (KaravanApi.authType === 'oidc' && SsoApi.keycloak?.token && SsoApi.keycloak?.token?.length > 0) { + headers.Authorization = "Bearer " + SsoApi.keycloak?.token; + ready = true; + } else if (KaravanApi.authType === 'basic' && KaravanApi.basicToken?.length > 0) { + headers.Authorization = "Basic " + KaravanApi.basicToken + ready = true; + } + if (ready) { + await fetchEventSource("/api/logwatch/" + type + "/" + podName, { + method: "GET", + headers: headers, + signal: controller.signal, + async onopen(response) { + if (response.ok && response.headers.get('content-type') === EventStreamContentType) { + return; // everything's good + } else if (response.status >= 400 && response.status < 500 && response.status !== 429) { + // client-side errors are usually non-retriable: + console.log("Server side error ", response); + } else { + console.log("Error ", response); + } + }, + onmessage(event) { + ProjectEventBus.sendLog('add', event.data); + }, + onclose() { + console.log("Connection closed by the server"); + }, + onerror(err) { + console.log("There was an error from server", err); + }, + }); } - await fetchEventSource("/api/logwatch/" + type + "/" + podName, { - method: "GET", - headers: headers, - signal: controller.signal, - async onopen(response) { - if (response.ok && response.headers.get('content-type') === EventStreamContentType) { - return; // everything's good - } else if (response.status >= 400 && response.status < 500 && response.status !== 429) { - // client-side errors are usually non-retriable: - console.log("Server side error ", response); - } else { - console.log("Error ", response); - } - }, - onmessage(event) { - ProjectEventBus.sendLog('add', event.data); - }, - onclose() { - console.log("Connection closed by the server"); - }, - onerror(err) { - console.log("There was an error from server", err); - }, - }); }; return fetchData(); } diff --git a/karavan-app/src/main/webui/src/api/NotificationApi.tsx b/karavan-app/src/main/webui/src/api/NotificationApi.tsx index fd41bc65..c4971e17 100644 --- a/karavan-app/src/main/webui/src/api/NotificationApi.tsx +++ b/karavan-app/src/main/webui/src/api/NotificationApi.tsx @@ -18,13 +18,12 @@ import {SsoApi} from "./SsoApi"; import {EventStreamContentType, fetchEventSource} from "@microsoft/fetch-event-source"; import {KaravanApi} from "./KaravanApi"; -import {EventBus} from "../designer/utils/EventBus"; import {EventSourceMessage} from "@microsoft/fetch-event-source/lib/cjs/parse"; import {KaravanEvent, NotificationEventBus} from "./NotificationService"; export class NotificationApi { - static getKaravanEvent (ev: EventSourceMessage, type: 'system' | 'user') { + static getKaravanEvent (ev: EventSourceMessage, type: 'system' | 'user') { const eventParts = ev.event?.split(':'); const event = eventParts?.length > 1 ? eventParts[0] : undefined; const className = eventParts?.length > 1 ? eventParts[1] : undefined; @@ -44,13 +43,20 @@ export class NotificationApi { static async notification(controller: AbortController) { const fetchData = async () => { const headers: any = { Accept: "text/event-stream" }; - if (KaravanApi.authType === 'oidc') { - headers.Authorization = "Bearer " + SsoApi.keycloak?.token + let ready = false; + if (KaravanApi.authType === 'oidc' && SsoApi.keycloak?.token && SsoApi.keycloak?.token?.length > 0) { + headers.Authorization = "Bearer " + SsoApi.keycloak?.token; + ready = true; + } else if (KaravanApi.authType === 'basic' && KaravanApi.basicToken?.length > 0) { + headers.Authorization = "Basic " + KaravanApi.basicToken + ready = true; } - NotificationApi.fetch('/api/notification/system', controller, headers, + if (ready) { + NotificationApi.fetch('/api/notification/system', controller, headers, ev => NotificationApi.onSystemMessage(ev)); - NotificationApi.fetch('/api/notification/user/' + KaravanApi.getUserId(), controller, headers, - ev => NotificationApi.onUserMessage(ev)); + NotificationApi.fetch('/api/notification/user/' + KaravanApi.getUserId(), controller, headers, + ev => NotificationApi.onUserMessage(ev)); + } }; return fetchData(); }; diff --git a/karavan-app/src/main/webui/src/api/ProjectStore.ts b/karavan-app/src/main/webui/src/api/ProjectStore.ts index 861a0c06..5bf4a103 100644 --- a/karavan-app/src/main/webui/src/api/ProjectStore.ts +++ b/karavan-app/src/main/webui/src/api/ProjectStore.ts @@ -30,6 +30,8 @@ import {createWithEqualityFn} from "zustand/traditional"; import {shallow} from "zustand/shallow"; interface AppConfigState { + isAuthorized: boolean; + setAuthorized: (isAuthorized: boolean) => void; loading: boolean; setLoading: (loading: boolean) => void; config: AppConfig; @@ -42,6 +44,10 @@ interface AppConfigState { } export const useAppConfigStore = createWithEqualityFn<AppConfigState>((set) => ({ + isAuthorized: false, + setAuthorized: (isAuthorized: boolean) => { + set({isAuthorized: isAuthorized}) + }, loading: false, setLoading: (loading: boolean) => { set({loading: loading})