195 lines
8.4 KiB
Go
195 lines
8.4 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
|
)
|
|
|
|
// ProjectResourceReader 将实时权限和资源读取合并,不向外部入口暴露全量基础仓储。
|
|
type ProjectResourceReader interface {
|
|
ListVisibleDomains(context.Context, string) ([]domain.Domain, error)
|
|
GetVisibleDomain(context.Context, string, string) (domain.Domain, error)
|
|
ListVisibleServers(context.Context, string) ([]deployment.Server, error)
|
|
GetVisibleServer(context.Context, string, string) (deployment.Server, error)
|
|
ListVisibleDomainServers(context.Context, string, string) ([]deployment.Server, error)
|
|
ListVisibleServerDomains(context.Context, string, string) ([]domain.Domain, error)
|
|
}
|
|
|
|
var _ ProjectResourceReader = (*SQLProjectMemberRepository)(nil)
|
|
|
|
const visibleDomainSelect = `SELECT domains.id, domains.name, domains.certificate_type,
|
|
domains.sans_json, domains.auto_renew, domains.acme_environment, domains.dns_provider,
|
|
domains.status, domains.expires_at, domains.created_at, domains.updated_at,
|
|
domains.project_id, domains.owner_id, domains.created_by, domains.updated_by, domains.challenge_type FROM domains`
|
|
|
|
// 列名只接受内部固定值,角色能力复用领域模型;所有身份和角色值均使用绑定参数。
|
|
// EXISTS 避免多角色账户重复返回资源,不按用户分组或全局普通管理员身份放行。
|
|
func resourceReadPredicate(projectColumn, actorID string, permission domain.ProjectPermission) (string, []any) {
|
|
valid := projectColumn == "domains.project_id" && permission == domain.PermissionCertificateRead ||
|
|
projectColumn == "servers.project_id" && permission == domain.PermissionServerRead ||
|
|
projectColumn == "dns_accounts.project_id" && (permission == domain.PermissionDNSRead || permission == domain.PermissionDNSCredentialUse || permission == domain.PermissionDNSManage)
|
|
if !valid {
|
|
return "1 = 0", nil
|
|
}
|
|
roles := []domain.ProjectRole{
|
|
domain.ProjectRoleAdmin, domain.ProjectRoleCertificateAdmin, domain.ProjectRoleDeploymentAdmin,
|
|
domain.ProjectRoleDNSAdmin, domain.ProjectRoleAuditViewer, domain.ProjectRoleReadOnly,
|
|
}
|
|
args := []any{actorID}
|
|
placeholders := make([]string, 0, len(roles))
|
|
for _, role := range roles {
|
|
if role.Allows(permission) {
|
|
placeholders = append(placeholders, "?")
|
|
args = append(args, string(role))
|
|
}
|
|
}
|
|
return `EXISTS (SELECT 1 FROM projects resource_project
|
|
JOIN users resource_actor ON resource_actor.id = ?
|
|
WHERE resource_project.id = ` + projectColumn + ` AND resource_actor.status = 'active'
|
|
AND (resource_actor.role = 'super_admin' OR (resource_actor.role IN ('admin', 'user')
|
|
AND EXISTS (SELECT 1 FROM project_members resource_member
|
|
WHERE resource_member.project_id = resource_project.id AND resource_member.user_id = resource_actor.id
|
|
AND resource_member.role IN (` + strings.Join(placeholders, ", ") + `)))))`, args
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) resourceReaderReady(actorID string) error {
|
|
if err := r.ensureDB(); err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(actorID) == "" || len(actorID) > 128 {
|
|
return ErrProjectAccessDenied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) ListVisibleDomains(ctx context.Context, actorID string) ([]domain.Domain, error) {
|
|
if err := r.resourceReaderReady(actorID); err != nil {
|
|
return nil, err
|
|
}
|
|
predicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
|
|
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, visibleDomainSelect+` WHERE `+predicate+` ORDER BY domains.name, domains.id`), args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("查询可见域名失败:%w", err)
|
|
}
|
|
defer rows.Close()
|
|
items := make([]domain.Domain, 0)
|
|
for rows.Next() {
|
|
item, err := scanDomain(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("读取可见域名失败:%w", err)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) GetVisibleDomain(ctx context.Context, actorID, id string) (domain.Domain, error) {
|
|
if err := r.resourceReaderReady(actorID); err != nil {
|
|
return domain.Domain{}, err
|
|
}
|
|
predicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
|
|
args = append(args, id)
|
|
item, err := scanDomain(r.db.QueryRowContext(ctx, Rebind(r.driver,
|
|
visibleDomainSelect+` WHERE `+predicate+` AND domains.id = ?`), args...))
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return domain.Domain{}, ErrProjectAccessDenied
|
|
}
|
|
return item, err
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) ListVisibleServers(ctx context.Context, actorID string) ([]deployment.Server, error) {
|
|
if err := r.resourceReaderReady(actorID); err != nil {
|
|
return nil, err
|
|
}
|
|
predicate, args := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
|
|
return r.readVisibleServers(ctx, serverSelectQualified+` WHERE `+predicate+` ORDER BY servers.name, servers.id`, args)
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) GetVisibleServer(ctx context.Context, actorID, id string) (deployment.Server, error) {
|
|
if err := r.resourceReaderReady(actorID); err != nil {
|
|
return deployment.Server{}, err
|
|
}
|
|
predicate, args := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
|
|
args = append(args, id)
|
|
item, err := scanServer(r.db.QueryRowContext(ctx, Rebind(r.driver,
|
|
serverSelectQualified+` WHERE `+predicate+` AND servers.id = ?`), args...))
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return deployment.Server{}, ErrProjectAccessDenied
|
|
}
|
|
return item, err
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) ListVisibleDomainServers(ctx context.Context, actorID, domainID string) ([]deployment.Server, error) {
|
|
// 对未知和无权限域名统一拒绝;随后的查询仍重判双方权限,撤权后不会返回旧结果。
|
|
if _, err := r.GetVisibleDomain(ctx, actorID, domainID); err != nil {
|
|
return nil, err
|
|
}
|
|
domainPredicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
|
|
serverPredicate, serverArgs := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
|
|
args = append(args, serverArgs...)
|
|
args = append(args, domainID)
|
|
return r.readVisibleServers(ctx, serverSelectQualified+`
|
|
JOIN domain_servers visible_binding ON visible_binding.server_id = servers.id
|
|
JOIN domains ON domains.id = visible_binding.domain_id AND domains.project_id = servers.project_id
|
|
WHERE `+domainPredicate+` AND `+serverPredicate+` AND domains.id = ? ORDER BY servers.name, servers.id`, args)
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) ListVisibleServerDomains(ctx context.Context, actorID, serverID string) ([]domain.Domain, error) {
|
|
if _, err := r.GetVisibleServer(ctx, actorID, serverID); err != nil {
|
|
return nil, err
|
|
}
|
|
domainPredicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
|
|
serverPredicate, serverArgs := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
|
|
args = append(args, serverArgs...)
|
|
args = append(args, serverID)
|
|
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, visibleDomainSelect+`
|
|
JOIN domain_servers visible_binding ON visible_binding.domain_id = domains.id
|
|
JOIN servers ON servers.id = visible_binding.server_id AND servers.project_id = domains.project_id
|
|
WHERE `+domainPredicate+` AND `+serverPredicate+` AND servers.id = ? ORDER BY domains.name, domains.id`), args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query visible server domains: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
items := make([]domain.Domain, 0)
|
|
for rows.Next() {
|
|
item, err := scanDomain(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("failed to read visible server domains: %w", err)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (r *SQLProjectMemberRepository) readVisibleServers(ctx context.Context, query string, args []any) ([]deployment.Server, error) {
|
|
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, query), args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("查询可见服务器失败:%w", err)
|
|
}
|
|
defer rows.Close()
|
|
items := make([]deployment.Server, 0)
|
|
for rows.Next() {
|
|
item, err := scanServer(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("读取可见服务器失败:%w", err)
|
|
}
|
|
return items, nil
|
|
}
|