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                                        Map<Object, Object> userData = theRequestDetails.getUserData();
185                                        userData.put(myRequestAuthorizedKey, Boolean.TRUE);
186                                        return;
187                        }
188                }
189        }
190
191        /**
192         * Check if this request is eligible for cached search results.
193         * We can't use a cached result if consent may use canSeeResource.
194         * This checks for AUTHORIZED requests, and the responses from shouldProcessCanSeeResource()
195         * to see if this holds.
196         * @return may the request be satisfied from cache.
197         */
198        @Hook(value = Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH)
199        public boolean interceptPreCheckForCachedSearch(@Nonnull RequestDetails theRequestDetails) {
200                return !isProcessCanSeeResource(theRequestDetails, null);
201        }
202
203        /**
204         * Check if the search results from this request might be reused by later searches.
205         * We can't use a cached result if consent may use canSeeResource.
206         * This checks for AUTHORIZED requests, and the responses from shouldProcessCanSeeResource()
207         * to see if this holds.
208         * If not, marks the result as single-use.
209         */
210        @Hook(value = Pointcut.STORAGE_PRESEARCH_REGISTERED)
211        public void interceptPreSearchRegistered(
212                        RequestDetails theRequestDetails, ICachedSearchDetails theCachedSearchDetails) {
213                if (isProcessCanSeeResource(theRequestDetails, null)) {
214                        theCachedSearchDetails.setCannotBeReused();
215                }
216        }
217
218        @Hook(value = Pointcut.STORAGE_PREACCESS_RESOURCES)
219        public void interceptPreAccess(
220                        RequestDetails theRequestDetails, IPreResourceAccessDetails thePreResourceAccessDetails) {
221
222                // Flags for each service
223                boolean[] processConsentSvcs = new boolean[myConsentService.size()];
224                boolean processAnyConsentSvcs = isProcessCanSeeResource(theRequestDetails, processConsentSvcs);
225
226                if (!processAnyConsentSvcs) {
227                        return;
228                }
229
230                IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources =
231                                getAlreadySeenResourcesMap(theRequestDetails);
232                for (int resourceIdx = 0; resourceIdx < thePreResourceAccessDetails.size(); resourceIdx++) {
233                        IBaseResource nextResource = thePreResourceAccessDetails.getResource(resourceIdx);
234                        for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) {
235                                IConsentService nextService = myConsentService.get(consentSvcIdx);
236
237                                if (!processConsentSvcs[consentSvcIdx]) {
238                                        continue;
239                                }
240
241                                ConsentOutcome outcome =
242                                                nextService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices);
243                                Validate.notNull(outcome, "Consent service returned null outcome");
244                                Validate.isTrue(
245                                                outcome.getResource() == null,
246                                                "Consent service returned a resource in its outcome. This is not permitted in canSeeResource(..)");
247
248                                boolean skipSubsequentServices = false;
249                                switch (outcome.getStatus()) {
250                                        case PROCEED:
251                                                break;
252                                        case AUTHORIZED:
253                                                alreadySeenResources.put(nextResource, ConsentOperationStatusEnum.AUTHORIZED);
254                                                skipSubsequentServices = true;
255                                                break;
256                                        case REJECT:
257                                                alreadySeenResources.put(nextResource, ConsentOperationStatusEnum.REJECT);
258                                                thePreResourceAccessDetails.setDontReturnResourceAtIndex(resourceIdx);
259                                                skipSubsequentServices = true;
260                                                break;
261                                }
262
263                                if (skipSubsequentServices) {
264                                        break;
265                                }
266                        }
267                }
268        }
269
270        /**
271         * Is canSeeResource() active in any services?
272         * @param theProcessConsentSvcsFlags filled in with the responses from shouldProcessCanSeeResource each service
273         * @return true of any service responded true to shouldProcessCanSeeResource()
274         */
275        private boolean isProcessCanSeeResource(
276                        @Nonnull RequestDetails theRequestDetails, @Nullable boolean[] theProcessConsentSvcsFlags) {
277                if (isRequestAuthorized(theRequestDetails)) {
278                        return false;
279                }
280                if (isSkipServiceForRequest(theRequestDetails)) {
281                        return false;
282                }
283                if (myConsentService.isEmpty()) {
284                        return false;
285                }
286
287                if (theProcessConsentSvcsFlags == null) {
288                        theProcessConsentSvcsFlags = new boolean[myConsentService.size()];
289                }
290                Validate.isTrue(theProcessConsentSvcsFlags.length == myConsentService.size());
291                boolean processAnyConsentSvcs = false;
292                for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) {
293                        IConsentService nextService = myConsentService.get(consentSvcIdx);
294
295                        boolean shouldCallCanSeeResource =
296                                        nextService.shouldProcessCanSeeResource(theRequestDetails, myContextConsentServices);
297                        processAnyConsentSvcs |= shouldCallCanSeeResource;
298                        theProcessConsentSvcsFlags[consentSvcIdx] = shouldCallCanSeeResource;
299                }
300                return processAnyConsentSvcs;
301        }
302
303        @Hook(value = Pointcut.STORAGE_PRESHOW_RESOURCES)
304        public void interceptPreShow(RequestDetails theRequestDetails, IPreResourceShowDetails thePreResourceShowDetails) {
305                if (isRequestAuthorized(theRequestDetails)) {
306                        return;
307                }
308                if (isAllowListedRequest(theRequestDetails)) {
309                        return;
310                }
311                if (isSkipServiceForRequest(theRequestDetails)) {
312                        return;
313                }
314                if (myConsentService.isEmpty()) {
315                        return;
316                }
317
318                IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources =
319                                getAlreadySeenResourcesMap(theRequestDetails);
320
321                for (int i = 0; i < thePreResourceShowDetails.size(); i++) {
322
323                        IBaseResource resource = thePreResourceShowDetails.getResource(i);
324                        if (resource == null
325                                        || alreadySeenResources.putIfAbsent(resource, ConsentOperationStatusEnum.PROCEED) != null) {
326                                continue;
327                        }
328
329                        for (IConsentService nextService : myConsentService) {
330                                ConsentOutcome nextOutcome =
331                                                nextService.willSeeResource(theRequestDetails, resource, myContextConsentServices);
332                                IBaseResource newResource = nextOutcome.getResource();
333
334                                switch (nextOutcome.getStatus()) {
335                                        case PROCEED:
336                                                if (newResource != null) {
337                                                        thePreResourceShowDetails.setResource(i, newResource);
338                                                        resource = newResource;
339                                                }
340                                                continue;
341                                        case AUTHORIZED:
342                                                alreadySeenResources.put(resource, ConsentOperationStatusEnum.AUTHORIZED);
343                                                if (newResource != null) {
344                                                        thePreResourceShowDetails.setResource(i, newResource);
345                                                }
346                                                continue;
347                                        case REJECT:
348                                                alreadySeenResources.put(resource, ConsentOperationStatusEnum.REJECT);
349                                                if (nextOutcome.getOperationOutcome() != null) {
350                                                        IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome();
351                                                        thePreResourceShowDetails.setResource(i, newOperationOutcome);
352                                                        alreadySeenResources.put(newOperationOutcome, ConsentOperationStatusEnum.PROCEED);
353                                                } else {
354                                                        resource = null;
355                                                        thePreResourceShowDetails.setResource(i, null);
356                                                }
357                                                continue;
358                                }
359                        }
360                }
361        }
362
363        @Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE)
364        public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseDetails) {
365                if (theResponseDetails.getResponseResource() == null) {
366                        return;
367                }
368                if (isRequestAuthorized(theRequestDetails)) {
369                        return;
370                }
371                if (isAllowListedRequest(theRequestDetails)) {
372                        return;
373                }
374                if (isSkipServiceForRequest(theRequestDetails)) {
375                        return;
376                }
377                if (myConsentService.isEmpty()) {
378                        return;
379                }
380
381                // Take care of outer resource first
382                IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources =
383                                getAlreadySeenResourcesMap(theRequestDetails);
384                if (alreadySeenResources.containsKey(theResponseDetails.getResponseResource())) {
385                        // we've already seen this resource before
386                        ConsentOperationStatusEnum decisionOnResource =
387                                        alreadySeenResources.get(theResponseDetails.getResponseResource());
388
389                        if (ConsentOperationStatusEnum.AUTHORIZED.equals(decisionOnResource)
390                                        || ConsentOperationStatusEnum.REJECT.equals(decisionOnResource)) {
391                                // the consent service decision on the resource was AUTHORIZED or REJECT.
392                                // In both cases, we can immediately return without checking children
393                                return;
394                        }
395                } else {
396                        // we haven't seen this resource before
397                        // mark it as seen now, set the initial consent decision value to PROCEED by default,
398                        // we will update if it changes another value below
399                        alreadySeenResources.put(theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.PROCEED);
400
401                        for (IConsentService next : myConsentService) {
402                                final ConsentOutcome outcome = next.willSeeResource(
403                                                theRequestDetails, theResponseDetails.getResponseResource(), myContextConsentServices);
404                                if (outcome.getResource() != null) {
405                                        theResponseDetails.setResponseResource(outcome.getResource());
406                                }
407
408                                // Clear the total
409                                if (theResponseDetails.getResponseResource() instanceof IBaseBundle) {
410                                        BundleUtil.setTotal(
411                                                        theRequestDetails.getFhirContext(),
412                                                        (IBaseBundle) theResponseDetails.getResponseResource(),
413                                                        null);
414                                }
415
416                                switch (outcome.getStatus()) {
417                                        case REJECT:
418                                                alreadySeenResources.put(
419                                                                theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.REJECT);
420                                                if (outcome.getOperationOutcome() != null) {
421                                                        theResponseDetails.setResponseResource(outcome.getOperationOutcome());
422                                                } else {
423                                                        theResponseDetails.setResponseResource(null);
424                                                        theResponseDetails.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT);
425                                                }
426                                                // Return immediately
427                                                return;
428                                        case AUTHORIZED:
429                                                alreadySeenResources.put(
430                                                                theResponseDetails.getResponseResource(), ConsentOperationStatusEnum.AUTHORIZED);
431                                                // Don't check children, so return immediately
432                                                return;
433                                        case PROCEED:
434                                                // Check children, so proceed
435                                                break;
436                                }
437                        }
438                }
439
440                // See child resources
441                IBaseResource outerResource = theResponseDetails.getResponseResource();
442                FhirContext ctx = theRequestDetails.getServer().getFhirContext();
443                IModelVisitor2 visitor = new IModelVisitor2() {
444                        @Override
445                        public boolean acceptElement(
446                                        IBase theElement,
447                                        List<IBase> theContainingElementPath,
448                                        List<BaseRuntimeChildDefinition> theChildDefinitionPath,
449                                        List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
450
451                                // Clear the total
452                                if (theElement instanceof IBaseBundle) {
453                                        BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theElement, null);
454                                }
455
456                                if (theElement == outerResource) {
457                                        return true;
458                                }
459                                if (theElement instanceof IBaseResource) {
460                                        IBaseResource resource = (IBaseResource) theElement;
461                                        if (alreadySeenResources.putIfAbsent(resource, ConsentOperationStatusEnum.PROCEED) != null) {
462                                                return true;
463                                        }
464
465                                        boolean shouldCheckChildren = true;
466                                        for (IConsentService next : myConsentService) {
467                                                ConsentOutcome childOutcome =
468                                                                next.willSeeResource(theRequestDetails, resource, myContextConsentServices);
469
470                                                IBaseResource replacementResource = null;
471                                                boolean shouldReplaceResource = false;
472
473                                                switch (childOutcome.getStatus()) {
474                                                        case REJECT:
475                                                                replacementResource = childOutcome.getOperationOutcome();
476                                                                shouldReplaceResource = true;
477                                                                alreadySeenResources.put(resource, ConsentOperationStatusEnum.REJECT);
478                                                                break;
479                                                        case PROCEED:
480                                                                replacementResource = childOutcome.getResource();
481                                                                shouldReplaceResource = replacementResource != null;
482                                                                break;
483                                                        case AUTHORIZED:
484                                                                replacementResource = childOutcome.getResource();
485                                                                shouldReplaceResource = replacementResource != null;
486                                                                shouldCheckChildren = false;
487                                                                alreadySeenResources.put(resource, ConsentOperationStatusEnum.AUTHORIZED);
488                                                                break;
489                                                }
490
491                                                if (shouldReplaceResource) {
492                                                        IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2);
493                                                        BaseRuntimeChildDefinition containerChildElement =
494                                                                        theChildDefinitionPath.get(theChildDefinitionPath.size() - 1);
495                                                        containerChildElement.getMutator().setValue(container, replacementResource);
496                                                        resource = replacementResource;
497                                                }
498                                        }
499
500                                        return shouldCheckChildren;
501                                }
502
503                                return true;
504                        }
505
506                        @Override
507                        public boolean acceptUndeclaredExtension(
508                                        IBaseExtension<?, ?> theNextExt,
509                                        List<IBase> theContainingElementPath,
510                                        List<BaseRuntimeChildDefinition> theChildDefinitionPath,
511                                        List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
512                                return true;
513                        }
514                };
515                ctx.newTerser().visit(outerResource, visitor);
516        }
517
518        @Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION)
519        public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) {
520                theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE);
521                for (IConsentService next : myConsentService) {
522                        next.completeOperationFailure(theRequest, theException, myContextConsentServices);
523                }
524        }
525
526        @Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY)
527        public void requestSucceeded(RequestDetails theRequest) {
528                if (Boolean.TRUE.equals(theRequest.getUserData().get(myRequestCompletedKey))) {
529                        return;
530                }
531                for (IConsentService next : myConsentService) {
532                        next.completeOperationSuccess(theRequest, myContextConsentServices);
533                }
534        }
535
536        protected RequestDetails getRequestDetailsForCurrentExportOperation(
537                        BulkExportJobParameters theParameters, IBaseResource theBaseResource) {
538                // bulk exports are system operations
539                SystemRequestDetails details = new SystemRequestDetails();
540                return details;
541        }
542
543        @Hook(value = Pointcut.STORAGE_BULK_EXPORT_RESOURCE_INCLUSION)
544        public boolean shouldBulkExportIncludeResource(BulkExportJobParameters theParameters, IBaseResource theResource) {
545                RequestDetails requestDetails = getRequestDetailsForCurrentExportOperation(theParameters, theResource);
546
547                for (IConsentService next : myConsentService) {
548                        ConsentOutcome nextOutcome = next.willSeeResource(requestDetails, theResource, myContextConsentServices);
549
550                        ConsentOperationStatusEnum status = nextOutcome.getStatus();
551                        switch (status) {
552                                case AUTHORIZED:
553                                case PROCEED:
554                                        // go to the next
555                                        break;
556                                case REJECT:
557                                        // if any consent service rejects,
558                                        // reject the resource
559                                        return false;
560                        }
561                }
562
563                // default is to include the resource
564                return true;
565        }
566
567        private boolean isRequestAuthorized(RequestDetails theRequestDetails) {
568                boolean retVal = false;
569                if (theRequestDetails != null) {
570                        Object authorizedObj = theRequestDetails.getUserData().get(myRequestAuthorizedKey);
571                        retVal = Boolean.TRUE.equals(authorizedObj);
572                }
573                return retVal;
574        }
575
576        private boolean isSkipServiceForRequest(RequestDetails theRequestDetails) {
577                return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails);
578        }
579
580        private boolean isAllowListedRequest(RequestDetails theRequestDetails) {
581                return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails);
582        }
583
584        private boolean isMetaOperation(RequestDetails theRequestDetails) {
585                return theRequestDetails != null && OPERATION_META.equals(theRequestDetails.getOperation());
586        }
587
588        private boolean isMetadataPath(RequestDetails theRequestDetails) {
589                return theRequestDetails != null && URL_TOKEN_METADATA.equals(theRequestDetails.getRequestPath());
590        }
591
592        private void validateParameter(Map<String, String[]> theParameterMap) {
593                if (theParameterMap != null) {
594                        if (theParameterMap.containsKey(Constants.PARAM_SEARCH_TOTAL_MODE)
595                                        && Arrays.stream(theParameterMap.get("_total")).anyMatch("accurate"::equals)) {
596                                throw new InvalidRequestException(Msg.code(2037) + Constants.PARAM_SEARCH_TOTAL_MODE
597                                                + "=accurate is not permitted on this server");
598                        }
599                        if (theParameterMap.containsKey(Constants.PARAM_SUMMARY)
600                                        && Arrays.stream(theParameterMap.get("_summary")).anyMatch("count"::equals)) {
601                                throw new InvalidRequestException(
602                                                Msg.code(2038) + Constants.PARAM_SUMMARY + "=count is not permitted on this server");
603                        }
604                }
605        }
606
607        /**
608         * The map returned by this method keeps track of the resources already processed by ConsentInterceptor in the
609         * context of a request.
610         * If the map contains a particular resource, it means that the resource has already been processed and the value
611         * is the status returned by consent services for that resource.
612         * @param theRequestDetails
613         * @return
614         */
615        @SuppressWarnings("unchecked")
616        private IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> getAlreadySeenResourcesMap(
617                        RequestDetails theRequestDetails) {
618                IdentityHashMap<IBaseResource, ConsentOperationStatusEnum> alreadySeenResources =
619                                (IdentityHashMap<IBaseResource, ConsentOperationStatusEnum>)
620                                                theRequestDetails.getUserData().get(myRequestSeenResourcesKey);
621                if (alreadySeenResources == null) {
622                        alreadySeenResources = new IdentityHashMap<>();
623                        theRequestDetails.getUserData().put(myRequestSeenResourcesKey, alreadySeenResources);
624                }
625                return alreadySeenResources;
626        }
627
628        private static ForbiddenOperationException toForbiddenOperationException(ConsentOutcome theOutcome) {
629                IBaseOperationOutcome operationOutcome = null;
630                if (theOutcome.getOperationOutcome() != null) {
631                        operationOutcome = theOutcome.getOperationOutcome();
632                }
633                return new ForbiddenOperationException("Rejected by consent service", operationOutcome);
634        }
635}