1
0
forked from baron/baron-sso

RP 관계 범위의 콘솔 접근 허용

This commit is contained in:
2026-04-20 10:46:17 +09:00
parent 0b8eaec636
commit 51e46a4d00
10 changed files with 376 additions and 109 deletions

View File

@@ -229,6 +229,54 @@ func TestListClients_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestListClients_UserSeesOnlyClientsAllowedByReBAC(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Path == "/clients" {
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
{"client_id": "client-denied", "client_name": "Denied App", "metadata": map[string]interface{}{"tenant_id": "tenant-a", "status": "active"}},
{"client_id": "client-allowed", "client_name": "Allowed App", "metadata": map[string]interface{}{"tenant_id": "tenant-b", "status": "active"}},
}), nil
}
return httpJSONAny(r, http.StatusNotFound, nil), nil
})
mockKeto := new(devMockKetoService)
mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "Tenant", "tenant-a", "view_dev_console").Return(false, nil)
mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-denied", "view").Return(false, nil)
mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "Tenant", "tenant-b", "view_dev_console").Return(false, nil)
mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-allowed", "view").Return(true, nil)
h := &DevHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: &http.Client{Transport: transport},
},
Keto: mockKeto,
}
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
tenantID := "tenant-a"
c.Locals("user_profile", &domain.UserProfileResponse{
ID: "user-1",
Role: domain.RoleUser,
TenantID: &tenantID,
})
return c.Next()
})
app.Get("/api/v1/dev/clients", h.ListClients)
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result clientListResponse
_ = json.NewDecoder(resp.Body).Decode(&result)
if assert.Len(t, result.Items, 1) {
assert.Equal(t, "client-allowed", result.Items[0].ID)
}
mockKeto.AssertExpectations(t)
}
func TestCreateClient_ReservedSystemNameForbidden(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
t.Fatalf("hydra should not be called when reserved system name is rejected")
@@ -1310,6 +1358,133 @@ func TestListAuditLogs_RPAdminScope(t *testing.T) {
assert.Equal(t, "evt-1", result.Items[0].EventID)
}
func TestListAuditLogs_UserAllowedByRPAuditPermission(t *testing.T) {
auditRepo := &mockAuditRepo{
logs: []domain.AuditLog{
{
EventID: "evt-allowed",
EventType: "POST /api/v1/dev/clients/client-allowed/secret/rotate",
Status: "success",
Timestamp: time.Now().UTC(),
Details: `{"target_id":"client-allowed","tenant_id":"tenant-a","action":"ROTATE_SECRET"}`,
},
{
EventID: "evt-denied",
EventType: "POST /api/v1/dev/clients/client-denied/secret/rotate",
Status: "success",
Timestamp: time.Now().UTC().Add(-time.Minute),
Details: `{"target_id":"client-denied","tenant_id":"tenant-b","action":"ROTATE_SECRET"}`,
},
},
}
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.URL.Path == "/clients" {
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
{"client_id": "client-allowed", "client_name": "Allowed App", "metadata": map[string]interface{}{"tenant_id": "tenant-a"}},
{"client_id": "client-denied", "client_name": "Denied App", "metadata": map[string]interface{}{"tenant_id": "tenant-b"}},
}), nil
}
return httpJSONAny(r, http.StatusNotFound, nil), nil
})
mockKeto := new(devMockKetoService)
mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-allowed", "view_audit_logs").Return(true, nil)
mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-denied", "view_audit_logs").Return(false, nil)
h := &DevHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: &http.Client{Transport: transport},
},
AuditRepo: auditRepo,
Keto: mockKeto,
}
app := fiber.New()
tenantID := "tenant-a"
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
ID: "user-1",
Role: domain.RoleUser,
TenantID: &tenantID,
})
return c.Next()
})
app.Get("/api/v1/dev/audit-logs", h.ListAuditLogs)
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/audit-logs?limit=50", nil)
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result devAuditListResponse
_ = json.NewDecoder(resp.Body).Decode(&result)
if assert.Len(t, result.Items, 1) {
assert.Equal(t, "evt-allowed", result.Items[0].EventID)
}
mockKeto.AssertExpectations(t)
}
func TestListConsents_UserAllowedByRPAdminsRelation(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
return httpJSONAny(r, http.StatusOK, map[string]any{
"client_id": "client-1",
"client_name": "App One",
"metadata": map[string]any{
"tenant_id": "tenant-1",
"status": "active",
},
}), nil
}
return httpJSONAny(r, http.StatusNotFound, nil), nil
})
mockKeto := new(devMockKetoService)
mockKeto.On("CheckPermission", mock.Anything, "User:user-1", "RelyingParty", "client-1", "view_consents").Return(true, nil)
h := &DevHandler{
Hydra: &service.HydraAdminService{
AdminURL: "http://hydra.test",
HTTPClient: &http.Client{Transport: transport},
},
ConsentRepo: &mockConsentRepo{
consents: []domain.ClientConsent{
{
ClientID: "client-1",
Subject: "subject-1",
GrantedScopes: []string{"openid", "profile"},
CreatedAt: time.Now().UTC(),
},
},
},
Keto: mockKeto,
}
app := fiber.New()
tenantID := "tenant-1"
app.Use(func(c *fiber.Ctx) error {
c.Locals("user_profile", &domain.UserProfileResponse{
ID: "user-1",
Role: domain.RoleUser,
TenantID: &tenantID,
})
return c.Next()
})
app.Get("/api/v1/dev/consents", h.ListConsents)
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/consents?client_id=client-1", nil)
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result consentListResponse
_ = json.NewDecoder(resp.Body).Decode(&result)
if assert.Len(t, result.Items, 1) {
assert.Equal(t, "client-1", result.Items[0].ClientID)
assert.Equal(t, "subject-1", result.Items[0].Subject)
}
mockKeto.AssertExpectations(t)
}
func TestListClientRelations_RPAdminAllowedByViewRelationshipsPermission(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
@@ -1330,7 +1505,7 @@ func TestListClientRelations_RPAdminAllowedByViewRelationshipsPermission(t *test
mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", "config_editor", "").Return([]service.RelationTuple{
{Object: "client-1", Relation: "config_editor", SubjectID: "User:user-2"},
}, nil)
for _, relation := range []string{"admins", "creator", "secret_rotator", "jwks_viewer", "jwks_operator", "consent_viewer", "consent_revoker", "relationship_viewer", "status_operator"} {
for _, relation := range []string{"admins", "creator", "secret_rotator", "jwks_viewer", "jwks_operator", "consent_viewer", "consent_revoker", "relationship_viewer", "audit_viewer", "status_operator"} {
mockKeto.On("ListRelations", mock.Anything, "RelyingParty", "client-1", relation, "").Return([]service.RelationTuple{}, nil)
}
mockKratos := new(devMockKratosAdmin)