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