fix: enforce resource ownership checks

This commit is contained in:
Your Name
2026-05-28 17:28:08 +08:00
parent 7eb5f9c7d4
commit 11232177d9
4 changed files with 209 additions and 22 deletions

View File

@@ -118,6 +118,7 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) {
deviceSvc := service.NewDeviceService(deviceRepo, userRepo)
loginLogSvc := service.NewLoginLogService(loginLogRepo)
opLogSvc := service.NewOperationLogService(opLogRepo)
webhookSvc := service.NewWebhookService(db)
captchaSvc := service.NewCaptchaService(cacheManager)
totpSvc := service.NewTOTPService(userRepo)
pwdResetCfg := service.DefaultPasswordResetConfig()
@@ -141,6 +142,7 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) {
permHandler := handler.NewPermissionHandler(permSvc)
deviceHandler := handler.NewDeviceHandler(deviceSvc)
logHandler := handler.NewLogHandler(loginLogSvc, opLogSvc)
webhookHandler := handler.NewWebhookHandler(webhookSvc)
captchaHandler := handler.NewCaptchaHandler(captchaSvc)
totpHandler := handler.NewTOTPHandler(authSvc, totpSvc)
pwdResetHandler := handler.NewPasswordResetHandler(pwdResetSvc)
@@ -149,7 +151,7 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) {
r := router.NewRouter(
authHandler, userHandler, roleHandler, permHandler, deviceHandler,
logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware,
pwdResetHandler, captchaHandler, totpHandler, nil,
pwdResetHandler, captchaHandler, totpHandler, webhookHandler,
nil, nil, nil, nil, nil, themeHandler, nil, nil, nil, avatarH,
)
engine := r.Setup()
@@ -233,6 +235,62 @@ func registerUser(baseURL, username, email, password string) bool {
return resp.StatusCode == http.StatusCreated
}
func createDeviceAndGetID(t *testing.T, baseURL, token, deviceID string) int64 {
t.Helper()
resp, body := doPost(baseURL+"/api/v1/devices", token, map[string]interface{}{
"device_id": deviceID,
"device_name": "Owned Device",
"device_type": 3,
"device_os": "Linux",
"device_browser": "Chrome",
})
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("create device failed: status=%d body=%s", resp.StatusCode, body)
}
var result struct {
Data struct {
ID int64 `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal([]byte(body), &result); err != nil {
t.Fatalf("decode create device response failed: %v body=%s", err, body)
}
if result.Data.ID == 0 {
t.Fatalf("expected non-zero device id, body=%s", body)
}
return result.Data.ID
}
func createWebhookAndGetID(t *testing.T, baseURL, token, name string) int64 {
t.Helper()
resp, body := doPost(baseURL+"/api/v1/webhooks", token, map[string]interface{}{
"name": name,
"url": "https://example.com/webhook",
"events": []string{"user.created"},
})
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("create webhook failed: status=%d body=%s", resp.StatusCode, body)
}
var result struct {
Data struct {
ID int64 `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal([]byte(body), &result); err != nil {
t.Fatalf("decode create webhook response failed: %v body=%s", err, body)
}
if result.Data.ID == 0 {
t.Fatalf("expected non-zero webhook id, body=%s", body)
}
return result.Data.ID
}
func bootstrapAdminToken(baseURL, username, email, password string) string {
payload, _ := json.Marshal(map[string]interface{}{
"username": username,
@@ -876,6 +934,73 @@ func TestDeviceHandler_CreateDevice_Success(t *testing.T) {
}
}
func TestDeviceHandler_DeviceByIDRoutes_ForbiddenForOtherUser(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "device-owner", "device-owner@test.com", "UserPass123!")
registerUser(server.URL, "device-attacker", "device-attacker@test.com", "UserPass123!")
ownerToken := getToken(server.URL, "device-owner", "UserPass123!")
attackerToken := getToken(server.URL, "device-attacker", "UserPass123!")
deviceID := createDeviceAndGetID(t, server.URL, ownerToken, "device-owner-001")
tests := []struct {
name string
method string
url string
body map[string]interface{}
}{
{name: "get", method: http.MethodGet, url: fmt.Sprintf("%s/api/v1/devices/%d", server.URL, deviceID)},
{name: "update", method: http.MethodPut, url: fmt.Sprintf("%s/api/v1/devices/%d", server.URL, deviceID), body: map[string]interface{}{"device_name": "hijacked"}},
{name: "delete", method: http.MethodDelete, url: fmt.Sprintf("%s/api/v1/devices/%d", server.URL, deviceID)},
{name: "status", method: http.MethodPut, url: fmt.Sprintf("%s/api/v1/devices/%d/status", server.URL, deviceID), body: map[string]interface{}{"status": "inactive"}},
{name: "trust", method: http.MethodPost, url: fmt.Sprintf("%s/api/v1/devices/%d/trust", server.URL, deviceID), body: map[string]interface{}{"trust_duration": "30d"}},
{name: "untrust", method: http.MethodDelete, url: fmt.Sprintf("%s/api/v1/devices/%d/trust", server.URL, deviceID)},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
resp, body := doRequest(tc.method, tc.url, attackerToken, tc.body)
defer resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("expected 403 for %s, got %d body=%s", tc.name, resp.StatusCode, body)
}
})
}
}
func TestWebhookHandler_OtherUserCannotManageForeignWebhook(t *testing.T) {
server, cleanup := setupHandlerTestServer(t)
defer cleanup()
registerUser(server.URL, "webhook-owner", "webhook-owner@test.com", "UserPass123!")
registerUser(server.URL, "webhook-attacker", "webhook-attacker@test.com", "UserPass123!")
ownerToken := getToken(server.URL, "webhook-owner", "UserPass123!")
attackerToken := getToken(server.URL, "webhook-attacker", "UserPass123!")
webhookID := createWebhookAndGetID(t, server.URL, ownerToken, "owner-webhook")
tests := []struct {
name string
method string
url string
body map[string]interface{}
}{
{name: "update", method: http.MethodPut, url: fmt.Sprintf("%s/api/v1/webhooks/%d", server.URL, webhookID), body: map[string]interface{}{"name": "hijacked"}},
{name: "delete", method: http.MethodDelete, url: fmt.Sprintf("%s/api/v1/webhooks/%d", server.URL, webhookID)},
{name: "deliveries", method: http.MethodGet, url: fmt.Sprintf("%s/api/v1/webhooks/%d/deliveries", server.URL, webhookID)},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
resp, body := doRequest(tc.method, tc.url, attackerToken, tc.body)
defer resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("expected 403 for webhook %s, got %d body=%s", tc.name, resp.StatusCode, body)
}
})
}
}
// =============================================================================
// Role Handler Tests
// =============================================================================