Skip to content

Commit 9c9a18d

Browse files
authored
Merge pull request open-webui#21971 from open-webui/dev
0.8.6
2 parents 1ac3dd4 + 67893b9 commit 9c9a18d

190 files changed

Lines changed: 9386 additions & 2115 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docker-build.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ jobs:
9595
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
9696
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
9797
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
98+
sbom: true
9899
build-args: |
99100
BUILD_HASH=${{ github.sha }}
100101
@@ -199,6 +200,7 @@ jobs:
199200
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
200201
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
201202
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
203+
sbom: true
202204
build-args: |
203205
BUILD_HASH=${{ github.sha }}
204206
USE_CUDA=true
@@ -304,6 +306,7 @@ jobs:
304306
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
305307
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
306308
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
309+
sbom: true
307310
build-args: |
308311
BUILD_HASH=${{ github.sha }}
309312
USE_CUDA=true
@@ -407,6 +410,7 @@ jobs:
407410
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
408411
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
409412
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
413+
sbom: true
410414
build-args: |
411415
BUILD_HASH=${{ github.sha }}
412416
USE_OLLAMA=true
@@ -509,6 +513,7 @@ jobs:
509513
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
510514
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
511515
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
516+
sbom: true
512517
build-args: |
513518
BUILD_HASH=${{ github.sha }}
514519
USE_SLIM=true

CHANGELOG.md

Lines changed: 67 additions & 0 deletions
Large diffs are not rendered by default.

CONTRIBUTOR_LICENSE_AGREEMENT

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# Open WebUI Contributor License Agreement
1+
# Contributor License Agreement
22

3-
By submitting my contributions to Open WebUI, I grant Open WebUI full freedom to use my work in any way they choose, under any terms they like, both now and in the future. This approach helps ensure the project remains unified, flexible, and easy to maintain, while empowering Open WebUI to respond quickly to the needs of its users and the wider community.
3+
By submitting my contributions to this repository in any form, I grant Open WebUI Inc. a perpetual, worldwide, irrevocable, royalty-free license, under copyright and patent, to use, modify, distribute, sublicense, and commercialize my work under any terms they choose, both now and in the future.
44

5-
Taking part in this process means my work can be seamlessly integrated and combined with others, ensuring longevity and adaptability for everyone who benefits from the Open WebUI project. This collaborative approach strengthens the project’s future and helps guarantee that improvements can always be shared and distributed in the most effective way possible.
5+
I represent that my contributions are my original work (or that I have sufficient rights to grant this license) and that I have the authority to enter into this agreement.
66

77
**_To the fullest extent permitted by law, my contributions are provided on an “as is” basis, with no warranties or guarantees of any kind, and I disclaim any liability for any issues or damages arising from their use or incorporation into the project, regardless of the type of legal claim._**

backend/open_webui/config.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ def __getattr__(self, key):
322322
if JWT_EXPIRES_IN.value == "-1":
323323
log.warning(
324324
"⚠️ SECURITY WARNING: JWT_EXPIRES_IN is set to '-1'\n"
325-
" See: https://docs.openwebui.com/getting-started/env-configuration\n"
325+
" See: https://docs.openwebui.com/reference/env-configuration\n"
326326
)
327327

328328
####################################
@@ -642,6 +642,18 @@ def __getattr__(self, key):
642642
os.environ.get("OAUTH_UPDATE_PICTURE_ON_LOGIN", "False").lower() == "true",
643643
)
644644

645+
OAUTH_UPDATE_NAME_ON_LOGIN = PersistentConfig(
646+
"OAUTH_UPDATE_NAME_ON_LOGIN",
647+
"oauth.update_name_on_login",
648+
os.environ.get("OAUTH_UPDATE_NAME_ON_LOGIN", "False").lower() == "true",
649+
)
650+
651+
OAUTH_UPDATE_EMAIL_ON_LOGIN = PersistentConfig(
652+
"OAUTH_UPDATE_EMAIL_ON_LOGIN",
653+
"oauth.update_email_on_login",
654+
os.environ.get("OAUTH_UPDATE_EMAIL_ON_LOGIN", "False").lower() == "true",
655+
)
656+
645657
OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID = (
646658
os.environ.get("OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID", "False").lower()
647659
== "true"
@@ -1171,6 +1183,20 @@ def reachable(host: str, port: int) -> bool:
11711183
tool_server_connections,
11721184
)
11731185

1186+
####################################
1187+
# TERMINAL_SERVER
1188+
####################################
1189+
1190+
terminal_server_connections = json.loads(
1191+
os.environ.get("TERMINAL_SERVER_CONNECTIONS", "[]")
1192+
)
1193+
1194+
TERMINAL_SERVER_CONNECTIONS = PersistentConfig(
1195+
"TERMINAL_SERVER_CONNECTIONS",
1196+
"terminal_server.connections",
1197+
terminal_server_connections,
1198+
)
1199+
11741200
####################################
11751201
# WEBUI
11761202
####################################
@@ -1433,6 +1459,11 @@ def reachable(host: str, port: int) -> bool:
14331459
== "true"
14341460
)
14351461

