mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
Merge branch 'v2-c' into knowledge_workflow
This commit is contained in:
commit
691cd8d2e7
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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,
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: '企业微信',
|
||||
|
|
|
|||
|
|
@ -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: '次顯示驗證碼',
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ const useLoginStore = defineStore('login', {
|
|||
return ok.data
|
||||
})
|
||||
},
|
||||
async samlLogin() {
|
||||
return LoginApi.samlLogin().then((ok) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue