feat: 流水线三级权限体系(手动多选+通配符授权)+ 菜单分级 + SSE 认证修复
权限模型(借鉴 Jenkins Role-Strategy 与云效,默认拒绝): - 三级权限:查看(流水线+成功/失败日志)/ 发布(+构建、批量发布、重试)/ 管理(+配置、成员);admin 全局豁免 - 权限组(管理员配置):默认手动多选流水线(免正则,操作友好);高级模式仅 * 通配符(cex-*) - 单流水线成员直授权作为例外补充;AI 批量发布逐 job 校验,越权明确拒绝 后端: - 新增 PipelineRole/PipelineMember 表与 /api/pipeline-roles、/api/pipelines/:id/members CRUD - RequirePipelineAccess/RequireRunAccess 中间件挂全流水线与运行路由;列表按可见性过滤并附 accessLevel/canRetry - /api/me 会话接口;logs/stream SSE 支持 ?token= 认证(原先完全免鉴权)并校验查看权限 - 凭证/主机/集群/构建节点/工具/Webhook/Jenkins/GitOps/插件/设置全部 AdminOnly;触发人以会话身份落库 前端: - useSession 会话 hook;Sidebar 菜单分级(资源/集成/系统管理 admin-only);admin 路由守卫 - 按钮级权限:执行/配置/删除/保存/重试按等级显隐;新页「流水线权限」与流水线成员弹窗(纯点选) - 登录页记住密码;GUIDE/API 文档补权限章节
This commit is contained in:
1 parent
7090c8efc2
commit
9103b13510
27 files changed
+1690
-190
No files matched your search
@@ -17,6 +17,7 @@ import {
|
||||
import { toast } from 'sonner';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api';
|
||||
import { useSession } from '@/providers/session';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
@@ -120,6 +121,7 @@ export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [form, setForm] = useState<CreatePipelineForm>(emptyForm);
|
||||
const { isAdmin } = useSession();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -163,6 +165,8 @@ export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (
|
||||
description: p.description || '',
|
||||
branch: p.sourceBranch || '-',
|
||||
type: p.type,
|
||||
// 后端附带的当前用户权限等级:1 查看 / 2 执行 / 3 管理 / 4 管理员
|
||||
accessLevel: (p as any).accessLevel ?? 4,
|
||||
deploy: deployTypeConfig[p.deployType] || fallbackDeploy,
|
||||
// 筛选用映射状态:lastRun 为空记为 pending(未执行)
|
||||
mappedStatus: p.lastRun?.status ?? 'pending',
|
||||
@@ -289,15 +293,17 @@ export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (
|
||||
<List className="w-3.5 h-3.5" style={{ color: 'var(--text-secondary)' }} />
|
||||
</button>
|
||||
</div>
|
||||
{/* New pipeline button */}
|
||||
<button
|
||||
className="flex items-center gap-1.5 px-3 py-[6px] text-[13px] font-medium rounded-md text-white"
|
||||
style={{ background: 'var(--accent-blue)' }}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
新建流水线
|
||||
</button>
|
||||
{/* New pipeline button(新建/删除仅管理员) */}
|
||||
{isAdmin && (
|
||||
<button
|
||||
className="flex items-center gap-1.5 px-3 py-[6px] text-[13px] font-medium rounded-md text-white"
|
||||
style={{ background: 'var(--accent-blue)' }}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
新建流水线
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -314,7 +320,11 @@ export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (
|
||||
<Search className="w-5 h-5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</div>
|
||||
<p className="text-[13px] font-medium" style={{ color: 'var(--text-secondary)' }}>未找到流水线</p>
|
||||
<p className="text-[12px] mt-1" style={{ color: 'var(--text-tertiary)' }}>尝试调整搜索条件或新建流水线</p>
|
||||
<p className="text-[12px] mt-1" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{items.length === 0 && !isAdmin
|
||||
? '暂无被授权的流水线,请联系管理员在「流水线权限」中授权'
|
||||
: '尝试调整搜索条件或新建流水线'}
|
||||
</p>
|
||||
</div>
|
||||
) : viewMode === 'card' ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
@@ -397,19 +407,21 @@ export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
runMutation.mutate(pipeline.raw.id);
|
||||
}}
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="执行"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" style={{ color: 'var(--accent-green)' }} />
|
||||
</button>
|
||||
{pipeline.accessLevel >= 2 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
runMutation.mutate(pipeline.raw.id);
|
||||
}}
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="执行"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" style={{ color: 'var(--accent-green)' }} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="配置"
|
||||
title={pipeline.accessLevel >= 3 ? '配置' : '查看'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectPipeline(pipeline.id);
|
||||
@@ -417,16 +429,18 @@ export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (
|
||||
>
|
||||
<Settings className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="删除"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(pipeline);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
{isAdmin && (
|
||||
<button
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="删除"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(pipeline);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -506,19 +520,21 @@ export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (
|
||||
<td className="px-4 py-3 text-[12px]" style={{ color: 'var(--text-secondary)' }}>{pipeline.triggerText}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-0">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
runMutation.mutate(pipeline.raw.id);
|
||||
}}
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="执行"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" style={{ color: 'var(--accent-green)' }} />
|
||||
</button>
|
||||
{pipeline.accessLevel >= 2 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
runMutation.mutate(pipeline.raw.id);
|
||||
}}
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="执行"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" style={{ color: 'var(--accent-green)' }} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="配置"
|
||||
title={pipeline.accessLevel >= 3 ? '配置' : '查看'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelectPipeline(pipeline.id);
|
||||
@@ -526,16 +542,18 @@ export default function PipelineList({ onSelectPipeline }: { onSelectPipeline: (
|
||||
>
|
||||
<Settings className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="删除"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(pipeline);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
{isAdmin && (
|
||||
<button
|
||||
className="p-1 rounded hover:bg-black/5"
|
||||
title="删除"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(pipeline);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" style={{ color: 'var(--text-tertiary)' }} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in new issue
Block a user