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