001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.server.interceptor.consent;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.api.Hook;
027import ca.uhn.fhir.interceptor.api.Interceptor;
028import ca.uhn.fhir.interceptor.api.Pointcut;
029import ca.uhn.fhir.rest.api.Constants;
030import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
031import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
032import ca.uhn.fhir.rest.api.server.RequestDetails;
033import ca.uhn.fhir.rest.api.server.ResponseDetails;
034import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
035import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters;
036import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
037import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
039import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationConstants;
040import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
041import ca.uhn.fhir.util.BundleUtil;
042import ca.uhn.fhir.util.IModelVisitor2;
043import jakarta.annotation.Nonnull;
044import jakarta.annotation.Nullable;
045import org.apache.commons.lang3.Validate;
046import org.hl7.fhir.instance.model.api.IBase;
047import org.hl7.fhir.instance.model.api.IBaseBundle;
048import org.hl7.fhir.instance.model.api.IBaseExtension;
049import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
050import org.hl7.fhir.instance.model.api.IBaseResource;
051
052import java.util.ArrayList;
053import java.util.Arrays;
054import java.util.Collections;
055import java.util.IdentityHashMap;
056import java.util.List;
057import java.util.Map;
058import java.util.concurrent.atomic.AtomicInteger;
059import java.util.stream.Collectors;
060
061import static ca.uhn.fhir.rest.api.Constants.URL_TOKEN_METADATA;
062import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_META;
063
064/**
065 * The ConsentInterceptor can be used to apply arbitrary consent rules and data access policies
066 * on responses from a FHIR server.
067 * <p>
068 * See <a href="https://hapifhir.io/hapi-fhir/docs/security/consent_interceptor.html">Consent Interceptor</a> for
069 * more information on this interceptor.
070 * </p>
071 */
072@Interceptor(order = AuthorizationConstants.ORDER_CONSENT_INTERCEPTOR)
073public class ConsentInterceptor {
074        private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
075        private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
076        private final String myRequestAuthorizedKey =
077                        ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_AUTHORIZED";
078        private final String myRequestCompletedKey =
079                        ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_COMPLETED";
080        private final String myRequestSeenResourcesKey =
081                        ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES";
082
083        private volatile List<IConsentService> myConsentService = Collections.emptyList();
084        private IConsentContextServices myContextConsentServices = IConsentContextServices.NULL_IMPL;
085
086        /**
087         * Constructor
088         */
089        public ConsentInterceptor() {
090                super();
091        }
092
093        /**
094         * Constructor
095         *
096         * @param theConsentService Must not be <code>null</code>
097         */
098        public ConsentInterceptor(IConsentService theConsentService) {
099                this(theConsentService, IConsentContextServices.NULL_IMPL);
100        }
101
102        /**
103         * Constructor
104         *
105         * @param theConsentService         Must not be <code>null</code>
106         * @param theContextConsentServices Must not be <code>null</code>
107         */
108        public ConsentInterceptor(IConsentService theConsentService, IConsentContextServices theContextConsentServices) {
109                setConsentService(theConsentService);
110                setContextConsentServices(theContextConsentServices);
111        }
112
113        public void setContextConsentServices(IConsentContextServices theContextConsentServices) {
114                Validate.notNull(theContextConsentServices, "theContextConsentServices must not be null");
115                myContextConsentServices = theContextConsentServices;
116        }
117
118        /**
119         * @deprecated Use {@link #registerConsentService(IConsentService)} instead
120         */
121        @Deprecated
122        public void setConsentService(IConsentService theConsentService) {
123                Validate.notNull(theConsentService, "theConsentService must not be null");
124                myConsentService = Collections.singletonList(theConsentService);
125        }
126
127        /**
128         * Adds a consent service to the chain.
129         * <p>
130         * Thread safety note: This method can be called while the service is actively processing requestes
131         *
132         * @param theConsentService The service to register. Must not be <code>null</code>.
133         * @since 6.0.0
134         */
135        public ConsentInterceptor registerConsentService(IConsentService theConsentService) {
136                Validate.notNull(theConsentService, "theConsentService must not be null");
137                List<IConsentService> newList = new ArrayList<>(myConsentService.size() + 1);
138                newList.addAll(myConsentService);
139                newList.add(theConsentService);
140                myConsentService = newList;
141                return this;
142        }
143
144        /**
145         * Removes a consent service from the chain.
146         * <p>
147         * Thread safety note: This method can be called while the service is actively processing requestes
148         *
149         * @param theConsentService The service to unregister. Must not be <code>null</code>.
150         * @since 6.0.0
151         */
152        public ConsentInterceptor unregisterConsentService(IConsentService theConsentService) {
153                Validate.notNull(theConsentService, "theConsentService must not be null");
154                List<IConsentService> newList =
155                                myConsentService.stream().filter(t -> t != theConsentService).collect(Collectors.toList());
156                myConsentService = newList;
157                return this;
158        }
159
160        @Hook(value = Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
161        public void interceptPreHandled(RequestDetails theRequestDetails) {
162                if (isSkipServiceForRequest(theRequestDetails)) {
163                        return;
164                }
165
166                validateParameter(theRequestDetails.getParameters());
167
168                for (IConsentService nextService : myConsentService) {
169                        ConsentOutcome outcome = nextService.startOperation(theRequestDetails, myContextConsentServices);
170                        Validate.notNull(outcome, "Consent service returned null outcome");
171
172                        switch (outcome.getStatus()) {
173                                case REJECT:
174                                        throw toForbiddenOperationException(outcome);
175                                case PROCEED:
176                                        continue;
177                                case AUTHORIZED:
178                                        Map<Object, Object> userData = theRequestDetails.getUserData();
179                                        userData.put(myRequestAuthorizedKey, Boolean.TRUE);
180                                        return;
181                        }
182                }
183        }
184
185        /**
186         * Check if this request is eligible for cached search results.
187         * We can't use a cached result if consent may use canSeeResource.
188         * This checks for AUTHORIZED requests, and the responses from shouldProcessCanSeeResource()
189         * to see if this holds.
190         * @return may the request be satisfied from cache.
191         */
192        @Hook(value = Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH)
193        public boolean interceptPreCheckForCachedSearch(@Nonnull RequestDetails theRequestDetails) {
194                return !isProcessCanSeeResource(theRequestDetails, null);
195        }
196
197        /**
198         * Check if the search results from this request might be reused by later searches.
199         * We can't use a cached result if consent may use canSeeResource.
200         * This checks for AUTHORIZED requests, and the responses from shouldProcessCanSeeResource()
201         * to see if this holds.
202         * If not, marks the result as single-use.
203         */
204        @Hook(value = Pointcut.STORAGE_PRESEARCH_REGISTERED)
205        public void interceptPreSearchRegistered(
206                        RequestDetails theRequestDetails, ICachedSearchDetails theCachedSearchDetails) {
207                if (isProcessCanSeeResource(theRequestDetails, null)) {
208                        theCachedSearchDetails.setCannotBeReused();
209                }
210        }
211
212        @Hook(value = Pointcut.STORAGE_PREACCESS_RESOURCES)
213        public void interceptPreAccess(
214                        RequestDetails theRequestDetails, IPreResourceAccessDetails thePreResourceAccessDetails) {
215
216                // Flags for each service
217                boolean[] processConsentSvcs = new boolean[myConsentService.size()];
218                boolean processAnyConsentSvcs = isProcessCanSeeResource(theRequestDetails, processConsentSvcs);
219
220                if (!processAnyConsentSvcs) {
221                        return;
222                }
223
224                IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
225                for (int resourceIdx = 0; resourceIdx < thePreResourceAccessDetails.size(); resourceIdx++) {
226                        IBaseResource nextResource = thePreResourceAccessDetails.getResource(resourceIdx);
227                        for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) {
228                                IConsentService nextService = myConsentService.get(consentSvcIdx);
229
230                                if (!processConsentSvcs[consentSvcIdx]) {
231                                        continue;
232                                }
233
234                                ConsentOutcome outcome =
235                                                nextService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices);
236                                Validate.notNull(outcome, "Consent service returned null outcome");
237                                Validate.isTrue(
238                                                outcome.getResource() == null,
239                                                "Consent service returned a resource in its outcome. This is not permitted in canSeeResource(..)");
240
241                                boolean skipSubsequentServices = false;
242                                switch (outcome.getStatus()) {
243                                        case PROCEED:
244                                                break;
245                                        case AUTHORIZED:
246                                                authorizedResources.put(nextResource, Boolean.TRUE);
247                                                skipSubsequentServices = true;
248                                                break;
249                                        case REJECT:
250                                                thePreResourceAccessDetails.setDontReturnResourceAtIndex(resourceIdx);
251                                                skipSubsequentServices = true;
252                                                break;
253                                }
254
255                                if (skipSubsequentServices) {
256                                        break;
257                                }
258                        }
259                }
260        }
261
262        /**
263         * Is canSeeResource() active in any services?
264         * @param theProcessConsentSvcsFlags filled in with the responses from shouldProcessCanSeeResource each service
265         * @return true of any service responded true to shouldProcessCanSeeResource()
266         */
267        private boolean isProcessCanSeeResource(
268                        @Nonnull RequestDetails theRequestDetails, @Nullable boolean[] theProcessConsentSvcsFlags) {
269                if (isRequestAuthorized(theRequestDetails)) {
270                        return false;
271                }
272                if (isSkipServiceForRequest(theRequestDetails)) {
273                        return false;
274                }
275                if (myConsentService.isEmpty()) {
276                        return false;
277                }
278
279                if (theProcessConsentSvcsFlags == null) {
280                        theProcessConsentSvcsFlags = new boolean[myConsentService.size()];
281                }
282                Validate.isTrue(theProcessConsentSvcsFlags.length == myConsentService.size());
283                boolean processAnyConsentSvcs = false;
284                for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) {
285                        IConsentService nextService = myConsentService.get(consentSvcIdx);
286
287                        boolean shouldCallCanSeeResource =
288                                        nextService.shouldProcessCanSeeResource(theRequestDetails, myContextConsentServices);
289                        processAnyConsentSvcs |= shouldCallCanSeeResource;
290                        theProcessConsentSvcsFlags[consentSvcIdx] = shouldCallCanSeeResource;
291                }
292                return processAnyConsentSvcs;
293        }
294
295        @Hook(value = Pointcut.STORAGE_PRESHOW_RESOURCES)
296        public void interceptPreShow(RequestDetails theRequestDetails, IPreResourceShowDetails thePreResourceShowDetails) {
297                if (isRequestAuthorized(theRequestDetails)) {
298                        return;
299                }
300                if (isAllowListedRequest(theRequestDetails)) {
301                        return;
302                }
303                if (isSkipServiceForRequest(theRequestDetails)) {
304                        return;
305                }
306                if (myConsentService.isEmpty()) {
307                        return;
308                }
309
310                IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
311
312                for (int i = 0; i < thePreResourceShowDetails.size(); i++) {
313
314                        IBaseResource resource = thePreResourceShowDetails.getResource(i);
315                        if (resource == null || authorizedResources.putIfAbsent(resource, Boolean.TRUE) != null) {
316                                continue;
317                        }
318
319                        for (IConsentService nextService : myConsentService) {
320                                ConsentOutcome nextOutcome =
321                                                nextService.willSeeResource(theRequestDetails, resource, myContextConsentServices);
322                                IBaseResource newResource = nextOutcome.getResource();
323
324                                switch (nextOutcome.getStatus()) {
325                                        case PROCEED:
326                                                if (newResource != null) {
327                                                        thePreResourceShowDetails.setResource(i, newResource);
328                                                        resource = newResource;
329                                                }
330                                                continue;
331                                        case AUTHORIZED:
332                                                if (newResource != null) {
333                                                        thePreResourceShowDetails.setResource(i, newResource);
334                                                }
335                                                continue;
336                                        case REJECT:
337                                                if (nextOutcome.getOperationOutcome() != null) {
338                                                        IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome();
339                                                        thePreResourceShowDetails.setResource(i, newOperationOutcome);
340                                                        authorizedResources.put(newOperationOutcome, true);
341                                                } else {
342                                                        resource = null;
343                                                        thePreResourceShowDetails.setResource(i, null);
344                                                }
345                                                continue;
346                                }
347                        }
348                }
349        }
350
351        @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE)
352        public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResource) {
353                if (theResource.getResponseResource() == null) {
354                        return;
355                }
356                if (isRequestAuthorized(theRequestDetails)) {
357                        return;
358                }
359                if (isAllowListedRequest(theRequestDetails)) {
360                        return;
361                }
362                if (isSkipServiceForRequest(theRequestDetails)) {
363                        return;
364                }
365                if (myConsentService.isEmpty()) {
366                        return;
367                }
368
369                IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
370
371                // See outer resource
372                if (authorizedResources.putIfAbsent(theResource.getResponseResource(), Boolean.TRUE) == null) {
373
374                        for (IConsentService next : myConsentService) {
375                                final ConsentOutcome outcome = next.willSeeResource(
376                                                theRequestDetails, theResource.getResponseResource(), myContextConsentServices);
377                                if (outcome.getResource() != null) {
378                                        theResource.setResponseResource(outcome.getResource());
379                                }
380
381                                // Clear the total
382                                if (theResource.getResponseResource() instanceof IBaseBundle) {
383                                        BundleUtil.setTotal(
384                                                        theRequestDetails.getFhirContext(), (IBaseBundle) theResource.getResponseResource(), null);
385                                }
386
387                                switch (outcome.getStatus()) {
388                                        case REJECT:
389                                                if (outcome.getOperationOutcome() != null) {
390                                                        theResource.setResponseResource(outcome.getOperationOutcome());
391                                                } else {
392                                                        theResource.setResponseResource(null);
393                                                        theResource.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT);
394                                                }
395                                                // Return immediately
396                                                return;
397                                        case AUTHORIZED:
398                                                // Don't check children, so return immediately
399                                                return;
400                                        case PROCEED:
401                                                // Check children, so proceed
402                                                break;
403                                }
404                        }
405                }
406
407                // See child resources
408                IBaseResource outerResource = theResource.getResponseResource();
409                FhirContext ctx = theRequestDetails.getServer().getFhirContext();
410                IModelVisitor2 visitor = new IModelVisitor2() {
411                        @Override
412                        public boolean acceptElement(
413                                        IBase theElement,
414                                        List<IBase> theContainingElementPath,
415                                        List<BaseRuntimeChildDefinition> theChildDefinitionPath,
416                                        List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
417
418                                // Clear the total
419                                if (theElement instanceof IBaseBundle) {
420                                        BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theElement, null);
421                                }
422
423                                if (theElement == outerResource) {
424                                        return true;
425                                }
426                                if (theElement instanceof IBaseResource) {
427                                        IBaseResource resource = (IBaseResource) theElement;
428                                        if (authorizedResources.putIfAbsent(resource, Boolean.TRUE) != null) {
429                                                return true;
430                                        }
431
432                                        boolean shouldCheckChildren = true;
433                                        for (IConsentService next : myConsentService) {
434                                                ConsentOutcome childOutcome =
435                                                                next.willSeeResource(theRequestDetails, resource, myContextConsentServices);
436
437                                                IBaseResource replacementResource = null;
438                                                boolean shouldReplaceResource = false;
439
440                                                switch (childOutcome.getStatus()) {
441                                                        case REJECT:
442                                                                replacementResource = childOutcome.getOperationOutcome();
443                                                                shouldReplaceResource = true;
444                                                                break;
445                                                        case PROCEED:
446                                                        case AUTHORIZED:
447                                                                replacementResource = childOutcome.getResource();
448                                                                shouldReplaceResource = replacementResource != null;
449                                                                shouldCheckChildren &= childOutcome.getStatus() == ConsentOperationStatusEnum.PROCEED;
450                                                                break;
451                                                }
452
453                                                if (shouldReplaceResource) {
454                                                        IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2);
455                                                        BaseRuntimeChildDefinition containerChildElement =
456                                                                        theChildDefinitionPath.get(theChildDefinitionPath.size() - 1);
457                                                        containerChildElement.getMutator().setValue(container, replacementResource);
458                                                        resource = replacementResource;
459                                                }
460                                        }
461
462                                        return shouldCheckChildren;
463                                }
464
465                                return true;
466                        }
467
468                        @Override
469                        public boolean acceptUndeclaredExtension(
470                                        IBaseExtension<?, ?> theNextExt,
471                                        List<IBase> theContainingElementPath,
472                                        List<BaseRuntimeChildDefinition> theChildDefinitionPath,
473                                        List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
474                                return true;
475                        }
476                };
477                ctx.newTerser().visit(outerResource, visitor);
478        }
479
480        private IdentityHashMap<IBaseResource, Boolean> getAuthorizedResourcesMap(RequestDetails theRequestDetails) {
481                return getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
482        }
483
484        @Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION)
485        public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) {
486                theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE);
487                for (IConsentService next : myConsentService) {
488                        next.completeOperationFailure(theRequest, theException, myContextConsentServices);
489                }
490        }
491
492        @Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY)
493        public void requestSucceeded(RequestDetails theRequest) {
494                if (Boolean.TRUE.equals(theRequest.getUserData().get(myRequestCompletedKey))) {
495                        return;
496                }
497                for (IConsentService next : myConsentService) {
498                        next.completeOperationSuccess(theRequest, myContextConsentServices);
499                }
500        }
501
502        protected RequestDetails getRequestDetailsForCurrentExportOperation(
503                        BulkExportJobParameters theParameters, IBaseResource theBaseResource) {
504                // bulk exports are system operations
505                SystemRequestDetails details = new SystemRequestDetails();
506                return details;
507        }
508
509        @Hook(value = Pointcut.STORAGE_BULK_EXPORT_RESOURCE_INCLUSION)
510        public boolean shouldBulkExportIncludeResource(BulkExportJobParameters theParameters, IBaseResource theResource) {
511                RequestDetails requestDetails = getRequestDetailsForCurrentExportOperation(theParameters, theResource);
512
513                for (IConsentService next : myConsentService) {
514                        ConsentOutcome nextOutcome = next.willSeeResource(requestDetails, theResource, myContextConsentServices);
515
516                        ConsentOperationStatusEnum status = nextOutcome.getStatus();
517                        switch (status) {
518                                case AUTHORIZED:
519                                case PROCEED:
520                                        // go to the next
521                                        break;
522                                case REJECT:
523                                        // if any consent service rejects,
524                                        // reject the resource
525                                        return false;
526                        }
527                }
528
529                // default is to include the resource
530                return true;
531        }
532
533        private boolean isRequestAuthorized(RequestDetails theRequestDetails) {
534                boolean retVal = false;
535                if (theRequestDetails != null) {
536                        Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey);
537                        retVal = Boolean.TRUE.equals(authorizedObj);
538                }
539                return retVal;
540        }
541
542        private boolean isSkipServiceForRequest(RequestDetails theRequestDetails) {
543                return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails);
544        }
545
546        private boolean isAllowListedRequest(RequestDetails theRequestDetails) {
547                return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails);
548        }
549
550        private boolean isMetaOperation(RequestDetails theRequestDetails) {
551                return theRequestDetails != null && OPERATION_META.equals(theRequestDetails.getOperation());
552        }
553
554        private boolean isMetadataPath(RequestDetails theRequestDetails) {
555                return theRequestDetails != null && URL_TOKEN_METADATA.equals(theRequestDetails.getRequestPath());
556        }
557
558        private void validateParameter(Map<String, String[]> theParameterMap) {
559                if (theParameterMap != null) {
560                        if (theParameterMap.containsKey(Constants.PARAM_SEARCH_TOTAL_MODE)
561                                        && Arrays.stream(theParameterMap.get("_total")).anyMatch("accurate"::equals)) {
562                                throw new InvalidRequestException(Msg.code(2037) + Constants.PARAM_SEARCH_TOTAL_MODE
563                                                + "=accurate is not permitted on this server");
564                        }
565                        if (theParameterMap.containsKey(Constants.PARAM_SUMMARY)
566                                        && Arrays.stream(theParameterMap.get("_summary")).anyMatch("count"::equals)) {
567                                throw new InvalidRequestException(
568                                                Msg.code(2038) + Constants.PARAM_SUMMARY + "=count is not permitted on this server");
569                        }
570                }
571        }
572
573        @SuppressWarnings("unchecked")
574        public static IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(
575                        RequestDetails theRequestDetails, String theKey) {
576                IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>)
577                                theRequestDetails.getUserData().get(theKey);
578                if (alreadySeenResources == null) {
579                        alreadySeenResources = new IdentityHashMap<>();
580                        theRequestDetails.getUserData().put(theKey, alreadySeenResources);
581                }
582                return alreadySeenResources;
583        }
584
585        private static ForbiddenOperationException toForbiddenOperationException(ConsentOutcome theOutcome) {
586                IBaseOperationOutcome operationOutcome = null;
587                if (theOutcome.getOperationOutcome() != null) {
588                        operationOutcome = theOutcome.getOperationOutcome();
589                }
590                return new ForbiddenOperationException("Rejected by consent service", operationOutcome);
591        }
592}