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
+22 -8
View File
@@ -18,10 +18,11 @@ import (
// --- mocks ---
type mockAuthStore struct {
registerUserFn func(ctx context.Context, email, passwordHash string) (store.User, store.Project, error)
getUserByEmailFn func(ctx context.Context, email string) (store.User, error)
getUserByIDFn func(ctx context.Context, userID uuid.UUID) (store.User, error)
getUserProjectFn func(ctx context.Context, userID uuid.UUID) (store.Project, error)
registerUserFn func(ctx context.Context, email, passwordHash string) (store.User, store.Project, error)
getUserByEmailFn func(ctx context.Context, email string) (store.User, error)
getUserByIDFn func(ctx context.Context, userID uuid.UUID) (store.User, error)
getUserProjectFn func(ctx context.Context, userID uuid.UUID) (store.Project, error)
getProjectOwnedFn func(ctx context.Context, projectID, userID uuid.UUID) (store.Project, error)
}
func (m *mockAuthStore) RegisterUser(ctx context.Context, email, passwordHash string) (store.User, store.Project, error) {
@@ -40,8 +41,13 @@ func (m *mockAuthStore) GetUserProject(ctx context.Context, userID uuid.UUID) (s
return m.getUserProjectFn(ctx, userID)
}
func (m *mockAuthStore) GetProjectOwned(ctx context.Context, projectID, userID uuid.UUID) (store.Project, error) {
return m.getProjectOwnedFn(ctx, projectID, userID)
}
type mockSessionManager struct {
createFn func(ctx context.Context, userID uuid.UUID) (string, time.Time, error)
createFn func(ctx context.Context, userID uuid.UUID) (string, time.Time, error)
validateFn func(ctx context.Context, token string) (uuid.UUID, error)
destroyCalled bool
destroyToken string
@@ -52,7 +58,10 @@ func (m *mockSessionManager) Create(ctx context.Context, userID uuid.UUID) (stri
return m.createFn(ctx, userID)
}
func (m *mockSessionManager) Validate(context.Context, string) (uuid.UUID, error) {
func (m *mockSessionManager) Validate(ctx context.Context, token string) (uuid.UUID, error) {
if m.validateFn != nil {
return m.validateFn(ctx, token)
}
return uuid.Nil, nil
}
@@ -338,7 +347,7 @@ func TestAuthLogout_ClearsSessionAndDestroys(t *testing.T) {
// now resolves the authenticated user via GetUserByID and returns their real
// email, instead of leaving it blank.
func TestAuthMe_ReturnsRealEmail(t *testing.T) {
a, authStore, _ := newTestAuthAPI()
a, authStore, sessions := newTestAuthAPI()
userID := uuid.New()
projectID := uuid.New()
authStore.getUserByIDFn = func(_ context.Context, id uuid.UUID) (store.User, error) {
@@ -350,10 +359,15 @@ func TestAuthMe_ReturnsRealEmail(t *testing.T) {
authStore.getUserProjectFn = func(_ context.Context, uid uuid.UUID) (store.Project, error) {
return store.Project{ID: projectID, UserID: uid, Name: "default"}, nil
}
// /me is now behind RequireAuth: the session cookie must resolve to
// userID via Sessions.Validate rather than being injected directly.
sessions.validateFn = func(context.Context, string) (uuid.UUID, error) {
return userID, nil
}
router := NewRouter(a)
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil)
req = req.WithContext(context.WithValue(req.Context(), ctxKeyUserID{}, userID))
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "some-valid-token"})
w := httptest.NewRecorder()
router.ServeHTTP(w, req)