Scert/backend/internal/repository/project_resources_test.go

177 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"context"
"database/sql"
"errors"
"path/filepath"
"testing"
"git.pcux.cn/xuans/Scert/backend/internal/config"
"git.pcux.cn/xuans/Scert/backend/internal/database"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/mysqltest"
)
func TestProjectResourcesSQLite(t *testing.T) {
cfg := config.Default(t.TempDir()).Database
cfg.DSN = filepath.Join(t.TempDir(), "project-resources.db")
checkProjectResources(t, cfg, nil)
}
func TestProjectResourcesMySQLReal(t *testing.T) {
server := mysqltest.New(t)
checkProjectResources(t, server.Database(t), server.Admin())
}
func checkProjectResources(t *testing.T, cfg config.DatabaseConfig, admin *sql.DB) {
t.Helper()
db, err := database.Open(t.Context(), cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
if err := database.Migrate(t.Context(), db.DB, cfg.Driver); err != nil {
t.Fatal(err)
}
repo := NewSQLProjectMemberRepository(db.DB, cfg.Driver)
projects := NewSQLProjectRepository(db.DB, cfg.Driver)
domains := NewSQLDomainRepository(db.DB, cfg.Driver)
servers := NewSQLServerRepository(db.DB, cfg.Driver)
for _, user := range []struct{ id, role string }{
{"root", "super_admin"}, {"manager", "user"}, {"cert", "user"}, {"deploy", "admin"},
{"dns", "user"}, {"audit", "user"}, {"reader", "user"}, {"outsider", "admin"},
} {
if _, err := db.DB.ExecContext(t.Context(), Rebind(cfg.Driver, `INSERT INTO users
(id, username, password_hash, role, status) VALUES (?, ?, 'test-only-no-login', ?, 'active')`),
user.id, user.id, user.role); err != nil {
t.Fatal(err)
}
}
for _, id := range []string{"a", "b"} {
if err := projects.CreateProject(t.Context(), domain.Project{ID: "project-" + id, Name: "资源项目"}); err != nil {
t.Fatal(err)
}
item, err := domain.NewDomain(id+".example.test", domain.CertificateSingle, nil)
if err != nil {
t.Fatal(err)
}
item.ID, item.ProjectID = "domain-"+id, "project-"+id
if err := domains.Create(t.Context(), item); err != nil {
t.Fatal(err)
}
server := deployment.Server{
ResourceScope: domain.ResourceScope{ProjectID: item.ProjectID},
ID: "server-" + id, Name: "资源服务器", Address: "192.0.2.10", Port: 22, Username: "deploy",
AuthMethod: deployment.SSHAuthPassword, PasswordRef: "secret://ssh/password/fixture", HostKeyFingerprint: "SHA256:test",
CertificatePath: "/ssl/cert.pem", PrivateKeyPath: "/ssl/key.pem", WebServer: deployment.WebServerNginx,
}
if err := servers.CreateServer(t.Context(), server); err != nil {
t.Fatal(err)
}
if err := servers.ReplaceDomainServers(t.Context(), item.ID, []string{server.ID}); err != nil {
t.Fatal(err)
}
}
for actor, role := range map[string]domain.ProjectRole{
"manager": domain.ProjectRoleAdmin, "cert": domain.ProjectRoleCertificateAdmin,
"deploy": domain.ProjectRoleDeploymentAdmin, "dns": domain.ProjectRoleDNSAdmin,
"audit": domain.ProjectRoleAuditViewer, "reader": domain.ProjectRoleReadOnly,
} {
if err := repo.GrantProjectRole(t.Context(), "root", "project-a", actor, role); err != nil {
t.Fatal(err)
}
}
if err := repo.GrantProjectRole(t.Context(), "root", "project-a", "manager", domain.ProjectRoleReadOnly); err != nil {
t.Fatal(err)
}
t.Run("lists_filter_and_deduplicate", func(t *testing.T) {
for _, tc := range []struct {
actor string
domains, servers int
}{
{"root", 2, 2}, {"manager", 1, 1}, {"cert", 1, 0}, {"deploy", 1, 1},
{"dns", 0, 0}, {"audit", 0, 0}, {"reader", 1, 1}, {"outsider", 0, 0}, {"missing", 0, 0},
} {
domainItems, err := repo.ListVisibleDomains(t.Context(), tc.actor)
if err != nil || domainItems == nil || len(domainItems) != tc.domains {
t.Fatalf("域名范围错误:%s,%+v,%v", tc.actor, domainItems, err)
}
serverItems, err := repo.ListVisibleServers(t.Context(), tc.actor)
if err != nil || serverItems == nil || len(serverItems) != tc.servers {
t.Fatalf("服务器范围错误:%s,%+v,%v", tc.actor, serverItems, err)
}
if tc.actor != "root" {
for _, item := range domainItems {
if item.ProjectID != "project-a" {
t.Fatalf("跨项目域名泄露:%+v", item)
}
}
for _, item := range serverItems {
if item.ProjectID != "project-a" {
t.Fatalf("跨项目服务器泄露:%+v", item)
}
}
}
}
})
t.Run("details_and_bindings_use_real_scope", func(t *testing.T) {
for _, id := range []string{"b", "missing", "a' OR 1=1 --"} {
if _, err := repo.GetVisibleDomain(t.Context(), "reader", "domain-"+id); !errors.Is(err, ErrProjectAccessDenied) {
t.Fatalf("越权域名应统一拒绝:%s,%v", id, err)
}
if _, err := repo.GetVisibleServer(t.Context(), "reader", "server-"+id); !errors.Is(err, ErrProjectAccessDenied) {
t.Fatalf("越权服务器应统一拒绝:%s,%v", id, err)
}
}
for _, actor := range []string{"root", "manager", "reader", "cert"} {
items, err := repo.ListVisibleDomainServers(t.Context(), actor, "domain-a")
want := 1
if actor == "cert" {
want = 0
}
if err != nil || len(items) != want || (want == 1 && items[0].ID != "server-a") {
t.Fatalf("绑定未核对双方读取权限:%s,%+v,%v", actor, items, err)
}
}
if _, err := repo.ListVisibleDomainServers(t.Context(), "reader", "domain-b"); !errors.Is(err, ErrProjectAccessDenied) {
t.Fatalf("越权绑定查询未拒绝:%v", err)
}
})
checkProjectBindings(t, db.DB, admin, repo, cfg.Driver)
t.Run("many_to_many_bindings", func(t *testing.T) {
checkManyToManyBindings(t, db.DB, admin, repo, cfg.Driver)
})
t.Run("revocation_and_disabled_account_are_not_cached", func(t *testing.T) {
if err := repo.RevokeProjectRole(t.Context(), "root", "project-a", "reader", domain.ProjectRoleReadOnly); err != nil {
t.Fatal(err)
}
if _, err := repo.GetVisibleDomain(t.Context(), "reader", "domain-a"); !errors.Is(err, ErrProjectAccessDenied) {
t.Fatalf("撤权后仍能读取:%v", err)
}
if _, err := db.DB.ExecContext(t.Context(), `UPDATE users SET status = 'disabled' WHERE id = 'manager'`); err != nil {
t.Fatal(err)
}
for _, actor := range []string{"reader", "manager"} {
if _, err := repo.ReplaceProjectDomainServers(t.Context(), actor, "domain-a", []string{}); !errors.Is(err, ErrProjectAccessDenied) {
t.Fatalf("撤权或停用后仍可修改绑定:%s,%v", actor, err)
}
items, err := repo.ListVisibleServers(t.Context(), actor)
if err != nil || len(items) != 0 {
t.Fatalf("撤权或停用后仍有资源:%s,%+v,%v", actor, items, err)
}
}
})
t.Run("canceled_and_missing_identity", func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
cancel()
if _, err := repo.ListVisibleDomains(ctx, "root"); err == nil {
t.Fatal("取消请求不应成功")
}
if _, err := repo.ListVisibleServers(t.Context(), ""); !errors.Is(err, ErrProjectAccessDenied) {
t.Fatalf("空身份未拒绝:%v", err)
}
})
}