001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2024 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                                Logs.getSecurityTroubleshootingLog()
091                                                .warn(
092                                                                "Ignoring invalid authority - {} does not match declared format: {}",
093                                                                theGrantedAuthority,
094                                                                permissionArgumentFormat.getType().getSimpleName());
095                        }
096                        return Optional.empty();
097                } else {
098                        Validate.isInstanceOf(theArgumentValueType, parsed.get());
099                        //noinspection unchecked - we just checked it above
100                        return (Optional<T>) parsed;
101                }
102        }
103
104        /**
105         * Parse a string against a defined format.
106         * @param thePermissionArgument the permission argument to parse
107         * @param theFormat the permission argument format
108         * @return The value-type for the format, or empty if the string doesn't match the format.
109         * @param <T> The concrete argument value type (e.g. {@link TypeInCompartmentWithOptionalFilter}), for the format.
110         */
111        public <T extends PermissionArgumentValue> Optional<T> parse(
112                        @Nullable String thePermissionArgument, @Nonnull PermissionArgumentFormat<T> theFormat) {
113                return theFormat.parse(thePermissionArgument, myIdBuilder);
114        }
115
116        /**
117         * Goofy check looking for the presence of filter expressions in an argument.
118         * Faster than a full parse.
119         * @param theGrantedAuthorityJson the authority to check
120         * @return true if the permission argument type can have a filter AND the filter is present
121         */
122        public static boolean hasFilter(GrantedAuthorityJson theGrantedAuthorityJson) {
123                return PermissionArgumentFormat.IOptionalFilterRestriction.class.isAssignableFrom(
124                                                theGrantedAuthorityJson.getPermission().getFormat().getType())
125                                && StringUtils.isNotEmpty(theGrantedAuthorityJson.getArgument())
126                                && theGrantedAuthorityJson.getArgument().indexOf(PermissionArgumentFormat.FILTER_DELIMITER) != -1;
127        }
128}