Browse Source

feat: basic RBAC support

Timothy J. Baek 1 year ago
parent
commit
8547b7807d

+ 18 - 5
backend/apps/ollama/main.py

@@ -8,7 +8,7 @@ import json
 
 
 from apps.web.models.users import Users
 from apps.web.models.users import Users
 from constants import ERROR_MESSAGES
 from constants import ERROR_MESSAGES
-from utils import extract_token_from_auth_header
+from utils.utils import extract_token_from_auth_header
 from config import OLLAMA_API_BASE_URL, OLLAMA_WEBUI_AUTH
 from config import OLLAMA_API_BASE_URL, OLLAMA_WEBUI_AUTH
 
 
 app = Flask(__name__)
 app = Flask(__name__)
@@ -25,24 +25,37 @@ TARGET_SERVER_URL = OLLAMA_API_BASE_URL
 def proxy(path):
 def proxy(path):
     # Combine the base URL of the target server with the requested path
     # Combine the base URL of the target server with the requested path
     target_url = f"{TARGET_SERVER_URL}/{path}"
     target_url = f"{TARGET_SERVER_URL}/{path}"
-    print(target_url)
+    print(path)
 
 
     # Get data from the original request
     # Get data from the original request
     data = request.get_data()
     data = request.get_data()
     headers = dict(request.headers)
     headers = dict(request.headers)
 
 
+    # Basic RBAC support
     if OLLAMA_WEBUI_AUTH:
     if OLLAMA_WEBUI_AUTH:
         if "Authorization" in headers:
         if "Authorization" in headers:
             token = extract_token_from_auth_header(headers["Authorization"])
             token = extract_token_from_auth_header(headers["Authorization"])
             user = Users.get_user_by_token(token)
             user = Users.get_user_by_token(token)
             if user:
             if user:
-                print(user)
-                pass
+                # Only user and admin roles can access
+                if user.role in ["user", "admin"]:
+                    if path in ["pull", "delete", "push", "copy", "create"]:
+                        # Only admin role can perform actions above
+                        if user.role == "admin":
+                            pass
+                        else:
+                            return (
+                                jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}),
+                                401,
+                            )
+                    else:
+                        pass
+                else:
+                    return jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), 401
             else:
             else:
                 return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
                 return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
         else:
         else:
             return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
             return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401
-
     else:
     else:
         pass
         pass
 
 

+ 3 - 2
backend/apps/web/models/auths.py

@@ -5,7 +5,7 @@ import uuid
 
 
 
 
 from apps.web.models.users import UserModel, Users
 from apps.web.models.users import UserModel, Users
-from utils import (
+from utils.utils import (
     verify_password,
     verify_password,
     get_password_hash,
     get_password_hash,
     bearer_scheme,
     bearer_scheme,
@@ -43,6 +43,7 @@ class UserResponse(BaseModel):
     email: str
     email: str
     name: str
     name: str
     role: str
     role: str
+    profile_image_url: str
 
 
 
 
 class SigninResponse(Token, UserResponse):
 class SigninResponse(Token, UserResponse):
@@ -66,7 +67,7 @@ class AuthsTable:
         self.table = db.auths
         self.table = db.auths
 
 
     def insert_new_auth(
     def insert_new_auth(
-        self, email: str, password: str, name: str, role: str = "user"
+        self, email: str, password: str, name: str, role: str = "pending"
     ) -> Optional[UserModel]:
     ) -> Optional[UserModel]:
         print("insert_new_auth")
         print("insert_new_auth")
 
 

+ 7 - 3
backend/apps/web/models/users.py

@@ -3,7 +3,9 @@ from typing import List, Union, Optional
 from pymongo import ReturnDocument
 from pymongo import ReturnDocument
 import time
 import time
 
 
-from utils import decode_token
+from utils.utils import decode_token
+from utils.misc import get_gravatar_url
+
 from config import DB
 from config import DB
 
 
 ####################
 ####################
@@ -15,7 +17,8 @@ class UserModel(BaseModel):
     id: str
     id: str
     name: str
     name: str
     email: str
     email: str
-    role: str = "user"
+    role: str = "pending"
+    profile_image_url: str = "/user.png"
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
 
 
@@ -30,7 +33,7 @@ class UsersTable:
         self.table = db.users
         self.table = db.users
 
 
     def insert_new_user(
     def insert_new_user(
-        self, id: str, name: str, email: str, role: str = "user"
+        self, id: str, name: str, email: str, role: str = "pending"
     ) -> Optional[UserModel]:
     ) -> Optional[UserModel]:
         user = UserModel(
         user = UserModel(
             **{
             **{
@@ -38,6 +41,7 @@ class UsersTable:
                 "name": name,
                 "name": name,
                 "email": email,
                 "email": email,
                 "role": role,
                 "role": role,
+                "profile_image_url": get_gravatar_url(email),
                 "created_at": int(time.time()),
                 "created_at": int(time.time()),
             }
             }
         )
         )

+ 8 - 2
backend/apps/web/routers/auths.py

@@ -9,12 +9,14 @@ import time
 import uuid
 import uuid
 
 
 from constants import ERROR_MESSAGES
 from constants import ERROR_MESSAGES
-from utils import (
+from utils.utils import (
     get_password_hash,
     get_password_hash,
     bearer_scheme,
     bearer_scheme,
     create_token,
     create_token,
 )
 )
 
 
+from utils.misc import get_gravatar_url
+
 from apps.web.models.auths import (
 from apps.web.models.auths import (
     SigninForm,
     SigninForm,
     SignupForm,
     SignupForm,
@@ -45,10 +47,12 @@ async def get_session_user(cred=Depends(bearer_scheme)):
             "email": user.email,
             "email": user.email,
             "name": user.name,
             "name": user.name,
             "role": user.role,
             "role": user.role,
+            "profile_image_url": user.profile_image_url,
         }
         }
     else:
     else:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,
+            detail=ERROR_MESSAGES.INVALID_TOKEN,
         )
         )
 
 
 
 
