Skip to content

Commit 6c86ff7

Browse files
authored
Merge pull request #1 from lgarceau768/feat/google-groups
Google Groups Functionaliity on top of version 0.6.34
2 parents 7a83e7d + 6dbc01c commit 6c86ff7

File tree

3 files changed

+535
-30
lines changed

3 files changed

+535
-30
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import pytest
2+
from unittest.mock import AsyncMock, patch, MagicMock
3+
import aiohttp
4+
from open_webui.utils.oauth import OAuthManager
5+
from open_webui.config import AppConfig
6+
7+
8+
class TestOAuthGoogleGroups:
9+
"""Basic tests for Google OAuth Groups functionality"""
10+
11+
def setup_method(self):
12+
"""Setup test fixtures"""
13+
self.oauth_manager = OAuthManager(app=MagicMock())
14+
15+
@pytest.mark.asyncio
16+
async def test_fetch_google_groups_success(self):
17+
"""Test successful Google groups fetching with proper aiohttp mocking"""
18+
# Mock response data from Google Cloud Identity API
19+
mock_response_data = {
20+
"memberships": [
21+
{
22+
"groupKey": {"id": "admin@company.com"},
23+
"group": "groups/123",
24+
"displayName": "Admin Group"
25+
},
26+
{
27+
"groupKey": {"id": "users@company.com"},
28+
"group": "groups/456",
29+
"displayName": "Users Group"
30+
}
31+
]
32+
}
33+
34+
# Create properly structured async mocks
35+
mock_response = MagicMock()
36+
mock_response.status = 200
37+
mock_response.json = AsyncMock(return_value=mock_response_data)
38+
39+
# Mock the async context manager for session.get()
40+
mock_get_context = MagicMock()
41+
mock_get_context.__aenter__ = AsyncMock(return_value=mock_response)
42+
mock_get_context.__aexit__ = AsyncMock(return_value=None)
43+
44+
# Mock the session
45+
mock_session = MagicMock()
46+
mock_session.get = MagicMock(return_value=mock_get_context)
47+
48+
# Mock the async context manager for ClientSession
49+
mock_session_context = MagicMock()
50+
mock_session_context.__aenter__ = AsyncMock(return_value=mock_session)
51+
mock_session_context.__aexit__ = AsyncMock(return_value=None)
52+
53+
with patch("aiohttp.ClientSession", return_value=mock_session_context):
54+
groups = await self.oauth_manager._fetch_google_groups_via_cloud_identity(
55+
access_token="test_token",
56+
user_email="user@company.com"
57+
)
58+
59+
# Verify the results
60+
assert groups == ["admin@company.com", "users@company.com"]
61+
62+
# Verify the HTTP call was made correctly
63+
mock_session.get.assert_called_once()
64+
call_args = mock_session.get.call_args
65+
66+
# Check the URL contains the user email (URL encoded)
67+
url_arg = call_args[0][0] # First positional argument
68+
assert "user%40company.com" in url_arg # @ is encoded as %40
69+
assert "searchTransitiveGroups" in url_arg
70+
71+
# Check headers contain the bearer token
72+
headers_arg = call_args[1]["headers"] # headers keyword argument
73+
assert headers_arg["Authorization"] == "Bearer test_token"
74+
assert headers_arg["Content-Type"] == "application/json"
75+
76+
@pytest.mark.asyncio
77+
async def test_fetch_google_groups_api_error(self):
78+
"""Test handling of API errors when fetching groups"""
79+
# Mock failed response
80+
mock_response = MagicMock()
81+
mock_response.status = 403
82+
mock_response.text = AsyncMock(return_value="Permission denied")
83+
84+
# Mock the async context manager for session.get()
85+
mock_get_context = MagicMock()
86+
mock_get_context.__aenter__ = AsyncMock(return_value=mock_response)
87+
mock_get_context.__aexit__ = AsyncMock(return_value=None)
88+
89+
# Mock the session
90+
mock_session = MagicMock()
91+
mock_session.get = MagicMock(return_value=mock_get_context)
92+
93+
# Mock the async context manager for ClientSession
94+
mock_session_context = MagicMock()
95+
mock_session_context.__aenter__ = AsyncMock(return_value=mock_session)
96+
mock_session_context.__aexit__ = AsyncMock(return_value=None)
97+
98+
with patch("aiohttp.ClientSession", return_value=mock_session_context):
99+
groups = await self.oauth_manager._fetch_google_groups_via_cloud_identity(
100+
access_token="test_token",
101+
user_email="user@company.com"
102+
)
103+
104+
# Should return empty list on error
105+
assert groups == []
106+
107+
@pytest.mark.asyncio
108+
async def test_fetch_google_groups_network_error(self):
109+
"""Test handling of network errors when fetching groups"""
110+
# Mock the session that raises an exception when get() is called
111+
mock_session = MagicMock()
112+
mock_session.get.side_effect = aiohttp.ClientError("Network error")
113+
114+
# Mock the async context manager for ClientSession
115+
mock_session_context = MagicMock()
116+
mock_session_context.__aenter__ = AsyncMock(return_value=mock_session)
117+
mock_session_context.__aexit__ = AsyncMock(return_value=None)
118+
119+
with patch("aiohttp.ClientSession", return_value=mock_session_context):
120+
groups = await self.oauth_manager._fetch_google_groups_via_cloud_identity(
121+
access_token="test_token",
122+
user_email="user@company.com"
123+
)
124+
125+
# Should return empty list on network error
126+
assert groups == []
127+
128+
@pytest.mark.asyncio
129+
async def test_get_user_role_with_google_groups(self):
130+
"""Test role assignment using Google groups"""
131+
# Mock configuration
132+
mock_config = MagicMock()
133+
mock_config.ENABLE_OAUTH_ROLE_MANAGEMENT = True
134+
mock_config.OAUTH_ROLES_CLAIM = "groups"
135+
mock_config.OAUTH_ALLOWED_ROLES = ["users@company.com"]
136+
mock_config.OAUTH_ADMIN_ROLES = ["admin@company.com"]
137+
mock_config.DEFAULT_USER_ROLE = "pending"
138+
mock_config.OAUTH_EMAIL_CLAIM = "email"
139+
140+
user_data = {"email": "user@company.com"}
141+
142+
# Mock Google OAuth scope check and Users class
143+
with patch("open_webui.utils.oauth.auth_manager_config", mock_config), \
144+
patch("open_webui.utils.oauth.GOOGLE_OAUTH_SCOPE") as mock_scope, \
145+
patch("open_webui.utils.oauth.Users") as mock_users, \
146+
patch.object(self.oauth_manager, "_fetch_google_groups_via_cloud_identity") as mock_fetch:
147+
148+
mock_scope.value = "openid email profile https://www.googleapis.com/auth/cloud-identity.groups.readonly"
149+
mock_fetch.return_value = ["admin@company.com", "users@company.com"]
150+
mock_users.get_num_users.return_value = 5 # Not first user
151+
152+
role = await self.oauth_manager.get_user_role(
153+
user=None,
154+
user_data=user_data,
155+
provider="google",
156+
access_token="test_token"
157+
)
158+
159+
# Should assign admin role since user is in admin group
160+
assert role == "admin"
161+
mock_fetch.assert_called_once_with("test_token", "user@company.com")
162+
163+
@pytest.mark.asyncio
164+
async def test_get_user_role_fallback_to_claims(self):
165+
"""Test fallback to traditional claims when Google groups fail"""
166+
mock_config = MagicMock()
167+
mock_config.ENABLE_OAUTH_ROLE_MANAGEMENT = True
168+
mock_config.OAUTH_ROLES_CLAIM = "groups"
169+
mock_config.OAUTH_ALLOWED_ROLES = ["users"]
170+
mock_config.OAUTH_ADMIN_ROLES = ["admin"]
171+
mock_config.DEFAULT_USER_ROLE = "pending"
172+
mock_config.OAUTH_EMAIL_CLAIM = "email"
173+
174+
user_data = {
175+
"email": "user@company.com",
176+
"groups": ["users"]
177+
}
178+
179+
with patch("open_webui.utils.oauth.auth_manager_config", mock_config), \
180+
patch("open_webui.utils.oauth.GOOGLE_OAUTH_SCOPE") as mock_scope, \
181+
patch("open_webui.utils.oauth.Users") as mock_users, \
182+
patch.object(self.oauth_manager, "_fetch_google_groups_via_cloud_identity") as mock_fetch:
183+
184+
# Mock scope without Cloud Identity
185+
mock_scope.value = "openid email profile"
186+
mock_users.get_num_users.return_value = 5 # Not first user
187+
188+
role = await self.oauth_manager.get_user_role(
189+
user=None,
190+
user_data=user_data,
191+
provider="google",
192+
access_token="test_token"
193+
)
194+
195+
# Should use traditional claims since Cloud Identity scope not present
196+
assert role == "user"
197+
mock_fetch.assert_not_called()
198+
199+
@pytest.mark.asyncio
200+
async def test_get_user_role_non_google_provider(self):
201+
"""Test that non-Google providers use traditional claims"""
202+
mock_config = MagicMock()
203+
mock_config.ENABLE_OAUTH_ROLE_MANAGEMENT = True
204+
mock_config.OAUTH_ROLES_CLAIM = "roles"
205+
mock_config.OAUTH_ALLOWED_ROLES = ["user"]
206+
mock_config.OAUTH_ADMIN_ROLES = ["admin"]
207+
mock_config.DEFAULT_USER_ROLE = "pending"
208+
209+
user_data = {"roles": ["user"]}
210+
211+
with patch("open_webui.utils.oauth.auth_manager_config", mock_config), \
212+
patch("open_webui.utils.oauth.Users") as mock_users, \
213+
patch.object(self.oauth_manager, "_fetch_google_groups_via_cloud_identity") as mock_fetch:
214+
215+
mock_users.get_num_users.return_value = 5 # Not first user
216+
217+
role = await self.oauth_manager.get_user_role(
218+
user=None,
219+
user_data=user_data,
220+
provider="microsoft",
221+
access_token="test_token"
222+
)
223+
224+
# Should use traditional claims for non-Google providers
225+
assert role == "user"
226+
mock_fetch.assert_not_called()
227+
228+
@pytest.mark.asyncio
229+
async def test_update_user_groups_with_google_groups(self):
230+
"""Test group management using Google groups from user_data"""
231+
mock_config = MagicMock()
232+
mock_config.OAUTH_GROUPS_CLAIM = "groups"
233+
mock_config.OAUTH_BLOCKED_GROUPS = "[]"
234+
mock_config.ENABLE_OAUTH_GROUP_CREATION = False
235+
236+
# Mock user with Google groups data
237+
mock_user = MagicMock()
238+
mock_user.id = "user123"
239+
240+
user_data = {
241+
"google_groups": ["developers@company.com", "employees@company.com"]
242+
}
243+
244+
# Mock existing groups and user groups
245+
mock_existing_group = MagicMock()
246+
mock_existing_group.name = "developers@company.com"
247+
mock_existing_group.id = "group1"
248+
mock_existing_group.user_ids = []
249+
mock_existing_group.permissions = {"read": True}
250+
mock_existing_group.description = "Developers group"
251+
252+
with patch("open_webui.utils.oauth.auth_manager_config", mock_config), \
253+
patch("open_webui.utils.oauth.Groups") as mock_groups:
254+
255+
mock_groups.get_groups_by_member_id.return_value = []
256+
mock_groups.get_groups.return_value = [mock_existing_group]
257+
258+
await self.oauth_manager.update_user_groups(
259+
user=mock_user,
260+
user_data=user_data,
261+
default_permissions={"read": True}
262+
)
263+
264+
# Should use Google groups instead of traditional claims
265+
mock_groups.get_groups_by_member_id.assert_called_once_with("user123")
266+
mock_groups.update_group_by_id.assert_called()

0 commit comments

Comments
 (0)