218 lines
6.7 KiB
Vue
218 lines
6.7 KiB
Vue
<script setup lang="ts">
|
|
import { NButton, NDataTable, NPopconfirm, NSwitch, useDialog, type DataTableColumns } from 'naive-ui'
|
|
import { h, ref } from 'vue'
|
|
|
|
import {
|
|
activateIdp,
|
|
createSignOnExemption,
|
|
deleteIdp,
|
|
deleteSignOnExemption,
|
|
downloadSamlMetadata,
|
|
listIdps,
|
|
listSignOnRules,
|
|
} from '@/api/tenant'
|
|
import FootNote from '@/components/FootNote.vue'
|
|
import StatusBadge from '@/components/StatusBadge.vue'
|
|
import IdpFormModal from '@/components/tenant/IdpFormModal.vue'
|
|
import { useAsync } from '@/composables/useAsync'
|
|
import type { IdentityProvider, SignOnRule } from '@/types/api'
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
const props = defineProps<{ cfgId: number; domainId?: string }>()
|
|
|
|
const message = useToast()
|
|
const dialog = useDialog()
|
|
const idps = useAsync(() => listIdps(props.cfgId, props.domainId))
|
|
const rules = useAsync(() => listSignOnRules(props.cfgId, props.domainId))
|
|
const showCreate = ref(false)
|
|
|
|
async function act(fn: () => Promise<unknown>, ok: string) {
|
|
try {
|
|
await fn()
|
|
message.success(ok)
|
|
void idps.run()
|
|
void rules.run()
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '操作失败')
|
|
}
|
|
}
|
|
|
|
const idpColumns: DataTableColumns<IdentityProvider> = [
|
|
{
|
|
title: 'IdP',
|
|
key: 'name',
|
|
minWidth: 220,
|
|
render: (row) =>
|
|
h('div', [
|
|
h('div', { class: 'font-medium' }, row.name),
|
|
h('div', { class: 'mono text-xs text-ink-3 mt-px break-all' }, row.partnerProviderId),
|
|
]),
|
|
},
|
|
{ title: '协议', key: 'type', width: 80 },
|
|
{
|
|
title: '状态',
|
|
key: 'enabled',
|
|
width: 100,
|
|
render: (row) =>
|
|
h(StatusBadge, {
|
|
kind: row.enabled ? 'run' : 'stop',
|
|
label: row.enabled ? '已启用' : '已停用',
|
|
}),
|
|
},
|
|
{
|
|
title: 'JIT',
|
|
key: 'jitEnabled',
|
|
width: 70,
|
|
render: (row) => h('span', { class: 'text-[13px]' }, row.jitEnabled ? '开启' : '关闭'),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 160,
|
|
render: (row) =>
|
|
h('div', { class: 'flex items-center gap-3' }, [
|
|
h(NSwitch, {
|
|
size: 'small',
|
|
value: row.enabled,
|
|
onUpdateValue: (v: boolean) =>
|
|
act(() => activateIdp(props.cfgId, row.id, v, props.domainId), v ? '已激活并显示到登录页' : '已停用'),
|
|
}),
|
|
h(
|
|
NButton,
|
|
{ size: 'tiny', quaternary: true, type: 'error', onClick: () => confirmDeleteIdp(row) },
|
|
{ default: () => '删除' },
|
|
),
|
|
]),
|
|
},
|
|
]
|
|
|
|
const ruleColumns: DataTableColumns<SignOnRule> = [
|
|
{ title: '#', key: 'sequence', width: 50 },
|
|
{
|
|
title: '规则',
|
|
key: 'name',
|
|
minWidth: 180,
|
|
render: (row) =>
|
|
h('div', [
|
|
h('div', { class: 'mono text-[12.5px]' }, row.name),
|
|
h(
|
|
'div',
|
|
{ class: 'text-xs text-ink-3 mt-px' },
|
|
row.conditionAttribute ? `${row.conditionAttribute} in ${row.conditionValue}` : '默认兜底规则',
|
|
),
|
|
]),
|
|
},
|
|
{
|
|
title: '认证因子',
|
|
key: 'authenticationFactor',
|
|
width: 120,
|
|
render: (row) =>
|
|
h(StatusBadge, {
|
|
kind: row.authenticationFactor === 'IDP' ? 'prov' : 'check',
|
|
label: row.authenticationFactor === 'IDP' ? '仅 IdP' : 'IdP + 2FA',
|
|
dot: false,
|
|
}),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 110,
|
|
render: (row) =>
|
|
row.builtIn
|
|
? h('span', { class: 'text-xs text-ink-3' }, '内置')
|
|
: h(
|
|
NPopconfirm,
|
|
{
|
|
onPositiveClick: () =>
|
|
act(() => deleteSignOnExemption(props.cfgId, row.id, props.domainId), '已删除免 MFA 规则并恢复优先级'),
|
|
},
|
|
{
|
|
trigger: () =>
|
|
h(NButton, { size: 'tiny', quaternary: true, type: 'error' }, { default: () => '删除' }),
|
|
default: () => '删除该免 MFA 规则?删除后经此 IdP 登录将重新要求 MFA',
|
|
},
|
|
),
|
|
},
|
|
]
|
|
|
|
async function doDownloadMetadata() {
|
|
try {
|
|
await downloadSamlMetadata(props.cfgId, props.domainId)
|
|
message.success('SAML 元数据已下载')
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '下载失败')
|
|
}
|
|
}
|
|
|
|
function confirmDeleteIdp(row: IdentityProvider) {
|
|
dialog.warning({
|
|
title: '删除身份提供商',
|
|
content: () =>
|
|
h('div', { class: 'text-[13px]' }, [
|
|
h('p', `确定删除 IdP「${row.name}」?此操作不可恢复。`),
|
|
h('p', { class: 'mt-1.5 text-ink-3' }, '将自动清理关联的免 MFA 规则,并从登录页移除'),
|
|
]),
|
|
positiveText: '确认删除',
|
|
negativeText: '取消',
|
|
onPositiveClick: () =>
|
|
act(() => deleteIdp(props.cfgId, row.id, props.domainId), `已删除 IdP「${row.name}」及其关联规则`),
|
|
})
|
|
}
|
|
|
|
function addExemption() {
|
|
const enabled = idps.data.value?.find((i) => i.enabled)
|
|
if (!enabled) {
|
|
message.warning('没有已启用的 IdP')
|
|
return
|
|
}
|
|
void act(
|
|
() => createSignOnExemption(props.cfgId, enabled.id, props.domainId),
|
|
`已为 ${enabled.name} 创建免 MFA 规则并置顶`,
|
|
)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col gap-4">
|
|
<div class="panel">
|
|
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
|
<div class="text-sm font-semibold">SAML 身份提供商</div>
|
|
<div class="flex items-center gap-2">
|
|
<NButton size="small" @click="doDownloadMetadata">下载 SP 元数据</NButton>
|
|
<NButton size="small" type="primary" @click="showCreate = true">创建 IdP</NButton>
|
|
</div>
|
|
</div>
|
|
<NDataTable
|
|
:columns="idpColumns"
|
|
:data="idps.data.value ?? []"
|
|
:loading="idps.loading.value"
|
|
:scroll-x="620"
|
|
:row-key="(r: IdentityProvider) => r.id"
|
|
/>
|
|
<FootNote>
|
|
推荐流程:下载 SP SAML 元数据导入 IdP 侧 → 创建 IdP → 激活(自动显示到登录页)→ 创建免 MFA
|
|
规则;停用与删除会先将 IdP 移出登录页
|
|
</FootNote>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
|
<div class="text-sm font-semibold">Sign-on 策略(免 MFA 规则)</div>
|
|
<NButton size="small" @click="addExemption">为已启用 IdP 创建免 MFA 规则</NButton>
|
|
</div>
|
|
<NDataTable
|
|
:columns="ruleColumns"
|
|
:data="rules.data.value ?? []"
|
|
:loading="rules.loading.value"
|
|
:scroll-x="560"
|
|
:row-key="(r: SignOnRule) => r.id"
|
|
/>
|
|
<FootNote>
|
|
sign-on 策略管控全租户控制台登录;Oracle 预置规则不可删除,请勿手工改动预置规则顺序
|
|
</FootNote>
|
|
</div>
|
|
|
|
<IdpFormModal v-model:show="showCreate" :cfg-id="cfgId" :domain-id="domainId" @created="idps.run()" />
|
|
</div>
|
|
</template>
|