forked from baron/baron-sso
RP 테넌트 접근 정책 변경 시 기존 consent 자동 폐기
This commit is contained in:
@@ -5120,6 +5120,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
|
||||
slog.Error("failed to get hydra consent request", "error", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information")
|
||||
}
|
||||
consentRequest.RequestedScope = mergeRequestedScopesWithClientRequirements(consentRequest.Client, consentRequest.RequestedScope)
|
||||
|
||||
// [DEBUG] Hydra 응답 상세 로깅
|
||||
slog.Info("GetConsentRequest Debug",
|
||||
@@ -5323,6 +5324,7 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
slog.Error("failed to get hydra consent request before accepting", "error", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information")
|
||||
}
|
||||
consentRequest.RequestedScope = mergeRequestedScopesWithClientRequirements(consentRequest.Client, consentRequest.RequestedScope)
|
||||
|
||||
// 2. 스코프 필터링 (사용자가 선택한 것만 허용)
|
||||
if len(req.GrantScope) > 0 {
|
||||
@@ -5339,6 +5341,7 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
||||
}
|
||||
consentRequest.RequestedScope = filteredScopes
|
||||
}
|
||||
consentRequest.RequestedScope = mergeRequestedScopesWithClientRequirements(consentRequest.Client, consentRequest.RequestedScope)
|
||||
|
||||
profile, err := h.resolveCurrentProfile(c)
|
||||
if err == nil && profile != nil {
|
||||
|
||||
@@ -69,6 +69,59 @@ func TestGetConsentRequest_Normal(t *testing.T) {
|
||||
assert.Equal(t, false, body["skip"])
|
||||
}
|
||||
|
||||
func TestGetConsentRequest_AddsMandatoryTenantScope(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-tenant-scope" {
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||
"challenge": "challenge-tenant-scope",
|
||||
"requested_scope": []string{"openid", "profile"},
|
||||
"skip": false,
|
||||
"subject": "user-123",
|
||||
"client": map[string]interface{}{
|
||||
"client_id": "client-app",
|
||||
"client_name": "Test App",
|
||||
"metadata": map[string]any{
|
||||
"tenant_access_restricted": true,
|
||||
"structured_scopes": []map[string]any{
|
||||
{"name": "openid", "mandatory": true},
|
||||
{"name": "tenant", "mandatory": true, "locked": true},
|
||||
{"name": "profile", "mandatory": false},
|
||||
},
|
||||
},
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
return httpResponse(r, http.StatusNotFound, "not found"), nil
|
||||
})
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
origDefault := http.DefaultClient
|
||||
http.DefaultClient = client
|
||||
defer func() { http.DefaultClient = origDefault }()
|
||||
|
||||
h := &AuthHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
HTTPClient: client,
|
||||
},
|
||||
}
|
||||
app := newConsentTestApp(h)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/consent?consent_challenge=challenge-tenant-scope", nil)
|
||||
resp, err := app.Test(req)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var body map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&body)
|
||||
|
||||
assert.Equal(t, []interface{}{"openid", "tenant", "profile"}, body["requested_scope"])
|
||||
scopeDetails := body["scope_details"].(map[string]interface{})
|
||||
tenantDetail := scopeDetails["tenant"].(map[string]interface{})
|
||||
assert.Equal(t, true, tenantDetail["mandatory"])
|
||||
}
|
||||
|
||||
func TestGetConsentRequest_Skip_AutoAccept(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
// Hydra: Get Consent Request
|
||||
@@ -202,3 +255,81 @@ func TestAcceptConsentRequest_Normal(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 1, len(auditRepo.logs))
|
||||
}
|
||||
|
||||
func TestAcceptConsentRequest_EnforcesMandatoryTenantScope(t *testing.T) {
|
||||
var capturedGrantScopes []string
|
||||
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-tenant-accept" {
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||
"challenge": "challenge-tenant-accept",
|
||||
"requested_scope": []string{"openid", "profile"},
|
||||
"subject": "user-123",
|
||||
"client": map[string]interface{}{
|
||||
"client_id": "client-app",
|
||||
"metadata": map[string]any{
|
||||
"tenant_id": "tenant-abc",
|
||||
"tenant_access_restricted": true,
|
||||
"structured_scopes": []map[string]any{
|
||||
{"name": "openid", "mandatory": true},
|
||||
{"name": "tenant", "mandatory": true, "locked": true},
|
||||
{"name": "profile", "mandatory": false},
|
||||
},
|
||||
},
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
if r.URL.Path == "/admin/identities/user-123" {
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||
"id": "user-123",
|
||||
"traits": map[string]interface{}{
|
||||
"email": "user@test.com",
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
if r.URL.Path == "/oauth2/auth/requests/consent/accept" && r.URL.Query().Get("consent_challenge") == "challenge-tenant-accept" {
|
||||
var payload map[string]any
|
||||
assert.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
|
||||
for _, scope := range payload["grant_scope"].([]interface{}) {
|
||||
capturedGrantScopes = append(capturedGrantScopes, scope.(string))
|
||||
}
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||
"redirect_to": "http://rp/cb",
|
||||
}), nil
|
||||
}
|
||||
return httpResponse(r, http.StatusNotFound, "not found"), nil
|
||||
})
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
origDefault := http.DefaultClient
|
||||
http.DefaultClient = client
|
||||
defer func() { http.DefaultClient = origDefault }()
|
||||
|
||||
h := &AuthHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
HTTPClient: client,
|
||||
},
|
||||
KratosAdmin: new(MockKratosAdminService),
|
||||
}
|
||||
h.KratosAdmin.(*MockKratosAdminService).On("GetIdentity", mock.Anything, "user-123").Return(&service.KratosIdentity{
|
||||
ID: "user-123",
|
||||
Traits: map[string]interface{}{
|
||||
"email": "user@test.com",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
app := newConsentTestApp(h)
|
||||
|
||||
body, _ := json.Marshal(map[string]interface{}{
|
||||
"consent_challenge": "challenge-tenant-accept",
|
||||
"grant_scope": []string{"openid", "profile"},
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, []string{"openid", "tenant", "profile"}, capturedGrantScopes)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -142,3 +143,98 @@ func isClientTenantAccessAllowed(profile *domain.UserProfileResponse, client dom
|
||||
}
|
||||
return clientTenantAccessAllowed(profile, client)
|
||||
}
|
||||
|
||||
type clientStructuredScope struct {
|
||||
Name string `json:"name"`
|
||||
Mandatory bool `json:"mandatory"`
|
||||
Locked bool `json:"locked"`
|
||||
}
|
||||
|
||||
func mergeRequestedScopesWithClientRequirements(client domain.HydraClient, requested []string) []string {
|
||||
combined := make([]string, 0, len(requested)+2)
|
||||
combined = append(combined, requested...)
|
||||
combined = append(combined, requiredClientScopes(client)...)
|
||||
|
||||
return normalizeScopesInConsentOrder(combined)
|
||||
}
|
||||
|
||||
func normalizeScopesInConsentOrder(scopes []string) []string {
|
||||
combined := make([]string, 0, len(scopes))
|
||||
combined = append(combined, scopes...)
|
||||
|
||||
seen := make(map[string]struct{}, len(combined))
|
||||
out := make([]string, 0, len(combined))
|
||||
|
||||
appendIfPresent := func(scope string) {
|
||||
scope = strings.TrimSpace(scope)
|
||||
if scope == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[scope]; ok {
|
||||
return
|
||||
}
|
||||
for _, candidate := range combined {
|
||||
if strings.TrimSpace(candidate) != scope {
|
||||
continue
|
||||
}
|
||||
seen[scope] = struct{}{}
|
||||
out = append(out, scope)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
appendIfPresent("openid")
|
||||
appendIfPresent("tenant")
|
||||
|
||||
for _, scope := range combined {
|
||||
scope = strings.TrimSpace(scope)
|
||||
if scope == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[scope]; ok {
|
||||
continue
|
||||
}
|
||||
seen[scope] = struct{}{}
|
||||
out = append(out, scope)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func requiredClientScopes(client domain.HydraClient) []string {
|
||||
required := make([]string, 0, 4)
|
||||
if clientTenantAccessRestricted(client.Metadata) {
|
||||
required = append(required, "tenant")
|
||||
}
|
||||
|
||||
if client.Metadata == nil {
|
||||
return normalizeScopesInConsentOrder(required)
|
||||
}
|
||||
|
||||
rawStructuredScopes, ok := client.Metadata["structured_scopes"]
|
||||
if !ok || rawStructuredScopes == nil {
|
||||
return normalizeScopesInConsentOrder(required)
|
||||
}
|
||||
|
||||
rawBytes, err := json.Marshal(rawStructuredScopes)
|
||||
if err != nil {
|
||||
return normalizeScopesInConsentOrder(required)
|
||||
}
|
||||
|
||||
var scopes []clientStructuredScope
|
||||
if err := json.Unmarshal(rawBytes, &scopes); err != nil {
|
||||
return normalizeScopesInConsentOrder(required)
|
||||
}
|
||||
|
||||
for _, scope := range scopes {
|
||||
name := strings.TrimSpace(scope.Name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if scope.Mandatory || scope.Locked {
|
||||
required = append(required, name)
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeScopesInConsentOrder(required)
|
||||
}
|
||||
|
||||
@@ -118,6 +118,22 @@ func TestCreateClient_RejectsTenantAccessWithoutAllowedTenants(t *testing.T) {
|
||||
assert.False(t, hydraCalled)
|
||||
}
|
||||
|
||||
func TestMergeRequestedScopesWithClientRequirements_AddsTenantScope(t *testing.T) {
|
||||
client := domain.HydraClient{
|
||||
Metadata: map[string]any{
|
||||
"tenant_access_restricted": true,
|
||||
"structured_scopes": []map[string]any{
|
||||
{"name": "openid", "mandatory": true},
|
||||
{"name": "tenant", "mandatory": true, "locked": true},
|
||||
{"name": "profile", "mandatory": false},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
merged := mergeRequestedScopesWithClientRequirements(client, []string{"openid", "profile"})
|
||||
assert.Equal(t, []string{"openid", "tenant", "profile"}, merged)
|
||||
}
|
||||
|
||||
func TestGetConsentRequest_DeniesTenantAccess(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
switch {
|
||||
|
||||
@@ -155,6 +155,22 @@ func (m *mockConsentRepo) ListBySubject(ctx context.Context, subject string) ([]
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (m *mockConsentRepo) ListSubjectsByClient(ctx context.Context, clientID string) ([]string, error) {
|
||||
seen := map[string]struct{}{}
|
||||
subjects := make([]string, 0, len(m.consents))
|
||||
for _, consent := range m.consents {
|
||||
if consent.ClientID != clientID {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[consent.Subject]; ok {
|
||||
continue
|
||||
}
|
||||
seen[consent.Subject] = struct{}{}
|
||||
subjects = append(subjects, consent.Subject)
|
||||
}
|
||||
return subjects, nil
|
||||
}
|
||||
|
||||
func (m *mockConsentRepo) Find(ctx context.Context, clientID, subject string) (*domain.ClientConsent, error) {
|
||||
for _, consent := range m.consents {
|
||||
if consent.ClientID == clientID && consent.Subject == subject {
|
||||
@@ -167,6 +183,17 @@ func (m *mockConsentRepo) Find(ctx context.Context, clientID, subject string) (*
|
||||
|
||||
func (m *mockConsentRepo) Delete(ctx context.Context, subject, clientID string) error { return nil }
|
||||
|
||||
func (m *mockConsentRepo) DeleteByClient(ctx context.Context, clientID string) error {
|
||||
filtered := m.consents[:0]
|
||||
for _, consent := range m.consents {
|
||||
if consent.ClientID != clientID {
|
||||
filtered = append(filtered, consent)
|
||||
}
|
||||
}
|
||||
m.consents = filtered
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockConsentRepo) List(ctx context.Context, clientID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) {
|
||||
results := make([]domain.ClientConsentWithTenantInfo, 0, len(m.consents))
|
||||
for _, consent := range m.consents {
|
||||
|
||||
@@ -618,6 +618,47 @@ func isProtectedSystemClientID(clientID string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func tenantAccessPolicyChanged(before, after map[string]interface{}) bool {
|
||||
if clientTenantAccessRestricted(before) != clientTenantAccessRestricted(after) {
|
||||
return true
|
||||
}
|
||||
|
||||
beforeAllowed := clientAllowedTenants(before)
|
||||
afterAllowed := clientAllowedTenants(after)
|
||||
if len(beforeAllowed) != len(afterAllowed) {
|
||||
return true
|
||||
}
|
||||
for i := range beforeAllowed {
|
||||
if beforeAllowed[i] != afterAllowed[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *DevHandler) revokeClientConsentsForPolicyChange(ctx context.Context, clientID string) error {
|
||||
if h.ConsentRepo == nil || h.Hydra == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
subjects, err := h.ConsentRepo.ListSubjectsByClient(ctx, clientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, subject := range subjects {
|
||||
subject = strings.TrimSpace(subject)
|
||||
if subject == "" {
|
||||
continue
|
||||
}
|
||||
if err := h.Hydra.RevokeConsentSessions(ctx, subject, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return h.ConsentRepo.DeleteByClient(ctx, clientID)
|
||||
}
|
||||
|
||||
func isProtectedSystemClient(client domain.HydraClient) bool {
|
||||
return isProtectedSystemClientID(client.ClientID)
|
||||
}
|
||||
@@ -1767,6 +1808,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
if err := validateReservedSystemClientName(updated.ClientID, updated.ClientName); err != nil {
|
||||
return errorJSON(c, fiber.StatusForbidden, err.Error())
|
||||
}
|
||||
tenantPolicyChanged := tenantAccessPolicyChanged(current.Metadata, updated.Metadata)
|
||||
|
||||
h.setAuditDetailsExtra(c, map[string]any{
|
||||
"action": "UPDATE_CLIENT",
|
||||
@@ -1788,6 +1830,11 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if tenantPolicyChanged {
|
||||
if err := h.revokeClientConsentsForPolicyChange(c.Context(), clientID); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "failed to revoke existing consents after tenant policy update: "+err.Error())
|
||||
}
|
||||
}
|
||||
h.syncHeadlessJWKSCache(c.Context(), *updatedClient, "client_update")
|
||||
|
||||
if updatedClient.ClientSecret != "" {
|
||||
|
||||
@@ -1662,6 +1662,167 @@ func TestUpdateClient_HeadlessLoginIgnoresExistingTopLevelJWKS(t *testing.T) {
|
||||
assert.False(t, hasRequestObjectAlg)
|
||||
}
|
||||
|
||||
func TestUpdateClient_RevokesExistingConsentsWhenTenantPolicyChanges(t *testing.T) {
|
||||
var revokedSubjects []string
|
||||
|
||||
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, domain.HydraClient{
|
||||
ClientID: "client-1",
|
||||
ClientName: "Tenant Guarded App",
|
||||
RedirectURIs: []string{"https://rp.example.com/callback"},
|
||||
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||
ResponseTypes: []string{"code"},
|
||||
Scope: "openid tenant profile email",
|
||||
TokenEndpointAuthMethod: "none",
|
||||
Metadata: map[string]interface{}{
|
||||
"tenant_access_restricted": true,
|
||||
"allowed_tenants": []string{"tenant-a"},
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
|
||||
var updated domain.HydraClient
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, json.Unmarshal(body, &updated))
|
||||
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||
"client_id": updated.ClientID,
|
||||
"client_name": updated.ClientName,
|
||||
"redirect_uris": updated.RedirectURIs,
|
||||
"grant_types": updated.GrantTypes,
|
||||
"response_types": updated.ResponseTypes,
|
||||
"scope": updated.Scope,
|
||||
"token_endpoint_auth_method": updated.TokenEndpointAuthMethod,
|
||||
"metadata": updated.Metadata,
|
||||
}), nil
|
||||
}
|
||||
if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
|
||||
revokedSubjects = append(revokedSubjects, r.URL.Query().Get("subject"))
|
||||
assert.Equal(t, "client-1", r.URL.Query().Get("client"))
|
||||
return httpResponse(r, http.StatusNoContent, ""), nil
|
||||
}
|
||||
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||
})
|
||||
|
||||
consentRepo := &mockConsentRepo{
|
||||
consents: []domain.ClientConsent{
|
||||
{ClientID: "client-1", Subject: "user-1"},
|
||||
{ClientID: "client-1", Subject: "user-2"},
|
||||
{ClientID: "other-client", Subject: "user-3"},
|
||||
},
|
||||
}
|
||||
|
||||
h := &DevHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
PublicURL: "http://hydra.public",
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
},
|
||||
ConsentRepo: consentRepo,
|
||||
Keto: new(devMockKetoService),
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||
return c.Next()
|
||||
})
|
||||
app.Put("/api/v1/dev/clients/:id", h.UpdateClient)
|
||||
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"metadata": map[string]any{
|
||||
"tenant_access_restricted": true,
|
||||
"allowed_tenants": []string{"tenant-b"},
|
||||
},
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, _ := app.Test(req, -1)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.ElementsMatch(t, []string{"user-1", "user-2"}, revokedSubjects)
|
||||
assert.Len(t, consentRepo.consents, 1)
|
||||
assert.Equal(t, "other-client", consentRepo.consents[0].ClientID)
|
||||
}
|
||||
|
||||
func TestUpdateClient_DoesNotRevokeConsentsWhenTenantPolicyUnchanged(t *testing.T) {
|
||||
revoked := false
|
||||
|
||||
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, domain.HydraClient{
|
||||
ClientID: "client-1",
|
||||
ClientName: "Tenant Guarded App",
|
||||
RedirectURIs: []string{"https://rp.example.com/callback"},
|
||||
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||
ResponseTypes: []string{"code"},
|
||||
Scope: "openid tenant profile email",
|
||||
TokenEndpointAuthMethod: "none",
|
||||
Metadata: map[string]interface{}{
|
||||
"tenant_access_restricted": true,
|
||||
"allowed_tenants": []string{"tenant-a"},
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
if r.Method == http.MethodPut && r.URL.Path == "/clients/client-1" {
|
||||
var updated domain.HydraClient
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, json.Unmarshal(body, &updated))
|
||||
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||
"client_id": updated.ClientID,
|
||||
"client_name": updated.ClientName,
|
||||
"redirect_uris": updated.RedirectURIs,
|
||||
"grant_types": updated.GrantTypes,
|
||||
"response_types": updated.ResponseTypes,
|
||||
"scope": updated.Scope,
|
||||
"token_endpoint_auth_method": updated.TokenEndpointAuthMethod,
|
||||
"metadata": updated.Metadata,
|
||||
}), nil
|
||||
}
|
||||
if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
|
||||
revoked = true
|
||||
return httpResponse(r, http.StatusNoContent, ""), nil
|
||||
}
|
||||
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||
})
|
||||
|
||||
consentRepo := &mockConsentRepo{
|
||||
consents: []domain.ClientConsent{
|
||||
{ClientID: "client-1", Subject: "user-1"},
|
||||
},
|
||||
}
|
||||
|
||||
h := &DevHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
PublicURL: "http://hydra.public",
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
},
|
||||
ConsentRepo: consentRepo,
|
||||
Keto: new(devMockKetoService),
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals("user_profile", &domain.UserProfileResponse{ID: "test-user", Role: domain.RoleSuperAdmin})
|
||||
return c.Next()
|
||||
})
|
||||
app.Put("/api/v1/dev/clients/:id", h.UpdateClient)
|
||||
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"name": "Renamed App",
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, _ := app.Test(req, -1)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.False(t, revoked)
|
||||
assert.Len(t, consentRepo.consents, 1)
|
||||
}
|
||||
|
||||
func TestRefreshHeadlessJWKSCache_ReturnsUpdatedCacheState(t *testing.T) {
|
||||
privateKey, jwks := mustHeadlessRSAJWK(t)
|
||||
_ = privateKey
|
||||
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
type ClientConsentRepository interface {
|
||||
Upsert(ctx context.Context, consent *domain.ClientConsent) error
|
||||
Delete(ctx context.Context, subject, clientID string) error
|
||||
DeleteByClient(ctx context.Context, clientID string) error
|
||||
List(ctx context.Context, clientID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error)
|
||||
ListByTenant(ctx context.Context, clientID, tenantID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error)
|
||||
ListBySubject(ctx context.Context, subject string) ([]domain.ClientConsent, error)
|
||||
ListSubjectsByClient(ctx context.Context, clientID string) ([]string, error)
|
||||
Find(ctx context.Context, clientID, subject string) (*domain.ClientConsent, error)
|
||||
}
|
||||
|
||||
@@ -56,6 +58,12 @@ func (r *clientConsentRepo) Delete(ctx context.Context, subject, clientID string
|
||||
Delete(&domain.ClientConsent{}).Error
|
||||
}
|
||||
|
||||
func (r *clientConsentRepo) DeleteByClient(ctx context.Context, clientID string) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("client_id = ?", clientID).
|
||||
Delete(&domain.ClientConsent{}).Error
|
||||
}
|
||||
|
||||
func (r *clientConsentRepo) List(ctx context.Context, clientID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) {
|
||||
var consents []domain.ClientConsentWithTenantInfo
|
||||
var total int64
|
||||
@@ -117,3 +125,14 @@ func (r *clientConsentRepo) ListBySubject(ctx context.Context, subject string) (
|
||||
Find(&consents).Error
|
||||
return consents, err
|
||||
}
|
||||
|
||||
func (r *clientConsentRepo) ListSubjectsByClient(ctx context.Context, clientID string) ([]string, error) {
|
||||
var subjects []string
|
||||
err := r.db.WithContext(ctx).Unscoped().
|
||||
Model(&domain.ClientConsent{}).
|
||||
Distinct("subject").
|
||||
Where("client_id = ?", clientID).
|
||||
Order("subject ASC").
|
||||
Pluck("subject", &subjects).Error
|
||||
return subjects, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user