From 9a8c40eee2f7a4ac2e98744c7abd5122ea5c3497 Mon Sep 17 00:00:00 2001 From: ulflow_phattt2901 Date: Thu, 5 Jun 2025 13:30:08 +0700 Subject: [PATCH] fix: implement resource access middleware and router setup with auth integration --- .gitignore | 1 + {docs => current}/AUTHENTICATION.md | 15 ++ {docs => current}/secrets.md | 0 {docs => current}/testing.md | 74 -------- {docs => current}/unit-testing.md | 0 docs/review.md | 80 --------- docs/roadmap.md | 40 +---- docs/session_20240524.md | 32 ---- .../http/middleware/resource_access.go | 106 ++++++++++++ .../http/middleware/resource_access_test.go | 162 ++++++++++++++++++ internal/transport/http/router.go | 19 ++ 11 files changed, 310 insertions(+), 219 deletions(-) rename {docs => current}/AUTHENTICATION.md (88%) rename {docs => current}/secrets.md (100%) rename {docs => current}/testing.md (50%) rename {docs => current}/unit-testing.md (100%) delete mode 100644 docs/review.md delete mode 100644 docs/session_20240524.md create mode 100644 internal/transport/http/middleware/resource_access.go create mode 100644 internal/transport/http/middleware/resource_access_test.go diff --git a/.gitignore b/.gitignore index cfdb4cc..ce62740 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ dist/ # OS specific files .DS_Store Thumbs.db +.obsidian diff --git a/docs/AUTHENTICATION.md b/current/AUTHENTICATION.md similarity index 88% rename from docs/AUTHENTICATION.md rename to current/AUTHENTICATION.md index 1b0ddd7..680cfce 100644 --- a/docs/AUTHENTICATION.md +++ b/current/AUTHENTICATION.md @@ -97,6 +97,21 @@ sequenceDiagram 2. Kiểm tra user có vai trò phù hợp không 3. Trả về lỗi nếu không có quyền +#### `ResourceOwnerCheck(paramName string)` +- **Mục đích**: Ngăn chặn Horizontal Privilege Escalation bằng cách kiểm tra quyền sở hữu tài nguyên +- **Luồng xử lý**: + 1. Lấy ID tài nguyên từ URL param + 2. Lấy thông tin user từ context + 3. Kiểm tra nếu user là chủ sở hữu (user ID = resource ID) + 4. Hoặc kiểm tra nếu user có role admin + 5. Từ chối truy cập nếu không thỏa điều kiện + +#### `RequireOwnerOrRole(paramName string, roles ...string)` +- **Mục đích**: Kết hợp kiểm tra quyền sở hữu và vai trò +- **Luồng xử lý**: + 1. Kiểm tra nếu user là chủ sở hữu tài nguyên + 2. Hoặc kiểm tra nếu user có một trong các role được chỉ định + ### Token Service #### `GenerateTokens(userID, sessionID)` diff --git a/docs/secrets.md b/current/secrets.md similarity index 100% rename from docs/secrets.md rename to current/secrets.md diff --git a/docs/testing.md b/current/testing.md similarity index 50% rename from docs/testing.md rename to current/testing.md index 7668cce..6bf94e8 100644 --- a/docs/testing.md +++ b/current/testing.md @@ -15,80 +15,6 @@ Testing là một phần quan trọng trong quy trình phát triển, đảm b - Test cases nên bao gồm cả happy path và error cases - Coverage yêu cầu tối thiểu: 80% - Sử dụng t.Run() để chạy các subtest -### Mẫu Unit Test -```go -// user_service_test.go -package user - -import ( - "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -// Mocking repository -type MockUserRepository struct { - mock.Mock -} - -func (m *MockUserRepository) FindByID(id string) (*User, error) { - args := m.Called(id) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*User), args.Error(1) -} - -func TestGetUser(t *testing.T) { - // Test cases - testCases := []struct { - name string - userID string - mockUser *User - mockError error - expectedUser *User - expectedError error - }{ - { - name: "successful_get", - userID: "123", - mockUser: &User{ID: "123", Name: "Test User"}, - mockError: nil, - expectedUser: &User{ID: "123", Name: "Test User"}, - expectedError: nil, - }, - { - name: "user_not_found", - userID: "456", - mockUser: nil, - mockError: ErrUserNotFound, - expectedUser: nil, - expectedError: ErrUserNotFound, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Setup mock - mockRepo := new(MockUserRepository) - mockRepo.On("FindByID", tc.userID).Return(tc.mockUser, tc.mockError) - - // Create service with mock dependency - service := NewUserService(mockRepo) - - // Call the method - user, err := service.GetUser(tc.userID) - - // Assert results - assert.Equal(t, tc.expectedUser, user) - assert.Equal(t, tc.expectedError, err) - - // Verify expectations - mockRepo.AssertExpectations(t) - }) - } -} -``` ## Integration Testing ### Approach diff --git a/docs/unit-testing.md b/current/unit-testing.md similarity index 100% rename from docs/unit-testing.md rename to current/unit-testing.md diff --git a/docs/review.md b/docs/review.md deleted file mode 100644 index b373e55..0000000 --- a/docs/review.md +++ /dev/null @@ -1,80 +0,0 @@ -🚀 Cải Thiện Luồng Xác Thực Cho Project Starter-Kit -Một Starter-kit chất lượng cần có hệ thống xác thực được xây dựng trên các nguyên tắc bảo mật và thực hành tốt nhất. Dưới đây là những cải thiện quan trọng: - -1. Bảo Mật Refresh Token (RT) Phía Client – Ưu Tiên Hàng Đầu -Vấn đề cốt lõi: Lưu RT trong localStorage hoặc sessionStorage khiến chúng dễ bị tấn công XSS. - -Giải pháp cho Starter-kit: - -Sử dụng HttpOnly Cookies cho Refresh Token: - -Bắt buộc: Starter-kit NÊN mặc định hoặc hướng dẫn rõ ràng việc sử dụng cookie HttpOnly để lưu RT. Điều này ngăn JavaScript phía client truy cập RT. - -Access Token (AT) có thể được lưu trong bộ nhớ JavaScript (an toàn hơn localStorage cho AT có đời sống ngắn) hoặc sessionStorage nếu cần thiết cho SPA. - -Thiết lập cờ Secure và SameSite cho Cookie: - -Secure: Đảm bảo cookie chỉ được gửi qua HTTPS. - -SameSite=Strict (hoặc SameSite=Lax): Giúp chống lại tấn công CSRF. Starter-kit NÊN có cấu hình này. - -2. Quản Lý Refresh Token Phía Server – Đảm Bảo An Toàn -Thực hành tốt đã có: Refresh Token Rotation (xoay vòng RT khi sử dụng) là rất tốt. - -Cải thiện cho Starter-kit: - -Vô hiệu hóa RT cũ NGAY LẬP TỨC khi xoay vòng: Đảm bảo RT đã sử dụng không còn giá trị. - -Thu hồi RT khi Logout: Endpoint /api/v1/auth/logout PHẢI xóa hoặc đánh dấu RT là đã thu hồi trong cơ sở dữ liệu. Chỉ xóa ở client là không đủ. - -(Khuyến nghị cho Starter-kit nâng cao): Cân nhắc cơ chế phát hiện việc sử dụng RT đã bị đánh cắp (ví dụ: nếu một RT cũ được dùng lại sau khi đã xoay vòng, hãy thu hồi tất cả RT của user đó). - -3. Tăng Cường Quy Trình Đăng Ký – Nền Tảng Người Dùng -Cải thiện cho Starter-kit: - -Chính Sách Mật Khẩu Tối Thiểu: - -Yêu cầu độ dài mật khẩu tối thiểu (ví dụ: 8 hoặc 10 ký tự). Starter-kit NÊN có điều này. - -(Tùy chọn): Khuyến khích hoặc yêu cầu kết hợp chữ hoa, chữ thường, số, ký tự đặc biệt. - -Xác Thực Email (Khuyến Nghị Mạnh Mẽ): - -Starter-kit NÊN bao gồm module hoặc hướng dẫn tích hợp quy trình gửi email xác thực để kích hoạt tài khoản. Điều này giúp đảm bảo email hợp lệ và là kênh liên lạc quan trọng. - -4. Bảo Vệ Chống Tấn Công Đăng Nhập – Lớp Phòng Thủ Cơ Bản -Cải thiện cho Starter-kit: - -Rate Limiting cho Endpoint Đăng Nhập: Áp dụng giới hạn số lần thử đăng nhập thất bại (/api/v1/auth/login) dựa trên IP hoặc username/email. - -Thông Báo Lỗi Chung Chung: Tránh các thông báo lỗi tiết lộ thông tin (ví dụ: "Username không tồn tại" hoặc "Sai mật khẩu"). Thay vào đó, sử dụng thông báo chung như "Tên đăng nhập hoặc mật khẩu không chính xác." - -5. Thực Hành Tốt Nhất với JWT – Cốt Lõi Của Xác Thực -Cải thiện cho Starter-kit: - -Quản Lý Secret Key An Toàn: - -Hướng dẫn lưu trữ JWT secret key trong biến môi trường (environment variables). - -Tuyệt đối KHÔNG hardcode secret key trong mã nguồn. - -Sử Dụng Thuật Toán Ký Mạnh: - -Mặc định sử dụng thuật toán đối xứng mạnh như HS256. - -Khuyến nghị và cung cấp tùy chọn cho thuật toán bất đối xứng như RS256 (yêu cầu quản lý cặp public/private key) cho các hệ thống phức tạp hơn. - -Giữ Payload của Access Token Nhỏ Gọn: - -Chỉ chứa thông tin cần thiết nhất (ví dụ: userId, roles). - -Cân nhắc thêm iss (issuer) và aud (audience) để tăng cường xác minh token. - -6. Xử Lý Lỗi và Ghi Log (Logging) An Toàn -Cải thiện cho Starter-kit: - -Không Ghi Log Thông Tin Nhạy Cảm: Tuyệt đối KHÔNG ghi log Access Token, Refresh Token, hoặc mật khẩu dưới bất kỳ hình thức nào. - -Ghi Log Sự Kiện An Ninh: Hướng dẫn hoặc cung cấp cơ chế ghi log các sự kiện quan trọng (đăng nhập thành công/thất bại, yêu cầu làm mới token, thay đổi mật khẩu) một cách an toàn, không kèm dữ liệu nhạy cảm, để phục vụ việc giám sát và điều tra. - -Bằng cách tích hợp những cải tiến này, Starter-kit của bạn sẽ cung cấp một điểm khởi đầu vững chắc và an toàn hơn cho các nhà phát triển. \ No newline at end of file diff --git a/docs/roadmap.md b/docs/roadmap.md index ee49e40..b1c8cca 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -10,10 +10,10 @@ - [x] Xác thực token với middleware - [x] Phân quyền cơ bản - [x] Database with GORM + Postgres -- [ ] Health Check -- [ ] Unit Test with testify (Template) -- [ ] CI/CD with Gitea for Dev Team -- [ ] Build and Deploy with Docker + Docker Compose on Local +- [x] Health Check +- [x] Unit Test with testify (Template) +- [x] CI/CD with Gitea for Dev Team +- [x] Build and Deploy with Docker + Docker Compose on Local ## Giai đoạn 1: Cơ sở hạ tầng cơ bản - [x] Thiết lập cấu trúc dự án theo mô hình DDD @@ -21,10 +21,9 @@ - [x] Cấu hình Docker và Docker Compose - [x] HTTP server với Gin - [x] Database setup với GORM và Postgres -- [ ] Health check API endpoints -- Timeline: Q2/2025 +- [x] Health check API endpoints -## Giai đoạn 2: Bảo mật và xác thực (Q2/2025) +## Giai đoạn 2: Bảo mật và xác thực ### 1. Xác thực và Ủy quyền - [x] **JWT Authentication** @@ -53,46 +52,21 @@ - [ ] Content Security Policy (CSP) tùy chỉnh - [ ] XSS protection -### 3. Theo dõi và Giám sát -- [ ] **Audit Logging** - - [ ] Ghi log các hoạt động quan trọng - - [ ] Theo dõi đăng nhập thất bại - - [ ] Cảnh báo bảo mật - -- [ ] **Monitoring** - - [ ] Tích hợp Prometheus - - [ ] Dashboard giám sát - - [ ] Cảnh báo bất thường - -### 4. Cải thiện Hiệu suất -- [ ] **Tối ưu hóa** - - [ ] Redis cho caching - - [ ] Tối ưu truy vấn database - - [ ] Compression response - -### Timeline -- Tuần 1-2: Hoàn thiện xác thực & phân quyền -- Tuần 3-4: Triển khai bảo mật API và headers -- Tuần 5-6: Hoàn thiện audit logging và monitoring -- Tuần 7-8: Tối ưu hiệu suất và kiểm thử bảo mật - ## Giai đoạn 3: Tự động hóa - [ ] Unit Test templates và mocks - [ ] CI/CD với Gitea - [ ] Automated deployment - [ ] Linting và code quality checks -- Timeline: Q3/2025 ## Giai đoạn 4: Mở rộng tính năng - [x] Go Feature Flag implementation - [ ] Notification system - [ ] Background job processing - [ ] API documentation -- Timeline: Q3/2025 + ## Giai đoạn 5: Production readiness - [x] Performance optimization - [ ] Monitoring và observability - [ ] Backup và disaster recovery - [ ] Security hardening -- Timeline: Q4/2025 diff --git a/docs/session_20240524.md b/docs/session_20240524.md deleted file mode 100644 index 62b968d..0000000 --- a/docs/session_20240524.md +++ /dev/null @@ -1,32 +0,0 @@ -# Tóm tắt phiên làm việc - 24/05/2025 - -## Các file đang mở -1. `docs/roadmap.md` - Đang xem mục tiêu phát triển -2. `configs/config.yaml` - File cấu hình ứng dụng -3. `docs/review.md` - Đang xem phần đánh giá code - -## Các thay đổi chính trong phiên - -### 1. Cập nhật Roadmap -- Đánh dấu hoàn thành các mục JWT Authentication -- Cập nhật chi tiết Giai đoạn 2 (Bảo mật và xác thực) -- Thêm timeline chi tiết cho từng tuần - -### 2. Giải thích luồng xác thực -- Đã giải thích chi tiết về luồng JWT authentication -- Mô tả các endpoint chính và cách hoạt động -- Giải thích về bảo mật token và xử lý lỗi - -### 3. Các lệnh đã sử dụng -- `/heyy` - Thảo luận về các bước tiếp theo -- `/yys` - Thử lưu trạng thái (không khả dụng) - -## Công việc đang thực hiện -- Đang xem xét phần đánh giá code liên quan đến xử lý lỗi khởi động service - -## Ghi chú -- Cần hoàn thiện phần Health Check API -- Cần triển khai API rate limiting và security headers - ---- -*Tự động tạo lúc: 2025-05-24 12:26* diff --git a/internal/transport/http/middleware/resource_access.go b/internal/transport/http/middleware/resource_access.go new file mode 100644 index 0000000..5296301 --- /dev/null +++ b/internal/transport/http/middleware/resource_access.go @@ -0,0 +1,106 @@ +package middleware + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +// ResourceOwnerCheck kiểm tra người dùng có phải là chủ sở hữu của tài nguyên không +// hoặc có quyền admin để truy cập tài nguyên của người khác +func (m *AuthMiddleware) ResourceOwnerCheck(paramName string) gin.HandlerFunc { + return func(c *gin.Context) { + // Lấy ID tài nguyên từ URL parameter + resourceID := c.Param(paramName) + if resourceID == "" { + // Không có param ID, bỏ qua kiểm tra + c.Next() + return + } + + // Lấy thông tin user hiện tại từ context + claims, err := GetUserFromContext(c) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "error": "Unauthorized access", + }) + return + } + + // Kiểm tra nếu người dùng là chủ sở hữu + currentUserID := claims.UserID + + // Trường hợp 1: Người dùng là chủ sở hữu tài nguyên + if resourceID == currentUserID { + c.Next() + return + } + + // Trường hợp 2: Kiểm tra nếu người dùng có role admin + isAdmin := false + for _, role := range claims.Roles { + if role == "admin" { + isAdmin = true + break + } + } + + if isAdmin { + // Có quyền admin, cho phép truy cập + c.Next() + return + } + + // Không có quyền truy cập + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ + "error": fmt.Sprintf("You don't have permission to access this %s", paramName), + }) + } +} + +// RequireOwnerOrRole kiểm tra người dùng là chủ sở hữu hoặc có một trong các vai trò được chỉ định +func (m *AuthMiddleware) RequireOwnerOrRole(paramName string, roles ...string) gin.HandlerFunc { + return func(c *gin.Context) { + // Lấy ID tài nguyên từ URL parameter + resourceID := c.Param(paramName) + if resourceID == "" { + // Không có param ID, sử dụng RequireRole middleware + mRoleCheck := m.RequireRole(roles...) + mRoleCheck(c) + return + } + + // Lấy thông tin user hiện tại từ context + claims, err := GetUserFromContext(c) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "error": "Unauthorized access", + }) + return + } + + // Kiểm tra nếu người dùng là chủ sở hữu + currentUserID := claims.UserID + if resourceID == currentUserID { + c.Next() + return + } + + // Không phải chủ sở hữu, kiểm tra role + for _, role := range roles { + for _, userRole := range claims.Roles { + if userRole == role { + // Có role phù hợp + c.Next() + return + } + } + } + + // Không có quyền truy cập + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ + "error": fmt.Sprintf("Require ownership of %s or one of these roles: %v", paramName, roles), + }) + } +} diff --git a/internal/transport/http/middleware/resource_access_test.go b/internal/transport/http/middleware/resource_access_test.go new file mode 100644 index 0000000..10e3f8d --- /dev/null +++ b/internal/transport/http/middleware/resource_access_test.go @@ -0,0 +1,162 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "starter-kit/internal/service" +) + +func TestResourceOwnerCheck_Success_SameUser(t *testing.T) { + // Setup + gin.SetMode(gin.TestMode) + // Sử dụng struct trống thay vì mock vì trong test này ta không cần gọi bất kỳ phương thức nào của AuthService + authMiddleware := &AuthMiddleware{} + + r := gin.New() + r.Use(func(c *gin.Context) { + // Simulate claims being set by Authenticate middleware + claims := &service.Claims{ + UserID: "user123", + Username: "testuser", + Roles: []string{"user"}, + } + c.Set(ContextKeyUser, claims) + }) + + // Add a route with resource owner check + r.GET("/users/:id", authMiddleware.ResourceOwnerCheck("id"), func(c *gin.Context) { + c.String(http.StatusOK, "Access granted") + }) + + // Test with the same user ID + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/users/user123", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "Access granted", w.Body.String()) +} + +func TestResourceOwnerCheck_Success_AdminUser(t *testing.T) { + // Setup + gin.SetMode(gin.TestMode) + // Sử dụng struct trống thay vì mock vì trong test này ta không cần gọi bất kỳ phương thức nào của AuthService + authMiddleware := &AuthMiddleware{} + + r := gin.New() + r.Use(func(c *gin.Context) { + // Simulate claims with admin role + claims := &service.Claims{ + UserID: "admin456", + Username: "adminuser", + Roles: []string{"admin"}, + } + c.Set(ContextKeyUser, claims) + }) + + // Add a route with resource owner check + r.GET("/users/:id", authMiddleware.ResourceOwnerCheck("id"), func(c *gin.Context) { + c.String(http.StatusOK, "Access granted") + }) + + // Test with different user ID but admin role + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/users/user123", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "Access granted", w.Body.String()) +} + +func TestResourceOwnerCheck_Forbidden(t *testing.T) { + // Setup + gin.SetMode(gin.TestMode) + // Sử dụng struct trống thay vì mock vì trong test này ta không cần gọi bất kỳ phương thức nào của AuthService + authMiddleware := &AuthMiddleware{} + + r := gin.New() + r.Use(func(c *gin.Context) { + // Regular user trying to access another user's resource + claims := &service.Claims{ + UserID: "user456", + Username: "anotheruser", + Roles: []string{"user"}, + } + c.Set(ContextKeyUser, claims) + }) + + // Add a route with resource owner check + r.GET("/users/:id", authMiddleware.ResourceOwnerCheck("id"), func(c *gin.Context) { + c.String(http.StatusOK, "Access granted") + }) + + // Test with different user ID + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/users/user123", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "You don't have permission") +} + +func TestRequireOwnerOrRole_Owner_Success(t *testing.T) { + // Setup + gin.SetMode(gin.TestMode) + // Sử dụng struct trống thay vì mock vì trong test này ta không cần gọi bất kỳ phương thức nào của AuthService + authMiddleware := &AuthMiddleware{} + + r := gin.New() + r.Use(func(c *gin.Context) { + claims := &service.Claims{ + UserID: "user123", + Username: "testuser", + Roles: []string{"user"}, + } + c.Set(ContextKeyUser, claims) + }) + + // Add a route with require owner or role check + r.GET("/users/:id", authMiddleware.RequireOwnerOrRole("id", "admin", "manager"), func(c *gin.Context) { + c.String(http.StatusOK, "Access granted") + }) + + // Test as owner + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/users/user123", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestRequireOwnerOrRole_Role_Success(t *testing.T) { + // Setup + gin.SetMode(gin.TestMode) + // Sử dụng struct trống thay vì mock vì trong test này ta không cần gọi bất kỳ phương thức nào của AuthService + authMiddleware := &AuthMiddleware{} + + r := gin.New() + r.Use(func(c *gin.Context) { + claims := &service.Claims{ + UserID: "manager789", + Username: "manageruser", + Roles: []string{"manager"}, + } + c.Set(ContextKeyUser, claims) + }) + + // Add a route with require owner or role check + r.GET("/users/:id", authMiddleware.RequireOwnerOrRole("id", "admin", "manager"), func(c *gin.Context) { + c.String(http.StatusOK, "Access granted") + }) + + // Test with allowed role + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/users/user123", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} diff --git a/internal/transport/http/router.go b/internal/transport/http/router.go index 62d78db..eb5de4a 100644 --- a/internal/transport/http/router.go +++ b/internal/transport/http/router.go @@ -87,5 +87,24 @@ func SetupRouter(cfg *config.Config, db *gorm.DB) *gin.Engine { // api.PUT("/profile", userHandler.UpdateProfile) } + // Special route to list all API endpoints (only in development mode) + if cfg.App.Environment != "production" { + router.GET("/routes", func(c *gin.Context) { + routes := []map[string]string{} + + for _, routeInfo := range router.Routes() { + routes = append(routes, map[string]string{ + "method": routeInfo.Method, + "path": routeInfo.Path, + }) + } + + c.JSON(200, gin.H{ + "routes": routes, + "count": len(routes), + }) + }) + } + return router }