@@ -70,9 +74,10 @@ async def signin(form_data: SigninForm):
             "email": user.email,
             "email": user.email,
             "name": user.name,
             "name": user.name,
             "role": user.role,
             "role": user.role,
+            "profile_image_url": user.profile_image_url,
         }
         }
     else:
     else:
-        raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT())
+        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
 
 
 
 
 ############################
 ############################
@@ -98,6 +103,7 @@ async def signup(form_data: SignupForm):
                     "email": user.email,
                     "email": user.email,
                     "name": user.name,
                     "name": user.name,
                     "role": user.role,
                     "role": user.role,
+                    "profile_image_url": user.profile_image_url,
                 }
                 }
             else:
             else:
                 raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
                 raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))

+ 5 - 0
backend/constants.py

@@ -7,7 +7,12 @@ class MESSAGES(str, Enum):
 
 
 class ERROR_MESSAGES(str, Enum):
 class ERROR_MESSAGES(str, Enum):
     DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
     DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}"
+    INVALID_TOKEN = (
+        "Your session has expired or the token is invalid. Please sign in again."
+    )
+    INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
     UNAUTHORIZED = "401 Unauthorized"
     UNAUTHORIZED = "401 Unauthorized"
+    ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
     NOT_FOUND = "We could not find what you're looking for :/"
     NOT_FOUND = "We could not find what you're looking for :/"
     USER_NOT_FOUND = "We could not find what you're looking for :/"
     USER_NOT_FOUND = "We could not find what you're looking for :/"
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."

+ 15 - 0
backend/utils/misc.py

@@ -0,0 +1,15 @@
+import hashlib
+
+
+def get_gravatar_url(email):
+    # Trim leading and trailing whitespace from
+    # an email address and force all characters
+    # to lower case
+    address = str(email).strip().lower()
+
+    # Create a SHA256 hash of the final string
+    hash_object = hashlib.sha256(address.encode())
+    hash_hex = hash_object.hexdigest()
+
+    # Grab the actual image URL
+    return f"https://www.gravatar.com/avatar/{hash_hex}"

+ 0 - 0
backend/utils.py → backend/utils/utils.py


+ 8 - 0
src/lib/components/chat/SettingsModal.svelte

@@ -149,6 +149,10 @@
 						if (data.error) {
 						if (data.error) {
 							throw data.error;
 							throw data.error;
 						}
 						}
