109 lines
3.7 KiB
Go
109 lines
3.7 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
|
)
|
|
|
|
var ErrProjectAccessDenied = errors.New("无权访问项目或执行此操作")
|
|
|
|
type ProjectAuthorizer interface {
|
|
AuthorizeProject(context.Context, string, string, domain.ProjectPermission) error
|
|
}
|
|
|
|
type SQLProjectMemberRepository struct {
|
|
db *sql.DB
|
|
driver string
|
|
}
|
|
|
|
var _ ProjectAuthorizer = (*SQLProjectMemberRepository)(nil)
|
|
|
|
func NewSQLProjectMemberRepository(db *sql.DB, driver ...string) *SQLProjectMemberRepository {
|
|
return &SQLProjectMemberRepository{db: db, driver: repositoryDriver(driver)}
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) ensureDB() error {
|
|
if r == nil || r.db == nil {
|
|
return errors.New("数据库连接不能为空")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AuthorizeProject 每次从数据库读取账户、项目和成员关系,不信任请求中的角色或用户分组。
|
|
// 这里只判定指定项目,业务入口仍须从真实资源取得项目,并在执行副作用前重新判权。
|
|
func (r *SQLProjectMemberRepository) AuthorizeProject(ctx context.Context, userID, projectID string, permission domain.ProjectPermission) error {
|
|
if err := r.ensureDB(); err != nil {
|
|
return err
|
|
}
|
|
if !permission.Valid() || strings.TrimSpace(userID) == "" || strings.TrimSpace(projectID) == "" {
|
|
return ErrProjectAccessDenied
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, `SELECT u.role, u.status, COALESCE(m.role, '')
|
|
FROM users u JOIN projects p ON p.id = ?
|
|
LEFT JOIN project_members m ON m.project_id = p.id AND m.user_id = u.id
|
|
WHERE u.id = ?`), strings.TrimSpace(projectID), strings.TrimSpace(userID))
|
|
if err != nil {
|
|
return fmt.Errorf("读取项目权限失败:%w", err)
|
|
}
|
|
defer rows.Close()
|
|
allowed := false
|
|
for rows.Next() {
|
|
var userRole, status string
|
|
var role domain.ProjectRole
|
|
if err := rows.Scan(&userRole, &status, &role); err != nil {
|
|
return fmt.Errorf("读取项目成员失败:%w", err)
|
|
}
|
|
if status != UserStatusActive {
|
|
continue
|
|
}
|
|
switch userRole {
|
|
case UserRoleSuperAdmin:
|
|
allowed = true
|
|
case UserRoleAdmin, UserRoleUser:
|
|
// 普通账户即使具有项目管理角色,也不能直接读取完整证书私钥。
|
|
if permission == domain.PermissionPrivateKeyRead && userRole == UserRoleUser {
|
|
continue
|
|
}
|
|
allowed = allowed || role.Allows(permission)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return fmt.Errorf("读取项目权限结果失败:%w", err)
|
|
}
|
|
if !allowed {
|
|
return ErrProjectAccessDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 授权与写入在同一条语句内判定,避免先检查再写入之间撤权导致越权。
|
|
// 系统管理员必须是当前有效超级管理员;普通管理员不会因用户组或全局角色自动获得项目权限。
|
|
const projectManagerPredicate = `EXISTS (SELECT 1 FROM users a WHERE a.id = ? AND a.status = 'active'
|
|
AND (a.role = 'super_admin' OR (a.role IN ('admin', 'user') AND EXISTS
|
|
(SELECT 1 FROM project_members manager WHERE manager.project_id = ?
|
|
AND manager.user_id = a.id AND manager.role = 'project_admin'))))`
|
|
|
|
func (r *SQLProjectMemberRepository) GrantProjectRole(ctx context.Context, actorID, projectID, userID string, role domain.ProjectRole) error {
|
|
return r.changeProjectRole(ctx, actorID, projectID, userID, role, true)
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) RevokeProjectRole(ctx context.Context, actorID, projectID, userID string, role domain.ProjectRole) error {
|
|
return r.changeProjectRole(ctx, actorID, projectID, userID, role, false)
|
|
}
|
|
|
|
func projectMemberWriteResult(result sql.Result) error {
|
|
count, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("读取项目成员变更结果失败:%w", err)
|
|
}
|
|
if count != 1 {
|
|
return ErrProjectAccessDenied
|
|
}
|
|
return nil
|
|
}
|