1
0
forked from baron/baron-sso

RP 테넌트 접근 정책 변경 시 기존 consent 자동 폐기

This commit is contained in:
2026-04-24 17:59:54 +09:00
parent b9232687b5
commit 3f85f6cfe3
8 changed files with 500 additions and 0 deletions

View File

@@ -5120,6 +5120,7 @@ func (h *AuthHandler) GetConsentRequest(c *fiber.Ctx) error {
slog.Error("failed to get hydra consent request", "error", err) slog.Error("failed to get hydra consent request", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information")
} }
consentRequest.RequestedScope = mergeRequestedScopesWithClientRequirements(consentRequest.Client, consentRequest.RequestedScope)
// [DEBUG] Hydra 응답 상세 로깅 // [DEBUG] Hydra 응답 상세 로깅
slog.Info("GetConsentRequest Debug", 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) slog.Error("failed to get hydra consent request before accepting", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get consent information")
} }
consentRequest.RequestedScope = mergeRequestedScopesWithClientRequirements(consentRequest.Client, consentRequest.RequestedScope)
// 2. 스코프 필터링 (사용자가 선택한 것만 허용) // 2. 스코프 필터링 (사용자가 선택한 것만 허용)
if len(req.GrantScope) > 0 { if len(req.GrantScope) > 0 {
@@ -5339,6 +5341,7 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
} }
consentRequest.RequestedScope = filteredScopes consentRequest.RequestedScope = filteredScopes
} }
consentRequest.RequestedScope = mergeRequestedScopesWithClientRequirements(consentRequest.Client, consentRequest.RequestedScope)
profile, err := h.resolveCurrentProfile(c) profile, err := h.resolveCurrentProfile(c)
if err == nil && profile != nil { if err == nil && profile != nil {

View File

@@ -69,6 +69,59 @@ func TestGetConsentRequest_Normal(t *testing.T) {
assert.Equal(t, false, body["skip"]) 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) { func TestGetConsentRequest_Skip_AutoAccept(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) { transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
// Hydra: Get Consent Request // Hydra: Get Consent Request
@@ -202,3 +255,81 @@ func TestAcceptConsentRequest_Normal(t *testing.T) {
assert.Equal(t, 1, len(auditRepo.logs)) 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)
}

View File

@@ -2,6 +2,7 @@ package handler
import ( import (
"baron-sso-backend/internal/domain" "baron-sso-backend/internal/domain"
"encoding/json"
"errors" "errors"
"sort" "sort"
"strings" "strings"
@@ -142,3 +143,98 @@ func isClientTenantAccessAllowed(profile *domain.UserProfileResponse, client dom
} }
return clientTenantAccessAllowed(profile, client) 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)
}

View File

@@ -118,6 +118,22 @@ func TestCreateClient_RejectsTenantAccessWithoutAllowedTenants(t *testing.T) {
assert.False(t, hydraCalled) 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) { func TestGetConsentRequest_DeniesTenantAccess(t *testing.T) {
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) { transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
switch { switch {

View File

@@ -155,6 +155,22 @@ func (m *mockConsentRepo) ListBySubject(ctx context.Context, subject string) ([]
return results, nil 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) { func (m *mockConsentRepo) Find(ctx context.Context, clientID, subject string) (*domain.ClientConsent, error) {
for _, consent := range m.consents { for _, consent := range m.consents {
if consent.ClientID == clientID && consent.Subject == subject { 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) 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) { func (m *mockConsentRepo) List(ctx context.Context, clientID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) {
results := make([]domain.ClientConsentWithTenantInfo, 0, len(m.consents)) results := make([]domain.ClientConsentWithTenantInfo, 0, len(m.consents))
for _, consent := range m.consents { for _, consent := range m.consents {

View File

@@ -618,6 +618,47 @@ func isProtectedSystemClientID(clientID string) bool {
return ok 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 { func isProtectedSystemClient(client domain.HydraClient) bool {
return isProtectedSystemClientID(client.ClientID) 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 { if err := validateReservedSystemClientName(updated.ClientID, updated.ClientName); err != nil {
return errorJSON(c, fiber.StatusForbidden, err.Error()) return errorJSON(c, fiber.StatusForbidden, err.Error())
} }
tenantPolicyChanged := tenantAccessPolicyChanged(current.Metadata, updated.Metadata)
h.setAuditDetailsExtra(c, map[string]any{ h.setAuditDetailsExtra(c, map[string]any{
"action": "UPDATE_CLIENT", "action": "UPDATE_CLIENT",
@@ -1788,6 +1830,11 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
if err != nil { if err != nil {
return errorJSON(c, fiber.StatusInternalServerError, err.Error()) 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") h.syncHeadlessJWKSCache(c.Context(), *updatedClient, "client_update")
if updatedClient.ClientSecret != "" { if updatedClient.ClientSecret != "" {

View File

@@ -1662,6 +1662,167 @@ func TestUpdateClient_HeadlessLoginIgnoresExistingTopLevelJWKS(t *testing.T) {
assert.False(t, hasRequestObjectAlg) 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) { func TestRefreshHeadlessJWKSCache_ReturnsUpdatedCacheState(t *testing.T) {
privateKey, jwks := mustHeadlessRSAJWK(t) privateKey, jwks := mustHeadlessRSAJWK(t)
_ = privateKey _ = privateKey

View File

@@ -11,9 +11,11 @@ import (
type ClientConsentRepository interface { type ClientConsentRepository interface {
Upsert(ctx context.Context, consent *domain.ClientConsent) error Upsert(ctx context.Context, consent *domain.ClientConsent) error
Delete(ctx context.Context, subject, clientID string) 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) 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) ListByTenant(ctx context.Context, clientID, tenantID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error)
ListBySubject(ctx context.Context, subject string) ([]domain.ClientConsent, 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) 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 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) { func (r *clientConsentRepo) List(ctx context.Context, clientID string, limit, offset int) ([]domain.ClientConsentWithTenantInfo, int64, error) {
var consents []domain.ClientConsentWithTenantInfo var consents []domain.ClientConsentWithTenantInfo
var total int64 var total int64
@@ -117,3 +125,14 @@ func (r *clientConsentRepo) ListBySubject(ctx context.Context, subject string) (
Find(&consents).Error Find(&consents).Error
return consents, err 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
}