fix: implement resource access middleware and router setup with auth integration
All checks were successful
CI Pipeline / Lint (push) Successful in 3m49s
CI Pipeline / Security Scan (push) Successful in 5m16s
CI Pipeline / Test (push) Successful in 2m21s
CI Pipeline / Build (push) Successful in 1m17s
CI Pipeline / Notification (push) Successful in 2s

This commit is contained in:
ulflow_phattt2901 2025-06-05 13:30:08 +07:00
parent 2b438ea0c2
commit 9a8c40eee2
11 changed files with 310 additions and 219 deletions

1
.gitignore vendored
View File

@ -48,3 +48,4 @@ dist/
# OS specific files
.DS_Store
Thumbs.db
.obsidian

View File

@ -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)`

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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*

View File

@ -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),
})
}
}

View File

@ -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)
}

View File

@ -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
}