feat(api): RequireAuth+RequireProjectAccess middleware, IDOR-scope check/apply по projectID
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user