diff --git a/frontend/src/api/subsystemAuth.ts b/frontend/src/api/subsystemAuth.ts new file mode 100644 index 0000000..bda2ba6 --- /dev/null +++ b/frontend/src/api/subsystemAuth.ts @@ -0,0 +1,143 @@ +import { getToken } from '@/utils/auth' + +export interface SubsystemAuthSystem { + key: string + name: string + enabled: boolean +} + +export interface WayneRole { + id: number + name: string + comment?: string + type: number +} + +export interface WaynePermission { + create: boolean + update: boolean + delete: boolean +} + +export interface WayneBusinessLineNamespace { + id: number + name: string + kubeNamespace: string + permissions?: WaynePermission + can_bind?: boolean + can_unbind?: boolean + permission_error?: string +} + +export interface WayneRoleBindingPayload { + groupIds?: number[] + replace?: boolean + requestId?: string + reason?: string + dryRun?: boolean +} + +export interface WayneUserRoles { + userId?: number + userName?: string + namespaces?: Array<{ + namespace?: { + id: number + name: string + } + groups?: Array<{ + id: number + name: string + }> + }> + apps?: unknown[] +} + +export const subsystemAuthApi = { + async listSystems(): Promise { + const data = await authRequest('/auth/api/v1/subsystem-auth/systems') + return Array.isArray(data.items) ? data.items : [] + }, + + async listWayneRoles(): Promise { + const data = await authRequest('/auth/api/v1/subsystem-auth/wayne/roles') + return Array.isArray(data.items) ? data.items : [] + }, + + async listWayneNamespaces(businessLineId: number): Promise { + const data = await authRequest(`/auth/api/v1/subsystem-auth/wayne/business-lines/${businessLineId}/namespaces`) + return Array.isArray(data.items) ? data.items : [] + }, + + async getWayneUserRoles(username: string): Promise { + const data = await authRequest(`/auth/api/v1/subsystem-auth/wayne/users/${encodeURIComponent(username)}/roles`) + return data.data || data + }, + + async bindWayneNamespaceRoles( + businessLineId: number, + namespaceId: number, + username: string, + payload: WayneRoleBindingPayload, + ): Promise { + return authRequest( + `/auth/api/v1/subsystem-auth/wayne/business-lines/${businessLineId}/namespaces/${namespaceId}/users/${encodeURIComponent(username)}/roles`, + { + method: 'PUT', + body: JSON.stringify(payload), + }, + ) + }, + + async unbindWayneNamespaceRoles( + businessLineId: number, + namespaceId: number, + username: string, + payload: WayneRoleBindingPayload = {}, + ): Promise { + return authRequest( + `/auth/api/v1/subsystem-auth/wayne/business-lines/${businessLineId}/namespaces/${namespaceId}/users/${encodeURIComponent(username)}/roles`, + { + method: 'DELETE', + body: JSON.stringify(payload), + }, + ) + }, + + async initWayneBusinessLineUser(businessLineId: number, userId: number): Promise { + return authRequest(`/auth/api/v1/subsystem-auth/wayne/business-lines/${businessLineId}/users/${userId}/init`, { + method: 'POST', + }) + }, +} + +async function authRequest(path: string, init: RequestInit = {}) { + const token = getToken() + const response = await fetch(path, { + ...init, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...init.headers, + }, + }) + const text = await response.text() + const data = parseResponseBody(text) + if (!response.ok) { + const message = data?.error || data?.message || text || `HTTP ${response.status}` + throw new Error(message) + } + return data || {} +} + +function parseResponseBody(text: string) { + if (!text.trim()) { + return {} + } + try { + return JSON.parse(text) + } catch { + return { error: text } + } +} diff --git a/frontend/src/components/Layout/AppSidebar.vue b/frontend/src/components/Layout/AppSidebar.vue index 295871e..a820f0e 100644 --- a/frontend/src/components/Layout/AppSidebar.vue +++ b/frontend/src/components/Layout/AppSidebar.vue @@ -97,7 +97,7 @@ const route = useRoute() const authStore = useAuthStore() const businessLineStore = useBusinessLineStore() const portalExpanded = ref(true) -const isPlatformAdmin = computed(() => authStore.user?.is_admin === true) +const isPlatformAdmin = computed(() => authStore.isAdmin) const isBusinessLineAdmin = computed(() => businessLineStore.isCurrentAdmin) const subsystems = [ diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index cd68e3a..ad844b4 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -1,11 +1,12 @@ import { defineStore } from 'pinia' -import { ref } from 'vue' +import { computed, ref } from 'vue' import { authApi } from '@/api/auth' import { getToken, setToken, removeToken, getUser, setUser, removeUser } from '@/utils/auth' export const useAuthStore = defineStore('auth', () => { const token = ref(getToken()) const user = ref(getUser()) + const isAdmin = computed(() => user.value?.is_admin === true || decodeAdminClaim(token.value)) function setAuth(newToken: string, newUser: any) { token.value = newToken @@ -45,6 +46,7 @@ export const useAuthStore = defineStore('auth', () => { return { token, user, + isAdmin, setAuth, setSessionToken, refreshUser, @@ -52,3 +54,19 @@ export const useAuthStore = defineStore('auth', () => { isLoggedIn, } }) + +function decodeAdminClaim(token: string | null): boolean { + if (!token) return false + try { + const payload = JSON.parse(decodeBase64Url(token.split('.')[1] || '')) + return payload.admin === true || payload.is_admin === true + } catch { + return false + } +} + +function decodeBase64Url(value: string): string { + const normalized = value.replace(/-/g, '+').replace(/_/g, '/') + const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=') + return atob(padded) +} diff --git a/frontend/src/views/subsystem/Authorization.vue b/frontend/src/views/subsystem/Authorization.vue index 12c06fa..a0d2e3f 100644 --- a/frontend/src/views/subsystem/Authorization.vue +++ b/frontend/src/views/subsystem/Authorization.vue @@ -3,118 +3,178 @@

子系统赋权

-

Wayne / CloudDM 入口权限、默认角色与授权状态

+

{{ currentBusinessLineName }} · Wayne namespace 角色授权

+
+
+ 刷新 + + 保存授权 +
- 新增授权
接入子系统
-
2
-
Wayne · CloudDM
+
{{ enabledSystemCount }}
+
{{ systemSummary }}
-
授权主体
-
6
-
用户 3 · 用户组 3
+
Wayne Namespace
+
{{ wayneNamespaces.length }}
+
当前业务线映射
-
待审批
-
2
-
最近提交 10:18
+
可选角色
+
{{ wayneRoles.length }}
+
{{ roleSummary }}
-
默认授权
-
● 生效
-
新用户默认只读
+
当前操作权限
+
● {{ operatorStateText }}
+
{{ operatorStateDetail }}
-
- - - - - - - - - - - - - - - - - -
-
-
+
- +
-

{{ system.name }}

-

{{ system.defaultPolicy }}

+

Wayne

+

业务线 namespace 角色绑定,默认新用户初始化为访客

-
+
{{ role.name }} - {{ role.count }} + #{{ role.id }} +
+
暂无角色
+
+
+ +
+
+ +
+

CloudDM

+

接口预留,当前不开放授权操作

+
+
+ 状态 + 未启用 +
+
+
+
+ +
+
+

Wayne 授权操作

+ 数据源:AuthServer · Wayne internal API +
+
+ + + + + + + + + + + + + + + + +
+ + 保存角色 + + + 清空角色 + + + 初始化访客 + +
+
+ +
+
授权规则
+

当前账号必须是平台管理员或当前业务线管理员。

+

保存前会再次校验 Wayne namespace 的授权能力。

+

用户加入业务线时后端会自动初始化 Wayne 访客角色。

+
+
+
+ +
+
+

当前权限

+ 只读模式 +
+
+ 当前账号没有 Wayne namespace 角色绑定权限,只展示现有权限。
-

授权列表

- 数据源:AuthServer · 子系统授权 +

当前用户 Wayne 角色

+ {{ selectedUsername || '未选择用户' }}
- - - - - - - - + + + + + - + - - + - - - + + + +
授权主体类型子系统角色 / 范围来源状态最近变更操作NamespaceKube Namespace当前角色授权能力操作
-
- {{ item.initial }} -
-
{{ item.principal }}
-
{{ item.detail }}
-
-
+
{{ namespace.name || '-' }}
+
id={{ namespace.id }}
{{ item.type === 'user' ? '用户' : '用户组' }}{{ item.system }}{{ namespace.kubeNamespace || '-' }} - {{ item.role }} - {{ item.scope }} + + {{ role }} + + 未绑定 {{ item.source }}● {{ item.statusText }}{{ item.updatedAt }} + ● {{ namespace.permission_error }} + ● 可授权 + ● 无授权权限 +
- - + +
当前业务线没有绑定 Wayne namespace
@@ -123,62 +183,272 @@