Merge branch 'v2-c' into knowledge_workflow

This commit is contained in:
CaptainB 2025-11-26 15:54:24 +08:00
commit 691cd8d2e7
17 changed files with 493 additions and 82 deletions

View File

@ -7,6 +7,8 @@ import os
import socket
import subprocess
import sys
import signal
import time
import uuid_utils.compat as uuid
from common.utils.logger import maxkb_logger
from django.utils.translation import gettext_lazy as _
@ -84,13 +86,14 @@ class ToolExecutor:
python_paths = CONFIG.get_sandbox_python_package_paths().split(',')
_exec_code = f"""
try:
import sys, json, base64, builtins
import os, sys, json, base64, builtins
path_to_exclude = ['/opt/py3/lib/python3.11/site-packages', '/opt/maxkb-app/apps']
sys.path = [p for p in sys.path if p not in path_to_exclude]
sys.path += {python_paths}
locals_v={'{}'}
keywords={keywords}
globals_v={'{}'}
os.environ.clear()
exec({dedent(code_str)!a}, globals_v, locals_v)
f_name, f = {action_function}
for local in locals_v:
@ -182,16 +185,14 @@ except Exception as e:
python_paths = CONFIG.get_sandbox_python_package_paths().split(',')
code = self._generate_mcp_server_code(code_str, params)
return f"""
import os
import sys
import logging
import os, sys, logging
logging.basicConfig(level=logging.WARNING)
logging.getLogger("mcp").setLevel(logging.ERROR)
logging.getLogger("mcp.server").setLevel(logging.ERROR)
path_to_exclude = ['/opt/py3/lib/python3.11/site-packages', '/opt/maxkb-app/apps']
sys.path = [p for p in sys.path if p not in path_to_exclude]
sys.path += {python_paths}
os.environ.clear()
exec({dedent(code)!a})
"""
@ -223,24 +224,40 @@ exec({dedent(code)!a})
return tool_config
def _exec_sandbox(self, _code):
kwargs = {'cwd': BASE_DIR}
kwargs['env'] = {
kwargs = {'cwd': BASE_DIR, 'env': {
'LD_PRELOAD': self.sandbox_so_path,
}
}}
maxkb_logger.debug(f"Sandbox execute code: {_code}")
compressed_and_base64_encoded_code_str = base64.b64encode(gzip.compress(_code.encode())).decode()
cmd = [
'su', '-s', python_directory, '-c',
f'import base64,gzip; exec(gzip.decompress(base64.b64decode(\'{compressed_and_base64_encoded_code_str}\')).decode())',
self.user
]
try:
subprocess_result = subprocess.run(
['su', '-s', python_directory, '-c',
f'import base64,gzip; exec(gzip.decompress(base64.b64decode(\'{compressed_and_base64_encoded_code_str}\')).decode())',
self.user],
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
capture_output=True,
timeout=self.process_timeout_seconds,
**kwargs)
**kwargs,
start_new_session=True
)
proc.wait(timeout=self.process_timeout_seconds)
return subprocess.CompletedProcess(
proc.args,
proc.returncode,
proc.stdout.read(),
proc.stderr.read()
)
except subprocess.TimeoutExpired:
pgid = os.getpgid(proc.pid)
os.killpg(pgid, signal.SIGTERM) #温和终止
time.sleep(1) #留出短暂时间让进程清理
if proc.poll() is None: #如果仍未终止,强制终止
os.killpg(pgid, signal.SIGKILL)
proc.wait()
raise Exception(_("Sandbox process execution timeout, consider increasing MAXKB_SANDBOX_PYTHON_PROCESS_TIMEOUT_SECONDS."))
return subprocess_result
def validate_mcp_transport(self, code_str):
servers = json.loads(code_str)

View File

@ -1,5 +1,8 @@
# coding=utf-8
import base64
import ipaddress
import socket
from urllib.parse import urlparse
import requests
from django.utils.translation import gettext_lazy as _
@ -83,7 +86,17 @@ class GetUrlView(APIView):
)
def get(self, request: Request):
url = request.query_params.get('url')
response = requests.get(url)
parsed = validate_url(url)
response = requests.get(
url,
timeout=3,
allow_redirects=False
)
final_host = urlparse(response.url).hostname
if is_private_ip(final_host):
raise ValueError("Blocked unsafe redirect to internal host")
# 返回状态码 响应内容大小 响应的contenttype 还有字节流
content_type = response.headers.get('Content-Type', '')
# 根据内容类型决定如何处理
@ -99,3 +112,43 @@ class GetUrlView(APIView):
'Content-Type': content_type,
'content': content,
})
def is_private_ip(host: str) -> bool:
"""检测 IP 是否属于内网、环回、云 metadata 的危险地址"""
try:
ip = ipaddress.ip_address(socket.gethostbyname(host))
return (
ip.is_private or
ip.is_loopback or
ip.is_reserved or
ip.is_link_local or
ip.is_multicast
)
except Exception:
return True
def validate_url(url: str):
"""验证 URL 是否安全"""
if not url:
raise ValueError("URL is required")
parsed = urlparse(url)
# 仅允许 http / https
if parsed.scheme not in ("http", "https"):
raise ValueError("Only http and https are allowed")
host = parsed.hostname
path = parsed.path
# 域名不能为空
if not host:
raise ValueError("Invalid URL")
# 禁止访问内部、保留、环回、云 metadata
if is_private_ip(host):
raise ValueError("Access to internal IP addresses is blocked")
return parsed

View File

@ -9,6 +9,7 @@
import json
import os
from django.contrib.postgres.fields import ArrayField
from django.core.cache import cache
from django.db import models
from django.db.models import QuerySet, Q, TextField
@ -343,10 +344,13 @@ class ResourceUserPermissionSerializer(serializers.Serializer):
"role": models.CharField(),
"role_setting.type": models.CharField(),
"user_role_relation.workspace_id": models.CharField(),
'tmp.type_list': ArrayField(models.CharField()),
'tmp.role_name_list_str': models.CharField()
}))
nick_name = instance.get('nick_name')
username = instance.get('username')
role_name = instance.get('role')
permission = instance.get('permission')
query_p_list = [None if p == "NOT_AUTH" else p for p in permission]
@ -375,15 +379,31 @@ class ResourceUserPermissionSerializer(serializers.Serializer):
**{"u.id__in": QuerySet(workspace_user_role_mapping_model).filter(
workspace_id=self.data.get('workspace_id')).values("user_id")})
if is_x_pack_ee:
user_query_set = user_query_set.filter(
**{'role_setting.type': "USER", 'user_role_relation.workspace_id': self.data.get('workspace_id')})
user_query_set = user_query_set.filter(**{
"tmp.type_list__contains": ["USER"]
})
role_name_and_type_query_set = QuerySet(model=get_dynamics_model({
'user_role_relation.workspace_id': models.CharField(),
})).filter(**{
"user_role_relation.workspace_id": self.data.get('workspace_id'),
})
if role_name:
user_query_set = user_query_set.filter(
**{'tmp.role_name_list_str__icontains': str(role_name)}
)
return {
'workspace_user_resource_permission_query_set': workspace_user_resource_permission_query_set,
'user_query_set': user_query_set,
'role_name_and_type_query_set': role_name_and_type_query_set
}
else:
user_query_set = user_query_set.filter(
**{'role': "USER"})
return {
'workspace_user_resource_permission_query_set': workspace_user_resource_permission_query_set,
'user_query_set': user_query_set
}
return {
'workspace_user_resource_permission_query_set': workspace_user_resource_permission_query_set,
'user_query_set': user_query_set
}
def list(self, instance, with_valid=True):
if with_valid:

View File

@ -1,34 +1,41 @@
SELECT
distinct(u.id),
DISTINCT u.id,
u.nick_name,
u.username,
case
when
wurp."permission" is null then 'NOT_AUTH'
else wurp."permission"
end
tmp.role_name_list AS role_name,
CASE
WHEN wurp."permission" IS NULL THEN 'NOT_AUTH'
ELSE wurp."permission"
END AS permission
FROM
public."user" u
LEFT JOIN (
SELECT
user_id ,
(case
when auth_type = 'ROLE'
and 'ROLE' = any( permission_list) then 'ROLE'
when auth_type = 'RESOURCE_PERMISSION_GROUP'
and 'MANAGE'= any(permission_list) then 'MANAGE'
when auth_type = 'RESOURCE_PERMISSION_GROUP'
and 'VIEW' = any( permission_list) then 'VIEW'
else null
end) as "permission"
user_id,
CASE
WHEN auth_type = 'ROLE'
AND 'ROLE' = ANY(permission_list) THEN 'ROLE'
WHEN auth_type = 'RESOURCE_PERMISSION_GROUP'
AND 'MANAGE' = ANY(permission_list) THEN 'MANAGE'
WHEN auth_type = 'RESOURCE_PERMISSION_GROUP'
AND 'VIEW' = ANY(permission_list) THEN 'VIEW'
ELSE NULL
END AS "permission"
FROM
workspace_user_resource_permission
${workspace_user_resource_permission_query_set}
) wurp
ON
u.id = wurp.user_id
left join user_role_relation user_role_relation
on user_role_relation.user_id = u.id
left join role_setting role_setting
on role_setting.id = user_role_relation.role_id
${workspace_user_resource_permission_query_set}
) wurp ON u.id = wurp.user_id
LEFT JOIN (
SELECT
ARRAY_AGG(role_setting.role_name) AS role_name_list,
ARRAY_AGG(role_setting.role_name)::text AS role_name_list_str,
ARRAY_AGG(role_setting.type) AS type_list,
user_role_relation.user_id
FROM user_role_relation user_role_relation
LEFT JOIN role_setting role_setting
ON role_setting.id = user_role_relation.role_id
${role_name_and_type_query_set}
GROUP BY
user_role_relation.user_id) tmp
ON u.id = tmp.user_id
${user_query_set}

View File

@ -196,6 +196,7 @@ class WorkspaceResourceUserPermissionView(APIView):
return result.success(ResourceUserPermissionSerializer(
data={'workspace_id': workspace_id, "target": target, 'auth_target_type': resource, }
).page({'username': request.query_params.get("username"),
'role': request.query_params.get("role"),
'nick_name': request.query_params.get("nick_name"),
'permission': request.query_params.getlist("permission[]")}, current_page, page_size,
))

View File

@ -431,7 +431,7 @@ class ToolSerializer(serializers.Serializer):
@staticmethod
def convert_value(name: str, value: str, _type: str, is_required: bool):
if not is_required and value is None:
if not is_required and (value is None or (isinstance(value, str) and len(value.strip()) == 0)):
return None
try:
if _type == 'int':

View File

@ -19,8 +19,12 @@
#include <time.h>
#include <execinfo.h>
#include <dlfcn.h>
#include <linux/sched.h>
#include <pty.h>
#define CONFIG_FILE ".sandbox.conf"
#define KEY_BANNED_HOSTS "SANDBOX_PYTHON_BANNED_HOSTS"
#define KEY_ALLOW_SUBPROCESS "SANDBOX_PYTHON_ALLOW_SUBPROCESS"
static char *banned_hosts = NULL;
static int allow_subprocess = 0; // 默认禁止
@ -57,10 +61,10 @@ static void load_sandbox_config() {
while (*value == ' ' || *value == '\t') value++;
char *vend = value + strlen(value) - 1;
while (vend > value && (*vend == ' ' || *vend == '\t')) *vend-- = '\0';
if (strcmp(key, "SANDBOX_PYTHON_BANNED_HOSTS") == 0) {
if (strcmp(key, KEY_BANNED_HOSTS) == 0) {
free(banned_hosts);
banned_hosts = strdup(value);
} else if (strcmp(key, "SANDBOX_PYTHON_ALLOW_SUBPROCESS") == 0) {
} else if (strcmp(key, KEY_ALLOW_SUBPROCESS) == 0) {
allow_subprocess = atoi(value);
}
}
@ -158,7 +162,7 @@ static int allow_create_subprocess() {
return allow_subprocess || !is_sandbox_user();
}
static int deny() {
fprintf(stderr, "[sandbox] Permission denied to create subprocess in sandbox.\n");
fprintf(stderr, "Permission denied to create subprocess.\n");
_exit(1);
return -1;
}
@ -167,7 +171,6 @@ static int deny() {
if (!real_##func) { \
real_##func = dlsym(RTLD_NEXT, #func); \
}
int execve(const char *filename, char *const argv[], char *const envp[]) {
RESOLVE_REAL(execve);
if (!allow_create_subprocess()) return deny();
@ -180,7 +183,21 @@ int execveat(int dirfd, const char *pathname,
if (!allow_create_subprocess()) return deny();
return real_execveat(dirfd, pathname, argv, envp, flags);
}
int __execve(const char *filename, char *const argv[], char *const envp[]) {
RESOLVE_REAL(__execve);
if (!allow_create_subprocess()) return deny();
return real___execve(filename, argv, envp);
}
int execvpe(const char *file, char *const argv[], char *const envp[]) {
RESOLVE_REAL(execvpe);
if (!allow_create_subprocess()) return deny();
return real_execvpe(file, argv, envp);
}
int __execvpe(const char *file, char *const argv[], char *const envp[]) {
RESOLVE_REAL(__execvpe);
if (!allow_create_subprocess()) return deny();
return real___execvpe(file, argv, envp);
}
pid_t fork(void) {
RESOLVE_REAL(fork);
if (!allow_create_subprocess()) return deny();
@ -203,7 +220,11 @@ int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...) {
va_end(ap);
return real_clone(fn, child_stack, flags, arg, (void *)a4, (void *)a5);
}
int clone3(struct clone_args *cl_args, size_t size) {
RESOLVE_REAL(clone3);
if (!allow_create_subprocess()) return deny();
return real_clone3(cl_args, size);
}
int posix_spawn(pid_t *pid, const char *path,
const posix_spawn_file_actions_t *file_actions,
const posix_spawnattr_t *attrp,
@ -249,9 +270,19 @@ int __libc_system(const char *command) {
if (!allow_create_subprocess()) return deny();
return real___libc_system(command);
}
pid_t forkpty(int *amaster, char *name, const struct termios *termp, const struct winsize *winp) {
RESOLVE_REAL(forkpty);
if (!allow_create_subprocess()) return deny();
return real_forkpty(amaster, name, termp, winp);
}
pid_t __forkpty(int *amaster, char *name, const struct termios *termp, const struct winsize *winp) {
RESOLVE_REAL(__forkpty);
if (!allow_create_subprocess()) return deny();
return real___forkpty(amaster, name, termp, winp);
}
long (*real_syscall)(long, ...) = NULL;
long syscall(long number, ...) {
if (!real_syscall) real_syscall = dlsym(RTLD_NEXT, "syscall");
RESOLVE_REAL(syscall);
va_list ap;
va_start(ap, number);
long a1 = va_arg(ap, long);
@ -261,9 +292,20 @@ long syscall(long number, ...) {
long a5 = va_arg(ap, long);
long a6 = va_arg(ap, long);
va_end(ap);
if (number == SYS_execve || number == SYS_execveat ||
number == SYS_fork || number == SYS_vfork || number == SYS_clone) {
if (!allow_create_subprocess()) return deny();
switch (number) {
case SYS_execve:
case SYS_execveat:
case SYS_fork:
case SYS_vfork:
case SYS_clone:
case SYS_clone3:
#ifdef SYS_posix_spawn
case SYS_posix_spawn:
#endif
#ifdef SYS_posix_spawnp
case SYS_posix_spawnp:
#endif
if (!allow_create_subprocess()) return deny();
}
return real_syscall(number, a1, a2, a3, a4, a5, a6);
}

View File

@ -50,7 +50,7 @@ dependencies = [
"xlrd==2.0.2",
"xlwt==1.3.0",
"pymupdf==1.26.3",
"pypdf==6.1.3",
"pypdf==6.4.0",
"pydub==0.25.1",
"pysilk==0.0.1",
"gunicorn==23.0.0",

View File

@ -99,7 +99,11 @@ const postLanguage: (data: any, loading?: Ref<boolean>) => Promise<Result<User>>
) => {
return post('/user/language', data, undefined, loading)
}
const samlLogin: (loading?: Ref<boolean>) => Promise<Result<any>> = (
loading,
) => {
return get('/saml2', '', loading)
}
export default {
login,
logout,
@ -112,5 +116,6 @@ export default {
getDingOauth2Callback,
getLarkCallback,
getQrSource,
ldapLogin
ldapLogin,
samlLogin
}

View File

@ -23,6 +23,7 @@
<el-option :label="$t('views.userManage.userForm.nick_name.label')" value="nick_name" />
<el-option :label="$t('views.login.loginForm.username.label')" value="username" />
<el-option :label="$t('views.model.modelForm.permissionType.label')" value="permission" />
<el-option v-if="hasPermission([EditionConst.IS_EE,EditionConst.IS_PE],'OR')" :label="$t('views.role.member.role')" value="role" />
</el-select>
<el-input
v-if="searchType === 'nick_name'"
@ -40,7 +41,14 @@
style="width: 220px"
clearable
/>
<el-input
v-if="searchType === 'role'"
v-model="searchForm.role"
@change="searchHandle"
:placeholder="$t('common.search')"
style="width: 220px"
clearable
/>
<el-select
v-else-if="searchType === 'permission'"
v-model="searchForm.permission"
@ -85,28 +93,15 @@
show-overflow-tooltip
:label="$t('views.login.loginForm.username.label')"
/>
<!-- <el-table-column prop="role_name" :label="$t('views.role.member.role')" width="210">
<el-table-column v-if="hasPermission([EditionConst.IS_EE,EditionConst.IS_PE],'OR')" prop="role_name" :label="$t('views.role.member.role')" width="210">
<template #default="{ row }">
<el-popover :width="400">
<template #reference>
<TagGroup
class="cursor"
style="width: fit-content"
:tags="row.role_name"
tooltipDisabled
/>
</template>
<template #default>
<el-table :data="row.role_workspace">
<el-table-column prop="role" :label="$t('views.role.member.role')">
</el-table-column>
<el-table-column prop="workspace" :label="$t('views.workspace.title')">
</el-table-column>
</el-table>
</template>
</el-popover>
</template>
</el-table-column> -->
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="340">
<template #default="{ row }">
<el-radio-group
@ -206,7 +201,7 @@ import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
const route = useRoute()
import useStore from '@/stores'
import { hasPermission } from '@/utils/permission/index'
import { PermissionConst, RoleConst } from '@/utils/permission/data'
import { EditionConst, PermissionConst, RoleConst } from '@/utils/permission/data'
const { user } = useStore()
const props = defineProps<{
@ -338,11 +333,12 @@ const searchType = ref('nick_name')
const searchForm = ref<any>({
nick_name: '',
username: '',
role: '',
permission: undefined,
})
const search_type_change = () => {
searchForm.value = { nick_name: '', username: '', permission: undefined }
searchForm.value = { nick_name: '', username: '', role: '', permission: undefined }
}
const paginationConfig = reactive({

View File

@ -71,6 +71,24 @@ export default {
filedMappingPlaceholder: 'Please enter field mapping',
enableAuthentication: 'Enable OAuth2 Authentication',
},
saml2: {
title: 'SAML2',
ldp: 'Idp MetaData Url',
ldpPlaceholder: 'Please enter Idp MetaData Url',
enableAuthnRequests: 'Enable request signature',
enableAssertions: 'Enable assertion signatures',
privateKey: 'SP Private Key',
privateKeyPlaceholder: 'Please enter SP Private Key',
certificate: 'SP Certificate',
certificatePlaceholder: 'Please enter SP Certificate',
filedMapping: 'Field Mapping',
spEntityId: 'SP Entity Id',
spEntityIdPlaceholder: 'Please enter SP Entity Id',
spAcs: 'SP Ace',
spAcsPlaceholder: 'Please enter SP Ace',
filedMappingPlaceholder: 'Please enter field mapping',
enableAuthentication: 'Enable SAML2 Authentication',
},
scanTheQRCode: {
title: 'Scan the QR code',
wecom: 'WeCom',
@ -133,9 +151,9 @@ export default {
type: 'Type',
management: 'management',
},
default_login: 'Default Login Method',
default_login: 'Default Login Method',
display_code: 'Account login verification code setting',
loginFailed: 'Login failed',
loginFailed: 'Login failed',
loginFailedMessage: 'Display verification code twice',
display_codeTip: 'When the value is -1, the verification code is not displayed',
time: 'Times',

View File

@ -73,6 +73,24 @@ export default {
filedMappingPlaceholder: '请输入字段映射',
enableAuthentication: '启用 OAuth2 认证',
},
saml2: {
title: 'SAML2',
ldp: 'Idp MetaData Url',
ldpPlaceholder: '请输入 Idp MetaData Url',
enableAuthnRequests: '开启请求签名',
enableAssertions: '开启断言签名',
privateKey: 'SP Private Key',
privateKeyPlaceholder: '请输入 SP Private Key',
certificate: 'SP Certificate',
certificatePlaceholder: '请输入 SP Certificate',
filedMapping: '字段映射',
spEntityId: 'SP Entity Id',
spEntityIdPlaceholder: '请输入 SP Entity Id',
spAcs: 'SP Ace',
spAcsPlaceholder: '请输入 SP Ace',
filedMappingPlaceholder: '请输入字段映射',
enableAuthentication: '启用 SAML2 认证',
},
scanTheQRCode: {
title: '扫码登录',
wecom: '企业微信',

View File

@ -71,6 +71,25 @@ export default {
filedMappingPlaceholder: '請輸入欄位對應',
enableAuthentication: '啟用 OAuth2 認證',
},
saml2: {
title: 'SAML2',
ldp: 'Idp MetaData Url',
ldpPlaceholder: '請輸入 Idp MetaData Url',
enableAuthnRequests: '開啟請求簽名',
enableAssertions: '開啟斷言簽名',
privateKey: 'SP Private Key',
privateKeyPlaceholder: '請輸入 SP Private Key',
certificate: 'SP Certificate',
certificatePlaceholder: '請輸入 SP Certificate',
filedMapping: '欄位映射',
spEntityId: 'SP Entity Id',
spEntityIdPlaceholder: '請輸入 SP Entity Id',
spAcs: 'SP Ace',
spAcsPlaceholder: '請輸入 SP Ace',
filedMappingPlaceholder: '請輸入欄位映射',
enableAuthentication: '啟用 SAML2 認證',
},
scanTheQRCode: {
title: '掃碼登入',
wecom: '企業微信',
@ -133,7 +152,7 @@ export default {
type: '類型',
management: '管理',
},
default_login: '預設登入方式',
default_login: '預設登入方式',
display_code: '帳號登入驗證碼設定',
loginFailed: '登入失敗',
loginFailedMessage: '次顯示驗證碼',

View File

@ -86,6 +86,10 @@ const useLoginStore = defineStore('login', {
return ok.data
})
},
async samlLogin() {
return LoginApi.samlLogin().then((ok) => {
})
}
},
})

View File

@ -329,6 +329,10 @@ function redirectAuth(authType: string, needMessage: boolean = true) {
if (config.scope) {
url += `&scope=${config.scope}`
}
} else if (authType === 'SAML2') {
loginApi.samlLogin().then((res: any) => {
window.location.href = res.data
})
}
if (!url) {
return

View File

@ -0,0 +1,201 @@
<template>
<div class="authentication-setting__main main-calc-height">
<el-scrollbar>
<div class="form-container p-24" v-loading="loading">
<el-form
ref="authFormRef"
:rules="rules"
:model="form"
label-position="top"
require-asterisk-position="right"
>
<el-form-item
:label="$t('views.system.authentication.saml2.ldp')"
prop="config.idpMetaUrl"
>
<el-input
v-model="form.config.idpMetaUrl"
:placeholder="$t('views.system.authentication.saml2.ldpPlaceholder')"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.config.wantAssertionsSigned">{{
$t('views.system.authentication.saml2.enableAuthnRequests')
}}
</el-checkbox>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.config.wantAuthnRequestsSigned">{{
$t('views.system.authentication.saml2.enableAssertions')
}}
</el-checkbox>
</el-form-item>
<el-form-item
:label="$t('views.system.authentication.saml2.privateKey')"
prop="config.privateKey"
>
<el-input
v-model="form.config.privateKey"
type="password"
:placeholder="$t('views.system.authentication.saml2.privateKeyPlaceholder')"
/>
</el-form-item>
<el-form-item
:label="$t('views.system.authentication.saml2.certificate')"
prop="config.certificate"
>
<el-input
v-model="form.config.certificate"
type="password"
:placeholder="$t('views.system.authentication.saml2.certificatePlaceholder')"
/>
</el-form-item>
<el-form-item :label="$t('views.system.authentication.saml2.filedMapping')"
prop="config.mapping">
<el-input
v-model="form.config.mapping"
:placeholder="$t('views.system.authentication.saml2.filedMappingPlaceholder')"
/>
</el-form-item>
<el-form-item
:label="$t('views.system.authentication.saml2.spEntityId')"
prop="config.spEntityId"
>
<el-input
v-model="form.config.spEntityId"
:placeholder="$t('views.system.authentication.saml2.spEntityIdPlaceholder')"
/>
</el-form-item>
<el-form-item
:label="$t('views.system.authentication.saml2.spAcs')"
prop="config.spAcs"
>
<el-input
v-model="form.config.spAcs"
:placeholder="$t('views.system.authentication.saml2.spAcsPlaceholder')"
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.is_active">{{
$t('views.system.authentication.saml2.enableAuthentication')
}}
</el-checkbox>
</el-form-item>
</el-form>
<div>
<span
v-hasPermission="
new ComplexPermission([RoleConst.ADMIN], [PermissionConst.LOGIN_AUTH_EDIT], [], 'OR')
"
class="mr-12"
>
<el-button @click="submit(authFormRef)" type="primary" :disabled="loading">
{{ $t('common.save') }}
</el-button>
</span>
<span>
<el-button @click="submit(authFormRef, 'test')" :disabled="loading">
{{ $t('views.system.test') }}</el-button
>
</span>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import {reactive, ref, watch, onMounted} from 'vue'
import authApi from '@/api/system-settings/auth-setting'
import type {FormInstance, FormRules} from 'element-plus'
import {t} from '@/locales'
import {MsgSuccess} from '@/utils/message'
import {PermissionConst, RoleConst} from '@/utils/permission/data'
import {ComplexPermission} from '@/utils/permission/type'
const form = ref<any>({
id: '',
auth_type: 'SAML2',
config: {
idpMetaUrl: '',
wantAssertionsSigned: true,
wantAuthnRequestsSigned: true,
privateKey: '',
certificate: '',
mapping: '',
spEntityId: window.location.origin + window.MaxKB.prefix + '/api/saml2/metadata',
spAcs: window.location.origin + window.MaxKB.prefix + '/api/saml2/sso',
},
is_active: true,
})
const authFormRef = ref()
const loading = ref(false)
const rules = reactive<FormRules<any>>({
'config.idpMetaUrl': [
{
required: true,
message: t('views.system.authentication.saml2.ldpPlaceholder'),
trigger: 'blur',
},
],
'config.privateKey': [
{
required: true,
message: t('views.system.authentication.saml2.privateKeyPlaceholder'),
trigger: 'blur',
},
],
'config.certificate': [
{
required: true,
message: t('views.system.authentication.saml2.certificatePlaceholder'),
trigger: 'blur',
},
],
'config.mapping': [
{
required: true,
message: t('views.system.authentication.saml2.filedMappingPlaceholder'),
trigger: 'blur',
},
],
})
const submit = async (formEl: FormInstance | undefined, test?: string) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
authApi.putAuthSetting(form.value.auth_type, form.value, loading).then((res) => {
MsgSuccess(t('common.saveSuccess'))
})
}
})
}
function getDetail() {
authApi.getAuthSetting(form.value.auth_type, loading).then((res: any) => {
if (res.data && JSON.stringify(res.data) !== '{}') {
form.value = res.data
if (res.data.config.mapping) {
form.value.config.mapping = JSON.stringify(JSON.parse(res.data.config.mapping))
}
if (!form.value.config.spEntityId) {
form.value.config.spEntityId = window.location.origin + window.MaxKB.prefix + '/api/saml2/metadata'
}
if (!form.value.config.spAcs) {
form.value.config.spAcs = window.location.origin + window.MaxKB.prefix + '/api/saml2/sso'
}
}
})
}
onMounted(() => {
getDetail()
})
</script>
<style lang="scss" scoped></style>

View File

@ -23,6 +23,7 @@ import CAS from './component/CAS.vue'
import OIDC from './component/OIDC.vue'
import SCAN from './component/SCAN.vue'
import OAuth2 from './component/OAuth2.vue'
import Saml2 from "./component/Saml2.vue";
import Setting from './component/Setting.vue'
import { t } from '@/locales'
import useStore from '@/stores'
@ -57,6 +58,11 @@ const tabList = [
name: 'OAuth2',
component: OAuth2,
},
{
label: t('views.system.authentication.saml2.title'),
name: 'SAML2',
component: Saml2,
},
{
label: t('views.system.authentication.scanTheQRCode.title'),
name: 'SCAN',