From d8c3f3bd6aab4e8b70b127935588bf5b31f0d094 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Mon, 14 Apr 2025 16:21:58 +0200 Subject: [PATCH 01/31] Added roles model --- backend/open_webui/models/roles.py | 83 ++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 backend/open_webui/models/roles.py diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py new file mode 100644 index 00000000000..4356ee3c5e2 --- /dev/null +++ b/backend/open_webui/models/roles.py @@ -0,0 +1,83 @@ +import time +from typing import Optional + +from open_webui.internal.db import Base, JSONField, get_db + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text + +#################### +# Role DB Schema +#################### + + +class Role(Base): + __tablename__ = "roles" + + id = Column(String, primary_key=True) + name = Column(String) + + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + +class RoleModel(BaseModel): + id: str + name: str + + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + +class RolesTable: + def insert_new_role( + self, + id: str, + name: str, + ) -> Optional[RoleModel]: + with get_db() as db: + role = RoleModel( + **{ + "id": id, + "name": name, + "created_at": int(time.time()), + "updated_at": int(time.time()), + } + ) + result = Role(**role.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return role + else: + return None + + def get_role_by_id(self, id: str) -> Optional[RoleModel]: + try: + with get_db() as db: + role = db.query(Role).filter_by(id=id).first() + return RoleModel.model_validate(role) + except Exception: + return None + + def get_roles( + self, skip: Optional[int] = None, limit: Optional[int] = None + ) -> list[RoleModel]: + with get_db() as db: + + query = db.query(Role).order_by(Role.created_at.desc()) + + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + + roles = query.all() + + return [RoleModel.model_validate(role) for role in roles] + + def get_num_roles(self) -> Optional[int]: + with get_db() as db: + return db.query(Role).count() + + +Roles = RolesTable() From 2222fae1671a3c8df67b176cf164aa69359787e6 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Mon, 14 Apr 2025 16:31:44 +0200 Subject: [PATCH 02/31] Added migration for roles table --- .../262aff902ca3_added_roles_tabel.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py diff --git a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py new file mode 100644 index 00000000000..77738d43e39 --- /dev/null +++ b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py @@ -0,0 +1,41 @@ +"""Added roles tabel + +Revision ID: 262aff902ca3 +Revises: 3781e22d8b01 +Create Date: 2025-04-14 14:25:33.528446 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + + +# revision identifiers, used by Alembic. +revision: str = '262aff902ca3' +down_revision: Union[str, None] = '3781e22d8b01' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + op.create_table( + "roles", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("name", sa.String()), + sa.Column( + "created_at", sa.DateTime(), nullable=False, server_default=sa.func.now() + ), + sa.Column( + "updated_at", + sa.DateTime(), + nullable=True, + server_default=sa.func.now(), + onupdate=sa.func.now(), + ), + ) + + +def downgrade(): + op.drop_table("roles") From 4193720d6ebd434a00e81c05949fffdd884c312c Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 15 Apr 2025 09:07:25 +0200 Subject: [PATCH 03/31] Added default roles to database --- .../versions/262aff902ca3_added_roles_tabel.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py index 77738d43e39..0d5ebcff19c 100644 --- a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py +++ b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py @@ -36,6 +36,19 @@ def upgrade(): ), ) + # Insert default roles into the database. + connection = op.get_bind() + connection.execute( + sa.text( + """ + INSERT INTO roles (name) VALUES + ('pending'), + ('admin'), + ('user') + """ + ) + ) + def downgrade(): op.drop_table("roles") From d75be6b46f3c94449e9bb96a5e09234483cf5211 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 15 Apr 2025 09:55:47 +0200 Subject: [PATCH 04/31] Added roles routes --- backend/open_webui/main.py | 2 + .../262aff902ca3_added_roles_tabel.py | 28 +++---- backend/open_webui/models/roles.py | 16 ++-- backend/open_webui/routers/roles.py | 78 +++++++++++++++++++ 4 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 backend/open_webui/routers/roles.py diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index a5aee4bb829..d4390edc5de 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -78,6 +78,7 @@ tools, users, utils, + roles, ) from open_webui.routers.retrieval import ( @@ -1052,6 +1053,7 @@ async def inspect_websocket(request: Request, call_next): ) app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"]) +app.include_router(roles.router, prefix="/api/v1/roles", tags=["roles"]) try: audit_level = AuditLevel(AUDIT_LOG_LEVEL) diff --git a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py index 0d5ebcff19c..98b0c0e33ec 100644 --- a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py +++ b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py @@ -5,12 +5,12 @@ Create Date: 2025-04-14 14:25:33.528446 """ +import time + from typing import Sequence, Union from alembic import op import sqlalchemy as sa -import open_webui.internal.db - # revision identifiers, used by Alembic. revision: str = '262aff902ca3' @@ -24,27 +24,21 @@ def upgrade(): "roles", sa.Column("id", sa.Integer, primary_key=True), sa.Column("name", sa.String()), - sa.Column( - "created_at", sa.DateTime(), nullable=False, server_default=sa.func.now() - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=True, - server_default=sa.func.now(), - onupdate=sa.func.now(), - ), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.Column("updated_at", sa.BigInteger(), nullable=True), ) # Insert default roles into the database. + current_time = int(time.time()) + connection = op.get_bind() connection.execute( sa.text( - """ - INSERT INTO roles (name) VALUES - ('pending'), - ('admin'), - ('user') + f""" + INSERT INTO roles (name, created_at, updated_at) VALUES + ('pending', {current_time}, {current_time}), + ('admin', {current_time}, {current_time}), + ('user', {current_time}, {current_time}) """ ) ) diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py index 4356ee3c5e2..70e969d6322 100644 --- a/backend/open_webui/models/roles.py +++ b/backend/open_webui/models/roles.py @@ -4,7 +4,9 @@ from open_webui.internal.db import Base, JSONField, get_db from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text +from sqlalchemy import BigInteger, Column, String, Integer + +from pprint import pprint #################### # Role DB Schema @@ -14,23 +16,25 @@ class Role(Base): __tablename__ = "roles" - id = Column(String, primary_key=True) + id = Column(Integer, primary_key=True) name = Column(String) updated_at = Column(BigInteger) created_at = Column(BigInteger) class RoleModel(BaseModel): - id: str + id: int name: str updated_at: int # timestamp in epoch created_at: int # timestamp in epoch + model_config = ConfigDict(from_attributes=True) + class RolesTable: def insert_new_role( self, - id: str, + id: int, name: str, ) -> Optional[RoleModel]: with get_db() as db: @@ -64,7 +68,7 @@ def get_roles( ) -> list[RoleModel]: with get_db() as db: - query = db.query(Role).order_by(Role.created_at.desc()) + query = db.query(Role).order_by(Role.id) if skip: query = query.offset(skip) @@ -73,6 +77,8 @@ def get_roles( roles = query.all() + pprint(roles) + return [RoleModel.model_validate(role) for role in roles] def get_num_roles(self) -> Optional[int]: diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py new file mode 100644 index 00000000000..472ccfde22e --- /dev/null +++ b/backend/open_webui/routers/roles.py @@ -0,0 +1,78 @@ +import logging +from typing import Optional + +from open_webui.models.auths import Auths +from open_webui.models.groups import Groups +from open_webui.models.chats import Chats +from open_webui.models.roles import ( + RoleModel, + Roles +) + + +from open_webui.socket.main import get_active_status_by_user_id +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from fastapi import APIRouter, Depends, HTTPException, Request, status +from pydantic import BaseModel + +from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user +from open_webui.utils.access_control import get_permissions + + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +router = APIRouter() + +############################ +# GetRoles +############################ + + +@router.get("/", response_model=list[RoleModel]) +async def get_roles( + skip: Optional[int] = None, + limit: Optional[int] = None, + user=Depends(get_admin_user), +): + return Roles.get_roles(skip, limit) + +############################ +# UpdateUserRole +############################ + + +# @router.post("/", response_model=Optional[RoleModel]) +# async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)): +# if user.id != form_data.id and form_data.id != Users.get_first_user().id: +# return Users.update_user_role_by_id(form_data.id, form_data.role) +# +# raise HTTPException( +# status_code=status.HTTP_403_FORBIDDEN, +# detail=ERROR_MESSAGES.ACTION_PROHIBITED, +# ) +# +# +# ############################ +# # DeleteUserById +# ############################ +# +# +# @router.delete("/{user_id}", response_model=bool) +# async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)): +# if user.id != user_id: +# result = Auths.delete_auth_by_id(user_id) +# +# if result: +# return True +# +# raise HTTPException( +# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, +# detail=ERROR_MESSAGES.DELETE_USER_ERROR, +# ) +# +# raise HTTPException( +# status_code=status.HTTP_403_FORBIDDEN, +# detail=ERROR_MESSAGES.ACTION_PROHIBITED, +# ) From a38cc2cef9313d49fda770c300726507242d04e4 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 15 Apr 2025 11:05:25 +0200 Subject: [PATCH 05/31] Added support for adding new roles --- backend/open_webui/models/roles.py | 30 +++++++++++++++++++---------- backend/open_webui/routers/roles.py | 29 +++++++++++++++------------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py index 70e969d6322..92e86eb0cff 100644 --- a/backend/open_webui/models/roles.py +++ b/backend/open_webui/models/roles.py @@ -31,27 +31,29 @@ class RoleModel(BaseModel): model_config = ConfigDict(from_attributes=True) +#################### +# Forms +#################### + +class RoleAddForm(BaseModel): + role: str + class RolesTable: def insert_new_role( self, - id: int, name: str, ) -> Optional[RoleModel]: with get_db() as db: - role = RoleModel( - **{ - "id": id, - "name": name, - "created_at": int(time.time()), - "updated_at": int(time.time()), - } + result = Role( + name=name, + created_at=int(time.time()), + updated_at=int(time.time()) ) - result = Role(**role.model_dump()) db.add(result) db.commit() db.refresh(result) if result: - return role + return RoleModel.model_validate(result) else: return None @@ -63,6 +65,14 @@ def get_role_by_id(self, id: str) -> Optional[RoleModel]: except Exception: return None + def get_role_by_name(self, name: str) -> Optional[RoleModel]: + try: + with get_db() as db: + role = db.query(Role).filter_by(name=name).first() + return RoleModel.model_validate(role) + except Exception: + return None + def get_roles( self, skip: Optional[int] = None, limit: Optional[int] = None ) -> list[RoleModel]: diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index 472ccfde22e..af1a8953b9b 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -6,7 +6,8 @@ from open_webui.models.chats import Chats from open_webui.models.roles import ( RoleModel, - Roles + Roles, + RoleAddForm, ) @@ -39,21 +40,23 @@ async def get_roles( return Roles.get_roles(skip, limit) ############################ -# UpdateUserRole +# AddRole ############################ -# @router.post("/", response_model=Optional[RoleModel]) -# async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)): -# if user.id != form_data.id and form_data.id != Users.get_first_user().id: -# return Users.update_user_role_by_id(form_data.id, form_data.role) -# -# raise HTTPException( -# status_code=status.HTTP_403_FORBIDDEN, -# detail=ERROR_MESSAGES.ACTION_PROHIBITED, -# ) -# -# +@router.post("/", response_model=Optional[RoleModel]) +async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)): + # Check if role already exists + existing_role = Roles.get_role_by_name(name=form_data.role) + if existing_role: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Role with name '{form_data.role}' already exists" + ) + + return Roles.insert_new_role(name=form_data.role) + + # ############################ # # DeleteUserById # ############################ From 6c754a37004a6e11d64c31cc0b0e259dc927fa52 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 15 Apr 2025 15:13:32 +0200 Subject: [PATCH 06/31] Added support for deleting role --- backend/open_webui/constants.py | 1 + backend/open_webui/models/roles.py | 15 ++++++++--- backend/open_webui/routers/roles.py | 39 +++++++++++++---------------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py index 95c54a0d270..735cf26bc4b 100644 --- a/backend/open_webui/constants.py +++ b/backend/open_webui/constants.py @@ -104,6 +104,7 @@ def __str__(self) -> str: ) FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding." + DELETE_ROLE_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the role. Please give it another shot." class TASKS(str, Enum): def __str__(self) -> str: diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py index 92e86eb0cff..f83c9022339 100644 --- a/backend/open_webui/models/roles.py +++ b/backend/open_webui/models/roles.py @@ -1,13 +1,13 @@ import time from typing import Optional +from pprint import pprint + from open_webui.internal.db import Base, JSONField, get_db from pydantic import BaseModel, ConfigDict from sqlalchemy import BigInteger, Column, String, Integer -from pprint import pprint - #################### # Role DB Schema #################### @@ -87,13 +87,20 @@ def get_roles( roles = query.all() - pprint(roles) - return [RoleModel.model_validate(role) for role in roles] def get_num_roles(self) -> Optional[int]: with get_db() as db: return db.query(Role).count() + def delete_by_id(self, role_id: str) -> bool: + try: + with get_db() as db: + db.query(Role).filter_by(id=role_id).delete() + db.commit() + + return True + except Exception: + return False Roles = RolesTable() diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index af1a8953b9b..8046658c497 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -1,6 +1,8 @@ import logging from typing import Optional +from pprint import pprint + from open_webui.models.auths import Auths from open_webui.models.groups import Groups from open_webui.models.chats import Chats @@ -57,25 +59,18 @@ async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)): return Roles.insert_new_role(name=form_data.role) -# ############################ -# # DeleteUserById -# ############################ -# -# -# @router.delete("/{user_id}", response_model=bool) -# async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)): -# if user.id != user_id: -# result = Auths.delete_auth_by_id(user_id) -# -# if result: -# return True -# -# raise HTTPException( -# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, -# detail=ERROR_MESSAGES.DELETE_USER_ERROR, -# ) -# -# raise HTTPException( -# status_code=status.HTTP_403_FORBIDDEN, -# detail=ERROR_MESSAGES.ACTION_PROHIBITED, -# ) +############################ +# DeleteRoleById +############################ + +# TODO(jeskr): Check if role is used by any users before deleting it. +@router.delete("/{role_id}", response_model=bool) +async def delete_role_by_id(role_id: str, user=Depends(get_admin_user)): + result = Roles.delete_by_id(role_id) + if result: + return True + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DELETE_ROLE_ERROR, + ) From 488bc033c6eb959f9759d8ff2010cddffd321a54 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 15 Apr 2025 16:15:03 +0200 Subject: [PATCH 07/31] Ensured dynamic role assignment is used in oauth --- backend/open_webui/models/roles.py | 12 ++++++++++++ backend/open_webui/utils/oauth.py | 24 ++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py index f83c9022339..e9d35b2acb6 100644 --- a/backend/open_webui/models/roles.py +++ b/backend/open_webui/models/roles.py @@ -103,4 +103,16 @@ def delete_by_id(self, role_id: str) -> bool: except Exception: return False + def add_role_if_role_do_not_exists(self, role_name: str) -> Optional[RoleModel]: + # Check if role already exists + existing_role = self.get_role_by_name(role_name) + if existing_role: + return existing_role + + # Role is allowed and doesn't exist, so create it + try: + return self.insert_new_role(name=role_name) + except Exception as e: + return None + Roles = RolesTable() diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index f6004515fc6..ad23297a0b2 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -16,6 +16,8 @@ from open_webui.models.auths import Auths from open_webui.models.users import Users + +from open_webui.models.roles import Roles from open_webui.models.groups import Groups, GroupModel, GroupUpdateForm, GroupForm from open_webui.config import ( DEFAULT_USER_ROLE, @@ -87,6 +89,17 @@ def __init__(self, app): def get_client(self, provider_name): return self.oauth.create_client(provider_name) + def find_first_role_match(self, oauth_roles, allowed_roles): + # Convert to sets for more efficient lookup if lists are large + oauth_roles_set = set(oauth_roles) + allowed_roles_set = set(allowed_roles) + + # Find the intersection of the two sets + matching_roles = oauth_roles_set.intersection(allowed_roles_set) + + # Return the first matching role if any matches found + return next(iter(matching_roles), None) + def get_user_role(self, user, user_data): if user and Users.get_num_users() == 1: # If the user is the only user, assign the role "admin" - actually repairs role for single user on login @@ -125,8 +138,15 @@ def get_user_role(self, user, user_data): for allowed_role in oauth_allowed_roles: # If the user has any of the allowed roles, assign the role "user" if allowed_role in oauth_roles: - log.debug("Assigned user the user role") - role = "user" + first_match = self.find_first_role_match(oauth_roles, oauth_allowed_roles) + if first_match: + Roles.add_role_if_role_do_not_exists(first_match) + role = first_match + else: + # Fallback to role user. + role = "user" + + log.debug(f"Assigned user the {role} role") break for admin_role in oauth_admin_roles: # If the user has any of the admin roles, assign the role "admin" From 5e8925db1b183b994a6706130e6de6034ab35dff Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 15 Apr 2025 16:27:31 +0200 Subject: [PATCH 08/31] Added roles apis to frontend --- src/lib/apis/roles/index.ts | 86 +++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/lib/apis/roles/index.ts diff --git a/src/lib/apis/roles/index.ts b/src/lib/apis/roles/index.ts new file mode 100644 index 00000000000..4cf741db8c6 --- /dev/null +++ b/src/lib/apis/roles/index.ts @@ -0,0 +1,86 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { getUserPosition } from '$lib/utils'; + +export const getRoles = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/roles`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addRole = async (token: string, role: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/roles`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteRole = async (token: string, roleId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; \ No newline at end of file From d93cb8833fd142844054f7f253e6d4790bdf1f39 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 22 Apr 2025 14:35:17 +0200 Subject: [PATCH 09/31] Added support for dynamic roles in frontend --- src/lib/apis/roles/index.ts | 4 ++-- src/lib/components/admin/Users/UserList.svelte | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib/apis/roles/index.ts b/src/lib/apis/roles/index.ts index 4cf741db8c6..310955f25da 100644 --- a/src/lib/apis/roles/index.ts +++ b/src/lib/apis/roles/index.ts @@ -4,7 +4,7 @@ import { getUserPosition } from '$lib/utils'; export const getRoles = async (token: string) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/roles`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/roles/`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -31,7 +31,7 @@ export const getRoles = async (token: string) => { export const addRole = async (token: string, role: string) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/roles`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/roles/`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/lib/components/admin/Users/UserList.svelte b/src/lib/components/admin/Users/UserList.svelte index 61816780ba5..0d4008dedac 100644 --- a/src/lib/components/admin/Users/UserList.svelte +++ b/src/lib/components/admin/Users/UserList.svelte @@ -21,6 +21,7 @@ import EditUserModal from '$lib/components/admin/Users/UserList/EditUserModal.svelte'; import UserChatsModal from '$lib/components/admin/Users/UserList/UserChatsModal.svelte'; import AddUserModal from '$lib/components/admin/Users/UserList/AddUserModal.svelte'; + import { getRoles } from '$lib/apis/roles'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import RoleUpdateConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; @@ -124,6 +125,14 @@ $: if (query !== null && orderBy && direction) { getUserList(); } + + let roles = []; + onMount(async () => { + roles = await getRoles(localStorage.token).catch((error) => { + toast.error(`${error}`); + return []; + }); + }); Date: Tue, 22 Apr 2025 14:44:06 +0200 Subject: [PATCH 10/31] Added support for dynamic roles in user add --- .../admin/Users/UserList/AddUserModal.svelte | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/lib/components/admin/Users/UserList/AddUserModal.svelte b/src/lib/components/admin/Users/UserList/AddUserModal.svelte index cad799eb546..319062870bc 100644 --- a/src/lib/components/admin/Users/UserList/AddUserModal.svelte +++ b/src/lib/components/admin/Users/UserList/AddUserModal.svelte @@ -3,6 +3,7 @@ import { createEventDispatcher } from 'svelte'; import { onMount, getContext } from 'svelte'; import { addUser } from '$lib/apis/auths'; + import { getRoles } from '$lib/apis/roles'; import { WEBUI_BASE_URL } from '$lib/constants'; @@ -34,6 +35,14 @@ }; } + let roles = []; + onMount(async () => { + roles = await getRoles(localStorage.token).catch((error) => { + toast.error(`${error}`); + return []; + }); + }); + const submitHandler = async () => { const stopLoading = () => { dispatch('save'); @@ -78,7 +87,7 @@ if (idx > 0) { if ( columns.length === 4 && - ['admin', 'user', 'pending'].includes(columns[3].toLowerCase()) + roles.map(role => role.name).includes(columns[3].toLowerCase()) ) { const res = await addUser( localStorage.token, @@ -189,9 +198,10 @@ placeholder={$i18n.t('Enter Your Role')} required > - - - + {#each roles as role} + + {/each} + From 59b9360ff03208be2a30c56cfdb56e0fc8eb5aa1 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 22 Apr 2025 17:02:53 +0200 Subject: [PATCH 11/31] Started on moving permissions into database --- ...6df61a317_added_permissions_to_database.py | 74 +++++++++++++++++++ backend/open_webui/models/permissions.py | 60 +++++++++++++++ backend/open_webui/models/roles.py | 2 - backend/open_webui/routers/auths.py | 9 ++- backend/open_webui/routers/roles.py | 9 --- backend/open_webui/routers/users.py | 24 ++---- 6 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py create mode 100644 backend/open_webui/models/permissions.py diff --git a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py new file mode 100644 index 00000000000..936efdf81fd --- /dev/null +++ b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py @@ -0,0 +1,74 @@ +"""Added permissions to the database + +Revision ID: 04c6df61a317 +Revises: 262aff902ca3 +Create Date: 2025-04-22 14:55:58.948054 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + + +# revision identifiers, used by Alembic. +revision: str = '04c6df61a317' +down_revision: Union[str, None] = '262aff902ca3' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + # Create a permissions table. + permissions_table = op.create_table( + 'permissions', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('name', sa.String(), nullable=False), + sa.Column('category', sa.String(), nullable=False), + sa.Column('description', sa.String()), + sa.Column('default_value', sa.Boolean(), default=False), + ) + + # Insert default permissions + default_permissions = [ + # Workspace permissions + {'category': 'workspace', 'name': 'models', 'default_value': False}, + {'category': 'workspace', 'name': 'knowledge', 'default_value': False}, + {'category': 'workspace', 'name': 'prompts', 'default_value': False}, + {'category': 'workspace', 'name': 'tools', 'default_value': False}, + # Sharing permissions + {'category': 'sharing', 'name': 'public_models', 'default_value': True}, + {'category': 'sharing', 'name': 'public_knowledge', 'default_value': True}, + {'category': 'sharing', 'name': 'public_prompts', 'default_value': True}, + {'category': 'sharing', 'name': 'public_tools', 'default_value': True}, + # Chat permissions. + {'category': 'chat', 'name': 'controls', 'default_value': True}, + {'category': 'chat', 'name': 'file_upload', 'default_value': True}, + {'category': 'chat', 'name': 'delete', 'default_value': True}, + {'category': 'chat', 'name': 'edit', 'default_value': True}, + {'category': 'chat', 'name': 'stt', 'default_value': True}, + {'category': 'chat', 'name': 'tts', 'default_value': True}, + {'category': 'chat', 'name': 'call', 'default_value': True}, + {'category': 'chat', 'name': 'multiple_models', 'default_value': True}, + {'category': 'chat', 'name': 'temporary', 'default_value': True}, + {'category': 'chat', 'name': 'temporary_enforced', 'default_value': True}, + # Features permissions. + {'category': 'features', 'name': 'direct_tool_servers', 'default_value': False}, + {'category': 'features', 'name': 'web_search', 'default_value': True}, + {'category': 'features', 'name': 'image_generation', 'default_value': True}, + {'category': 'features', 'name': 'code_interpreter', 'default_value': True}, + ] + + for perm in default_permissions: + op.execute( + permissions_table.insert().values( + name=perm['name'], + category=perm['category'], + default_value=perm['default_value'] + ) + ) + + +def downgrade(): + op.drop_table('permissions') diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py new file mode 100644 index 00000000000..9f217dcad25 --- /dev/null +++ b/backend/open_webui/models/permissions.py @@ -0,0 +1,60 @@ +from enum import Enum +from typing import Optional + +from open_webui.internal.db import Base, JSONField, get_db + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import Boolean, Column, String, JSON, Enum as SQLAlchemyEnum + +#################### +# Role DB Schema +#################### + +class PermissionCategory(str, Enum): + workspace = "workspace" + sharing = "sharing" + chat = "chat" + features = "features" + +class Permission(Base): + __tablename__ = "permissions" + + id = Column(String, primary_key=True) + name = Column(String, nullable=False) + # workspace, sharing, chat, features + category = Column(SQLAlchemyEnum(PermissionCategory), nullable=False) + description = Column(String) + default_value = Column(Boolean, default=False) + + +class PermissionsModel(BaseModel): + id: int + name: str + category: PermissionCategory + description: str | None + default_value: bool + + model_config = ConfigDict(from_attributes=True) + +#################### +# Forms +#################### + +class PermissionsTable: + def get_permissions_by_category(self) -> dict[PermissionCategory, list[PermissionsModel]]: + with get_db() as db: + + query = db.query(Permission).order_by(Permission.id) + all_permissions = query.all() + + # Group permissions by category + permissions = {} + for perm in all_permissions: + if perm.category not in permissions: + permissions[perm.category] = {} + + permissions[perm.category][perm.name] = perm.default_value + + return permissions + +Permissions = PermissionsTable() diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py index e9d35b2acb6..e0cea8f18ce 100644 --- a/backend/open_webui/models/roles.py +++ b/backend/open_webui/models/roles.py @@ -1,8 +1,6 @@ import time from typing import Optional -from pprint import pprint - from open_webui.internal.db import Base, JSONField, get_db from pydantic import BaseModel, ConfigDict diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 793bdfd30a2..e750e31cb48 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -48,6 +48,7 @@ ) from open_webui.utils.webhook import post_webhook from open_webui.utils.access_control import get_permissions +from open_webui.models.permissions import Permissions from typing import Optional, List @@ -108,7 +109,7 @@ async def get_session_user( ) user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS + user.id, Permissions.get_permissions_by_category() ) return { @@ -329,7 +330,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): ) user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS + user.id, Permissions.get_permissions_by_category() ) return { @@ -427,7 +428,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm): ) user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS + user.id, Permissions.get_permissions_by_category() ) return { @@ -537,7 +538,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm): ) user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS + user.id, Permissions.get_permissions_by_category() ) if user_count == 0: diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index 8046658c497..d91ef27fbef 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -1,26 +1,17 @@ import logging from typing import Optional -from pprint import pprint - -from open_webui.models.auths import Auths -from open_webui.models.groups import Groups -from open_webui.models.chats import Chats from open_webui.models.roles import ( RoleModel, Roles, RoleAddForm, ) - -from open_webui.socket.main import get_active_status_by_user_id from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS from fastapi import APIRouter, Depends, HTTPException, Request, status -from pydantic import BaseModel from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user -from open_webui.utils.access_control import get_permissions log = logging.getLogger(__name__) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 8702ae50bae..ba03acec4f2 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -23,6 +23,8 @@ from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user from open_webui.utils.access_control import get_permissions, has_permission +from open_webui.utils.access_control import get_permissions +from open_webui.models.permissions import Permissions log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -86,7 +88,7 @@ async def get_user_groups(user=Depends(get_verified_user)): @router.get("/permissions") async def get_user_permissisions(request: Request, user=Depends(get_verified_user)): user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS + user.id, Permissions.get_permissions_by_category() ) return user_permissions @@ -138,25 +140,13 @@ class UserPermissions(BaseModel): chat: ChatPermissions features: FeaturesPermissions - +# TODO: Fix the response model. @router.get("/default/permissions", response_model=UserPermissions) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): - return { - "workspace": WorkspacePermissions( - **request.app.state.config.USER_PERMISSIONS.get("workspace", {}) - ), - "sharing": SharingPermissions( - **request.app.state.config.USER_PERMISSIONS.get("sharing", {}) - ), - "chat": ChatPermissions( - **request.app.state.config.USER_PERMISSIONS.get("chat", {}) - ), - "features": FeaturesPermissions( - **request.app.state.config.USER_PERMISSIONS.get("features", {}) - ), - } - + return Permissions.get_permissions_by_category() +# TODO: Change to use database. +# TODO: Get config.USER_PERMISSIONS merged into database permissions. @router.post("/default/permissions") async def update_default_user_permissions( request: Request, form_data: UserPermissions, user=Depends(get_admin_user) From a76f8b8155547830492426acab63dbe3309e3f70 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 23 Apr 2025 12:08:58 +0200 Subject: [PATCH 12/31] Set default permission in database based on config --- backend/open_webui/config.py | 9 +--- backend/open_webui/main.py | 3 +- ...6df61a317_added_permissions_to_database.py | 53 ++++--------------- backend/open_webui/models/permissions.py | 48 ++++++++++++++--- backend/open_webui/routers/users.py | 1 - 5 files changed, 54 insertions(+), 60 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index b1955b056d2..31f047e4c4b 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -31,7 +31,7 @@ ) from open_webui.internal.db import Base, get_db from open_webui.utils.redis import get_redis_connection - +from open_webui.models.permissions import Permissions class EndpointFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: @@ -1130,7 +1130,6 @@ def oidc_oauth_register(client): os.environ.get("USER_PERMISSIONS_FEATURES_NOTES", "True").lower() == "true" ) - DEFAULT_USER_PERMISSIONS = { "workspace": { "models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS, @@ -1167,11 +1166,7 @@ def oidc_oauth_register(client): }, } -USER_PERMISSIONS = PersistentConfig( - "USER_PERMISSIONS", - "user.permissions", - DEFAULT_USER_PERMISSIONS, -) +USER_PERMISSIONS = Permissions.get_or_create_permissions(DEFAULT_USER_PERMISSIONS) ENABLE_CHANNELS = PersistentConfig( "ENABLE_CHANNELS", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index d4390edc5de..3459e8a8e80 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -589,6 +589,7 @@ async def lifespan(app: FastAPI): app.state.config.RESPONSE_WATERMARK = RESPONSE_WATERMARK app.state.config.USER_PERMISSIONS = USER_PERMISSIONS + app.state.config.WEBHOOK_URL = WEBHOOK_URL app.state.config.BANNERS = WEBUI_BANNERS app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST @@ -1407,7 +1408,7 @@ async def get_app_config(request: Request): "max_size": app.state.config.FILE_MAX_SIZE, "max_count": app.state.config.FILE_MAX_COUNT, }, - "permissions": {**app.state.config.USER_PERMISSIONS}, + "permissions": USER_PERMISSIONS, "google_drive": { "client_id": GOOGLE_DRIVE_CLIENT_ID.value, "api_key": GOOGLE_DRIVE_API_KEY.value, diff --git a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py index 936efdf81fd..c75d7fdddc5 100644 --- a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py +++ b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py @@ -9,7 +9,7 @@ from alembic import op import sqlalchemy as sa -import open_webui.internal.db +from sqlalchemy import Enum # revision identifiers, used by Alembic. @@ -20,55 +20,20 @@ def upgrade(): + # Create the enum type first + #op.execute("CREATE TYPE permissioncategory AS ENUM ('workspace', 'sharing', 'chat', 'features')") + # Create a permissions table. - permissions_table = op.create_table( + op.create_table( 'permissions', - sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('name', sa.String(), nullable=False), - sa.Column('category', sa.String(), nullable=False), + sa.Column('category', Enum('workspace', 'sharing', 'chat', 'features', name='permissioncategory'), nullable=False), sa.Column('description', sa.String()), sa.Column('default_value', sa.Boolean(), default=False), ) - # Insert default permissions - default_permissions = [ - # Workspace permissions - {'category': 'workspace', 'name': 'models', 'default_value': False}, - {'category': 'workspace', 'name': 'knowledge', 'default_value': False}, - {'category': 'workspace', 'name': 'prompts', 'default_value': False}, - {'category': 'workspace', 'name': 'tools', 'default_value': False}, - # Sharing permissions - {'category': 'sharing', 'name': 'public_models', 'default_value': True}, - {'category': 'sharing', 'name': 'public_knowledge', 'default_value': True}, - {'category': 'sharing', 'name': 'public_prompts', 'default_value': True}, - {'category': 'sharing', 'name': 'public_tools', 'default_value': True}, - # Chat permissions. - {'category': 'chat', 'name': 'controls', 'default_value': True}, - {'category': 'chat', 'name': 'file_upload', 'default_value': True}, - {'category': 'chat', 'name': 'delete', 'default_value': True}, - {'category': 'chat', 'name': 'edit', 'default_value': True}, - {'category': 'chat', 'name': 'stt', 'default_value': True}, - {'category': 'chat', 'name': 'tts', 'default_value': True}, - {'category': 'chat', 'name': 'call', 'default_value': True}, - {'category': 'chat', 'name': 'multiple_models', 'default_value': True}, - {'category': 'chat', 'name': 'temporary', 'default_value': True}, - {'category': 'chat', 'name': 'temporary_enforced', 'default_value': True}, - # Features permissions. - {'category': 'features', 'name': 'direct_tool_servers', 'default_value': False}, - {'category': 'features', 'name': 'web_search', 'default_value': True}, - {'category': 'features', 'name': 'image_generation', 'default_value': True}, - {'category': 'features', 'name': 'code_interpreter', 'default_value': True}, - ] - - for perm in default_permissions: - op.execute( - permissions_table.insert().values( - name=perm['name'], - category=perm['category'], - default_value=perm['default_value'] - ) - ) - - def downgrade(): op.drop_table('permissions') + # Drop the enum type + op.execute("DROP TYPE permissioncategory") diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index 9f217dcad25..dc85a8e4c4a 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -1,10 +1,10 @@ from enum import Enum -from typing import Optional +from typing import Dict, Any from open_webui.internal.db import Base, JSONField, get_db from pydantic import BaseModel, ConfigDict -from sqlalchemy import Boolean, Column, String, JSON, Enum as SQLAlchemyEnum +from sqlalchemy import Boolean, Column, String, Integer, Enum as SQLAlchemyEnum #################### # Role DB Schema @@ -19,7 +19,7 @@ class PermissionCategory(str, Enum): class Permission(Base): __tablename__ = "permissions" - id = Column(String, primary_key=True) + id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String, nullable=False) # workspace, sharing, chat, features category = Column(SQLAlchemyEnum(PermissionCategory), nullable=False) @@ -41,7 +41,7 @@ class PermissionsModel(BaseModel): #################### class PermissionsTable: - def get_permissions_by_category(self) -> dict[PermissionCategory, list[PermissionsModel]]: + def get_permissions_by_category(self) -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: query = db.query(Permission).order_by(Permission.id) @@ -50,11 +50,45 @@ def get_permissions_by_category(self) -> dict[PermissionCategory, list[Permissio # Group permissions by category permissions = {} for perm in all_permissions: - if perm.category not in permissions: - permissions[perm.category] = {} + if perm.category.value not in permissions: + permissions[perm.category.value] = {} - permissions[perm.category][perm.name] = perm.default_value + permissions[perm.category.value][perm.name] = perm.default_value return permissions + def get_or_create_permissions(self, default_permissions: dict[PermissionCategory, dict[str, bool]]) -> dict[PermissionCategory, dict[str, bool]]: + with get_db() as db: + # Check if any permissions exist + existing_count = db.query(Permission).count() + + if existing_count == 0: + # No permissions exist, initialize with defaults + for category_str, perms in default_permissions.items(): + # Convert string category to enum + try: + category = PermissionCategory(category_str) + except ValueError: + continue # Skip invalid categories + + for perm_name, default_value in perms.items(): + new_permission = Permission( + name=perm_name, + category=category, + default_value=default_value, + description=f"Default {category.value} permission for {perm_name}" + ) + db.add(new_permission) + + try: + db.commit() + except Exception as e: + db.rollback() + raise e + + # Return current permissions structure + return self.get_permissions_by_category() + + + Permissions = PermissionsTable() diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index ba03acec4f2..a5ffc6f67ef 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -146,7 +146,6 @@ async def get_default_user_permissions(request: Request, user=Depends(get_admin_ return Permissions.get_permissions_by_category() # TODO: Change to use database. -# TODO: Get config.USER_PERMISSIONS merged into database permissions. @router.post("/default/permissions") async def update_default_user_permissions( request: Request, form_data: UserPermissions, user=Depends(get_admin_user) From 70409a05d3cb8d19687a92ddfd10aa042b8b56c3 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 23 Apr 2025 16:16:46 +0200 Subject: [PATCH 13/31] Added support for adding new permission --- ...6df61a317_added_permissions_to_database.py | 3 --- backend/open_webui/models/permissions.py | 25 ++++++++++++++++--- backend/open_webui/routers/users.py | 10 ++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py index c75d7fdddc5..eb56fa67f3e 100644 --- a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py +++ b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py @@ -20,9 +20,6 @@ def upgrade(): - # Create the enum type first - #op.execute("CREATE TYPE permissioncategory AS ENUM ('workspace', 'sharing', 'chat', 'features')") - # Create a permissions table. op.create_table( 'permissions', diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index dc85a8e4c4a..6f33968417c 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -3,7 +3,7 @@ from open_webui.internal.db import Base, JSONField, get_db -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import Boolean, Column, String, Integer, Enum as SQLAlchemyEnum #################### @@ -27,8 +27,8 @@ class Permission(Base): default_value = Column(Boolean, default=False) -class PermissionsModel(BaseModel): - id: int +class PermissionModel(BaseModel): + id: int = Field(default=None, exclude=True) name: str category: PermissionCategory description: str | None @@ -57,6 +57,7 @@ def get_permissions_by_category(self) -> dict[PermissionCategory, dict[str, bool return permissions + # TODO: if config is not persistent enabled, just return default permissions def get_or_create_permissions(self, default_permissions: dict[PermissionCategory, dict[str, bool]]) -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: # Check if any permissions exist @@ -89,6 +90,22 @@ def get_or_create_permissions(self, default_permissions: dict[PermissionCategory # Return current permissions structure return self.get_permissions_by_category() - + def new_permission(self, permission: PermissionModel) -> PermissionModel | None: + from pprint import pprint + pprint(permission) + with get_db() as db: + result = Permission( + name=permission['name'], + category=permission['category'], + description=permission['description'], + default_value=permission['default_value'] + ) + db.add(result) + db.commit() + db.refresh(result) + if result: + return PermissionModel.model_validate(result) + else: + return None Permissions = PermissionsTable() diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index a5ffc6f67ef..5420bed7e5a 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -24,7 +24,7 @@ from open_webui.utils.access_control import get_permissions, has_permission from open_webui.utils.access_control import get_permissions -from open_webui.models.permissions import Permissions +from open_webui.models.permissions import Permissions, PermissionCategory, PermissionModel log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -141,17 +141,17 @@ class UserPermissions(BaseModel): features: FeaturesPermissions # TODO: Fix the response model. -@router.get("/default/permissions", response_model=UserPermissions) +@router.get("/default/permissions", response_model=dict[PermissionCategory, dict[str, bool]]) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): return Permissions.get_permissions_by_category() # TODO: Change to use database. @router.post("/default/permissions") async def update_default_user_permissions( - request: Request, form_data: UserPermissions, user=Depends(get_admin_user) + request: Request, form_data: PermissionModel, user=Depends(get_admin_user) ): - request.app.state.config.USER_PERMISSIONS = form_data.model_dump() - return request.app.state.config.USER_PERMISSIONS + + return Permissions.new_permission(form_data.model_dump()) ############################ From 0425618e8a35acb4f18305d4c7c6ffef008d30f3 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 24 Apr 2025 15:34:53 +0200 Subject: [PATCH 14/31] Added support for updateing permission --- backend/open_webui/config.py | 2 +- backend/open_webui/models/permissions.py | 37 +++++++++++++++++++++--- backend/open_webui/routers/auths.py | 8 ++--- backend/open_webui/routers/roles.py | 4 ++- backend/open_webui/routers/users.py | 36 +++++++++++++++++++---- 5 files changed, 71 insertions(+), 16 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 31f047e4c4b..22fc1051302 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1166,7 +1166,7 @@ def oidc_oauth_register(client): }, } -USER_PERMISSIONS = Permissions.get_or_create_permissions(DEFAULT_USER_PERMISSIONS) +USER_PERMISSIONS = Permissions.get_or_create(DEFAULT_USER_PERMISSIONS) ENABLE_CHANNELS = PersistentConfig( "ENABLE_CHANNELS", diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index 6f33968417c..68502770451 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -41,7 +41,7 @@ class PermissionModel(BaseModel): #################### class PermissionsTable: - def get_permissions_by_category(self) -> dict[PermissionCategory, dict[str, bool]]: + def get_by_category(self) -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: query = db.query(Permission).order_by(Permission.id) @@ -58,7 +58,7 @@ def get_permissions_by_category(self) -> dict[PermissionCategory, dict[str, bool return permissions # TODO: if config is not persistent enabled, just return default permissions - def get_or_create_permissions(self, default_permissions: dict[PermissionCategory, dict[str, bool]]) -> dict[PermissionCategory, dict[str, bool]]: + def get_or_create(self, default_permissions: dict[PermissionCategory, dict[str, bool]]) -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: # Check if any permissions exist existing_count = db.query(Permission).count() @@ -88,9 +88,9 @@ def get_or_create_permissions(self, default_permissions: dict[PermissionCategory raise e # Return current permissions structure - return self.get_permissions_by_category() + return self.get_by_category() - def new_permission(self, permission: PermissionModel) -> PermissionModel | None: + def add(self, permission: PermissionModel) -> PermissionModel | None: from pprint import pprint pprint(permission) with get_db() as db: @@ -108,4 +108,33 @@ def new_permission(self, permission: PermissionModel) -> PermissionModel | None: else: return None + def update(self, permission: PermissionModel) -> PermissionModel | None: + with get_db() as db: + try: + db_permission = db.query(Permission).filter_by( + name=permission['name'], + category=permission['category'] + ).first() + + if not db_permission: + return None + + db_permission.default_value = permission['default_value'] + if 'description' in permission: + db_permission.description = permission['description'] + + db.commit() + db.refresh(db_permission) + + return PermissionModel.model_validate(db_permission) + + except Exception: + db.rollback() + return None + + def exists(self, permission: PermissionModel) -> bool: + with get_db() as db: + result = db.query(Permission).filter_by(name=permission['name'], category=permission['category']).first() + return result is not None + Permissions = PermissionsTable() diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index e750e31cb48..d227c2c7599 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -109,7 +109,7 @@ async def get_session_user( ) user_permissions = get_permissions( - user.id, Permissions.get_permissions_by_category() + user.id, Permissions.get_by_category() ) return { @@ -330,7 +330,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): ) user_permissions = get_permissions( - user.id, Permissions.get_permissions_by_category() + user.id, Permissions.get_by_category() ) return { @@ -428,7 +428,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm): ) user_permissions = get_permissions( - user.id, Permissions.get_permissions_by_category() + user.id, Permissions.get_by_category() ) return { @@ -538,7 +538,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm): ) user_permissions = get_permissions( - user.id, Permissions.get_permissions_by_category() + user.id, Permissions.get_by_category() ) if user_count == 0: diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index d91ef27fbef..dd4ed704aa3 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -39,7 +39,7 @@ async def get_roles( @router.post("/", response_model=Optional[RoleModel]) async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)): - # Check if role already exists + # Check if the role already exists existing_role = Roles.get_role_by_name(name=form_data.role) if existing_role: raise HTTPException( @@ -65,3 +65,5 @@ async def delete_role_by_id(role_id: str, user=Depends(get_admin_user)): status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=ERROR_MESSAGES.DELETE_ROLE_ERROR, ) + +# TODO: Added end-point to update/add permissions on a given role. diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 5420bed7e5a..296d31be1ea 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -88,7 +88,7 @@ async def get_user_groups(user=Depends(get_verified_user)): @router.get("/permissions") async def get_user_permissisions(request: Request, user=Depends(get_verified_user)): user_permissions = get_permissions( - user.id, Permissions.get_permissions_by_category() + user.id, Permissions.get_by_category() ) return user_permissions @@ -143,15 +143,39 @@ class UserPermissions(BaseModel): # TODO: Fix the response model. @router.get("/default/permissions", response_model=dict[PermissionCategory, dict[str, bool]]) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): - return Permissions.get_permissions_by_category() + return Permissions.get_by_category() -# TODO: Change to use database. @router.post("/default/permissions") async def update_default_user_permissions( - request: Request, form_data: PermissionModel, user=Depends(get_admin_user) + request: Request, form_data: UserPermissions, user=Depends(get_admin_user) ): - - return Permissions.new_permission(form_data.model_dump()) + updated_permissions = [] + permissions_dict = form_data.model_dump() + + for category_str, permissions in permissions_dict.items(): + try: + category = PermissionCategory(category_str) + + for permission_name, value in permissions.items(): + permission_data = { + 'name': permission_name, + 'category': category, + 'default_value': value, + 'description': f"Default {category.value} permission for {permission_name}" + } + + if Permissions.exists(permission_data): + Permissions.update(permission_data) + else: + new_permission = Permissions.add(permission_data) + if new_permission: + updated_permissions.append(new_permission) + + except ValueError: + # Skip invalid categories + continue + + return updated_permissions ############################ From bd02edddb3eb744a941ca5f476b14fccbe5308e8 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 24 Apr 2025 15:44:19 +0200 Subject: [PATCH 15/31] Clean up permission routes and database --- .../04c6df61a317_added_permissions_to_database.py | 2 +- backend/open_webui/models/permissions.py | 14 +++++++------- backend/open_webui/routers/users.py | 3 +-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py index eb56fa67f3e..e6e99772d9a 100644 --- a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py +++ b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py @@ -27,7 +27,7 @@ def upgrade(): sa.Column('name', sa.String(), nullable=False), sa.Column('category', Enum('workspace', 'sharing', 'chat', 'features', name='permissioncategory'), nullable=False), sa.Column('description', sa.String()), - sa.Column('default_value', sa.Boolean(), default=False), + sa.Column('value', sa.Boolean(), default=False), ) def downgrade(): diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index 68502770451..d9a08e92a61 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -24,7 +24,7 @@ class Permission(Base): # workspace, sharing, chat, features category = Column(SQLAlchemyEnum(PermissionCategory), nullable=False) description = Column(String) - default_value = Column(Boolean, default=False) + value = Column(Boolean, default=False) class PermissionModel(BaseModel): @@ -32,7 +32,7 @@ class PermissionModel(BaseModel): name: str category: PermissionCategory description: str | None - default_value: bool + value: bool model_config = ConfigDict(from_attributes=True) @@ -53,7 +53,7 @@ def get_by_category(self) -> dict[PermissionCategory, dict[str, bool]]: if perm.category.value not in permissions: permissions[perm.category.value] = {} - permissions[perm.category.value][perm.name] = perm.default_value + permissions[perm.category.value][perm.name] = perm.value return permissions @@ -72,11 +72,11 @@ def get_or_create(self, default_permissions: dict[PermissionCategory, dict[str, except ValueError: continue # Skip invalid categories - for perm_name, default_value in perms.items(): + for perm_name, value in perms.items(): new_permission = Permission( name=perm_name, category=category, - default_value=default_value, + value=value, description=f"Default {category.value} permission for {perm_name}" ) db.add(new_permission) @@ -98,7 +98,7 @@ def add(self, permission: PermissionModel) -> PermissionModel | None: name=permission['name'], category=permission['category'], description=permission['description'], - default_value=permission['default_value'] + value=permission['value'] ) db.add(result) db.commit() @@ -119,7 +119,7 @@ def update(self, permission: PermissionModel) -> PermissionModel | None: if not db_permission: return None - db_permission.default_value = permission['default_value'] + db_permission.value = permission['value'] if 'description' in permission: db_permission.description = permission['description'] diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 296d31be1ea..3d44f3a2b1d 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -140,7 +140,6 @@ class UserPermissions(BaseModel): chat: ChatPermissions features: FeaturesPermissions -# TODO: Fix the response model. @router.get("/default/permissions", response_model=dict[PermissionCategory, dict[str, bool]]) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): return Permissions.get_by_category() @@ -160,7 +159,7 @@ async def update_default_user_permissions( permission_data = { 'name': permission_name, 'category': category, - 'default_value': value, + 'value': value, 'description': f"Default {category.value} permission for {permission_name}" } From 773940089ebfeb2816f3988a1195c6cebcc87980 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Mon, 28 Apr 2025 14:14:14 +0200 Subject: [PATCH 16/31] Added relationship berween permissions and roles --- backend/open_webui/config.py | 2 +- ...6df61a317_added_permissions_to_database.py | 15 +- backend/open_webui/models/permissions.py | 340 +++++++++++++++--- backend/open_webui/models/roles.py | 57 ++- backend/open_webui/routers/auths.py | 8 +- backend/open_webui/routers/roles.py | 119 +++++- backend/open_webui/routers/users.py | 19 +- 7 files changed, 467 insertions(+), 93 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 22fc1051302..8b2f866e37d 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1166,7 +1166,7 @@ def oidc_oauth_register(client): }, } -USER_PERMISSIONS = Permissions.get_or_create(DEFAULT_USER_PERMISSIONS) +USER_PERMISSIONS = Permissions.set_initial_permissions(DEFAULT_USER_PERMISSIONS) ENABLE_CHANNELS = PersistentConfig( "ENABLE_CHANNELS", diff --git a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py index e6e99772d9a..bcd7f4955cd 100644 --- a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py +++ b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py @@ -27,10 +27,21 @@ def upgrade(): sa.Column('name', sa.String(), nullable=False), sa.Column('category', Enum('workspace', 'sharing', 'chat', 'features', name='permissioncategory'), nullable=False), sa.Column('description', sa.String()), - sa.Column('value', sa.Boolean(), default=False), ) + # Create a role_permissions join table to allow many-to-many relationships between roles and permissions. + op.create_table( + 'role_permissions', + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.Column('value', sa.Boolean(), default=False), # Added value column here + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'permission_id') + ) + + def downgrade(): + op.drop_table('role_permissions') op.drop_table('permissions') - # Drop the enum type op.execute("DROP TYPE permissioncategory") diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index d9a08e92a61..53285c2facd 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -1,10 +1,12 @@ from enum import Enum -from typing import Dict, Any - -from open_webui.internal.db import Base, JSONField, get_db from pydantic import BaseModel, ConfigDict, Field -from sqlalchemy import Boolean, Column, String, Integer, Enum as SQLAlchemyEnum +from sqlalchemy.orm import relationship +from sqlalchemy import Column, String, Integer, Enum as SQLAlchemyEnum + +from open_webui.internal.db import Base, get_db +from open_webui.models.roles import RolePermission, Role + #################### # Role DB Schema @@ -16,15 +18,29 @@ class PermissionCategory(str, Enum): chat = "chat" features = "features" + +class RolePermissionModel(BaseModel): + role_id: int + permission_id: int + value: bool = False + + model_config = ConfigDict(from_attributes=True) + + class Permission(Base): __tablename__ = "permissions" - id = Column(Integer, primary_key=True, autoincrement=True) + id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String, nullable=False) - # workspace, sharing, chat, features category = Column(SQLAlchemyEnum(PermissionCategory), nullable=False) description = Column(String) - value = Column(Boolean, default=False) + + role_associations = relationship("RolePermission", back_populates="permission") + + # Convenience property to access roles directly if needed + @property + def roles(self): + return [assoc.role for assoc in self.role_associations] class PermissionModel(BaseModel): @@ -32,38 +48,88 @@ class PermissionModel(BaseModel): name: str category: PermissionCategory description: str | None - value: bool + value: bool = False model_config = ConfigDict(from_attributes=True) + #################### # Forms #################### +class PermissionRoleForm(BaseModel): + permission_name: str + category: PermissionCategory + value: bool = False + + +#################### +# Database operations +#################### + + class PermissionsTable: - def get_by_category(self) -> dict[PermissionCategory, dict[str, bool]]: + + # TODO: if config is not persistent enabled, just return default permissions + def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: + result = {} - query = db.query(Permission).order_by(Permission.id) - all_permissions = query.all() + # First, get the user role permissions as defaults if requesting permissions for a different role + if role_name != "user": + user_role = db.query(Role).filter_by(name="user").first() + if user_role: + # Query the RolePermission association objects for the user role + user_role_permissions = db.query(RolePermission).filter_by(role_id=user_role.id).all() - # Group permissions by category - permissions = {} - for perm in all_permissions: - if perm.category.value not in permissions: - permissions[perm.category.value] = {} + # Create a mapping of permission_id to value for user role + user_permission_values = {rp.permission_id: rp.value for rp in user_role_permissions} - permissions[perm.category.value][perm.name] = perm.value + # Get all permissions associated with user role + user_permissions = db.query(Permission).filter( + Permission.id.in_([rp.permission_id for rp in user_role_permissions]) + ).all() - return permissions + # Build default permissions dictionary + for perm in user_permissions: + category = perm.category.value + if category not in result: + result[category] = {} - # TODO: if config is not persistent enabled, just return default permissions - def get_or_create(self, default_permissions: dict[PermissionCategory, dict[str, bool]]) -> dict[PermissionCategory, dict[str, bool]]: + result[category][perm.name] = user_permission_values[perm.id] + + # Now get the specified role's permissions + target_role = db.query(Role).filter_by(name=role_name).first() + if not target_role: + # Return user defaults if target role doesn't exist + return result + + target_role_permissions = db.query(RolePermission).filter_by(role_id=target_role.id).all() + target_permission_values = {rp.permission_id: rp.value for rp in target_role_permissions} + target_permissions = db.query(Permission).filter( + Permission.id.in_([rp.permission_id for rp in target_role_permissions]) + ).all() + + # Merge target role permissions with defaults + for perm in target_permissions: + category = perm.category.value + if category not in result: + result[category] = {} + + # Override default with target role's value + result[category][perm.name] = target_permission_values[perm.id] + + return result + + def set_initial_permissions(self, default_permissions: dict[PermissionCategory, dict[str, bool]], + role_name: str = "user") -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: # Check if any permissions exist existing_count = db.query(Permission).count() if existing_count == 0: + role = db.query(Role).filter_by(name=role_name).order_by(Role.id).first() + # No permissions exist, initialize with defaults for category_str, perms in default_permissions.items(): # Convert string category to enum @@ -76,10 +142,18 @@ def get_or_create(self, default_permissions: dict[PermissionCategory, dict[str, new_permission = Permission( name=perm_name, category=category, - value=value, description=f"Default {category.value} permission for {perm_name}" ) db.add(new_permission) + db.flush() + + # Create the association with the value + role_permission = RolePermission( + role_id=role.id, + permission_id=new_permission.id, + value=value + ) + db.add(role_permission) try: db.commit() @@ -88,27 +162,77 @@ def get_or_create(self, default_permissions: dict[PermissionCategory, dict[str, raise e # Return current permissions structure - return self.get_by_category() + return self.get_ordre_by_category() - def add(self, permission: PermissionModel) -> PermissionModel | None: - from pprint import pprint - pprint(permission) + def get(self, permission_name: str, category: PermissionCategory): with get_db() as db: - result = Permission( - name=permission['name'], - category=permission['category'], - description=permission['description'], - value=permission['value'] - ) - db.add(result) - db.commit() - db.refresh(result) - if result: - return PermissionModel.model_validate(result) - else: + try: + # Find the permission + db_permission = db.query(Permission).filter_by( + name=permission_name, + category=category + ).first() + + if not db_permission: + return None + + # Create and return a PermissionModel instance + return PermissionModel( + id=db_permission.id, + name=db_permission.name, + category=db_permission.category, + description=db_permission.description, + # Default value to False as we don't have a role context + value=False + ) + + except Exception as e: + print(f"Error getting permission: {e}") + return None + + def add(self, permission: PermissionModel, role_name: str = "user") -> PermissionModel | None: + with get_db() as db: + try: + role = db.query(Role).filter_by(name=role_name).order_by(Role.id).first() + if not role: + return None + + existing_permission = db.query(Permission).filter_by( + name=permission.name, + category=permission.category + ).first() + + if existing_permission: + # Permission exists. Fail, as one should use the `add_default_permission_to_role` end-point. + return None + + new_permission = Permission( + name=permission.name, + category=permission.category, + description=permission.description, + ) + db.add(new_permission) + db.flush() + + # Create the association with the value + role_permission = RolePermission( + role_id=role.id, + permission_id=new_permission.id, + value=permission.value + ) + db.add(role_permission) + + db.commit() + db.refresh(new_permission) + + return PermissionModel.model_validate(new_permission) + + except Exception as e: + print(f"Error adding permission: {e}") + db.rollback() return None - def update(self, permission: PermissionModel) -> PermissionModel | None: + def update(self, permission: PermissionModel, role_name: str = "user") -> PermissionModel | None: with get_db() as db: try: db_permission = db.query(Permission).filter_by( @@ -119,22 +243,146 @@ def update(self, permission: PermissionModel) -> PermissionModel | None: if not db_permission: return None - db_permission.value = permission['value'] + role = db.query(Role).filter_by(name=role_name).first() + if not role: + return None + + association = db.query(RolePermission).filter_by( + role_id=role.id, + permission_id=db_permission.id + ).first() + association.value = permission['value'] + if 'description' in permission: db_permission.description = permission['description'] db.commit() - db.refresh(db_permission) - return PermissionModel.model_validate(db_permission) + return PermissionModel( + id=db_permission.id, + name=db_permission.name, + category=db_permission.category, + description=db_permission.description, + value=association.value + ) - except Exception: - db.rollback() + except Exception as e: + print(f"Error updating permission: {e}") + db.rollback() + return None + + def delete(self, permission_name: str, category: PermissionCategory) -> bool: + with get_db() as db: + try: + # Find the permission + db_permission = db.query(Permission).filter_by( + name=permission_name, + category=category + ).first() + + if not db_permission: + return False + + # Delete all role associations first to avoid foreign key constraints + db.query(RolePermission).filter_by( + permission_id=db_permission.id + ).delete() + + # Then delete the permission itself + db.delete(db_permission) + db.commit() + return True + + except Exception as e: + print(f"Error deleting permission: {e}") + db.rollback() + return False + + def link(self, permission_id: int, role_id: int, value: bool = False) -> RolePermissionModel | None: + with get_db() as db: + try: + # Check if permission exists + permission = db.query(Permission).filter_by(id=permission_id).first() + if not permission: return None - def exists(self, permission: PermissionModel) -> bool: + # Check if role exists + role = db.query(Role).filter_by(id=role_id).first() + if not role: + return None + + # Check if the link already exists + existing_link = db.query(RolePermission).filter_by( + role_id=role_id, + permission_id=permission_id + ).first() + + if existing_link: + # If link already exists, update its value + existing_link.value = value + db.commit() + return RolePermissionModel.model_validate(existing_link) + + # Create new association + role_permission = RolePermission( + role_id=role_id, + permission_id=permission_id, + value=value + ) + from pprint import pprint + pprint(role_permission) + + db.add(role_permission) + db.commit() + db.refresh(role_permission) + + return RolePermissionModel.model_validate(role_permission) + + except Exception as e: + print(f"Error linking permission to role: {e}") + db.rollback() + return None + + def unlink(self, permission_id: int, role_id: int) -> bool: with get_db() as db: - result = db.query(Permission).filter_by(name=permission['name'], category=permission['category']).first() - return result is not None + try: + # Find the association + deleted = db.query(RolePermission).filter_by( + role_id=role_id, + permission_id=permission_id + ).delete() + + db.commit() + return deleted > 0 + + except Exception as e: + print(f"Error unlinking permission from role: {e}") + db.rollback() + return False + + def exists(self, permission: PermissionModel, role_name: str = "user") -> bool: + with get_db() as db: + # First find the role + role = db.query(Role).filter_by(name=role_name).first() + if not role: + return False + + # Then check if there's a permission with this name and category + db_permission = db.query(Permission).filter_by( + name=permission['name'], + category=permission['category'] + ).first() + + if not db_permission: + return False + + # Finally, check if there's an association between them + association = db.query(RolePermission).filter_by( + role_id=role.id, + permission_id=db_permission.id + ).first() + + return association is not None + Permissions = PermissionsTable() diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py index e0cea8f18ce..b7707669ba5 100644 --- a/backend/open_webui/models/roles.py +++ b/backend/open_webui/models/roles.py @@ -1,34 +1,56 @@ import time + from typing import Optional +from pydantic import BaseModel, ConfigDict +from sqlalchemy import Boolean, BigInteger, Column, String, Integer, ForeignKey +from sqlalchemy.orm import relationship -from open_webui.internal.db import Base, JSONField, get_db +from open_webui.internal.db import Base, get_db -from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Integer #################### -# Role DB Schema +# Association table for the many-to-many relationship (role <-> permission) with value #################### +class RolePermission(Base): + __tablename__ = 'role_permissions' + + role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True) + permission_id = Column(Integer, ForeignKey('permissions.id'), primary_key=True) + value = Column(Boolean, default=False) + + # Add relationships to both sides + role = relationship("Role", back_populates="permission_associations") + permission = relationship("Permission", back_populates="role_associations") +#################### +# Role DB Schema +#################### + class Role(Base): __tablename__ = "roles" id = Column(Integer, primary_key=True) name = Column(String) - updated_at = Column(BigInteger) created_at = Column(BigInteger) + permission_associations = relationship("RolePermission", back_populates="role") + + @property + def permissions(self): + return [assoc.permission for assoc in self.permission_associations] + + class RoleModel(BaseModel): id: int name: str - - updated_at: int # timestamp in epoch - created_at: int # timestamp in epoch + updated_at: int + created_at: int model_config = ConfigDict(from_attributes=True) + #################### # Forms #################### @@ -36,10 +58,11 @@ class RoleModel(BaseModel): class RoleAddForm(BaseModel): role: str + class RolesTable: def insert_new_role( - self, - name: str, + self, + name: str, ) -> Optional[RoleModel]: with get_db() as db: result = Role( @@ -52,8 +75,8 @@ def insert_new_role( db.refresh(result) if result: return RoleModel.model_validate(result) - else: - return None + + return None def get_role_by_id(self, id: str) -> Optional[RoleModel]: try: @@ -71,9 +94,7 @@ def get_role_by_name(self, name: str) -> Optional[RoleModel]: except Exception: return None - def get_roles( - self, skip: Optional[int] = None, limit: Optional[int] = None - ) -> list[RoleModel]: + def get_roles(self, skip: Optional[int] = None, limit: Optional[int] = None) -> list[RoleModel]: with get_db() as db: query = db.query(Role).order_by(Role.id) @@ -110,7 +131,11 @@ def add_role_if_role_do_not_exists(self, role_name: str) -> Optional[RoleModel]: # Role is allowed and doesn't exist, so create it try: return self.insert_new_role(name=role_name) - except Exception as e: + except Exception: return None + def exists(self, role_name: str) -> bool: + return self.get_role_by_name(role_name) is not None + + Roles = RolesTable() diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index d227c2c7599..bbdbd33c2bb 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -109,7 +109,7 @@ async def get_session_user( ) user_permissions = get_permissions( - user.id, Permissions.get_by_category() + user.id, Permissions.get_ordre_by_category(user.role) ) return { @@ -330,7 +330,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): ) user_permissions = get_permissions( - user.id, Permissions.get_by_category() + user.id, Permissions.get_ordre_by_category(user.role) ) return { @@ -428,7 +428,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm): ) user_permissions = get_permissions( - user.id, Permissions.get_by_category() + user.id, Permissions.get_ordre_by_category(user.role) ) return { @@ -538,7 +538,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm): ) user_permissions = get_permissions( - user.id, Permissions.get_by_category() + user.id, Permissions.get_ordre_by_category(user.role) ) if user_count == 0: diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index dd4ed704aa3..f6a21cf0aa5 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -1,17 +1,21 @@ import logging from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, status +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from open_webui.utils.auth import get_admin_user from open_webui.models.roles import ( RoleModel, Roles, - RoleAddForm, + RoleAddForm +) +from open_webui.models.permissions import ( + Permissions, + PermissionModel, + PermissionCategory, + PermissionRoleForm ) - -from open_webui.constants import ERROR_MESSAGES -from open_webui.env import SRC_LOG_LEVELS -from fastapi import APIRouter, Depends, HTTPException, Request, status - -from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user log = logging.getLogger(__name__) @@ -19,19 +23,17 @@ router = APIRouter() + ############################ # GetRoles ############################ @router.get("/", response_model=list[RoleModel]) -async def get_roles( - skip: Optional[int] = None, - limit: Optional[int] = None, - user=Depends(get_admin_user), -): +async def get_roles(skip: Optional[int] = None, limit: Optional[int] = None, user=Depends(get_admin_user)): return Roles.get_roles(skip, limit) + ############################ # AddRole ############################ @@ -54,6 +56,7 @@ async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)): # DeleteRoleById ############################ + # TODO(jeskr): Check if role is used by any users before deleting it. @router.delete("/{role_id}", response_model=bool) async def delete_role_by_id(role_id: str, user=Depends(get_admin_user)): @@ -66,4 +69,94 @@ async def delete_role_by_id(role_id: str, user=Depends(get_admin_user)): detail=ERROR_MESSAGES.DELETE_ROLE_ERROR, ) -# TODO: Added end-point to update/add permissions on a given role. + +############################ +# GetPermissionForRole +############################ + + +@router.get("/{role_name}/permissions") +async def get_default_permissions_by_role_name(role_name: str, user=Depends(get_admin_user)): + if not Roles.exists(role_name): + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Role do not exists. Please check the role name and try again.', + ) + + permissions = Permissions.get_ordre_by_category(role_name) + + if permissions: + return permissions + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Permissions fetch failed. Please try again later.', + ) + + +############################ +# AddNewPermissionToRole +############################ + +@router.post("/{role_name}/permission") +async def add_new_default_permission_with_role(role_name: str, form_data: PermissionModel, + user=Depends(get_admin_user)): + perm = Permissions.add(permission=form_data, role_name=role_name) + + if perm: + return True + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Something went wrong. Please try again.', + ) + + +############################ +# DeletePermissionFromRole +########################### + +@router.post("/{role_name}/permission/link") +async def link_default_permission_to_role(role_name: str, form_data: PermissionRoleForm, user=Depends(get_admin_user)): + permission = Permissions.get(form_data.permission_name, form_data.category) + if not permission: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Permission with name '{form_data.permission_name}' and category '{form_data.category}' not found.", + ) + + role = Roles.get_role_by_name(role_name) + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Role with name '{role_name}' not found.", + ) + + result = Permissions.link(permission_id=permission.id, role_id=role.id, value=form_data.value) + if result: + return True + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Something went wrong. Please try again.', + ) + + +@router.delete("/{role_id}/permission/{permission_category}/{permission_name}") +async def unlink_default_permission_from_role(role_id: int, permission_name: str, category: PermissionCategory, + user=Depends(get_admin_user)): + permission = Permissions.get(permission_name, category) + if not permission: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Permission with name '{permission_name}' and category '{category}' not found.", + ) + + result = Permissions.unlink(permission_id=permission.id, role_id=role_id) + if result: + return True + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Something went wrong. Please try again.', + ) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 3d44f3a2b1d..12539cdd31d 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -20,9 +20,9 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status from pydantic import BaseModel -from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user from open_webui.utils.access_control import get_permissions, has_permission +from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user, get_current_user from open_webui.utils.access_control import get_permissions from open_webui.models.permissions import Permissions, PermissionCategory, PermissionModel @@ -88,7 +88,7 @@ async def get_user_groups(user=Depends(get_verified_user)): @router.get("/permissions") async def get_user_permissisions(request: Request, user=Depends(get_verified_user)): user_permissions = get_permissions( - user.id, Permissions.get_by_category() + user.id, Permissions.get_ordre_by_category(user.role) ) return user_permissions @@ -142,13 +142,10 @@ class UserPermissions(BaseModel): @router.get("/default/permissions", response_model=dict[PermissionCategory, dict[str, bool]]) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): - return Permissions.get_by_category() + return Permissions.get_ordre_by_category() @router.post("/default/permissions") -async def update_default_user_permissions( - request: Request, form_data: UserPermissions, user=Depends(get_admin_user) -): - updated_permissions = [] +async def update_default_user_permissions(form_data: UserPermissions, user=Depends(get_admin_user)): permissions_dict = form_data.model_dump() for category_str, permissions in permissions_dict.items(): @@ -164,17 +161,17 @@ async def update_default_user_permissions( } if Permissions.exists(permission_data): + print(f"Updating existing permission: {permission_data}") Permissions.update(permission_data) else: - new_permission = Permissions.add(permission_data) - if new_permission: - updated_permissions.append(new_permission) + print(f"Adding new permission: {permission_data}") + Permissions.add(permission_data) except ValueError: # Skip invalid categories continue - return updated_permissions + return Permissions.get_ordre_by_category() ############################ From ad71a10781ced57f4a049601e3647fbd5ba99b63 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 29 Apr 2025 10:44:22 +0200 Subject: [PATCH 17/31] Changed roles API end-point to use role names --- backend/open_webui/routers/roles.py | 33 +++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index f6a21cf0aa5..2a1eccfee48 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -58,9 +58,16 @@ async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)): # TODO(jeskr): Check if role is used by any users before deleting it. -@router.delete("/{role_id}", response_model=bool) -async def delete_role_by_id(role_id: str, user=Depends(get_admin_user)): - result = Roles.delete_by_id(role_id) +@router.delete("/{role_name}", response_model=bool) +async def delete_role_by_id(role_name: str, user=Depends(get_admin_user)): + role = Roles.get_role_by_name(role_name) + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Role with name '{role_name}' not found.", + ) + + result = Roles.delete_by_id(role.id) if result: return True @@ -142,9 +149,21 @@ async def link_default_permission_to_role(role_name: str, form_data: PermissionR ) -@router.delete("/{role_id}/permission/{permission_category}/{permission_name}") -async def unlink_default_permission_from_role(role_id: int, permission_name: str, category: PermissionCategory, - user=Depends(get_admin_user)): +@router.delete("/{role_name}/permission/{permission_category}/{permission_name}") +async def unlink_default_permission_from_role( + role_name: str, + permission_name: str, + category: PermissionCategory, + user=Depends(get_admin_user) + ): + + role = Roles.get_role_by_name(role_name) + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Role with name '{role_name}' not found.", + ) + permission = Permissions.get(permission_name, category) if not permission: raise HTTPException( @@ -152,7 +171,7 @@ async def unlink_default_permission_from_role(role_id: int, permission_name: str detail=f"Permission with name '{permission_name}' and category '{category}' not found.", ) - result = Permissions.unlink(permission_id=permission.id, role_id=role_id) + result = Permissions.unlink(permission_id=permission.id, role_id=role.id) if result: return True From 1dd3b5a272b1b092b5e4e25c16a4cef12cbaaddd Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 29 Apr 2025 21:36:03 +0200 Subject: [PATCH 18/31] Adding support for roles API in frontend --- backend/open_webui/models/roles.py | 10 +- backend/open_webui/routers/roles.py | 20 +- src/lib/apis/roles/index.ts | 131 ++++++++- src/lib/components/admin/Users.svelte | 29 +- .../components/admin/Users/RoleList.svelte | 271 ++++++++++++++++++ .../admin/Users/Roles/AddRoleModal.svelte | 114 ++++++++ .../admin/Users/Roles/EditRoleModal.svelte | 186 ++++++++++++ src/lib/components/common/Permissions.svelte | 96 +++++++ 8 files changed, 849 insertions(+), 8 deletions(-) create mode 100644 src/lib/components/admin/Users/RoleList.svelte create mode 100644 src/lib/components/admin/Users/Roles/AddRoleModal.svelte create mode 100644 src/lib/components/admin/Users/Roles/EditRoleModal.svelte create mode 100644 src/lib/components/common/Permissions.svelte diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py index b7707669ba5..916124e556c 100644 --- a/backend/open_webui/models/roles.py +++ b/backend/open_webui/models/roles.py @@ -55,7 +55,7 @@ class RoleModel(BaseModel): # Forms #################### -class RoleAddForm(BaseModel): +class RoleForm(BaseModel): role: str @@ -94,6 +94,14 @@ def get_role_by_name(self, name: str) -> Optional[RoleModel]: except Exception: return None + def update_name_by_id(self, role_id: str, name: str) -> Optional[RoleModel]: + with get_db() as db: + db.query(Role).filter_by(id=role_id).update( + {"name": name, "updated_at": int(time.time())} + ) + db.commit() + return self.get_role_by_id(role_id) + def get_roles(self, skip: Optional[int] = None, limit: Optional[int] = None) -> list[RoleModel]: with get_db() as db: diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index 2a1eccfee48..ee10055b100 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -8,7 +8,7 @@ from open_webui.models.roles import ( RoleModel, Roles, - RoleAddForm + RoleForm ) from open_webui.models.permissions import ( Permissions, @@ -40,7 +40,7 @@ async def get_roles(skip: Optional[int] = None, limit: Optional[int] = None, use @router.post("/", response_model=Optional[RoleModel]) -async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)): +async def add_role(form_data: RoleForm, user=Depends(get_admin_user)): # Check if the role already exists existing_role = Roles.get_role_by_name(name=form_data.role) if existing_role: @@ -52,6 +52,22 @@ async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)): return Roles.insert_new_role(name=form_data.role) +############################ +# UpdateRoleById +############################ + +@router.post("/{role_id}", response_model=Optional[RoleModel]) +async def update_role_name(role_id: int, form_data: RoleForm, user=Depends(get_admin_user)): + # Check if the role already exists + existing_role = Roles.get_role_by_id(role_id) + if not existing_role: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Role with name '{form_data.role}' do not exists" + ) + + return Roles.update_name_by_id(role_id, form_data.role) + ############################ # DeleteRoleById ############################ diff --git a/src/lib/apis/roles/index.ts b/src/lib/apis/roles/index.ts index 310955f25da..bc5bc164c32 100644 --- a/src/lib/apis/roles/index.ts +++ b/src/lib/apis/roles/index.ts @@ -28,7 +28,7 @@ export const getRoles = async (token: string) => { return res; }; -export const addRole = async (token: string, role: string) => { +export const addRole = async (token: string, roleName: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/roles/`, { @@ -38,7 +38,7 @@ export const addRole = async (token: string, role: string) => { Authorization: `Bearer ${token}` }, body: JSON.stringify({ - role: role + role: roleName }) }) .then(async (res) => { @@ -58,10 +58,40 @@ export const addRole = async (token: string, role: string) => { return res; }; -export const deleteRole = async (token: string, roleId: string) => { +export const updateRole = async (token: string, roleId: number, roleName: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + role: roleName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteRole = async (token: string, roleName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', @@ -83,4 +113,97 @@ export const deleteRole = async (token: string, roleId: string) => { } return res; -}; \ No newline at end of file +}; + +export const getRolePermissions = async (token: string, roleName: string ) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permissions`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +// GET /api/v1/roles/{role_name}/permission Add New Default Permission With Role +// { +// "name": "string", +// "category": "workspace", +// "description": "string", +// "value": false +// } + +export const linkRoleToPermissions = async (token: string, roleName: string, categoryName: string, permissionName: string, value: boolean ) => { + let error = null; + const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission/link`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + permission_name: permissionName, + category: categoryName, + value: value + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const unlinkRoleFromPermissions = async (token: string, roleName: string, categoryName: string, permissionName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission/${categoryName}/${permissionName}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/admin/Users.svelte b/src/lib/components/admin/Users.svelte index e777757e0ea..8a6e4e040c4 100644 --- a/src/lib/components/admin/Users.svelte +++ b/src/lib/components/admin/Users.svelte @@ -6,6 +6,7 @@ import { user } from '$lib/stores'; import UserList from './Users/UserList.svelte'; + import Roles from './Users/RoleList.svelte'; import Groups from './Users/Groups.svelte'; const i18n = getContext('i18n'); @@ -85,13 +86,39 @@
{$i18n.t('Groups')}
+ +
{#if selectedTab === 'overview'} {:else if selectedTab === 'groups'} - + + {:else if selectedTab === 'roles'} + {/if}
diff --git a/src/lib/components/admin/Users/RoleList.svelte b/src/lib/components/admin/Users/RoleList.svelte new file mode 100644 index 00000000000..8b23377960d --- /dev/null +++ b/src/lib/components/admin/Users/RoleList.svelte @@ -0,0 +1,271 @@ + + + { + deleteRoleHandler(selectedRole.name); + }} +/> + +{#key selectedRole} + { + roles = await getRoles(localStorage.token); + }} + /> +{/key} + + { + roles = await getRoles(localStorage.token); + }} +/> + +
+
+
+ {$i18n.t('Roles')} +
+
+
+ +
+
+
+ + + +
+
+
+
+ +
+ + + + + + + + + + + {#each roles as role, roleIdx} + + + + + + + + + {/each} + +
setSortKey('role')}> +
+ {$i18n.t('Identifier')} + + {#if sortKey === 'role'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('name')}> +
+ {$i18n.t('Name')} + + {#if sortKey === 'name'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('created_at')}> +
+ {$i18n.t('Created at')} + {#if sortKey === 'created_at'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
+
+
{role.id}
+
+
+
{role.name}
+
+
+ {dayjs(role.created_at * 1000).format('LL')} + +
+ + + + + {#if !lockedRoles.includes(role.name)} + + + + {/if} + +
+
+
diff --git a/src/lib/components/admin/Users/Roles/AddRoleModal.svelte b/src/lib/components/admin/Users/Roles/AddRoleModal.svelte new file mode 100644 index 00000000000..4a172e9beb6 --- /dev/null +++ b/src/lib/components/admin/Users/Roles/AddRoleModal.svelte @@ -0,0 +1,114 @@ + + + +
+
+
{$i18n.t('Add Role')}
+ +
+ +
+
+
{ + submitHandler(); + }}> + +
+
{$i18n.t('Name')}
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+
+
diff --git a/src/lib/components/admin/Users/Roles/EditRoleModal.svelte b/src/lib/components/admin/Users/Roles/EditRoleModal.svelte new file mode 100644 index 00000000000..e04fbeb3833 --- /dev/null +++ b/src/lib/components/admin/Users/Roles/EditRoleModal.svelte @@ -0,0 +1,186 @@ + + + +
+
+
{$i18n.t('Edit Role')}
+ +
+
+ +
+
+
{ + e.preventDefault(); + submitHandler(); + }} + > +
+
+ {#if tabs.includes('general')} + + {/if} + + {#if tabs.includes('permissions')} + + {/if} +
+ +
+ {#if selectedTab === 'general'} +
+
{$i18n.t('Name')}
+ +
+ +
+
+ {:else if selectedTab === 'permissions'} + + {/if} +
+
+ +
+ +
+
+
+
+
+
diff --git a/src/lib/components/common/Permissions.svelte b/src/lib/components/common/Permissions.svelte new file mode 100644 index 00000000000..098d3d5f774 --- /dev/null +++ b/src/lib/components/common/Permissions.svelte @@ -0,0 +1,96 @@ + + +
+ {#each Object.entries(permissions) as [categoryName, category], index} + {#if index !== 0} +
+ {/if} +
+
{$i18n.t(formatPermissionCategory(categoryName))}
+ + {#each Object.entries(category) as [key, value]} +
+
+ {$i18n.t(formatPermissionName(key))} +
+ + +
+ {/each} +
+ {/each} +
From 8054aac75960b1e94ba012a7607a01537080ccbd Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 30 Apr 2025 20:35:27 +0200 Subject: [PATCH 19/31] Updated default permission API to include all permissions --- backend/open_webui/models/permissions.py | 54 +++++++++++++++++++++++- backend/open_webui/routers/roles.py | 5 ++- backend/open_webui/routers/users.py | 2 +- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index 53285c2facd..33b199a35cc 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -70,7 +70,46 @@ class PermissionRoleForm(BaseModel): class PermissionsTable: - # TODO: if config is not persistent enabled, just return default permissions + def get_default(self) -> dict[PermissionCategory, dict[str, bool]]: + with get_db() as db: + result = {} + + # First, get the user role permissions as defaults if requesting permissions for a different role + user_role = db.query(Role).filter_by(name="user").first() + if user_role: + # Query the RolePermission association objects for the user role + user_role_permissions = db.query(RolePermission).filter_by(role_id=user_role.id).all() + + # Create a mapping of permission_id to value for user role + user_permission_values = {rp.permission_id: rp.value for rp in user_role_permissions} + + # Get all permissions associated with user role + user_permissions = db.query(Permission).filter( + Permission.id.in_([rp.permission_id for rp in user_role_permissions]) + ).all() + + # Build default permissions dictionary + for perm in user_permissions: + category = perm.category.value + if category not in result: + result[category] = {} + + result[category][perm.name] = user_permission_values[perm.id] + + # Add all permissions from the permission table that are missing in the result + # with a default value of False + all_permissions = db.query(Permission).all() + for perm in all_permissions: + category = perm.category.value + if category not in result: + result[category] = {} + + # Only add if not already in result + if perm.name not in result[category]: + result[category][perm.name] = False + + return result + def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: result = {} @@ -119,8 +158,21 @@ def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCateg # Override default with target role's value result[category][perm.name] = target_permission_values[perm.id] + # Add all permissions from the permission table that are missing in the result + # with a default value of False + all_permissions = db.query(Permission).all() + for perm in all_permissions: + category = perm.category.value + if category not in result: + result[category] = {} + + # Only add if not already in result + if perm.name not in result[category]: + result[category][perm.name] = False + return result + # TODO: if config is not persistent enabled, just override permissions given def set_initial_permissions(self, default_permissions: dict[PermissionCategory, dict[str, bool]], role_name: str = "user") -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index ee10055b100..5356ed92fb4 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -136,7 +136,7 @@ async def add_new_default_permission_with_role(role_name: str, form_data: Permis ############################ -# DeletePermissionFromRole +# LinkPermissionToRole ########################### @router.post("/{role_name}/permission/link") @@ -164,6 +164,9 @@ async def link_default_permission_to_role(role_name: str, form_data: PermissionR detail='Something went wrong. Please try again.', ) +############################ +# UnlinkPermissionToRole +########################### @router.delete("/{role_name}/permission/{permission_category}/{permission_name}") async def unlink_default_permission_from_role( diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 12539cdd31d..bc951f8bfd0 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -142,7 +142,7 @@ class UserPermissions(BaseModel): @router.get("/default/permissions", response_model=dict[PermissionCategory, dict[str, bool]]) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): - return Permissions.get_ordre_by_category() + return Permissions.get_default() @router.post("/default/permissions") async def update_default_user_permissions(form_data: UserPermissions, user=Depends(get_admin_user)): From 351f1ceb93760af70d1afa2de3d6ef52feaad447 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 30 Apr 2025 21:06:06 +0200 Subject: [PATCH 20/31] Adding support for new permission in UI --- src/lib/apis/roles/index.ts | 38 ++++++++++--- .../admin/Users/Roles/AddPermission.svelte | 56 +++++++++++++++++++ .../admin/Users/Roles/EditRoleModal.svelte | 35 +++++++++++- src/lib/components/common/Permissions.svelte | 36 +----------- 4 files changed, 120 insertions(+), 45 deletions(-) create mode 100644 src/lib/components/admin/Users/Roles/AddPermission.svelte diff --git a/src/lib/apis/roles/index.ts b/src/lib/apis/roles/index.ts index bc5bc164c32..90f9b371414 100644 --- a/src/lib/apis/roles/index.ts +++ b/src/lib/apis/roles/index.ts @@ -142,13 +142,37 @@ export const getRolePermissions = async (token: string, roleName: string ) => { return res; }; -// GET /api/v1/roles/{role_name}/permission Add New Default Permission With Role -// { -// "name": "string", -// "category": "workspace", -// "description": "string", -// "value": false -// } +export const addNewPermission = async (token: string, roleName: string, permissionName: string, categoryName: string, description: string, value: boolean ) => { + let error = null; + const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: permissionName, + category: categoryName, + description: description, + value: value + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; export const linkRoleToPermissions = async (token: string, roleName: string, categoryName: string, permissionName: string, value: boolean ) => { let error = null; diff --git a/src/lib/components/admin/Users/Roles/AddPermission.svelte b/src/lib/components/admin/Users/Roles/AddPermission.svelte new file mode 100644 index 00000000000..2ca7dfed3b7 --- /dev/null +++ b/src/lib/components/admin/Users/Roles/AddPermission.svelte @@ -0,0 +1,56 @@ + + +
+
+
{$i18n.t('Add Role')}
+ +
+ +
+
+
+
+
{$i18n.t('Name')}
+ +
+ +
+
+
+
+
+
diff --git a/src/lib/components/admin/Users/Roles/EditRoleModal.svelte b/src/lib/components/admin/Users/Roles/EditRoleModal.svelte index e04fbeb3833..56103c42d4d 100644 --- a/src/lib/components/admin/Users/Roles/EditRoleModal.svelte +++ b/src/lib/components/admin/Users/Roles/EditRoleModal.svelte @@ -5,11 +5,14 @@ import { onMount, getContext } from 'svelte'; import { updateRole, getRolePermissions, linkRoleToPermissions } from '$lib/apis/roles'; + import { getUserDefaultPermissions } from '$lib/apis/users' import Modal from '$lib/components/common/Modal.svelte'; import localizedFormat from 'dayjs/plugin/localizedFormat'; import Permissions from "$lib/components/common/Permissions.svelte"; import WrenchSolid from "$lib/components/icons/WrenchSolid.svelte"; + import Plus from "$lib/components/icons/Plus.svelte"; + import AddPermission from "$lib/components/admin/Users/Roles/AddPermission.svelte"; const i18n = getContext('i18n'); const dispatch = createEventDispatcher(); @@ -17,10 +20,11 @@ export let show = false; export let selectedRole; - export let tabs = ['general', 'permissions']; + export let tabs = ['general', 'permissions', 'new']; export let lockedRoles = ['pending', 'user', 'admin'] let selectedTab = 'general'; + let defaultPermissions = {}; let permissions = {}; let _role = { id: '', @@ -52,6 +56,11 @@ if (selectedRole) { _role = selectedRole; + defaultPermissions = await getUserDefaultPermissions(localStorage.token).catch((error) => { + toast.error(`${error}`); + return []; + }); + permissions = await getRolePermissions(localStorage.token, _role.name).catch((error) => { toast.error(`${error}`); return []; @@ -141,7 +150,25 @@
-
{$i18n.t('Permissions')}
+
{$i18n.t('Permissions')}
+ + {/if} + + {#if tabs.includes('new')} + {/if}
@@ -166,7 +193,9 @@ {:else if selectedTab === 'permissions'} - + + {:else if selectedTab === 'new'} + {/if} diff --git a/src/lib/components/common/Permissions.svelte b/src/lib/components/common/Permissions.svelte index 098d3d5f774..300ca8b58ed 100644 --- a/src/lib/components/common/Permissions.svelte +++ b/src/lib/components/common/Permissions.svelte @@ -3,42 +3,8 @@ const i18n = getContext('i18n'); import Switch from '$lib/components/common/Switch.svelte'; - import Tooltip from '$lib/components/common/Tooltip.svelte'; - - // Default values for permissions (fallback) - const defaultPermissions = { - workspace: { - models: false, - knowledge: false, - prompts: false, - tools: false - }, - sharing: { - public_models: false, - public_knowledge: false, - public_prompts: false, - public_tools: false - }, - chat: { - controls: true, - file_upload: true, - delete: true, - edit: true, - stt: true, - tts: true, - call: true, - multiple_models: true, - temporary: true, - temporary_enforced: false - }, - features: { - direct_tool_servers: false, - web_search: true, - image_generation: true, - code_interpreter: true - } - }; + export let defaultPermissions = {}; export let permissions = {}; // Reactive statement to ensure all fields are present in `permissions` From 0b1292e716336017ec603748dd18928b472aa802 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 1 May 2025 09:27:12 +0200 Subject: [PATCH 21/31] Moved permission into own end-point --- backend/open_webui/main.py | 3 + backend/open_webui/models/permissions.py | 66 +++++------- backend/open_webui/routers/permissions.py | 72 +++++++++++++ backend/open_webui/routers/roles.py | 19 ---- backend/open_webui/routers/users.py | 2 +- src/lib/apis/permissions/index.ts | 118 ++++++++++++++++++++++ src/lib/apis/roles/index.ts | 33 ------ 7 files changed, 220 insertions(+), 93 deletions(-) create mode 100644 backend/open_webui/routers/permissions.py create mode 100644 src/lib/apis/permissions/index.ts diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 3459e8a8e80..9f8a37d4aec 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -79,6 +79,7 @@ users, utils, roles, + permissions, ) from open_webui.routers.retrieval import ( @@ -1055,6 +1056,8 @@ async def inspect_websocket(request: Request, call_next): app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"]) app.include_router(roles.router, prefix="/api/v1/roles", tags=["roles"]) +app.include_router(permissions.router, prefix="/api/v1/permissions", tags=["permissions"]) + try: audit_level = AuditLevel(AUDIT_LOG_LEVEL) diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index 33b199a35cc..ce760ff94ca 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -70,46 +70,6 @@ class PermissionRoleForm(BaseModel): class PermissionsTable: - def get_default(self) -> dict[PermissionCategory, dict[str, bool]]: - with get_db() as db: - result = {} - - # First, get the user role permissions as defaults if requesting permissions for a different role - user_role = db.query(Role).filter_by(name="user").first() - if user_role: - # Query the RolePermission association objects for the user role - user_role_permissions = db.query(RolePermission).filter_by(role_id=user_role.id).all() - - # Create a mapping of permission_id to value for user role - user_permission_values = {rp.permission_id: rp.value for rp in user_role_permissions} - - # Get all permissions associated with user role - user_permissions = db.query(Permission).filter( - Permission.id.in_([rp.permission_id for rp in user_role_permissions]) - ).all() - - # Build default permissions dictionary - for perm in user_permissions: - category = perm.category.value - if category not in result: - result[category] = {} - - result[category][perm.name] = user_permission_values[perm.id] - - # Add all permissions from the permission table that are missing in the result - # with a default value of False - all_permissions = db.query(Permission).all() - for perm in all_permissions: - category = perm.category.value - if category not in result: - result[category] = {} - - # Only add if not already in result - if perm.name not in result[category]: - result[category][perm.name] = False - - return result - def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: result = {} @@ -242,6 +202,32 @@ def get(self, permission_name: str, category: PermissionCategory): print(f"Error getting permission: {e}") return None + + def get_all(self) -> list[PermissionModel]: + with get_db() as db: + try: + # Query all permissions from the database + db_permissions = db.query(Permission).all() + + # Convert database objects to PermissionModel instances + permissions = [ + PermissionModel( + id=p.id, + name=p.name, + category=p.category, + description=p.description, + # Default value to False as we don't have a role context + value=False + ) for p in db_permissions + ] + + return permissions + + except Exception as e: + print(f"Error getting all permissions: {e}") + return [] + + def add(self, permission: PermissionModel, role_name: str = "user") -> PermissionModel | None: with get_db() as db: try: diff --git a/backend/open_webui/routers/permissions.py b/backend/open_webui/routers/permissions.py new file mode 100644 index 00000000000..0692cda4cfc --- /dev/null +++ b/backend/open_webui/routers/permissions.py @@ -0,0 +1,72 @@ +import logging +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, status + +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS +from open_webui.utils.auth import get_admin_user +from open_webui.models.roles import ( + RoleModel, + Roles, + RoleForm +) +from open_webui.models.permissions import ( + Permissions, + PermissionModel, + PermissionCategory, + PermissionRoleForm +) + + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +router = APIRouter() + + +############################ +# GetPermissions +############################ + + +@router.get("/", response_model=list[PermissionModel]) +async def get_permissions(user=Depends(get_admin_user)): + return Permissions.get_all() + +############################ +# AddPermission +############################ + + +@router.post("/", response_model=Optional[PermissionModel]) +async def add_permissions(form_data: RoleForm, user=Depends(get_admin_user)): + pass + # perm = Permissions.add(permission=form_data, role_name=role_name) + # + # if perm: + # return True + # + # raise HTTPException( + # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + # detail='Something went wrong. Please try again.', + # ) + + +############################ +# UpdatePermissionById +############################ + +@router.post("/{permission_id}", response_model=Optional[PermissionModel]) +async def update_permission_name(role_id: int, form_data: RoleForm, user=Depends(get_admin_user)): + pass + + +############################ +# DeletePermissionById +############################ + + +# TODO(jeskr): Check if role is used by any users before deleting it. +@router.delete("/{permission_id}", response_model=bool) +async def delete_permission_by_name(role_name: str, user=Depends(get_admin_user)): + pass \ No newline at end of file diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index 5356ed92fb4..df9c3cb179c 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -116,25 +116,6 @@ async def get_default_permissions_by_role_name(role_name: str, user=Depends(get_ detail='Permissions fetch failed. Please try again later.', ) - -############################ -# AddNewPermissionToRole -############################ - -@router.post("/{role_name}/permission") -async def add_new_default_permission_with_role(role_name: str, form_data: PermissionModel, - user=Depends(get_admin_user)): - perm = Permissions.add(permission=form_data, role_name=role_name) - - if perm: - return True - - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='Something went wrong. Please try again.', - ) - - ############################ # LinkPermissionToRole ########################### diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index bc951f8bfd0..12539cdd31d 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -142,7 +142,7 @@ class UserPermissions(BaseModel): @router.get("/default/permissions", response_model=dict[PermissionCategory, dict[str, bool]]) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): - return Permissions.get_default() + return Permissions.get_ordre_by_category() @router.post("/default/permissions") async def update_default_user_permissions(form_data: UserPermissions, user=Depends(get_admin_user)): diff --git a/src/lib/apis/permissions/index.ts b/src/lib/apis/permissions/index.ts new file mode 100644 index 00000000000..c5f191aee85 --- /dev/null +++ b/src/lib/apis/permissions/index.ts @@ -0,0 +1,118 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getPermissions = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/permissions/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addPermission = async (token: string, name: string, category: string, description: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/permission/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: name, + category: category, + description: description + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updatePermission = async (token: string, permissionId: number, permissionName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/permission/${permissionId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: permissionName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deletePermission = async (token: string, permissionId: number) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/permission/${permissionId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + diff --git a/src/lib/apis/roles/index.ts b/src/lib/apis/roles/index.ts index 90f9b371414..ebc7b4a01e6 100644 --- a/src/lib/apis/roles/index.ts +++ b/src/lib/apis/roles/index.ts @@ -1,5 +1,4 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; -import { getUserPosition } from '$lib/utils'; export const getRoles = async (token: string) => { let error = null; @@ -142,38 +141,6 @@ export const getRolePermissions = async (token: string, roleName: string ) => { return res; }; -export const addNewPermission = async (token: string, roleName: string, permissionName: string, categoryName: string, description: string, value: boolean ) => { - let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - body: JSON.stringify({ - name: permissionName, - category: categoryName, - description: description, - value: value - }) - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); - - if (error) { - throw error; - } - - return res; -}; - export const linkRoleToPermissions = async (token: string, roleName: string, categoryName: string, permissionName: string, value: boolean ) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission/link`, { From b276fe13969a5974f80caa7a0e7096a18db09684 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 1 May 2025 15:31:40 +0200 Subject: [PATCH 22/31] Using same shared component for permissions --- backend/open_webui/config.py | 37 ++++++++- ...6df61a317_added_permissions_to_database.py | 1 + backend/open_webui/models/permissions.py | 82 +++++++++---------- backend/open_webui/routers/permissions.py | 31 ++++--- src/lib/components/admin/Users/Groups.svelte | 36 +------- .../admin/Users/Groups/EditGroupModal.svelte | 2 +- .../admin/Users/Roles/AddPermission.svelte | 56 ------------- .../admin/Users/Roles/EditRoleModal.svelte | 23 +----- src/lib/components/common/Permissions.svelte | 73 +++++++++++------ 9 files changed, 145 insertions(+), 196 deletions(-) delete mode 100644 src/lib/components/admin/Users/Roles/AddPermission.svelte diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 8b2f866e37d..6d023aff488 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1166,7 +1166,42 @@ def oidc_oauth_register(client): }, } -USER_PERMISSIONS = Permissions.set_initial_permissions(DEFAULT_USER_PERMISSIONS) +DEFAULT_USER_PERMISSIONS_LABELS = { + "workspace": { + "models": "Models Access", + "knowledge": "Knowledge Access", + "prompts": "Prompts Access", + "tools": "Tools Access", + }, + "sharing": { + "public_models": "Models Public Sharing", + "public_knowledge": "Knowledge Public Sharing", + "public_prompts": "Prompts Public Sharing", + "public_tools": "Tools Public Sharing", + }, + "chat": { + "controls": "Allow Chat Controls", + "file_upload": "Allow File Upload", + "delete": "Allow Chat Delete", + "edit": "Allow Chat Edit", + "share": "Allow Chat Share", + "export": "Allow Chat Export", + "stt": "Allow Speech to Text", + "tts": "Allow Text to Speech", + "call": "Allow Call", + "multiple_models": "Allow Multiple Models in Chat", + "temporary": "Allow Temporary Chat", + "temporary_enforced": "Enforce Temporary Chat", + }, + "features": { + "direct_tool_servers": "Direct Tool Servers", + "web_search": "Web Search", + "image_generation": "Image Generation", + "code_interpreter": "Code Interpreter", + }, +} + +USER_PERMISSIONS = Permissions.set_initial_permissions(DEFAULT_USER_PERMISSIONS, DEFAULT_USER_PERMISSIONS_LABELS) ENABLE_CHANNELS = PersistentConfig( "ENABLE_CHANNELS", diff --git a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py index bcd7f4955cd..55d1057cfba 100644 --- a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py +++ b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py @@ -25,6 +25,7 @@ def upgrade(): 'permissions', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), sa.Column('name', sa.String(), nullable=False), + sa.Column('label', sa.String(), nullable=False), sa.Column('category', Enum('workspace', 'sharing', 'chat', 'features', name='permissioncategory'), nullable=False), sa.Column('description', sa.String()), ) diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index ce760ff94ca..bea7dc98c4d 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -32,6 +32,7 @@ class Permission(Base): id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String, nullable=False) + label = Column(String, nullable=False) category = Column(SQLAlchemyEnum(PermissionCategory), nullable=False) description = Column(String) @@ -44,14 +45,31 @@ def roles(self): class PermissionModel(BaseModel): - id: int = Field(default=None, exclude=True) + id: int = Field(default=None) name: str + label: str category: PermissionCategory description: str | None value: bool = False model_config = ConfigDict(from_attributes=True) +class PermissionEmptyModel(BaseModel): + id: int = Field(default=None) + name: str + label: str + category: PermissionCategory + description: str | None + + model_config = ConfigDict(from_attributes=True) + +class PermissionCreateModel(BaseModel): + name: str + label: str + category: PermissionCategory + description: str | None = None + + model_config = ConfigDict(from_attributes=True) #################### # Forms @@ -62,6 +80,10 @@ class PermissionRoleForm(BaseModel): category: PermissionCategory value: bool = False +class PermissionAddForm(BaseModel): + name: str + category: PermissionCategory + description: str | None = None #################### # Database operations @@ -133,8 +155,11 @@ def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCateg return result # TODO: if config is not persistent enabled, just override permissions given - def set_initial_permissions(self, default_permissions: dict[PermissionCategory, dict[str, bool]], - role_name: str = "user") -> dict[PermissionCategory, dict[str, bool]]: + def set_initial_permissions(self, + default_permissions: dict[PermissionCategory, dict[str, bool]], + default_labels: dict[PermissionCategory, dict[str, str]], + role_name: str = "user" + ) -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: # Check if any permissions exist existing_count = db.query(Permission).count() @@ -154,6 +179,7 @@ def set_initial_permissions(self, default_permissions: dict[PermissionCategory, new_permission = Permission( name=perm_name, category=category, + label=default_labels[category_str][perm_name], description=f"Default {category.value} permission for {perm_name}" ) db.add(new_permission) @@ -192,6 +218,7 @@ def get(self, permission_name: str, category: PermissionCategory): return PermissionModel( id=db_permission.id, name=db_permission.name, + label=db_permission.label, category=db_permission.category, description=db_permission.description, # Default value to False as we don't have a role context @@ -203,7 +230,7 @@ def get(self, permission_name: str, category: PermissionCategory): return None - def get_all(self) -> list[PermissionModel]: + def get_all(self) -> list[PermissionEmptyModel]: with get_db() as db: try: # Query all permissions from the database @@ -211,13 +238,12 @@ def get_all(self) -> list[PermissionModel]: # Convert database objects to PermissionModel instances permissions = [ - PermissionModel( + PermissionEmptyModel( id=p.id, name=p.name, + label=p.label, category=p.category, description=p.description, - # Default value to False as we don't have a role context - value=False ) for p in db_permissions ] @@ -228,49 +254,36 @@ def get_all(self) -> list[PermissionModel]: return [] - def add(self, permission: PermissionModel, role_name: str = "user") -> PermissionModel | None: + def add(self, permission: PermissionCreateModel) -> PermissionEmptyModel | None: with get_db() as db: try: - role = db.query(Role).filter_by(name=role_name).order_by(Role.id).first() - if not role: - return None - existing_permission = db.query(Permission).filter_by( name=permission.name, category=permission.category ).first() if existing_permission: - # Permission exists. Fail, as one should use the `add_default_permission_to_role` end-point. + # Permission exists. return None new_permission = Permission( name=permission.name, + label=permission.label, category=permission.category, description=permission.description, ) db.add(new_permission) - db.flush() - - # Create the association with the value - role_permission = RolePermission( - role_id=role.id, - permission_id=new_permission.id, - value=permission.value - ) - db.add(role_permission) - db.commit() db.refresh(new_permission) - return PermissionModel.model_validate(new_permission) + return PermissionEmptyModel.model_validate(new_permission) except Exception as e: print(f"Error adding permission: {e}") db.rollback() return None - def update(self, permission: PermissionModel, role_name: str = "user") -> PermissionModel | None: + def update(self, permission: PermissionEmptyModel) -> PermissionEmptyModel | None: with get_db() as db: try: db_permission = db.query(Permission).filter_by( @@ -281,28 +294,13 @@ def update(self, permission: PermissionModel, role_name: str = "user") -> Permis if not db_permission: return None - role = db.query(Role).filter_by(name=role_name).first() - if not role: - return None - - association = db.query(RolePermission).filter_by( - role_id=role.id, - permission_id=db_permission.id - ).first() - association.value = permission['value'] - if 'description' in permission: db_permission.description = permission['description'] db.commit() + db.refresh(db_permission) - return PermissionModel( - id=db_permission.id, - name=db_permission.name, - category=db_permission.category, - description=db_permission.description, - value=association.value - ) + return PermissionEmptyModel.model_validate(db_permission) except Exception as e: print(f"Error updating permission: {e}") diff --git a/backend/open_webui/routers/permissions.py b/backend/open_webui/routers/permissions.py index 0692cda4cfc..c6fc420761a 100644 --- a/backend/open_webui/routers/permissions.py +++ b/backend/open_webui/routers/permissions.py @@ -6,15 +6,15 @@ from open_webui.env import SRC_LOG_LEVELS from open_webui.utils.auth import get_admin_user from open_webui.models.roles import ( - RoleModel, - Roles, RoleForm ) from open_webui.models.permissions import ( Permissions, PermissionModel, + PermissionCreateModel, + PermissionEmptyModel, PermissionCategory, - PermissionRoleForm + PermissionAddForm ) @@ -29,27 +29,26 @@ ############################ -@router.get("/", response_model=list[PermissionModel]) +@router.get("/", response_model=list[PermissionEmptyModel]) async def get_permissions(user=Depends(get_admin_user)): return Permissions.get_all() + ############################ # AddPermission ############################ -@router.post("/", response_model=Optional[PermissionModel]) -async def add_permissions(form_data: RoleForm, user=Depends(get_admin_user)): - pass - # perm = Permissions.add(permission=form_data, role_name=role_name) - # - # if perm: - # return True - # - # raise HTTPException( - # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - # detail='Something went wrong. Please try again.', - # ) +@router.post("/", response_model=Optional[PermissionEmptyModel]) +async def add_permissions(form_data: PermissionAddForm, user=Depends(get_admin_user)): + permission = Permissions.add(permission=form_data) + if permission: + return permission + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Something went wrong. Please try again.', + ) ############################ diff --git a/src/lib/components/admin/Users/Groups.svelte b/src/lib/components/admin/Users/Groups.svelte index 70e5832ea62..93312a8cfa8 100644 --- a/src/lib/components/admin/Users/Groups.svelte +++ b/src/lib/components/admin/Users/Groups.svelte @@ -50,41 +50,7 @@ }); let search = ''; - let defaultPermissions = { - workspace: { - models: false, - knowledge: false, - prompts: false, - tools: false - }, - sharing: { - public_models: false, - public_knowledge: false, - public_prompts: false, - public_tools: false - }, - chat: { - controls: true, - file_upload: true, - delete: true, - edit: true, - share: true, - export: true, - stt: true, - tts: true, - call: true, - multiple_models: true, - temporary: true, - temporary_enforced: false - }, - features: { - direct_tool_servers: false, - web_search: true, - image_generation: true, - code_interpreter: true, - notes: true - } - }; + let defaultPermissions = {}; let showCreateGroupModal = false; let showDefaultPermissionsModal = false; diff --git a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte index 8cc353064c1..55ff9f87755 100644 --- a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte +++ b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte @@ -5,7 +5,7 @@ import Modal from '$lib/components/common/Modal.svelte'; import Display from './Display.svelte'; - import Permissions from './Permissions.svelte'; + import Permissions from '$lib/components/common/Permissions.svelte'; import Users from './Users.svelte'; import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte'; import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte'; diff --git a/src/lib/components/admin/Users/Roles/AddPermission.svelte b/src/lib/components/admin/Users/Roles/AddPermission.svelte deleted file mode 100644 index 2ca7dfed3b7..00000000000 --- a/src/lib/components/admin/Users/Roles/AddPermission.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - -
-
-
{$i18n.t('Add Role')}
- -
- -
-
-
-
-
{$i18n.t('Name')}
- -
- -
-
-
-
-
-
diff --git a/src/lib/components/admin/Users/Roles/EditRoleModal.svelte b/src/lib/components/admin/Users/Roles/EditRoleModal.svelte index 56103c42d4d..4afb2af4d84 100644 --- a/src/lib/components/admin/Users/Roles/EditRoleModal.svelte +++ b/src/lib/components/admin/Users/Roles/EditRoleModal.svelte @@ -12,7 +12,6 @@ import Permissions from "$lib/components/common/Permissions.svelte"; import WrenchSolid from "$lib/components/icons/WrenchSolid.svelte"; import Plus from "$lib/components/icons/Plus.svelte"; - import AddPermission from "$lib/components/admin/Users/Roles/AddPermission.svelte"; const i18n = getContext('i18n'); const dispatch = createEventDispatcher(); @@ -20,7 +19,7 @@ export let show = false; export let selectedRole; - export let tabs = ['general', 'permissions', 'new']; + export let tabs = ['general', 'permissions']; export let lockedRoles = ['pending', 'user', 'admin'] let selectedTab = 'general'; @@ -153,24 +152,6 @@
{$i18n.t('Permissions')}
{/if} - - {#if tabs.includes('new')} - - {/if}
{:else if selectedTab === 'permissions'} - {:else if selectedTab === 'new'} - {/if}
diff --git a/src/lib/components/common/Permissions.svelte b/src/lib/components/common/Permissions.svelte index 300ca8b58ed..c4b5b91d2fa 100644 --- a/src/lib/components/common/Permissions.svelte +++ b/src/lib/components/common/Permissions.svelte @@ -1,12 +1,18 @@
- {#each Object.entries(permissions) as [categoryName, category], index} - {#if index !== 0} -
- {/if} -
-
{$i18n.t(formatPermissionCategory(categoryName))}
- - {#each Object.entries(category) as [key, value]} -
-
- {$i18n.t(formatPermissionName(key))} -
- - -
- {/each} + {#if loading} +
+ Loading permissions...
- {/each} + {:else} + {#each Object.entries(permissions) as [categoryName, category], index} + {#if index !== 0} +
+ {/if} +
+
{$i18n.t(formatPermissionCategory(categoryName))}
+ + {#each Object.entries(category) as [key, value]} +
+
+ {$i18n.t(formatPermissionName(key))} +
+ + +
+ {/each} +
+ {/each} + {/if}
From 415ab9d947d100abf4607b7e9ed56d1280371973 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 1 May 2025 16:59:00 +0200 Subject: [PATCH 23/31] Added label to permisison and overview to UI --- backend/open_webui/models/permissions.py | 1 + src/lib/apis/permissions/index.ts | 9 +- src/lib/components/admin/Users.svelte | 21 +- .../admin/Users/PermissionList.svelte | 206 ++++++++++++++++++ .../Permissions/AddPermissionModal.svelte | 148 +++++++++++++ 5 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 src/lib/components/admin/Users/PermissionList.svelte create mode 100644 src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index bea7dc98c4d..92d8ef28f2c 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -82,6 +82,7 @@ class PermissionRoleForm(BaseModel): class PermissionAddForm(BaseModel): name: str + label: str category: PermissionCategory description: str | None = None diff --git a/src/lib/apis/permissions/index.ts b/src/lib/apis/permissions/index.ts index c5f191aee85..51366698d8f 100644 --- a/src/lib/apis/permissions/index.ts +++ b/src/lib/apis/permissions/index.ts @@ -27,10 +27,10 @@ export const getPermissions = async (token: string) => { return res; }; -export const addPermission = async (token: string, name: string, category: string, description: string) => { +export const addPermission = async (token: string, category: string, name: string, label: string, description: string) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/permission/`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/permissions/`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -38,6 +38,7 @@ export const addPermission = async (token: string, name: string, category: strin }, body: JSON.stringify({ name: name, + label: label, category: category, description: description }) @@ -62,7 +63,7 @@ export const addPermission = async (token: string, name: string, category: strin export const updatePermission = async (token: string, permissionId: number, permissionName: string) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/permission/${permissionId}`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/permissions/${permissionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -92,7 +93,7 @@ export const updatePermission = async (token: string, permissionId: number, perm export const deletePermission = async (token: string, permissionId: number) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/permission/${permissionId}`, { + const res = await fetch(`${WEBUI_API_BASE_URL}/permissions/${permissionId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', diff --git a/src/lib/components/admin/Users.svelte b/src/lib/components/admin/Users.svelte index 8a6e4e040c4..ee4468d43fc 100644 --- a/src/lib/components/admin/Users.svelte +++ b/src/lib/components/admin/Users.svelte @@ -6,8 +6,10 @@ import { user } from '$lib/stores'; import UserList from './Users/UserList.svelte'; - import Roles from './Users/RoleList.svelte'; import Groups from './Users/Groups.svelte'; + import Roles from './Users/RoleList.svelte'; + import PermissionList from './Users/PermissionList.svelte'; + import Wrench from "$lib/components/icons/Wrench.svelte"; const i18n = getContext('i18n'); @@ -110,6 +112,21 @@
{$i18n.t('Roles')}
+ +
@@ -119,6 +136,8 @@ {:else if selectedTab === 'roles'} + {:else if selectedTab === 'permissions'} + {/if}
diff --git a/src/lib/components/admin/Users/PermissionList.svelte b/src/lib/components/admin/Users/PermissionList.svelte new file mode 100644 index 00000000000..03bc2e3bf80 --- /dev/null +++ b/src/lib/components/admin/Users/PermissionList.svelte @@ -0,0 +1,206 @@ + + + { + permissions = await getPermissions(localStorage.token); + }} +/> + +
+
+
+ {$i18n.t('Permissions')} +
+
+
+ +
+
+
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + + {#each permissions as perm} + + + + + + + + {/each} + +
setSortKey('id')}> +
+ {$i18n.t('Identifier')} + + {#if sortKey === 'id'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('category')}> +
+ {$i18n.t('Category')} + + {#if sortKey === 'category'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('name')}> +
+ {$i18n.t('Name')} + + {#if sortKey === 'name'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('label')}> +
+ {$i18n.t('Label')} + {#if sortKey === 'label'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('description')}> +
+ {$i18n.t('Description')} + {#if sortKey === 'description'} + {#if sortOrder === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
+
{perm.id}
+
+
+
{perm.category}
+
+
+
+
{perm.name}
+
+
+
+
{perm.label}
+
+
+
+
{perm.description}
+
+
+
diff --git a/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte b/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte new file mode 100644 index 00000000000..39f5db5bd1c --- /dev/null +++ b/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte @@ -0,0 +1,148 @@ + + + +
+
+
{$i18n.t('Add Permission')}
+ +
+ +
+
+
{ + submitHandler(); + }}> + +
+
{$i18n.t('Category')}
+ +
+ +
+
+ +
+
{$i18n.t('Name')}
+ +
+ +
+
+ +
+
{$i18n.t('Label')}
+ +
+ +
+
+ +
+
{$i18n.t('Description')}
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+
+
From a355de76b5e7cd3d0780b8129f65cf3c476b75ba Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Fri, 2 May 2025 10:35:35 +0200 Subject: [PATCH 24/31] Added categroies as selector in permission UI --- .../Permissions/AddPermissionModal.svelte | 20 ++++++++++--------- vite.config.ts | 3 ++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte b/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte index 39f5db5bd1c..3ed542cc24b 100644 --- a/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte +++ b/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte @@ -21,6 +21,7 @@ label: '', description: '' }; + const _categories = ['workspace', 'sharing', 'chat', 'features']; const submitHandler = async () => { const stopLoading = () => { @@ -73,16 +74,17 @@ }}>
-
{$i18n.t('Category')}
- +
{$i18n.t('Category')}
- +
diff --git a/vite.config.ts b/vite.config.ts index ed690f5023b..0629094e8da 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -35,7 +35,8 @@ export default defineConfig({ APP_BUILD_HASH: JSON.stringify(process.env.APP_BUILD_HASH || 'dev-build') }, build: { - sourcemap: true + sourcemap: true, + cache: true }, worker: { format: 'es' From 218cd2a5d9d43d776e97733bdd7f478c8afa931f Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Fri, 2 May 2025 10:45:39 +0200 Subject: [PATCH 25/31] Resovled merge conflict in users --- src/lib/components/admin/Users.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/admin/Users.svelte b/src/lib/components/admin/Users.svelte index ee4468d43fc..8cedd28567c 100644 --- a/src/lib/components/admin/Users.svelte +++ b/src/lib/components/admin/Users.svelte @@ -133,7 +133,7 @@ {#if selectedTab === 'overview'} {:else if selectedTab === 'groups'} - + {:else if selectedTab === 'roles'} {:else if selectedTab === 'permissions'} From e9b362c2a1f270cc94d0710f4e68325bf91eded8 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Mon, 5 May 2025 10:34:12 +0200 Subject: [PATCH 26/31] Fixed default permission for groups --- src/lib/components/admin/Users.svelte | 4 +- .../admin/Users/Groups/EditGroupModal.svelte | 39 +++++-------------- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/src/lib/components/admin/Users.svelte b/src/lib/components/admin/Users.svelte index 8cedd28567c..4e4987fb8ca 100644 --- a/src/lib/components/admin/Users.svelte +++ b/src/lib/components/admin/Users.svelte @@ -9,7 +9,7 @@ import Groups from './Users/Groups.svelte'; import Roles from './Users/RoleList.svelte'; import PermissionList from './Users/PermissionList.svelte'; - import Wrench from "$lib/components/icons/Wrench.svelte"; + import WrenchSolid from "$lib/components/icons/WrenchSolid.svelte"; const i18n = getContext('i18n'); @@ -123,7 +123,7 @@ }} >
- +
{$i18n.t('Permissions')}
diff --git a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte index 55ff9f87755..1a7b0f4180b 100644 --- a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte +++ b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte @@ -10,6 +10,7 @@ import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte'; import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + import {getUserDefaultPermissions} from "$lib/apis/users"; export let onSubmit: Function = () => {}; export let onDelete: Function = () => {}; @@ -31,33 +32,8 @@ export let name = ''; export let description = ''; - export let permissions = { - workspace: { - models: false, - knowledge: false, - prompts: false, - tools: false - }, - sharing: { - public_models: false, - public_knowledge: false, - public_prompts: false, - public_tools: false - }, - chat: { - controls: true, - file_upload: true, - delete: true, - edit: true, - temporary: true - }, - features: { - direct_tool_servers: false, - web_search: true, - image_generation: true, - code_interpreter: true - } - }; + export let defaultPermissions = {}; + export let permissions = {}; export let userIds = []; const submitHandler = async () => { @@ -90,7 +66,12 @@ init(); } - onMount(() => { + onMount(async() => { + defaultPermissions = await getUserDefaultPermissions(localStorage.token).catch((error) => { + toast.error(`${error}`); + return []; + }); + console.log(tabs); selectedTab = tabs[0]; init(); }); @@ -223,7 +204,7 @@ {#if selectedTab == 'general'} {:else if selectedTab == 'permissions'} - + {:else if selectedTab == 'users'} {/if} From fe4cfb4e0bef7ef34d79f40e22f8be8d47af7362 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Mon, 5 May 2025 10:40:35 +0200 Subject: [PATCH 27/31] Resovled merge with permission components with core --- backend/open_webui/config.py | 1 + .../admin/Users/Groups/Permissions.svelte | 393 ------------------ 2 files changed, 1 insertion(+), 393 deletions(-) delete mode 100644 src/lib/components/admin/Users/Groups/Permissions.svelte diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 6d023aff488..2bfccb42a58 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1198,6 +1198,7 @@ def oidc_oauth_register(client): "web_search": "Web Search", "image_generation": "Image Generation", "code_interpreter": "Code Interpreter", + "notes": "Notes" }, } diff --git a/src/lib/components/admin/Users/Groups/Permissions.svelte b/src/lib/components/admin/Users/Groups/Permissions.svelte deleted file mode 100644 index 6af935813bb..00000000000 --- a/src/lib/components/admin/Users/Groups/Permissions.svelte +++ /dev/null @@ -1,393 +0,0 @@ - - -
- - -
-
{$i18n.t('Workspace Permissions')}
- -
-
- {$i18n.t('Models Access')} -
- -
- -
-
- {$i18n.t('Knowledge Access')} -
- -
- -
-
- {$i18n.t('Prompts Access')} -
- -
- -
- -
- {$i18n.t('Tools Access')} -
- -
-
-
- -
- -
-
{$i18n.t('Sharing Permissions')}
- -
-
- {$i18n.t('Models Public Sharing')} -
- -
- -
-
- {$i18n.t('Knowledge Public Sharing')} -
- -
- -
-
- {$i18n.t('Prompts Public Sharing')} -
- -
- -
-
- {$i18n.t('Tools Public Sharing')} -
- -
-
- -
- -
-
{$i18n.t('Chat Permissions')}
- -
-
- {$i18n.t('Allow File Upload')} -
- - -
- -
-
- {$i18n.t('Allow Chat Controls')} -
- - -
- -
-
- {$i18n.t('Allow Chat Delete')} -
- - -
- -
-
- {$i18n.t('Allow Chat Edit')} -
- - -
- -
-
- {$i18n.t('Allow Chat Share')} -
- - -
- -
-
- {$i18n.t('Allow Chat Export')} -
- - -
- -
-
- {$i18n.t('Allow Speech to Text')} -
- - -
-
-
- {$i18n.t('Allow Text to Speech')} -
- - -
- -
-
- {$i18n.t('Allow Call')} -
- - -
- -
-
- {$i18n.t('Allow Multiple Models in Chat')} -
- - -
- -
-
- {$i18n.t('Allow Temporary Chat')} -
- - -
- - {#if permissions.chat.temporary} -
-
- {$i18n.t('Enforce Temporary Chat')} -
- - -
- {/if} -
- -
- -
-
{$i18n.t('Features Permissions')}
- -
-
- {$i18n.t('Direct Tool Servers')} -
- - -
- -
-
- {$i18n.t('Web Search')} -
- - -
- -
-
- {$i18n.t('Image Generation')} -
- - -
- -
-
- {$i18n.t('Code Interpreter')} -
- - -
- -
-
- {$i18n.t('Notes')} -
- - -
-
-
From 725025c7863dac99b1c737563a1d6d95a986bccc Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Mon, 5 May 2025 11:24:37 +0200 Subject: [PATCH 28/31] Ensured ENABLE_PERSISTENT_CONFIG is used for permissions --- backend/open_webui/models/permissions.py | 51 +++++++++++++++--------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index 92d8ef28f2c..58f6e3d371f 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -1,3 +1,4 @@ +import os from enum import Enum from pydantic import BaseModel, ConfigDict, Field @@ -7,6 +8,9 @@ from open_webui.internal.db import Base, get_db from open_webui.models.roles import RolePermission, Role +persistent_config = ( + os.environ.get("ENABLE_PERSISTENT_CONFIG", "True").lower() == "true" +) #################### # Role DB Schema @@ -155,28 +159,31 @@ def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCateg return result - # TODO: if config is not persistent enabled, just override permissions given def set_initial_permissions(self, default_permissions: dict[PermissionCategory, dict[str, bool]], default_labels: dict[PermissionCategory, dict[str, str]], role_name: str = "user" ) -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: - # Check if any permissions exist - existing_count = db.query(Permission).count() + role = db.query(Role).filter_by(name=role_name).order_by(Role.id).first() + if not role: + raise ValueError(f"Role '{role_name}' not found") - if existing_count == 0: - role = db.query(Role).filter_by(name=role_name).order_by(Role.id).first() + # No permissions exist, initialize with defaults + for category_str, perms in default_permissions.items(): + # Convert string category to enum + try: + category = PermissionCategory(category_str) + except ValueError: + continue # Skip invalid categories - # No permissions exist, initialize with defaults - for category_str, perms in default_permissions.items(): - # Convert string category to enum - try: - category = PermissionCategory(category_str) - except ValueError: - continue # Skip invalid categories + for perm_name, value in perms.items(): + existing_permission = db.query(Permission).filter_by( + name=perm_name, + category=category + ).first() - for perm_name, value in perms.items(): + if not existing_permission: new_permission = Permission( name=perm_name, category=category, @@ -193,12 +200,20 @@ def set_initial_permissions(self, value=value ) db.add(role_permission) + else: + if not persistent_config: + # Override store permission with the ones from env. + role_permission = db.query(RolePermission).filter_by( + role_id=role.id, + permission_id=existing_permission.id + ).first() + role_permission.value = value - try: - db.commit() - except Exception as e: - db.rollback() - raise e + try: + db.commit() + except Exception as e: + db.rollback() + raise e # Return current permissions structure return self.get_ordre_by_category() From 7daad70f930ae2cd49a2a9688f022e12c25bb63b Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Mon, 5 May 2025 13:33:15 +0200 Subject: [PATCH 29/31] Fixed database migration conflict --- .../migrations/versions/262aff902ca3_added_roles_tabel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py index 98b0c0e33ec..dea7462989b 100644 --- a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py +++ b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py @@ -1,7 +1,7 @@ """Added roles tabel Revision ID: 262aff902ca3 -Revises: 3781e22d8b01 +Revises: 9f0c9cd09105 Create Date: 2025-04-14 14:25:33.528446 """ @@ -14,7 +14,7 @@ # revision identifiers, used by Alembic. revision: str = '262aff902ca3' -down_revision: Union[str, None] = '3781e22d8b01' +down_revision: Union[str, None] = '9f0c9cd09105' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None From 8e83aaa0ad36a12bba72673ed5f0a41a6db2416c Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Mon, 5 May 2025 15:57:05 +0200 Subject: [PATCH 30/31] Code clean up in roles and permissions --- backend/open_webui/config.py | 7 +- backend/open_webui/constants.py | 17 +- backend/open_webui/main.py | 4 +- ...6df61a317_added_permissions_to_database.py | 41 +- .../262aff902ca3_added_roles_tabel.py | 5 +- backend/open_webui/models/permissions.py | 177 ++++--- backend/open_webui/models/roles.py | 20 +- backend/open_webui/routers/permissions.py | 28 +- backend/open_webui/routers/roles.py | 75 +-- backend/open_webui/routers/users.py | 33 +- backend/open_webui/utils/oauth.py | 4 +- src/lib/apis/permissions/index.ts | 100 +--- src/lib/apis/roles/index.ts | 162 +++--- src/lib/components/admin/Users.svelte | 4 +- .../admin/Users/Groups/EditGroupModal.svelte | 6 +- .../admin/Users/PermissionList.svelte | 331 ++++++------ .../Permissions/AddPermissionModal.svelte | 21 +- .../components/admin/Users/RoleList.svelte | 472 +++++++++--------- .../admin/Users/Roles/AddRoleModal.svelte | 14 +- .../admin/Users/Roles/EditRoleModal.svelte | 23 +- .../admin/Users/UserList/AddUserModal.svelte | 3 +- src/lib/components/common/Permissions.svelte | 20 +- 22 files changed, 831 insertions(+), 736 deletions(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 2bfccb42a58..2c355fc7292 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -33,6 +33,7 @@ from open_webui.utils.redis import get_redis_connection from open_webui.models.permissions import Permissions + class EndpointFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: return record.getMessage().find("/health") == -1 @@ -1198,11 +1199,13 @@ def oidc_oauth_register(client): "web_search": "Web Search", "image_generation": "Image Generation", "code_interpreter": "Code Interpreter", - "notes": "Notes" + "notes": "Notes", }, } -USER_PERMISSIONS = Permissions.set_initial_permissions(DEFAULT_USER_PERMISSIONS, DEFAULT_USER_PERMISSIONS_LABELS) +USER_PERMISSIONS = Permissions.set_initial_permissions( + DEFAULT_USER_PERMISSIONS, DEFAULT_USER_PERMISSIONS_LABELS +) ENABLE_CHANNELS = PersistentConfig( "ENABLE_CHANNELS", diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py index 735cf26bc4b..5b4f6362de5 100644 --- a/backend/open_webui/constants.py +++ b/backend/open_webui/constants.py @@ -104,7 +104,22 @@ def __str__(self) -> str: ) FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding." - DELETE_ROLE_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the role. Please give it another shot." + PERMISSION_FETCH_FAILED = "Permissions fetch failed. Please try again later." + PERMISSION_NOT_FOUND = ( + lambda name="", category="": f"Permission '{name}' with category '{category}' was not found" + ) + + ROLE_ALREADY_EXISTS = ( + lambda role="": f"A role with the name '{role}' already exists" + ) + ROLE_DO_NOT_EXISTS = "A role does not exist." + ROLE_ALREADY_ASSIGNED_TO_GROUP = () + ROLE_DELETE_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the role. Please give it another shot." + ROLE_NOT_FOUND = ( + "This role does not exist. Please check the role name and try again." + ) + ROLE_ERROR = "Oops! Something went wrong. Please try again later." + class TASKS(str, Enum): def __str__(self) -> str: diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 9f8a37d4aec..1f33b5b33f8 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1056,7 +1056,9 @@ async def inspect_websocket(request: Request, call_next): app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"]) app.include_router(roles.router, prefix="/api/v1/roles", tags=["roles"]) -app.include_router(permissions.router, prefix="/api/v1/permissions", tags=["permissions"]) +app.include_router( + permissions.router, prefix="/api/v1/permissions", tags=["permissions"] +) try: diff --git a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py index 55d1057cfba..c13f9cd2649 100644 --- a/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py +++ b/backend/open_webui/migrations/versions/04c6df61a317_added_permissions_to_database.py @@ -5,6 +5,7 @@ Create Date: 2025-04-22 14:55:58.948054 """ + from typing import Sequence, Union from alembic import op @@ -13,8 +14,8 @@ # revision identifiers, used by Alembic. -revision: str = '04c6df61a317' -down_revision: Union[str, None] = '262aff902ca3' +revision: str = "04c6df61a317" +down_revision: Union[str, None] = "262aff902ca3" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -22,27 +23,33 @@ def upgrade(): # Create a permissions table. op.create_table( - 'permissions', - sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), - sa.Column('name', sa.String(), nullable=False), - sa.Column('label', sa.String(), nullable=False), - sa.Column('category', Enum('workspace', 'sharing', 'chat', 'features', name='permissioncategory'), nullable=False), - sa.Column('description', sa.String()), + "permissions", + sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), + sa.Column("name", sa.String(), nullable=False), + sa.Column("label", sa.String(), nullable=False), + sa.Column( + "category", + Enum("workspace", "sharing", "chat", "features", name="permissioncategory"), + nullable=False, + ), + sa.Column("description", sa.String()), ) # Create a role_permissions join table to allow many-to-many relationships between roles and permissions. op.create_table( - 'role_permissions', - sa.Column('role_id', sa.Integer(), nullable=False), - sa.Column('permission_id', sa.Integer(), nullable=False), - sa.Column('value', sa.Boolean(), default=False), # Added value column here - sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('role_id', 'permission_id') + "role_permissions", + sa.Column("role_id", sa.Integer(), nullable=False), + sa.Column("permission_id", sa.Integer(), nullable=False), + sa.Column("value", sa.Boolean(), default=False), # Added value column here + sa.ForeignKeyConstraint(["role_id"], ["roles.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["permission_id"], ["permissions.id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("role_id", "permission_id"), ) def downgrade(): - op.drop_table('role_permissions') - op.drop_table('permissions') + op.drop_table("role_permissions") + op.drop_table("permissions") op.execute("DROP TYPE permissioncategory") diff --git a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py index dea7462989b..41357a582d6 100644 --- a/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py +++ b/backend/open_webui/migrations/versions/262aff902ca3_added_roles_tabel.py @@ -5,6 +5,7 @@ Create Date: 2025-04-14 14:25:33.528446 """ + import time from typing import Sequence, Union @@ -13,8 +14,8 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '262aff902ca3' -down_revision: Union[str, None] = '9f0c9cd09105' +revision: str = "262aff902ca3" +down_revision: Union[str, None] = "9f0c9cd09105" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/backend/open_webui/models/permissions.py b/backend/open_webui/models/permissions.py index 58f6e3d371f..1a915bdf1c4 100644 --- a/backend/open_webui/models/permissions.py +++ b/backend/open_webui/models/permissions.py @@ -8,14 +8,13 @@ from open_webui.internal.db import Base, get_db from open_webui.models.roles import RolePermission, Role -persistent_config = ( - os.environ.get("ENABLE_PERSISTENT_CONFIG", "True").lower() == "true" -) +persistent_config = os.environ.get("ENABLE_PERSISTENT_CONFIG", "True").lower() == "true" #################### # Role DB Schema #################### + class PermissionCategory(str, Enum): workspace = "workspace" sharing = "sharing" @@ -58,6 +57,7 @@ class PermissionModel(BaseModel): model_config = ConfigDict(from_attributes=True) + class PermissionEmptyModel(BaseModel): id: int = Field(default=None) name: str @@ -67,6 +67,7 @@ class PermissionEmptyModel(BaseModel): model_config = ConfigDict(from_attributes=True) + class PermissionCreateModel(BaseModel): name: str label: str @@ -75,21 +76,25 @@ class PermissionCreateModel(BaseModel): model_config = ConfigDict(from_attributes=True) + #################### # Forms #################### + class PermissionRoleForm(BaseModel): permission_name: str category: PermissionCategory value: bool = False + class PermissionAddForm(BaseModel): name: str label: str category: PermissionCategory description: str | None = None + #################### # Database operations #################### @@ -97,7 +102,9 @@ class PermissionAddForm(BaseModel): class PermissionsTable: - def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCategory, dict[str, bool]]: + def get_ordre_by_category( + self, role_name: str = "user" + ) -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: result = {} @@ -106,15 +113,25 @@ def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCateg user_role = db.query(Role).filter_by(name="user").first() if user_role: # Query the RolePermission association objects for the user role - user_role_permissions = db.query(RolePermission).filter_by(role_id=user_role.id).all() + user_role_permissions = ( + db.query(RolePermission).filter_by(role_id=user_role.id).all() + ) # Create a mapping of permission_id to value for user role - user_permission_values = {rp.permission_id: rp.value for rp in user_role_permissions} + user_permission_values = { + rp.permission_id: rp.value for rp in user_role_permissions + } # Get all permissions associated with user role - user_permissions = db.query(Permission).filter( - Permission.id.in_([rp.permission_id for rp in user_role_permissions]) - ).all() + user_permissions = ( + db.query(Permission) + .filter( + Permission.id.in_( + [rp.permission_id for rp in user_role_permissions] + ) + ) + .all() + ) # Build default permissions dictionary for perm in user_permissions: @@ -130,11 +147,21 @@ def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCateg # Return user defaults if target role doesn't exist return result - target_role_permissions = db.query(RolePermission).filter_by(role_id=target_role.id).all() - target_permission_values = {rp.permission_id: rp.value for rp in target_role_permissions} - target_permissions = db.query(Permission).filter( - Permission.id.in_([rp.permission_id for rp in target_role_permissions]) - ).all() + target_role_permissions = ( + db.query(RolePermission).filter_by(role_id=target_role.id).all() + ) + target_permission_values = { + rp.permission_id: rp.value for rp in target_role_permissions + } + target_permissions = ( + db.query(Permission) + .filter( + Permission.id.in_( + [rp.permission_id for rp in target_role_permissions] + ) + ) + .all() + ) # Merge target role permissions with defaults for perm in target_permissions: @@ -159,10 +186,11 @@ def get_ordre_by_category(self, role_name: str = "user") -> dict[PermissionCateg return result - def set_initial_permissions(self, + def set_initial_permissions( + self, default_permissions: dict[PermissionCategory, dict[str, bool]], default_labels: dict[PermissionCategory, dict[str, str]], - role_name: str = "user" + role_name: str = "user", ) -> dict[PermissionCategory, dict[str, bool]]: with get_db() as db: role = db.query(Role).filter_by(name=role_name).order_by(Role.id).first() @@ -178,17 +206,18 @@ def set_initial_permissions(self, continue # Skip invalid categories for perm_name, value in perms.items(): - existing_permission = db.query(Permission).filter_by( - name=perm_name, - category=category - ).first() + existing_permission = ( + db.query(Permission) + .filter_by(name=perm_name, category=category) + .first() + ) if not existing_permission: new_permission = Permission( name=perm_name, category=category, label=default_labels[category_str][perm_name], - description=f"Default {category.value} permission for {perm_name}" + description=f"Default {category.value} permission for {perm_name}", ) db.add(new_permission) db.flush() @@ -197,16 +226,20 @@ def set_initial_permissions(self, role_permission = RolePermission( role_id=role.id, permission_id=new_permission.id, - value=value + value=value, ) db.add(role_permission) else: if not persistent_config: # Override store permission with the ones from env. - role_permission = db.query(RolePermission).filter_by( - role_id=role.id, - permission_id=existing_permission.id - ).first() + role_permission = ( + db.query(RolePermission) + .filter_by( + role_id=role.id, + permission_id=existing_permission.id, + ) + .first() + ) role_permission.value = value try: @@ -222,10 +255,11 @@ def get(self, permission_name: str, category: PermissionCategory): with get_db() as db: try: # Find the permission - db_permission = db.query(Permission).filter_by( - name=permission_name, - category=category - ).first() + db_permission = ( + db.query(Permission) + .filter_by(name=permission_name, category=category) + .first() + ) if not db_permission: return None @@ -238,14 +272,13 @@ def get(self, permission_name: str, category: PermissionCategory): category=db_permission.category, description=db_permission.description, # Default value to False as we don't have a role context - value=False + value=False, ) except Exception as e: print(f"Error getting permission: {e}") return None - def get_all(self) -> list[PermissionEmptyModel]: with get_db() as db: try: @@ -260,7 +293,8 @@ def get_all(self) -> list[PermissionEmptyModel]: label=p.label, category=p.category, description=p.description, - ) for p in db_permissions + ) + for p in db_permissions ] return permissions @@ -269,14 +303,14 @@ def get_all(self) -> list[PermissionEmptyModel]: print(f"Error getting all permissions: {e}") return [] - def add(self, permission: PermissionCreateModel) -> PermissionEmptyModel | None: with get_db() as db: try: - existing_permission = db.query(Permission).filter_by( - name=permission.name, - category=permission.category - ).first() + existing_permission = ( + db.query(Permission) + .filter_by(name=permission.name, category=permission.category) + .first() + ) if existing_permission: # Permission exists. @@ -302,16 +336,17 @@ def add(self, permission: PermissionCreateModel) -> PermissionEmptyModel | None: def update(self, permission: PermissionEmptyModel) -> PermissionEmptyModel | None: with get_db() as db: try: - db_permission = db.query(Permission).filter_by( - name=permission['name'], - category=permission['category'] - ).first() + db_permission = ( + db.query(Permission) + .filter_by(name=permission["name"], category=permission["category"]) + .first() + ) if not db_permission: return None - if 'description' in permission: - db_permission.description = permission['description'] + if "description" in permission: + db_permission.description = permission["description"] db.commit() db.refresh(db_permission) @@ -327,10 +362,11 @@ def delete(self, permission_name: str, category: PermissionCategory) -> bool: with get_db() as db: try: # Find the permission - db_permission = db.query(Permission).filter_by( - name=permission_name, - category=category - ).first() + db_permission = ( + db.query(Permission) + .filter_by(name=permission_name, category=category) + .first() + ) if not db_permission: return False @@ -350,7 +386,9 @@ def delete(self, permission_name: str, category: PermissionCategory) -> bool: db.rollback() return False - def link(self, permission_id: int, role_id: int, value: bool = False) -> RolePermissionModel | None: + def link( + self, permission_id: int, role_id: int, value: bool = False + ) -> RolePermissionModel | None: with get_db() as db: try: # Check if permission exists @@ -364,10 +402,11 @@ def link(self, permission_id: int, role_id: int, value: bool = False) -> RolePer return None # Check if the link already exists - existing_link = db.query(RolePermission).filter_by( - role_id=role_id, - permission_id=permission_id - ).first() + existing_link = ( + db.query(RolePermission) + .filter_by(role_id=role_id, permission_id=permission_id) + .first() + ) if existing_link: # If link already exists, update its value @@ -377,11 +416,10 @@ def link(self, permission_id: int, role_id: int, value: bool = False) -> RolePer # Create new association role_permission = RolePermission( - role_id=role_id, - permission_id=permission_id, - value=value + role_id=role_id, permission_id=permission_id, value=value ) from pprint import pprint + pprint(role_permission) db.add(role_permission) @@ -399,10 +437,11 @@ def unlink(self, permission_id: int, role_id: int) -> bool: with get_db() as db: try: # Find the association - deleted = db.query(RolePermission).filter_by( - role_id=role_id, - permission_id=permission_id - ).delete() + deleted = ( + db.query(RolePermission) + .filter_by(role_id=role_id, permission_id=permission_id) + .delete() + ) db.commit() return deleted > 0 @@ -420,19 +459,21 @@ def exists(self, permission: PermissionModel, role_name: str = "user") -> bool: return False # Then check if there's a permission with this name and category - db_permission = db.query(Permission).filter_by( - name=permission['name'], - category=permission['category'] - ).first() + db_permission = ( + db.query(Permission) + .filter_by(name=permission["name"], category=permission["category"]) + .first() + ) if not db_permission: return False # Finally, check if there's an association between them - association = db.query(RolePermission).filter_by( - role_id=role.id, - permission_id=db_permission.id - ).first() + association = ( + db.query(RolePermission) + .filter_by(role_id=role.id, permission_id=db_permission.id) + .first() + ) return association is not None diff --git a/backend/open_webui/models/roles.py b/backend/open_webui/models/roles.py index 916124e556c..caabc738045 100644 --- a/backend/open_webui/models/roles.py +++ b/backend/open_webui/models/roles.py @@ -12,10 +12,10 @@ # Association table for the many-to-many relationship (role <-> permission) with value #################### class RolePermission(Base): - __tablename__ = 'role_permissions' + __tablename__ = "role_permissions" - role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True) - permission_id = Column(Integer, ForeignKey('permissions.id'), primary_key=True) + role_id = Column(Integer, ForeignKey("roles.id"), primary_key=True) + permission_id = Column(Integer, ForeignKey("permissions.id"), primary_key=True) value = Column(Boolean, default=False) # Add relationships to both sides @@ -27,6 +27,7 @@ class RolePermission(Base): # Role DB Schema #################### + class Role(Base): __tablename__ = "roles" @@ -55,20 +56,19 @@ class RoleModel(BaseModel): # Forms #################### + class RoleForm(BaseModel): role: str class RolesTable: def insert_new_role( - self, - name: str, + self, + name: str, ) -> Optional[RoleModel]: with get_db() as db: result = Role( - name=name, - created_at=int(time.time()), - updated_at=int(time.time()) + name=name, created_at=int(time.time()), updated_at=int(time.time()) ) db.add(result) db.commit() @@ -102,7 +102,9 @@ def update_name_by_id(self, role_id: str, name: str) -> Optional[RoleModel]: db.commit() return self.get_role_by_id(role_id) - def get_roles(self, skip: Optional[int] = None, limit: Optional[int] = None) -> list[RoleModel]: + def get_roles( + self, skip: Optional[int] = None, limit: Optional[int] = None + ) -> list[RoleModel]: with get_db() as db: query = db.query(Role).order_by(Role.id) diff --git a/backend/open_webui/routers/permissions.py b/backend/open_webui/routers/permissions.py index c6fc420761a..f0966c3d5aa 100644 --- a/backend/open_webui/routers/permissions.py +++ b/backend/open_webui/routers/permissions.py @@ -5,16 +5,14 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS from open_webui.utils.auth import get_admin_user -from open_webui.models.roles import ( - RoleForm -) +from open_webui.models.roles import RoleForm from open_webui.models.permissions import ( Permissions, PermissionModel, PermissionCreateModel, PermissionEmptyModel, PermissionCategory, - PermissionAddForm + PermissionAddForm, ) @@ -47,25 +45,5 @@ async def add_permissions(form_data: PermissionAddForm, user=Depends(get_admin_u raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='Something went wrong. Please try again.', + detail="Something went wrong. Please try again.", ) - - -############################ -# UpdatePermissionById -############################ - -@router.post("/{permission_id}", response_model=Optional[PermissionModel]) -async def update_permission_name(role_id: int, form_data: RoleForm, user=Depends(get_admin_user)): - pass - - -############################ -# DeletePermissionById -############################ - - -# TODO(jeskr): Check if role is used by any users before deleting it. -@router.delete("/{permission_id}", response_model=bool) -async def delete_permission_by_name(role_name: str, user=Depends(get_admin_user)): - pass \ No newline at end of file diff --git a/backend/open_webui/routers/roles.py b/backend/open_webui/routers/roles.py index df9c3cb179c..cd2732b0188 100644 --- a/backend/open_webui/routers/roles.py +++ b/backend/open_webui/routers/roles.py @@ -5,16 +5,12 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS from open_webui.utils.auth import get_admin_user -from open_webui.models.roles import ( - RoleModel, - Roles, - RoleForm -) +from open_webui.models.roles import RoleModel, Roles, RoleForm from open_webui.models.permissions import ( Permissions, PermissionModel, PermissionCategory, - PermissionRoleForm + PermissionRoleForm, ) @@ -30,7 +26,11 @@ @router.get("/", response_model=list[RoleModel]) -async def get_roles(skip: Optional[int] = None, limit: Optional[int] = None, user=Depends(get_admin_user)): +async def get_roles( + skip: Optional[int] = None, + limit: Optional[int] = None, + user=Depends(get_admin_user), +): return Roles.get_roles(skip, limit) @@ -46,7 +46,7 @@ async def add_role(form_data: RoleForm, user=Depends(get_admin_user)): if existing_role: raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail=f"Role with name '{form_data.role}' already exists" + detail=ERROR_MESSAGES.ROLE_ALREADY_EXISTS(role=form_data.role), ) return Roles.insert_new_role(name=form_data.role) @@ -56,31 +56,34 @@ async def add_role(form_data: RoleForm, user=Depends(get_admin_user)): # UpdateRoleById ############################ + @router.post("/{role_id}", response_model=Optional[RoleModel]) -async def update_role_name(role_id: int, form_data: RoleForm, user=Depends(get_admin_user)): +async def update_role_name( + role_id: int, form_data: RoleForm, user=Depends(get_admin_user) +): # Check if the role already exists existing_role = Roles.get_role_by_id(role_id) if not existing_role: raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail=f"Role with name '{form_data.role}' do not exists" + detail=ERROR_MESSAGES.ROLE_DO_NOT_EXISTS, ) return Roles.update_name_by_id(role_id, form_data.role) + ############################ # DeleteRoleById ############################ -# TODO(jeskr): Check if role is used by any users before deleting it. @router.delete("/{role_name}", response_model=bool) async def delete_role_by_id(role_name: str, user=Depends(get_admin_user)): role = Roles.get_role_by_name(role_name) if not role: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Role with name '{role_name}' not found.", + detail=ERROR_MESSAGES.ROLE_DO_NOT_EXISTS, ) result = Roles.delete_by_id(role.id) @@ -89,7 +92,7 @@ async def delete_role_by_id(role_name: str, user=Depends(get_admin_user)): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ERROR_MESSAGES.DELETE_ROLE_ERROR, + detail=ERROR_MESSAGES.ROLE_DELETE_ERROR, ) @@ -99,11 +102,13 @@ async def delete_role_by_id(role_name: str, user=Depends(get_admin_user)): @router.get("/{role_name}/permissions") -async def get_default_permissions_by_role_name(role_name: str, user=Depends(get_admin_user)): +async def get_default_permissions_by_role_name( + role_name: str, user=Depends(get_admin_user) +): if not Roles.exists(role_name): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='Role do not exists. Please check the role name and try again.', + detail=ERROR_MESSAGES.ROLE_NOT_FOUND, ) permissions = Permissions.get_ordre_by_category(role_name) @@ -113,62 +118,74 @@ async def get_default_permissions_by_role_name(role_name: str, user=Depends(get_ raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='Permissions fetch failed. Please try again later.', + detail=ERROR_MESSAGES.PERMISSION_FETCH_FAILED, ) + ############################ # LinkPermissionToRole ########################### + @router.post("/{role_name}/permission/link") -async def link_default_permission_to_role(role_name: str, form_data: PermissionRoleForm, user=Depends(get_admin_user)): +async def link_default_permission_to_role( + role_name: str, form_data: PermissionRoleForm, user=Depends(get_admin_user) +): permission = Permissions.get(form_data.permission_name, form_data.category) if not permission: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Permission with name '{form_data.permission_name}' and category '{form_data.category}' not found.", + detail=ERROR_MESSAGES.PERMISSION_NOT_FOUND( + name=form_data.permission_name, category=form_data.category.value + ), ) role = Roles.get_role_by_name(role_name) if not role: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Role with name '{role_name}' not found.", + detail=ERROR_MESSAGES.ROLE_NOT_FOUND, ) - result = Permissions.link(permission_id=permission.id, role_id=role.id, value=form_data.value) + result = Permissions.link( + permission_id=permission.id, role_id=role.id, value=form_data.value + ) if result: return True raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='Something went wrong. Please try again.', + detail=ERROR_MESSAGES.ROLE_ERROR, ) + ############################ # UnlinkPermissionToRole ########################### + @router.delete("/{role_name}/permission/{permission_category}/{permission_name}") async def unlink_default_permission_from_role( - role_name: str, - permission_name: str, - category: PermissionCategory, - user=Depends(get_admin_user) - ): + role_name: str, + permission_name: str, + category: PermissionCategory, + user=Depends(get_admin_user), +): role = Roles.get_role_by_name(role_name) if not role: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Role with name '{role_name}' not found.", + detail=ERROR_MESSAGES.ROLE_NOT_FOUND, ) permission = Permissions.get(permission_name, category) if not permission: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Permission with name '{permission_name}' and category '{category}' not found.", + detail=ERROR_MESSAGES.PERMISSION_NOT_FOUND( + name=permission_name, category=category + ), ) result = Permissions.unlink(permission_id=permission.id, role_id=role.id) @@ -177,5 +194,5 @@ async def unlink_default_permission_from_role( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail='Something went wrong. Please try again.', + detail=ERROR_MESSAGES.ROLE_ERROR, ) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 12539cdd31d..f71fbfcd528 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -20,11 +20,18 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status from pydantic import BaseModel -from open_webui.utils.access_control import get_permissions, has_permission - -from open_webui.utils.auth import get_admin_user, get_password_hash, get_verified_user, get_current_user +from open_webui.utils.auth import ( + get_admin_user, + get_password_hash, + get_verified_user, + get_current_user, +) from open_webui.utils.access_control import get_permissions -from open_webui.models.permissions import Permissions, PermissionCategory, PermissionModel +from open_webui.models.permissions import ( + Permissions, + PermissionCategory, + PermissionModel, +) log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -140,12 +147,18 @@ class UserPermissions(BaseModel): chat: ChatPermissions features: FeaturesPermissions -@router.get("/default/permissions", response_model=dict[PermissionCategory, dict[str, bool]]) + +@router.get( + "/default/permissions", response_model=dict[PermissionCategory, dict[str, bool]] +) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): return Permissions.get_ordre_by_category() + @router.post("/default/permissions") -async def update_default_user_permissions(form_data: UserPermissions, user=Depends(get_admin_user)): +async def update_default_user_permissions( + form_data: UserPermissions, user=Depends(get_admin_user) +): permissions_dict = form_data.model_dump() for category_str, permissions in permissions_dict.items(): @@ -154,10 +167,10 @@ async def update_default_user_permissions(form_data: UserPermissions, user=Depen for permission_name, value in permissions.items(): permission_data = { - 'name': permission_name, - 'category': category, - 'value': value, - 'description': f"Default {category.value} permission for {permission_name}" + "name": permission_name, + "category": category, + "value": value, + "description": f"Default {category.value} permission for {permission_name}", } if Permissions.exists(permission_data): diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index ad23297a0b2..64cc7885cf8 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -138,7 +138,9 @@ def get_user_role(self, user, user_data): for allowed_role in oauth_allowed_roles: # If the user has any of the allowed roles, assign the role "user" if allowed_role in oauth_roles: - first_match = self.find_first_role_match(oauth_roles, oauth_allowed_roles) + first_match = self.find_first_role_match( + oauth_roles, oauth_allowed_roles + ) if first_match: Roles.add_role_if_role_do_not_exists(first_match) role = first_match diff --git a/src/lib/apis/permissions/index.ts b/src/lib/apis/permissions/index.ts index 51366698d8f..df2edd6d6f0 100644 --- a/src/lib/apis/permissions/index.ts +++ b/src/lib/apis/permissions/index.ts @@ -10,15 +10,15 @@ export const getPermissions = async (token: string) => { Authorization: `Bearer ${token}` } }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; @@ -27,7 +27,13 @@ export const getPermissions = async (token: string) => { return res; }; -export const addPermission = async (token: string, category: string, name: string, label: string, description: string) => { +export const addPermission = async ( + token: string, + category: string, + name: string, + label: string, + description: string +) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/permissions/`, { @@ -43,45 +49,15 @@ export const addPermission = async (token: string, category: string, name: strin description: description }) }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); - - if (error) { - throw error; - } - - return res; -}; - -export const updatePermission = async (token: string, permissionId: number, permissionName: string) => { - let error = null; - - const res = await fetch(`${WEBUI_API_BASE_URL}/permissions/${permissionId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - body: JSON.stringify({ - name: permissionName + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); }) - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; @@ -89,31 +65,3 @@ export const updatePermission = async (token: string, permissionId: number, perm return res; }; - -export const deletePermission = async (token: string, permissionId: number) => { - let error = null; - - const res = await fetch(`${WEBUI_API_BASE_URL}/permissions/${permissionId}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); - - if (error) { - throw error; - } - - return res; -}; - diff --git a/src/lib/apis/roles/index.ts b/src/lib/apis/roles/index.ts index ebc7b4a01e6..d7876aa50cf 100644 --- a/src/lib/apis/roles/index.ts +++ b/src/lib/apis/roles/index.ts @@ -10,15 +10,15 @@ export const getRoles = async (token: string) => { Authorization: `Bearer ${token}` } }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; @@ -40,15 +40,15 @@ export const addRole = async (token: string, roleName: string) => { role: roleName }) }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; @@ -70,15 +70,15 @@ export const updateRole = async (token: string, roleId: number, roleName: string role: roleName }) }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; @@ -95,17 +95,17 @@ export const deleteRole = async (token: string, roleName: string) => { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` - }, + } }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; @@ -114,7 +114,7 @@ export const deleteRole = async (token: string, roleName: string) => { return res; }; -export const getRolePermissions = async (token: string, roleName: string ) => { +export const getRolePermissions = async (token: string, roleName: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permissions`, { @@ -124,15 +124,15 @@ export const getRolePermissions = async (token: string, roleName: string ) => { Authorization: `Bearer ${token}` } }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; @@ -141,7 +141,13 @@ export const getRolePermissions = async (token: string, roleName: string ) => { return res; }; -export const linkRoleToPermissions = async (token: string, roleName: string, categoryName: string, permissionName: string, value: boolean ) => { +export const linkRoleToPermissions = async ( + token: string, + roleName: string, + categoryName: string, + permissionName: string, + value: boolean +) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission/link`, { method: 'POST', @@ -155,15 +161,15 @@ export const linkRoleToPermissions = async (token: string, roleName: string, cat value: value }) }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; @@ -172,25 +178,33 @@ export const linkRoleToPermissions = async (token: string, roleName: string, cat return res; }; -export const unlinkRoleFromPermissions = async (token: string, roleName: string, categoryName: string, permissionName: string) => { +export const unlinkRoleFromPermissions = async ( + token: string, + roleName: string, + categoryName: string, + permissionName: string +) => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission/${categoryName}/${permissionName}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - console.log(err); - error = err.detail; - return null; - }); + const res = await fetch( + `${WEBUI_API_BASE_URL}/roles/${roleName}/permission/${categoryName}/${permissionName}`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); if (error) { throw error; diff --git a/src/lib/components/admin/Users.svelte b/src/lib/components/admin/Users.svelte index 4e4987fb8ca..1d66d62554e 100644 --- a/src/lib/components/admin/Users.svelte +++ b/src/lib/components/admin/Users.svelte @@ -9,7 +9,7 @@ import Groups from './Users/Groups.svelte'; import Roles from './Users/RoleList.svelte'; import PermissionList from './Users/PermissionList.svelte'; - import WrenchSolid from "$lib/components/icons/WrenchSolid.svelte"; + import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte'; const i18n = getContext('i18n'); @@ -106,7 +106,7 @@ class="size-4" >
diff --git a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte index 1a7b0f4180b..3837056ec8d 100644 --- a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte +++ b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte @@ -10,7 +10,7 @@ import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte'; import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; - import {getUserDefaultPermissions} from "$lib/apis/users"; + import { getUserDefaultPermissions } from '$lib/apis/users'; export let onSubmit: Function = () => {}; export let onDelete: Function = () => {}; @@ -66,7 +66,7 @@ init(); } - onMount(async() => { + onMount(async () => { defaultPermissions = await getUserDefaultPermissions(localStorage.token).catch((error) => { toast.error(`${error}`); return []; @@ -204,7 +204,7 @@ {#if selectedTab == 'general'} {:else if selectedTab == 'permissions'} - + {:else if selectedTab == 'users'} {/if} diff --git a/src/lib/components/admin/Users/PermissionList.svelte b/src/lib/components/admin/Users/PermissionList.svelte index 03bc2e3bf80..f2558c0d204 100644 --- a/src/lib/components/admin/Users/PermissionList.svelte +++ b/src/lib/components/admin/Users/PermissionList.svelte @@ -1,206 +1,231 @@ { + bind:show={showAddPermissionModal} + on:save={async () => { permissions = await getPermissions(localStorage.token); }} />
-
-
- {$i18n.t('Permissions')} -
-
-
- -
-
-
- - - -
-
-
+ > + + + +
+
+ -
- - - - + + + + + {#each permissions as perm} + + + + + + + + {/each} + +
setSortKey('id')}> -
- {$i18n.t('Identifier')} - - {#if sortKey === 'id'} +
+ + + + - + - + - + - + - - - - - {#each permissions as perm} - - - - - - - - {/each} - -
setSortKey('id')} + > +
+ {$i18n.t('Identifier')} + + {#if sortKey === 'id'} {#if sortOrder === 'asc'} - + >{#if sortOrder === 'asc'} + {:else} - + {/if} - {:else} + {:else} - {/if} -
-
setSortKey('category')}> -
- {$i18n.t('Category')} - - {#if sortKey === 'category'} + {/if} +
+
setSortKey('category')} + > +
+ {$i18n.t('Category')} + + {#if sortKey === 'category'} {#if sortOrder === 'asc'} - + >{#if sortOrder === 'asc'} + {:else} - + {/if} - {:else} + {:else} - {/if} -
-
setSortKey('name')}> -
- {$i18n.t('Name')} - - {#if sortKey === 'name'} + {/if} +
+
setSortKey('name')} + > +
+ {$i18n.t('Name')} + + {#if sortKey === 'name'} {#if sortOrder === 'asc'} - + >{#if sortOrder === 'asc'} + {:else} - + {/if} - {:else} + {:else} - {/if} -
-
setSortKey('label')}> -
- {$i18n.t('Label')} - {#if sortKey === 'label'} + {/if} +
+
setSortKey('label')} + > +
+ {$i18n.t('Label')} + {#if sortKey === 'label'} {#if sortOrder === 'asc'} - + >{#if sortOrder === 'asc'} + {:else} - + {/if} - {:else} + {:else} - {/if} -
-
setSortKey('description')}> -
- {$i18n.t('Description')} - {#if sortKey === 'description'} + {/if} +
+
setSortKey('description')} + > +
+ {$i18n.t('Description')} + {#if sortKey === 'description'} {#if sortOrder === 'asc'} - + >{#if sortOrder === 'asc'} + {:else} - + {/if} - {:else} + {:else} - {/if} -
-
-
{perm.id}
-
-
-
{perm.category}
-
-
-
-
{perm.name}
-
-
-
-
{perm.label}
-
-
-
-
{perm.description}
-
-
+ {/if} +
+
+
{perm.id}
+
+
+
{perm.category}
+
+
+
+
{perm.name}
+
+
+
+
{perm.label}
+
+
+
+
{perm.description}
+
+
diff --git a/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte b/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte index 3ed542cc24b..0ee53eee4b5 100644 --- a/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte +++ b/src/lib/components/admin/Users/Permissions/AddPermissionModal.svelte @@ -30,7 +30,13 @@ }; loading = true; - const res = await addPermission(localStorage.token, _permission.category, _permission.name, _permission.label, _permission.description).catch((error) => { + const res = await addPermission( + localStorage.token, + _permission.category, + _permission.name, + _permission.label, + _permission.description + ).catch((error) => { toast.error(`${error}`); }); @@ -68,18 +74,19 @@
-
{ submitHandler(); - }}> - + }} + >
{$i18n.t('Category')}
diff --git a/src/lib/components/common/Permissions.svelte b/src/lib/components/common/Permissions.svelte index c4b5b91d2fa..d717ed40a70 100644 --- a/src/lib/components/common/Permissions.svelte +++ b/src/lib/components/common/Permissions.svelte @@ -1,16 +1,16 @@