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
+13 -8
View File
@@ -20,18 +20,23 @@ type mockCheckApplier struct {
lastReq service.ApplyRequest
}
func (m *mockCheckApplier) Check(context.Context, uuid.UUID) (diff.Changeset, error) {
func (m *mockCheckApplier) Check(context.Context, uuid.UUID, uuid.UUID) (diff.Changeset, error) {
d := model.Record{Type: model.A, Name: "a.example.com.", TTL: 300, Values: []string{"1.1.1.1"}}
return diff.Changeset{Diffs: []diff.RecordDiff{{Kind: diff.Add, Type: d.Type, Name: d.Name, Desired: &d}}}, nil
}
func (m *mockCheckApplier) Apply(_ context.Context, _ uuid.UUID, req service.ApplyRequest) (diff.Changeset, error) {
func (m *mockCheckApplier) Apply(_ context.Context, _, _ uuid.UUID, req service.ApplyRequest) (diff.Changeset, error) {
m.lastReq = req
return diff.Changeset{}, nil
}
// newTestAPI wires a fixed authenticated user who owns whatever project id
// is requested (via alwaysOwnedAuthStore/alwaysValidSessions in
// middleware_test.go) — these tests exercise check/apply behavior past the
// RequireAuth/RequireProjectAccess boundary, which is covered separately by
// middleware_test.go's own tests and the IDOR regression.
func newTestAPI() (*API, *mockCheckApplier) {
m := &mockCheckApplier{}
return &API{Svc: m}, m // остальные зависимости (store/cipher) nil — CRUD-тесты добавит реализатор
return &API{Svc: m, Auth: alwaysOwnedAuthStore(), Sessions: alwaysValidSessions(uuid.New())}, m
}
func TestCheckEndpoint(t *testing.T) {
@@ -39,7 +44,7 @@ func TestCheckEndpoint(t *testing.T) {
router := NewRouter(a)
did := uuid.New().String()
req := httptest.NewRequest(http.MethodGet,
req := requestWithSessionCookie(http.MethodGet,
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/"+did+"/check", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
@@ -62,7 +67,7 @@ func TestApplyDefaultsPruneFalse(t *testing.T) {
did := uuid.New().String()
body := `{"applyUpdates":true}` // applyPrunes отсутствует → false
req := httptest.NewRequest(http.MethodPost,
req := requestWithSessionCookie(http.MethodPost,
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/"+did+"/apply",
strings.NewReader(body))
w := httptest.NewRecorder()
@@ -81,7 +86,7 @@ func TestApplyEmptyBodyOK(t *testing.T) {
router := NewRouter(a)
did := uuid.New().String()
req := httptest.NewRequest(http.MethodPost,
req := requestWithSessionCookie(http.MethodPost,
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/"+did+"/apply", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
@@ -100,7 +105,7 @@ func TestApplyMalformedBody(t *testing.T) {
did := uuid.New().String()
body := `{"applyUpdates":`
req := httptest.NewRequest(http.MethodPost,
req := requestWithSessionCookie(http.MethodPost,
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/"+did+"/apply",
strings.NewReader(body))
w := httptest.NewRecorder()
@@ -114,7 +119,7 @@ func TestApplyMalformedBody(t *testing.T) {
func TestApplyBadUUID(t *testing.T) {
a, _ := newTestAPI()
router := NewRouter(a)
req := httptest.NewRequest(http.MethodPost,
req := requestWithSessionCookie(http.MethodPost,
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/not-a-uuid/apply",
bytes.NewReader([]byte(`{}`)))
w := httptest.NewRecorder()