You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2024/03/06 02:32:12 UTC

(camel-karavan) branch main updated: Basic authentication

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})