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