fix: enforce resource ownership checks
This commit is contained in:
@@ -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
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user