+
+						if (data.detail) {
+							throw data.detail;
+						}
 						if (data.status) {
 						if (data.status) {
 							if (!data.status.includes('downloading')) {
 							if (!data.status.includes('downloading')) {
 								toast.success(data.status);
 								toast.success(data.status);
@@ -206,6 +210,10 @@
 						if (data.error) {
 						if (data.error) {
 							throw data.error;
 							throw data.error;
 						}
 						}
+						if (data.detail) {
+							throw data.detail;
+						}
+
 						if (data.status) {
 						if (data.status) {
 						}
 						}
 					} else {
 					} else {

+ 7 - 7
src/lib/components/layout/Navbar.svelte

@@ -388,17 +388,17 @@
 				{#if $user !== undefined}
 				{#if $user !== undefined}
 					<button
 					<button
 						class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
 						class=" flex rounded-md py-3 px-3.5 w-full hover:bg-gray-900 transition"
-						on:focus={() => {
-							showDropdown = true;
+						on:click={() => {
+							showDropdown = !showDropdown;
 						}}
 						}}
 						on:focusout={() => {
 						on:focusout={() => {
 							setTimeout(() => {
 							setTimeout(() => {
 								showDropdown = false;
 								showDropdown = false;
-							}, 100);
+							}, 150);
 						}}
 						}}
 					>
 					>
 						<div class=" self-center mr-3">
 						<div class=" self-center mr-3">
-							<img src="/user.png" class=" max-w-[30px] object-cover rounded-full" />
+							<img src={$user.profile_image_url} class=" max-w-[30px] object-cover rounded-full" />
 						</div>
 						</div>
 						<div class=" self-center font-semibold">{$user.name}</div>
 						<div class=" self-center font-semibold">{$user.name}</div>
 					</button>
 					</button>
@@ -406,7 +406,7 @@
 					{#if showDropdown}
 					{#if showDropdown}
 						<div
 						<div
 							id="dropdownDots"
 							id="dropdownDots"
-							class="absolute z-10 bottom-[4.5rem] rounded-lg shadow w-[240px] bg-gray-900"
+							class="absolute z-10 bottom-[70px] 4.5rem rounded-lg shadow w-[240px] bg-gray-900"
 						>
 						>
 							<div class="py-2 w-full">
 							<div class="py-2 w-full">
 								<button
 								<button
@@ -440,14 +440,14 @@
 								</button>
 								</button>
 							</div>
 							</div>
 
 
-							<hr class=" dark:border-gray-700 m-0 p-0" />
+							<hr class=" border-gray-700 m-0 p-0" />
 
 
 							<div class="py-2 w-full">
 							<div class="py-2 w-full">
 								<button
 								<button
 									class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
 									class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
 									on:click={() => {
 									on:click={() => {
 										localStorage.removeItem('token');
 										localStorage.removeItem('token');
-										location.href = '/';
+										location.href = '/auth';
 									}}
 									}}
 								>
 								>
 									<div class=" self-center mr-3">
 									<div class=" self-center mr-3">

+ 11 - 4
src/routes/(app)/+layout.svelte

@@ -1,12 +1,19 @@
 <script>
 <script>
 	import { config, user } from '$lib/stores';
 	import { config, user } from '$lib/stores';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
+	import { onMount, tick } from 'svelte';
 
 
-	if ($config && $config.auth && $user === undefined) {
-		goto('/auth');
-	}
+	let loaded = false;
+	onMount(async () => {
+		if ($config && $config.auth && $user === undefined) {
+			await goto('/auth');
+		}
+
+		await tick();
+		loaded = true;
+	});
 </script>
 </script>
 
 
-{#if $config !== undefined}
+{#if loaded}
 	<slot />
 	<slot />
 {/if}
 {/if}

+ 22 - 9
src/routes/(app)/+page.svelte

@@ -16,7 +16,7 @@
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
 	import Suggestions from '$lib/components/chat/Suggestions.svelte';
 	import Suggestions from '$lib/components/chat/Suggestions.svelte';
-	import { user } from '$lib/stores';
+	import { config, user } from '$lib/stores';
 
 
 	let API_BASE_URL = BUILD_TIME_API_BASE_URL;
 	let API_BASE_URL = BUILD_TIME_API_BASE_URL;
 	let db;
 	let db;
@@ -1224,14 +1224,27 @@
 								<div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
 								<div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
 									<div class=" flex w-full">
 									<div class=" flex w-full">
 										<div class=" mr-4">
 										<div class=" mr-4">
-											<img
-												src="{message.role == 'user'
-													? settings.gravatarUrl
-														? settings.gravatarUrl
-														: '/user'
-													: '/favicon'}.png"
-												class=" max-w-[28px] object-cover rounded-full"
-											/>
+											{#if message.role === 'user'}
+												{#if $config === null}
+													<img
+														src="{settings.gravatarUrl ? settings.gravatarUrl : '/user'}.png"
+														class=" max-w-[28px] object-cover rounded-full"
+														alt="User profile"
+													/>
+												{:else}
+													<img
+														src={$user.profile_image_url}
+														class=" max-w-[28px] object-cover rounded-full"
+														alt="User profile"
+													/>
+												{/if}
+											{:else}
+												<img
+													src="/favicon.png"
+													class=" max-w-[28px] object-cover rounded-full"
+													alt="Ollama profile"
+												/>
+											{/if}
 										</div>
 										</div>
 
 
 										<div class="w-full">
 										<div class="w-full">

+ 12 - 7
src/routes/+layout.svelte

@@ -11,7 +11,7 @@
 	let loaded = false;
 	let loaded = false;
 
 
 	onMount(async () => {
 	onMount(async () => {
-		const webBackendStatus = await fetch(`${WEBUI_API_BASE_URL}/`, {
+		const resBackend = await fetch(`${WEBUI_API_BASE_URL}/`, {
 			method: 'GET',
 			method: 'GET',
 			headers: {
 			headers: {
 				'Content-Type': 'application/json'
 				'Content-Type': 'application/json'
@@ -26,11 +26,11 @@
 				return null;
 				return null;
 			});
 			});
 
 
-		console.log(webBackendStatus);
-		await config.set(webBackendStatus);
+		console.log(resBackend);
+		await config.set(resBackend);
 
 
-		if (webBackendStatus) {
-			if (webBackendStatus.auth) {
+		if ($config) {
+			if ($config.auth) {
 				if (localStorage.token) {
 				if (localStorage.token) {
 					const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
 					const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, {
 						method: 'GET',
 						method: 'GET',
@@ -49,9 +49,14 @@
 							return null;
 							return null;
 						});
 						});
 
 
-					await user.set(res);
+					if (res) {
+						await user.set(res);
+					} else {
+						localStorage.removeItem('token');
+						await goto('/auth');
+					}
 				} else {
 				} else {
-					goto('/auth');
+					await goto('/auth');
 				}
 				}
 			}
 			}
 		}
 		}

+ 150 - 5
src/routes/auth/+page.svelte

@@ -2,8 +2,10 @@
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import { WEBUI_API_BASE_URL } from '$lib/constants';
 	import { WEBUI_API_BASE_URL } from '$lib/constants';
 	import { config, user } from '$lib/stores';
 	import { config, user } from '$lib/stores';
+	import { onMount } from 'svelte';
 	import toast from 'svelte-french-toast';
 	import toast from 'svelte-french-toast';
 
 
+	let loaded = false;
 	let mode = 'signin';
 	let mode = 'signin';
 
 
 	let name = '';
 	let name = '';
@@ -33,7 +35,7 @@
 
 
 		if (res) {
 		if (res) {
 			console.log(res);
 			console.log(res);
-			toast.success(`You're now logged in. Redirecting you to the main page."`);
+			toast.success(`You're now logged in. Redirecting you to the main page.`);
 			localStorage.token = res.token;
 			localStorage.token = res.token;
 			await user.set(res);
 			await user.set(res);
 			goto('/');
 			goto('/');
@@ -71,12 +73,15 @@
 		}
 		}
 	};
 	};
 
 
-	if ($config === null || !$config.auth || ($config.auth && $user !== undefined)) {
-		goto('/');
-	}
+	onMount(async () => {
+		if ($config === null || !$config.auth || ($config.auth && $user !== undefined)) {
+			await goto('/');
+		}
+		loaded = true;
+	});
 </script>
 </script>
 
 
-{#if $config && $config.auth}
+{#if loaded && $config && $config.auth}
 	<div class="fixed m-10 z-50">
 	<div class="fixed m-10 z-50">
 		<div class="flex space-x-2">
 		<div class="flex space-x-2">
 			<div class=" self-center">
 			<div class=" self-center">
@@ -1065,6 +1070,146 @@
 		</div>
 		</div>
 	</div>
 	</div>
 
 
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
+	<div class=" my-auto pb-36 w-full px-4">
+		<div class=" text-center flex flex-col justify-center">
+			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
+
+			<div
+				class=" mt-4 flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-1 px-3 justify-center"
+			>
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Log in
+				</button>
+
+				<button class=" flex-1 px-4 py-3.5 bg-blue-700 text-white font-medium rounded-lg">
+					Sign up
+				</button>
+			</div>
+		</div>
+	</div>
+</div> -->
+	<!-- <div class=" bg-white min-h-screen w-full flex flex-col">
+	<div class=" mt-6 mx-6">
+		<div class="flex space-x-2">
+			<div class=" self-center text-2xl font-semibold">Ollama</div>
+			<div class=" self-center">
+				<img src="/ollama.png" class=" w-5" />
+			</div>
+		</div>
+	</div>
+
 	<div class=" my-auto pb-36 w-full px-4">
 	<div class=" my-auto pb-36 w-full px-4">
 		<div class=" text-center flex flex-col justify-center">
 		<div class=" text-center flex flex-col justify-center">
 			<div class=" text-xl md:text-2xl font-bold">Get Started</div>
 			<div class=" text-xl md:text-2xl font-bold">Get Started</div>