feat(api): RequireAuth+RequireProjectAccess middleware, IDOR-scope check/apply по projectID

This commit is contained in:
2026-07-03 20:47:40 +07:00
parent 35ffe73ae3
commit 4533b0ca25
16 changed files with 498 additions and 143 deletions
+8 -3
View File
@@ -162,9 +162,14 @@ SELECT d.zone_id, a.provider, a.secret_enc, t.doc
FROM domains d
JOIN provider_accounts a ON a.id = d.provider_account_id
LEFT JOIN templates t ON t.id = d.template_id
WHERE d.id = $1
WHERE d.id = $1 AND d.project_id = $2
`
type LoadDomainFullParams struct {
ID uuid.UUID `json:"id"`
ProjectID uuid.UUID `json:"project_id"`
}
type LoadDomainFullRow struct {
ZoneID string `json:"zone_id"`
Provider string `json:"provider"`
@@ -172,8 +177,8 @@ type LoadDomainFullRow struct {
Doc *dto.TemplateDoc `json:"doc"`
}
func (q *Queries) LoadDomainFull(ctx context.Context, id uuid.UUID) (LoadDomainFullRow, error) {
row := q.db.QueryRow(ctx, loadDomainFull, id)
func (q *Queries) LoadDomainFull(ctx context.Context, arg LoadDomainFullParams) (LoadDomainFullRow, error) {
row := q.db.QueryRow(ctx, loadDomainFull, arg.ID, arg.ProjectID)
var i LoadDomainFullRow
err := row.Scan(
&i.ZoneID,
+5 -3
View File
@@ -13,9 +13,11 @@ import (
)
// LoadDomain joins domains+provider_accounts+templates to build the
// service.DomainRef needed to check/apply a domain's DNS records.
func (s *Store) LoadDomain(ctx context.Context, domainID uuid.UUID) (service.DomainRef, error) {
row, err := s.q.LoadDomainFull(ctx, domainID)
// service.DomainRef needed to check/apply a domain's DNS records. Scoped by
// projectID so a domain belonging to another tenant's project can never be
// loaded, even if its domainID is guessed/leaked (closes IDOR).
func (s *Store) LoadDomain(ctx context.Context, projectID, domainID uuid.UUID) (service.DomainRef, error) {
row, err := s.q.LoadDomainFull(ctx, db.LoadDomainFullParams{ID: domainID, ProjectID: projectID})
if err != nil {
return service.DomainRef{}, err
}
+2 -2
View File
@@ -40,7 +40,7 @@ func TestLoadDomainAndSaveCheckRun(t *testing.T) {
t.Fatal(err)
}
ref, err := s.LoadDomain(ctx, domain.ID)
ref, err := s.LoadDomain(ctx, defaultProject, domain.ID)
if err != nil {
t.Fatal(err)
}
@@ -87,7 +87,7 @@ func TestLoadDomainNoTemplate(t *testing.T) {
t.Fatal(err)
}
if _, err := s.LoadDomain(ctx, domain.ID); err == nil {
if _, err := s.LoadDomain(ctx, defaultProject, domain.ID); err == nil {
t.Fatal("expected error for domain without template, got nil")
}
}
+1 -1
View File
@@ -27,4 +27,4 @@ SELECT d.zone_id, a.provider, a.secret_enc, t.doc
FROM domains d
JOIN provider_accounts a ON a.id = d.provider_account_id
LEFT JOIN templates t ON t.id = d.template_id
WHERE d.id = $1;
WHERE d.id = $1 AND d.project_id = $2;
+2 -2
View File
@@ -214,7 +214,7 @@ func TestSetDomainTemplate_ClosesImportCheckLoop(t *testing.T) {
dom := doms[0]
// Before binding, the domain is not checkable.
if _, err := s.LoadDomain(ctx, dom.ID); err == nil {
if _, err := s.LoadDomain(ctx, defaultProject, dom.ID); err == nil {
t.Fatal("expected LoadDomain to fail before a template is bound")
}
@@ -234,7 +234,7 @@ func TestSetDomainTemplate_ClosesImportCheckLoop(t *testing.T) {
t.Fatalf("expected domain.TemplateID=%s, got %+v", tpl.ID, updated.TemplateID)
}
ref, err := s.LoadDomain(ctx, dom.ID)
ref, err := s.LoadDomain(ctx, defaultProject, dom.ID)
if err != nil {
t.Fatalf("expected LoadDomain to succeed after binding template, got error: %v", err)
}