1462+
USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = (
1463+
os.environ.get("USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS", "True").lower()
1464+
== "true"
1465+
)
1466+
14361467

14371468
USER_PERMISSIONS_CHAT_CONTROLS = (
14381469
os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true"
@@ -1590,6 +1621,9 @@ def reachable(host: str, port: int) -> bool:
15901621
"notes": USER_PERMISSIONS_NOTES_ALLOW_SHARING,
15911622
"public_notes": USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
15921623
},
1624+
"access_grants": {
1625+
"allow_users": USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS,
1626+
},
15931627
"chat": {
15941628
"controls": USER_PERMISSIONS_CHAT_CONTROLS,
15951629
"valves": USER_PERMISSIONS_CHAT_VALVES,
@@ -3177,17 +3211,24 @@ class BannerModel(BaseModel):
31773211
)
31783212

31793213

3214+
try:
3215+
web_search_domain_filter_list = json.loads(
3216+
os.getenv("WEB_SEARCH_DOMAIN_FILTER_LIST", "[]")
3217+
)
3218+
except Exception as e:
3219+
web_search_domain_filter_list = [
3220+
# "wikipedia.com",
3221+
# "wikimedia.org",
3222+
# "wikidata.org",
3223+
# "!stackoverflow.com",
3224+
]
3225+
31803226
# You can provide a list of your own websites to filter after performing a web search.
31813227
# This ensures the highest level of safety and reliability of the information sources.
31823228
WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
31833229
"WEB_SEARCH_DOMAIN_FILTER_LIST",
31843230
"rag.web.search.domain.filter_list",
3185-
[
3186-
# "wikipedia.com",
3187-
# "wikimedia.org",
3188-
# "wikidata.org",
3189-
# "!stackoverflow.com",
3190-
],
3231+
web_search_domain_filter_list,
31913232
)
31923233

31933234
WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(

backend/open_webui/env.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,3 +1041,16 @@ def parse_section(section):
10411041
####################################
10421042

10431043
EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL")
1044+
1045+
####################################
1046+
# GROUP DEFAULTS
1047+
####################################
1048+
1049+
# Controls the default "Who can share to this group" setting for new groups.
1050+
# Env var values: "true" (anyone), "false" (no one), "members" (only group members).
1051+
_default_group_share = (
1052+
os.environ.get("DEFAULT_GROUP_SHARE_PERMISSION", "members").strip().lower()
1053+
)
1054+
DEFAULT_GROUP_SHARE_PERMISSION = (
1055+
"members" if _default_group_share == "members" else _default_group_share == "true"
1056+
)

backend/open_webui/main.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
users,
9797
utils,
9898
scim,
99+
terminals,
99100
)
100101

101102
from open_webui.routers.retrieval import (
@@ -132,6 +133,8 @@
132133
THREAD_POOL_SIZE,
133134
# Tool Server Configs
134135
TOOL_SERVER_CONNECTIONS,
136+
# Terminal Server
137+
TERMINAL_SERVER_CONNECTIONS,
135138
# Code Execution
136139
ENABLE_CODE_EXECUTION,
137140
CODE_EXECUTION_ENGINE,
@@ -524,7 +527,7 @@
524527
process_chat_payload,
525528
process_chat_response,
526529
)
527-
from open_webui.utils.tools import set_tool_servers
530+
from open_webui.utils.tools import set_tool_servers, set_terminal_servers
528531

529532
from open_webui.utils.auth import (
530533
get_license_data,
@@ -690,8 +693,13 @@ async def lifespan(app: FastAPI):
690693
)
691694
await set_tool_servers(mock_request)
692695
log.info(f"Initialized {len(app.state.TOOL_SERVERS)} tool server(s)")
696+
697+
await set_terminal_servers(mock_request)
698+
log.info(
699+
f"Initialized {len(app.state.TERMINAL_SERVERS)} terminal server(s)"
700+
)
693701
except Exception as e:
694-
log.warning(f"Failed to initialize tool servers at startup: {e}")
702+
log.warning(f"Failed to initialize tool/terminal servers at startup: {e}")
695703

696704
yield
697705

@@ -775,6 +783,15 @@ async def lifespan(app: FastAPI):
775783
app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS
776784
app.state.TOOL_SERVERS = []
777785

786+
########################################
787+
#
788+
# TERMINAL SERVER
789+
#
790+
########################################
791+
792+
app.state.config.TERMINAL_SERVER_CONNECTIONS = TERMINAL_SERVER_CONNECTIONS
793+
app.state.TERMINAL_SERVERS = []
794+
778795
########################################
779796
#
780797
# DIRECT CONNECTIONS
@@ -1540,6 +1557,7 @@ async def inspect_websocket(request: Request, call_next):
15401557
if ENABLE_ADMIN_ANALYTICS:
15411558
app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"])
15421559
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
1560+
app.include_router(terminals.router, prefix="/api/v1/terminals", tags=["terminals"])
15431561

