Skip to content

Commit 622ffdd

Browse files
authored
feat: require OAuth signup and merge cross-tenant agent lists (#3086)
- Block email/password registration for ASSET_OWNER invites; complete signup via OAuth - Merge ASSET_OWNER-scoped agents into /agent/list and published-agent list for other tenants - Add v2.2.0 migration for SU asset-owner invite permissions and ASSET_OWNER nav/CRUD RBAC - Frontend: map virtual tenant_id for ASSET_OWNER sessions and show OAuth-only signup error
1 parent 60818b3 commit 622ffdd

21 files changed

Lines changed: 1068 additions & 18 deletions

backend/apps/agent_app.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,15 @@ async def list_published_agents_api(
607607
"""
608608
try:
609609
user_id, tenant_id, _ = get_current_user_info(authorization, request)
610-
return await list_published_agents_impl(tenant_id=tenant_id, user_id=user_id)
610+
agent_list = await list_published_agents_impl(
611+
tenant_id=tenant_id, user_id=user_id
612+
)
613+
if tenant_id != ASSET_OWNER_TENANT_ID:
614+
asset_agent_list = await list_published_agents_impl(
615+
tenant_id=ASSET_OWNER_TENANT_ID, user_id=user_id
616+
)
617+
return agent_list + asset_agent_list
618+
return agent_list
611619
except Exception as e:
612620
logger.error(f"Published agents list error: {str(e)}")
613621
raise HTTPException(

backend/apps/user_management_app.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from supabase_auth.errors import AuthApiError, AuthWeakPasswordError
1010

11+
from consts.const import ASSET_OWNER_SIGNUP_USE_OAUTH_DETAIL
1112
from consts.model import UserSignInRequest, UserSignUpRequest, UpdatePasswordRequest
1213
from consts.exceptions import (
1314
NoInviteCodeException,
@@ -70,9 +71,14 @@ async def signup(request: UserSignUpRequest):
7071
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
7172
detail="INVITE_CODE_INVALID")
7273
except ValidationError as e:
73-
logging.warning(
74-
f"User registration rejected by feature flag: {str(e)}")
75-
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
74+
detail = str(e)
75+
if detail == ASSET_OWNER_SIGNUP_USE_OAUTH_DETAIL:
76+
logging.warning(
77+
"User registration rejected: asset owner invite requires OAuth")
78+
else:
79+
logging.warning(
80+
f"User registration rejected by validation: {detail}")
81+
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=detail)
7682
except UserRegistrationException as e:
7783
logging.error(
7884
f"User registration failed by registration service: {str(e)}")

backend/consts/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ class VectorDatabaseType(str, Enum):
128128
ENABLE_ASSET_OWNER_ROLE = os.getenv(
129129
"ENABLE_ASSET_OWNER_ROLE", "false").lower() == "true"
130130

131+
# HTTP detail key: asset owner must register via OAuth, not email/password signup.
132+
ASSET_OWNER_SIGNUP_USE_OAUTH_DETAIL = "ASSET_OWNER_USE_OAUTH"
133+
131134
# Roles that can edit all resources within a tenant (permission = EDIT).
132135
# Keep this centralized to avoid drifting role logic across modules.
133136
CAN_EDIT_ALL_USER_ROLES = {"SU", "ADMIN", "SPEED", "ASSET_OWNER"}

backend/services/oauth_service.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@
1212
from pydantic import EmailStr, TypeAdapter, ValidationError as PydanticValidationError
1313

1414
from consts.const import (
15+
ASSET_OWNER_INVITE_CODE_TYPE,
16+
ASSET_OWNER_ROLE,
17+
ASSET_OWNER_TENANT_ID,
1518
DEFAULT_TENANT_ID,
1619
OAUTH_CALLBACK_BASE_URL,
1720
OAUTH_SSL_VERIFY,
1821
OAUTH_CA_BUNDLE,
1922
SUPABASE_JWT_SECRET,
2023
)
2124
from consts.exceptions import OAuthLinkError, OAuthProviderError
25+
from services.asset_owner_visibility import require_asset_owner_enabled
2226
from consts.oauth_providers import (
2327
get_all_provider_definitions,
2428
get_provider_definition,
@@ -359,6 +363,9 @@ def _role_from_invitation_type(code_type: str) -> str:
359363
return "ADMIN"
360364
if code_type == "DEV_INVITE":
361365
return "DEV"
366+
if code_type == ASSET_OWNER_INVITE_CODE_TYPE:
367+
require_asset_owner_enabled()
368+
return ASSET_OWNER_ROLE
362369
return "USER"
363370

364371

@@ -431,7 +438,10 @@ async def complete_pending_oauth_account(
431438
supabase_user_id = create_resp.user.id
432439

433440
tenant_id = invitation_info["tenant_id"]
441+
if invitation_info.get("code_type") == ASSET_OWNER_INVITE_CODE_TYPE:
442+
tenant_id = ASSET_OWNER_TENANT_ID
434443
user_role = _role_from_invitation_type(invitation_info.get("code_type", "USER_INVITE"))
444+
is_asset_owner_registration = user_role == ASSET_OWNER_ROLE
435445

436446
insert_user_tenant(
437447
user_id=supabase_user_id,
@@ -446,12 +456,13 @@ async def complete_pending_oauth_account(
446456
from utils.str_utils import convert_string_to_list
447457

448458
group_ids = convert_string_to_list(group_ids)
449-
if group_ids:
459+
if group_ids and not is_asset_owner_registration:
450460
add_user_to_groups(supabase_user_id, group_ids, supabase_user_id)
451461

452462
if user_role == "ADMIN":
453463
await generate_tts_stt_4_admin(tenant_id, supabase_user_id)
454-
await init_tool_list_for_tenant(tenant_id, supabase_user_id)
464+
if not is_asset_owner_registration:
465+
await init_tool_list_for_tenant(tenant_id, supabase_user_id)
455466

456467
create_or_update_oauth_account(
457468
user_id=supabase_user_id,

backend/services/user_management_service.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,20 @@
2828
ASSET_OWNER_TENANT_ID,
2929
ASSET_OWNER_INVITE_CODE_TYPE,
3030
ASSET_OWNER_ROLE,
31+
ASSET_OWNER_SIGNUP_USE_OAUTH_DETAIL,
3132
)
3233

3334
from services.asset_owner_visibility import (
3435
filter_accessible_routes_for_asset_owner_feature,
3536
require_asset_owner_enabled,
3637
)
37-
from consts.const import INVITE_CODE, SUPABASE_URL, SUPABASE_KEY, DEFAULT_TENANT_ID
38-
from consts.exceptions import NoInviteCodeException, IncorrectInviteCodeException, UserRegistrationException, UnauthorizedError
38+
from consts.exceptions import (
39+
NoInviteCodeException,
40+
IncorrectInviteCodeException,
41+
UserRegistrationException,
42+
UnauthorizedError,
43+
ValidationError,
44+
)
3945
from consts.error_code import ErrorCode
4046
from consts.exceptions import AppException
4147

@@ -189,13 +195,15 @@ async def signup_user_with_invitation(email: EmailStr,
189195
user_role = "DEV"
190196
elif code_type == ASSET_OWNER_INVITE_CODE_TYPE:
191197
require_asset_owner_enabled()
192-
user_role = ASSET_OWNER_ROLE
198+
raise ValidationError(ASSET_OWNER_SIGNUP_USE_OAUTH_DETAIL)
193199

194200
logging.info(
195201
f"Invitation code {invite_code} validated successfully, will assign role: {user_role}")
196202

197203
except IncorrectInviteCodeException:
198204
raise
205+
except ValidationError:
206+
raise
199207
except Exception as e:
200208
logging.error(
201209
f"Invitation code {invite_code} validation failed: {str(e)}")
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
-- Migration: ASSET_OWNER role permissions and invitation type comment
2+
-- Date: 2026-05-29
3+
-- Description: Add ASSET_OWNER role permissions, SU asset-owner invite permissions,
4+
-- update invitation code_type comment, and ensure ag_skill_info_t.tenant_id exists
5+
-- Source: commit 15cece97692db2372a978cbdf21b5d5316e79f30 (init.sql)
6+
7+
SET search_path TO nexent;
8+
9+
BEGIN;
10+
11+
COMMENT ON COLUMN nexent.tenant_invitation_code_t.code_type IS
12+
'Invitation code type: ADMIN_INVITE, DEV_INVITE, USER_INVITE, ASSET_OWNER_INVITE';
13+
14+
INSERT INTO nexent.role_permission_t
15+
(role_permission_id, user_role, permission_category, permission_type, permission_subtype)
16+
VALUES
17+
(188, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'CREATE'),
18+
(189, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'READ'),
19+
(190, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'UPDATE'),
20+
(191, 'SU', 'RESOURCE', 'INVITE.ASSET_OWNER', 'DELETE'),
21+
(192, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/'),
22+
(193, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/agents'),
23+
(194, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/knowledges'),
24+
(195, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/chat'),
25+
(196, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/space'),
26+
(197, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/market'),
27+
(198, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/models'),
28+
(199, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'CREATE'),
29+
(200, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'READ'),
30+
(201, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'UPDATE'),
31+
(202, 'ASSET_OWNER', 'RESOURCE', 'AGENT', 'DELETE'),
32+
(203, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'CREATE'),
33+
(204, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'READ'),
34+
(205, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'UPDATE'),
35+
(206, 'ASSET_OWNER', 'RESOURCE', 'SKILL', 'DELETE'),
36+
(207, 'ASSET_OWNER', 'RESOURCE', 'KB', 'CREATE'),
37+
(208, 'ASSET_OWNER', 'RESOURCE', 'KB', 'READ'),
38+
(209, 'ASSET_OWNER', 'RESOURCE', 'KB', 'UPDATE'),
39+
(210, 'ASSET_OWNER', 'RESOURCE', 'KB', 'DELETE'),
40+
(211, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'CREATE'),
41+
(212, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'READ'),
42+
(213, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'UPDATE'),
43+
(214, 'ASSET_OWNER', 'RESOURCE', 'MCP', 'DELETE'),
44+
(215, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'CREATE'),
45+
(216, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'READ'),
46+
(217, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'UPDATE'),
47+
(218, 'ASSET_OWNER', 'RESOURCE', 'MODEL', 'DELETE'),
48+
(219, 'ASSET_OWNER', 'RESOURCE', 'USER.ROLE', 'READ'),
49+
(220, 'ASSET_OWNER', 'VISIBILITY', 'LEFT_NAV_MENU', '/users'),
50+
(221, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/asset-owner-resources')
51+
ON CONFLICT (role_permission_id) DO NOTHING;
52+
53+
COMMIT;

frontend/components/auth/registerModal.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,16 @@ export function RegisterModal() {
312312
value: values.inviteCode,
313313
},
314314
]);
315+
} else if (errorType === "ASSET_OWNER_USE_OAUTH") {
316+
const errorMsg = t("auth.assetOwnerUseOAuth");
317+
message.error(errorMsg);
318+
form.setFields([
319+
{
320+
name: "inviteCode",
321+
errors: [errorMsg],
322+
value: values.inviteCode,
323+
},
324+
]);
315325
}
316326
// Invalid email format
317327
else if (errorType === "INVALID_EMAIL_FORMAT") {

frontend/public/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,7 @@
11041104
"auth.registerAdmin": "Register Administrator Account",
11051105
"auth.inviteCodeNotConfigured": "Admin invite code is not configured yet. Please contact system admin for help.",
11061106
"auth.inviteCodeInvalid": "Invalid administrator invite code, please check and try again",
1107+
"auth.assetOwnerUseOAuth": "Asset owner accounts cannot be registered with email and password. Please sign in with GitHub, WeChat, or another OAuth provider and enter your asset owner invitation code to complete registration.",
11071108
"auth.emailAlreadyExists": "This email is already registered, please use another email or try logging in",
11081109
"auth.weakPassword": "Password is too weak, please set a more secure password",
11091110
"auth.invalidEmailFormat": "Invalid email format, please check and try again",

frontend/public/locales/zh/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,7 @@
10941094
"auth.registerAdmin": "注册管理员账号",
10951095
"auth.inviteCodeNotConfigured": "管理员注册功能暂未开放,请联系系统管理员配置邀请码",
10961096
"auth.inviteCodeInvalid": "管理员邀请码错误,请检查后重新输入",
1097+
"auth.assetOwnerUseOAuth": "资产管理员账号不支持邮箱密码注册。请使用 GitHub、微信等 OAuth 方式登录,并填写资产管理员邀请码完成注册。",
10971098
"auth.emailAlreadyExists": "该邮箱已被注册,请使用其他邮箱地址或尝试登录现有账号",
10981099
"auth.weakPassword": "密码强度不够,请设置更安全的密码",
10991100
"auth.invalidEmailFormat": "邮箱格式不正确,请检查后重新输入",

frontend/services/authService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ export const authService = {
197197
if (!response.ok) {
198198
return {
199199
error: {
200-
message: data.message || "Registration failed",
200+
message:
201+
data.detail || data.message || "Registration failed",
201202
code: response.status,
202203
data: data.data || null,
203204
},

0 commit comments

Comments
 (0)