Skip to content

Commit 7d07a8a

Browse files
Remove recursive purpose and element metadata enrichment logic from portal backend
Instead, now it rely on details=true flag of consent endpoints
1 parent d16fb3e commit 7d07a8a

8 files changed

Lines changed: 64 additions & 261 deletions

File tree

portal/backend/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ When credentials are enabled, origins must be explicitly allowlisted (wildcard o
6161

6262
Portal-facing user endpoints:
6363

64-
- `GET /me/consents` -> upstream `GET /api/v1/consents` with forced `userIds=<placeholder>`
65-
- `GET /me/consents/{consentId}` -> upstream `GET /api/v1/consents/{consentId}`
66-
- `POST /me/consents/{consentId}/approve` -> BFF fetches the current consent, resolves its exact bound purpose and element versions, validates selected optional elements by stable IDs and versions, automatically approves mandatory elements, preserves existing approvals, and sends a full upstream consent update with the consent's immutable `groupId` as the trusted `group-id` header
64+
- `GET /me/consents` -> upstream `GET /api/v1/consents` with forced `userIds=<placeholder>` and `details=true`
65+
- `GET /me/consents/{consentId}` -> one upstream `GET /api/v1/consents/{consentId}?details=true`; the detailed consent snapshot is returned unchanged
66+
- `POST /me/consents/{consentId}/approve` -> BFF fetches the current consent with `details=true`, validates selected optional elements against its embedded stable IDs and versions, automatically approves mandatory elements, preserves existing approvals, and sends a full upstream consent update with the consent's immutable `groupId` as the trusted `group-id` header
6767
- `POST /me/consents/{consentId}/revoke` -> upstream `POST /api/v1/consents/{consentId}/revoke`
6868

6969
Proxy hardening:

portal/backend/internal/me/handler.go

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func (h *Handler) Consents(w http.ResponseWriter, r *http.Request) {
6868
}
6969
if err := h.svc.Forward(w, r, http.MethodGet, "/api/v1/consents", func(q url.Values) {
7070
q.Set("userIds", userID)
71+
q.Set("details", "true")
7172
}, nil); err != nil {
7273
writeProxyError(w, err)
7374
}
@@ -93,25 +94,12 @@ func (h *Handler) ConsentByID(w http.ResponseWriter, r *http.Request) {
9394
return
9495
}
9596

96-
baseResp, err := h.svc.ForwardRaw(r, http.MethodGet, "/api/v1/consents/"+url.PathEscape(consentID), nil, nil)
97-
if err != nil {
98-
writeProxyError(w, err)
99-
return
100-
}
101-
if baseResp.StatusCode != http.StatusOK {
102-
h.svc.WriteUpstreamResponse(w, baseResp)
103-
return
104-
}
105-
106-
aggregatedBody, err := h.svc.BuildAggregatedConsentResponse(r, baseResp.Body)
107-
if err != nil {
97+
if err := h.svc.Forward(w, r, http.MethodGet, "/api/v1/consents/"+url.PathEscape(consentID), func(q url.Values) {
98+
q.Set("details", "true")
99+
q.Del("includeStatusHistory")
100+
}, nil); err != nil {
108101
writeProxyError(w, err)
109-
return
110102
}
111-
112-
w.Header().Set("Content-Type", "application/json")
113-
w.WriteHeader(http.StatusOK)
114-
_, _ = w.Write(aggregatedBody)
115103
}
116104

117105
// ConsentApprove handles POST /me/consents/{consentId}/approve.
@@ -144,7 +132,10 @@ func (h *Handler) ConsentApprove(w http.ResponseWriter, r *http.Request) {
144132
writeJSONError(w, http.StatusBadRequest, "INVALID_PAYLOAD", "invalid request payload")
145133
return
146134
}
147-
baseResp, err := h.svc.ForwardRaw(r, http.MethodGet, "/api/v1/consents/"+url.PathEscape(consentID), nil, nil)
135+
baseResp, err := h.svc.ForwardRaw(r, http.MethodGet, "/api/v1/consents/"+url.PathEscape(consentID), func(q url.Values) {
136+
q.Set("details", "true")
137+
q.Del("includeStatusHistory")
138+
}, nil)
148139
if err != nil {
149140
writeProxyError(w, err)
150141
return
@@ -154,7 +145,7 @@ func (h *Handler) ConsentApprove(w http.ResponseWriter, r *http.Request) {
154145
return
155146
}
156147

157-
payload, trustedGroupID, err := h.svc.BuildApprovalUpdatePayload(r, baseResp.Body, selections, userID)
148+
payload, trustedGroupID, err := h.svc.BuildApprovalUpdatePayload(baseResp.Body, selections, userID)
158149
if err != nil {
159150
if errors.Is(err, proxy.ErrUpstreamTimeout) || errors.Is(err, proxy.ErrUpstreamUnavailable) || errors.Is(err, proxy.ErrUpstreamResponseTooLarge) {
160151
writeProxyError(w, err)

portal/backend/internal/me/service.go

Lines changed: 10 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -56,23 +56,19 @@ type consentPurposeItem struct {
5656
Version string `json:"version"`
5757
DisplayName *string `json:"displayName,omitempty"`
5858
Description *string `json:"description,omitempty"`
59-
Properties map[string]string `json:"properties,omitempty"`
6059
Elements []consentElementItem `json:"elements"`
6160
}
6261

6362
type consentElementItem struct {
64-
ElementID string `json:"elementId"`
65-
Name string `json:"name"`
66-
Namespace string `json:"namespace"`
67-
Version string `json:"version"`
68-
DisplayName *string `json:"displayName,omitempty"`
69-
Approved bool `json:"approved"`
70-
Mandatory bool `json:"mandatory"`
71-
Value any `json:"value,omitempty"`
72-
Type string `json:"type,omitempty"`
73-
Description *string `json:"description,omitempty"`
74-
Schema *string `json:"schema,omitempty"`
75-
Properties map[string]string `json:"properties,omitempty"`
63+
ElementID string `json:"elementId"`
64+
Name string `json:"name"`
65+
Namespace string `json:"namespace"`
66+
Version string `json:"version"`
67+
DisplayName *string `json:"displayName,omitempty"`
68+
Description *string `json:"description,omitempty"`
69+
Approved bool `json:"approved"`
70+
Mandatory bool `json:"mandatory"`
71+
Value any `json:"value,omitempty"`
7672
}
7773

7874
type consentAuthorizationEntry struct {
@@ -123,37 +119,6 @@ type consentUpdatePayload struct {
123119
Authorizations []consentAuthorizationPayload `json:"authorizations"`
124120
}
125121

126-
type purposeVersionResponse struct {
127-
PurposeID string `json:"purposeId"`
128-
Name string `json:"name"`
129-
GroupID string `json:"groupId"`
130-
Version string `json:"version"`
131-
DisplayName *string `json:"displayName,omitempty"`
132-
Description *string `json:"description,omitempty"`
133-
Properties map[string]string `json:"properties,omitempty"`
134-
Elements []purposeVersionElement `json:"elements"`
135-
}
136-
137-
type purposeVersionElement struct {
138-
ElementID string `json:"elementId"`
139-
Name string `json:"name"`
140-
Namespace string `json:"namespace"`
141-
Version string `json:"version"`
142-
Mandatory bool `json:"mandatory"`
143-
}
144-
145-
type elementVersionResponse struct {
146-
ElementID string `json:"elementId"`
147-
Name string `json:"name"`
148-
Namespace string `json:"namespace"`
149-
Version string `json:"version"`
150-
Type string `json:"type"`
151-
DisplayName *string `json:"displayName,omitempty"`
152-
Description *string `json:"description,omitempty"`
153-
Schema *string `json:"schema,omitempty"`
154-
Properties map[string]string `json:"properties,omitempty"`
155-
}
156-
157122
// NewService builds a me service from app config.
158123
func NewService(cfg config.ProxyConfig) (*Service, error) {
159124
svc, err := proxy.NewService(cfg)
@@ -187,115 +152,6 @@ func toConsentApprovalKey(purposeID, purposeVersion, elementID, elementVersion s
187152
return strings.Join([]string{purposeID, purposeVersion, elementID, elementVersion}, "\x00")
188153
}
189154

190-
// BuildAggregatedConsentResponse enriches a consent with metadata from its exact bound versions.
191-
func (s *Service) BuildAggregatedConsentResponse(r *http.Request, baseBody []byte) ([]byte, error) {
192-
var consent consentRetrievalResponse
193-
if err := json.Unmarshal(baseBody, &consent); err != nil {
194-
return nil, proxy.ErrUpstreamUnavailable
195-
}
196-
if err := s.enrichConsentMetadata(r, &consent); err != nil {
197-
return nil, err
198-
}
199-
200-
aggregated, err := json.Marshal(consent)
201-
if err != nil {
202-
return nil, proxy.ErrUpstreamUnavailable
203-
}
204-
return aggregated, nil
205-
}
206-
207-
func (s *Service) enrichConsentMetadata(r *http.Request, consent *consentRetrievalResponse) error {
208-
elementMetadata := make(map[string]elementVersionResponse)
209-
for purposeIndex := range consent.Purposes {
210-
purpose := &consent.Purposes[purposeIndex]
211-
if purpose.PurposeID == "" || purpose.Version == "" {
212-
return proxy.ErrUpstreamUnavailable
213-
}
214-
215-
metadata, err := s.fetchPurposeVersion(r, purpose.PurposeID, purpose.Version)
216-
if err != nil {
217-
return err
218-
}
219-
if metadata.PurposeID != purpose.PurposeID || metadata.Version != purpose.Version {
220-
return proxy.ErrUpstreamUnavailable
221-
}
222-
purpose.Name = metadata.Name
223-
purpose.DisplayName = metadata.DisplayName
224-
purpose.Description = metadata.Description
225-
purpose.Properties = metadata.Properties
226-
227-
mappedElements := make(map[string]purposeVersionElement, len(metadata.Elements))
228-
for _, element := range metadata.Elements {
229-
mappedElements[toConsentApprovalKey(purpose.PurposeID, purpose.Version, element.ElementID, element.Version)] = element
230-
}
231-
232-
for elementIndex := range purpose.Elements {
233-
element := &purpose.Elements[elementIndex]
234-
key := toConsentApprovalKey(purpose.PurposeID, purpose.Version, element.ElementID, element.Version)
235-
mapped, ok := mappedElements[key]
236-
if !ok {
237-
return proxy.ErrUpstreamUnavailable
238-
}
239-
element.Name = mapped.Name
240-
element.Namespace = mapped.Namespace
241-
element.Mandatory = mapped.Mandatory
242-
243-
elementKey := element.ElementID + "\x00" + element.Version
244-
details, ok := elementMetadata[elementKey]
245-
if !ok {
246-
details, err = s.fetchElementVersion(r, element.ElementID, element.Version)
247-
if err != nil {
248-
return err
249-
}
250-
if details.ElementID != element.ElementID || details.Version != element.Version {
251-
return proxy.ErrUpstreamUnavailable
252-
}
253-
elementMetadata[elementKey] = details
254-
}
255-
element.Name = details.Name
256-
element.Namespace = details.Namespace
257-
element.DisplayName = details.DisplayName
258-
element.Type = details.Type
259-
element.Description = details.Description
260-
element.Schema = details.Schema
261-
element.Properties = details.Properties
262-
}
263-
}
264-
return nil
265-
}
266-
267-
func (s *Service) fetchPurposeVersion(r *http.Request, purposeID, version string) (purposeVersionResponse, error) {
268-
path := "/api/v1/consent-purposes/" + url.PathEscape(purposeID) + "/versions/" + url.PathEscape(version)
269-
resp, err := s.proxy.ForwardRaw(r, http.MethodGet, path, nil, nil)
270-
if err != nil {
271-
return purposeVersionResponse{}, err
272-
}
273-
if resp.StatusCode != http.StatusOK {
274-
return purposeVersionResponse{}, proxy.ErrUpstreamUnavailable
275-
}
276-
var payload purposeVersionResponse
277-
if err := json.Unmarshal(resp.Body, &payload); err != nil {
278-
return purposeVersionResponse{}, proxy.ErrUpstreamUnavailable
279-
}
280-
return payload, nil
281-
}
282-
283-
func (s *Service) fetchElementVersion(r *http.Request, elementID, version string) (elementVersionResponse, error) {
284-
path := "/api/v1/consent-elements/" + url.PathEscape(elementID) + "/versions/" + url.PathEscape(version)
285-
resp, err := s.proxy.ForwardRaw(r, http.MethodGet, path, nil, nil)
286-
if err != nil {
287-
return elementVersionResponse{}, err
288-
}
289-
if resp.StatusCode != http.StatusOK {
290-
return elementVersionResponse{}, proxy.ErrUpstreamUnavailable
291-
}
292-
var payload elementVersionResponse
293-
if err := json.Unmarshal(resp.Body, &payload); err != nil {
294-
return elementVersionResponse{}, proxy.ErrUpstreamUnavailable
295-
}
296-
return payload, nil
297-
}
298-
299155
func buildRevokePayload(in []byte, userID string) ([]byte, error) {
300156
userID = strings.TrimSpace(userID)
301157
if userID == "" {
@@ -335,14 +191,11 @@ func parseApprovalSelections(in []byte) ([]consentApprovalSelection, error) {
335191
}
336192

337193
// BuildApprovalUpdatePayload builds the consent update payload for an approval action.
338-
func (s *Service) BuildApprovalUpdatePayload(r *http.Request, baseBody []byte, selections []consentApprovalSelection, userID string) ([]byte, string, error) {
194+
func (s *Service) BuildApprovalUpdatePayload(baseBody []byte, selections []consentApprovalSelection, userID string) ([]byte, string, error) {
339195
var consent consentRetrievalResponse
340196
if err := json.Unmarshal(baseBody, &consent); err != nil {
341197
return nil, "", proxy.ErrUpstreamUnavailable
342198
}
343-
if err := s.enrichConsentMetadata(r, &consent); err != nil {
344-
return nil, "", err
345-
}
346199

347200
selectedOptionalElements := make(map[string]struct{}, len(selections))
348201
for _, selection := range selections {

portal/backend/openapi/bff.yaml

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ paths:
4040
description: >-
4141
Proxies to `GET /api/v1/consents`, preserving supported search filters
4242
while replacing any browser-supplied `userIds` with the trusted current
43-
user. Response payload shape is inherited from consent-management API.
43+
user and forcing `details=true` so purpose and element display details
44+
are included.
4445
parameters:
4546
- in: query
4647
name: consentStatuses
@@ -77,18 +78,16 @@ paths:
7778
/me/consents/{consentId}:
7879
get:
7980
tags: [User]
80-
summary: Get enriched current-user consent details
81+
summary: Get current-user consent details
8182
description: >-
82-
Fetches the consent from `GET /api/v1/consents/{consentId}` and enriches
83-
its purposes and elements using their exact bound version endpoints.
84-
Purpose descriptions/properties and element type, description, schema,
85-
and properties are added without falling back to newer definitions.
86-
Status audit history is not exposed by this `/me` route.
83+
Fetches one consent with `details=true` and returns its embedded purpose
84+
and element display names and descriptions unchanged. Status audit
85+
history is not exposed by this `/me` route.
8786
parameters:
8887
- $ref: '#/components/parameters/ConsentID'
8988
responses:
9089
'200':
91-
description: Enriched consent
90+
description: Detailed consent snapshot
9291
content:
9392
application/json:
9493
schema: { $ref: '#/components/schemas/ConsentDetail' }
@@ -101,9 +100,9 @@ paths:
101100
tags: [User]
102101
summary: Approve selected optional elements
103102
description: >-
104-
Fetches the current consent and exact bound metadata, validates selected
105-
optional elements by purpose/element ID and version, and builds a full
106-
`PUT /api/v1/consents/{consentId}` update. Mandatory and selected
103+
Fetches the current consent with `details=true`, validates selected
104+
optional elements against its embedded IDs and versions, and builds a
105+
full `PUT /api/v1/consents/{consentId}` update. Mandatory and selected
107106
elements are approved, existing approvals and consent fields are
108107
preserved, and the trusted user's authorization is set to `APPROVED`.
109108
The consent's immutable `groupId` is sent as the trusted upstream
@@ -123,7 +122,7 @@ paths:
123122
items: { $ref: '#/components/schemas/ConsentApprovalSelection' }
124123
responses:
125124
'200':
126-
description: Consent server update response, returned without additional enrichment.
125+
description: Consent server update response, returned unchanged.
127126
content:
128127
application/json:
129128
schema: { $ref: '#/components/schemas/ConsentDetail' }
@@ -410,9 +409,6 @@ components:
410409
version: { type: string }
411410
displayName: { type: string }
412411
description: { type: string }
413-
properties:
414-
type: object
415-
additionalProperties: { type: string }
416412
elements:
417413
type: array
418414
items: { $ref: '#/components/schemas/ConsentElement' }
@@ -427,12 +423,7 @@ components:
427423
displayName: { type: string }
428424
approved: { type: boolean }
429425
mandatory: { type: boolean }
430-
type: { type: string }
431426
description: { type: string }
432-
schema: { type: string }
433-
properties:
434-
type: object
435-
additionalProperties: { type: string }
436427
value: {}
437428
ConsentApprovalSelection:
438429
type: object

0 commit comments

Comments
 (0)