15441562
# SCIM 2.0 API for identity management
15451563
if ENABLE_SCIM:

backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ def upgrade() -> None:
127127

128128
timestamp = message.get("timestamp", now)
129129

130+
try:
131+
timestamp = int(float(timestamp))
132+
except Exception as e:
133+
timestamp = now
134+
130135
# Normalize timestamp: convert ms to seconds, validate range
131136
if timestamp > 10_000_000_000:
132137
timestamp = timestamp // 1000

backend/open_webui/models/access_grants.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,43 @@ def has_public_read_access_grant(access_grants: Optional[list]) -> bool:
204204
return False
205205

206206

207+
def has_user_access_grant(access_grants: Optional[list]) -> bool:
208+
"""
209+
Returns True when a direct grant list includes any non-wildcard user grant.
210+
"""
211+
for grant in normalize_access_grants(access_grants):
212+
if grant["principal_type"] == "user" and grant["principal_id"] != "*":
213+
return True
214+
return False
215+
216+
217+
def strip_user_access_grants(access_grants: Optional[list]) -> list:
218+
"""
219+
Remove all non-wildcard user grants from the list.
220+
Keeps group grants and the public wildcard (user:*) intact.
221+
"""
222+
if not access_grants:
223+
return []
224+
return [
225+
grant
226+
for grant in access_grants
227+
if not (
228+
(
229+
grant.get("principal_type")
230+
if isinstance(grant, dict)
231+
else getattr(grant, "principal_type", None)
232+
)
233+
== "user"
234+
and (
235+
grant.get("principal_id")
236+
if isinstance(grant, dict)
237+
else getattr(grant, "principal_id", None)
238+
)
239+
!= "*"
240+
)
241+
]
242+
243+
207244
def grants_to_access_control(grants: list) -> Optional[dict]:
208245
"""
209246
Convert a list of grant objects (AccessGrantModel or AccessGrantResponse)

backend/open_webui/models/auths.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def authenticate_user(
146146
def authenticate_user_by_api_key(
147147
self, api_key: str, db: Optional[Session] = None
148148
) -> Optional[UserModel]:
149-
log.info(f"authenticate_user_by_api_key: {api_key}")
149+
log.info(f"authenticate_user_by_api_key")
150150
# if no api_key, return None
151151
if not api_key:
152152
return None
@@ -197,7 +197,10 @@ def update_email_by_id(
197197
with get_db_context(db) as db:
198198
result = db.query(Auth).filter_by(id=id).update({"email": email})
199199
db.commit()
200-
return True if result == 1 else False
200+
if result == 1:
201+
Users.update_user_by_id(id, {"email": email}, db=db)
202+
return True
203+
return False
201204
except Exception:
202205
return False
203206

backend/open_webui/models/groups.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from sqlalchemy.orm import Session
88
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
9+
from open_webui.env import DEFAULT_GROUP_SHARE_PERMISSION
910

1011
from open_webui.models.files import FileMetadataResponse
1112

@@ -130,13 +131,26 @@ class GroupListResponse(BaseModel):
130131

131132

132133
class GroupTable:
134+
def _ensure_default_share_config(self, group_data: dict) -> dict:
135+
"""Ensure the group data dict has a default share config if not already set."""
136+
if "data" not in group_data or group_data["data"] is None:
137+
group_data["data"] = {}
138+
if "config" not in group_data["data"]:
139+
group_data["data"]["config"] = {}
140+
if "share" not in group_data["data"]["config"]:
141+
group_data["data"]["config"]["share"] = DEFAULT_GROUP_SHARE_PERMISSION
142+
return group_data
143+
133144
def insert_new_group(
134145
self, user_id: str, form_data: GroupForm, db: Optional[Session] = None
135146
) -> Optional[GroupModel]:
136147
with get_db_context(db) as db:
148+
group_data = self._ensure_default_share_config(
149+
form_data.model_dump(exclude_none=True)
150+
)
137151
group = GroupModel(
138152
**{
139-
**form_data.model_dump(exclude_none=True),
153+
**group_data,
140154
"id": str(uuid.uuid4()),
141155
"user_id": user_id,
142156
"created_at": int(time.time()),
@@ -504,6 +518,11 @@ def create_groups_by_group_names(
504518
user_id=user_id,
505519
name=group_name,
506520
description="",
521+
data={
522+
"config": {
523+
"share": DEFAULT_GROUP_SHARE_PERMISSION,
524+
}
525+
},
507526
created_at=int(time.time()),
508527
updated_at=int(time.time()),
509528
)

0 commit comments

Comments
 (0)