001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2025 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.api.security.permission;
011
012import ca.cdr.api.log.Logs;
013import ca.cdr.api.model.json.GrantedAuthorityJson;
014import ca.uhn.fhir.context.FhirContext;
015import jakarta.annotation.Nonnull;
016import jakarta.annotation.Nullable;
017import org.apache.commons.lang3.StringUtils;
018import org.apache.commons.lang3.Validate;
019
020import java.util.Optional;
021
022/**
023 * Parser for PermissionEnum arguments
024 */
025public final class PermissionArgumentParser {
026        /**
027         * Singleton for thread-safe non-fhir parser
028         */
029        static final PermissionArgumentParser NO_FHIR_CONTEXT_PARSER =
030                        new PermissionArgumentParser(IdBuilder.withoutFhirContext());
031
032        private final IdBuilder myIdBuilder;
033
034        PermissionArgumentParser(IdBuilder theIdBuilder) {
035                myIdBuilder = theIdBuilder;
036        }
037
038        @Nonnull
039        public static PermissionArgumentParser withFhirContext(@Nonnull FhirContext theFhirContext) {
040                return new PermissionArgumentParser(IdBuilder.fromFhirContext(theFhirContext));
041        }
042
043        @Nonnull
044        public static PermissionArgumentParser withoutFhirContext() {
045                return NO_FHIR_CONTEXT_PARSER;
046        }
047
048        /**
049         * Parse any authority generically.
050         * Parses the GrantedAuthorityJson argument via the declared format of the PermissionEnum.
051         * Use {@link ca.cdr.api.security.permission.PermissionArgumentFormat.IPermissionComponent} type checks
052         * to extract information.
053         *
054         * @param theGrantedAuthority the authority (permission and argument) to parse
055         * @return the parsed value, if the format was valid
056         */
057        public Optional<PermissionArgumentValue> parse(@Nonnull GrantedAuthorityJson theGrantedAuthority) {
058                return parseForValue(theGrantedAuthority, PermissionArgumentValue.class);
059        }
060
061        /**
062         * Parse the argument string from the GrantedAuthorityJson into the declared argument value type.
063         * It is an error to request the wrong type.  Use {@link #parse(GrantedAuthorityJson)} to parse
064         * for any permission.
065         *
066         * @param theGrantedAuthority   the permission and argument to parse
067         * @param theArgumentValueType  the concrete parsed value type or a component interface
068         * @param <T>                   the type or type component to be parsed
069         * @return the parsed data, if the declared argument format agrees with theArgumentValueType
070         * @throws IllegalArgumentException if the declared GA type does not match theArgumentValueType
071         */
072        @Nonnull
073        public <T extends PermissionArgumentValue> Optional<T> parseForValue(
074                        @Nonnull GrantedAuthorityJson theGrantedAuthority, @Nonnull Class<T> theArgumentValueType) {
075                PermissionArgumentFormat<?> permissionArgumentFormat =
076                                theGrantedAuthority.getPermission().getFormat();
077                Validate.isAssignableFrom(
078                                theArgumentValueType,
079                                permissionArgumentFormat.getType(),
080                                "Cannot assign the permission format %s for %s to a %s",
081                                permissionArgumentFormat.getType().getSimpleName(),
082                                theGrantedAuthority.getPermission(),
083                                theArgumentValueType.getSimpleName());
084
085                Optional<? extends PermissionArgumentFormat.IPermissionComponent> parsed =
086                                parse(theGrantedAuthority.getArgument(), permissionArgumentFormat);
087
088                if (parsed.isEmpty()) {
089                        if (theGrantedAuthority.getPermission().isRequiresArgument()) {
090                                String linkToApiDocs = String.format(
091                                                "https://smilecdr.com/docs/apidocs/cdr-api-public-java/ca/cdr/api/security/permission/%s.html",
092                                                permissionArgumentFormat.getType().getSimpleName());
093                                Logs.getSecurityTroubleshootingLog()
094                                                .warn(
095                                                                "Ignoring invalid authority - {} does not match declared format: {}. For more formatting information, please visit {}",
096                                                                theGrantedAuthority,
097                                                                permissionArgumentFormat.getType().getSimpleName(),
098                                                                linkToApiDocs);
099                        }
100                        return Optional.empty();
101                } else {
102                        Validate.isInstanceOf(theArgumentValueType, parsed.get());
103                        //noinspection unchecked - we just checked it above
104                        return (Optional<T>) parsed;
105                }
106        }
107
108        /**
109         * Parse a string against a defined format.
110         * @param thePermissionArgument the permission argument to parse
111         * @param theFormat the permission argument format
112         * @return The value-type for the format, or empty if the string doesn't match the format.
113         * @param <T> The concrete argument value type (e.g. {@link TypeInCompartmentWithOptionalFilter}), for the format.
114         */
115        public <T extends PermissionArgumentValue> Optional<T> parse(
116                        @Nullable String thePermissionArgument, @Nonnull PermissionArgumentFormat<T> theFormat) {
117                return theFormat.parse(thePermissionArgument, myIdBuilder);
118        }
119
120        /**
121         * Goofy check looking for the presence of filter expressions in an argument.
122         * Faster than a full parse.
123         * @param theGrantedAuthorityJson the authority to check
124         * @return true if the permission argument type can have a filter AND the filter is present
125         */
126        public static boolean hasFilter(GrantedAuthorityJson theGrantedAuthorityJson) {
127                return PermissionArgumentFormat.IOptionalFilterRestriction.class.isAssignableFrom(
128                                                theGrantedAuthorityJson.getPermission().getFormat().getType())
129                                && StringUtils.isNotEmpty(theGrantedAuthorityJson.getArgument())
130                                && theGrantedAuthorityJson.getArgument().indexOf(PermissionArgumentFormat.FILTER_DELIMITER) != -1;
131        }
132}