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.i18n.ILocalizer;
013import ca.cdr.api.model.enm.PermissionEnum;
014import ca.cdr.api.model.json.GrantedAuthorityJson;
015import jakarta.annotation.Nonnull;
016import jakarta.annotation.Nullable;
017import org.apache.commons.lang3.Validate;
018import org.hl7.fhir.instance.model.api.IIdType;
019
020import java.util.Collection;
021import java.util.Optional;
022import java.util.regex.Pattern;
023
024/**
025 * Public interface for the various permission argument formats.
026 * @param <F> The type this format parses/formats
027 */
028public interface PermissionArgumentFormat<F extends PermissionArgumentValue> {
029
030        char FILTER_DELIMITER = '?';
031        char TYPE_COMPARTMENT_DELIMITER = ':';
032        String RESOURCE_TYPE_LIST_DELIMITER = " ";
033        String RESOURCE_TYPE_PATTERN = "[A-Z][a-zA-Z\\d]*";
034        /**
035         * format for resource ids - used for compartments.
036         * The spec says [a-zA-Z0-9.\-]{1,36}, but hybrid might do any crazy thing,
037         * so we only verify we don't have any of our permission-format chars.
038         * @see ca.uhn.fhir.model.primitive.IdDt */
039        Pattern ID_FORMAT = Pattern.compile(RESOURCE_TYPE_PATTERN + "/[^" + FILTER_DELIMITER + TYPE_COMPARTMENT_DELIMITER
040                        + RESOURCE_TYPE_LIST_DELIMITER + "]+");
041
042        /**
043         * Typesafe authority builder.
044         * Validates the argument format against the declared format for thePermissionEnum
045         * @param thePermissionEnum the permission
046         * @param theArgumentValue the concrete parsed argument type - use {@link NoArgument#INSTANCE} for no-arg.
047         */
048        static GrantedAuthorityJson buildAuthority(
049                        @Nonnull PermissionEnum thePermissionEnum, @Nonnull PermissionArgumentValue theArgumentValue) {
050                String formattedArg = thePermissionEnum.getFormat().formatIfValid(theArgumentValue);
051                return new GrantedAuthorityJson(thePermissionEnum, formattedArg);
052        }
053
054        /**
055         * Parse a permission argument string.
056         * @param theArgumentString The string to parse.  null is treated as empty.
057         * @param theIdBuilder factory for parsing IIdType types
058         * @return the parsed arg value if the input matches this format
059         */
060        @Nonnull
061        Optional<F> parse(@Nullable String theArgumentString, @Nonnull IdBuilder theIdBuilder);
062
063        @Nonnull
064        default <T extends PermissionArgumentValue> String formatIfValid(@Nonnull T theArgument) {
065                Validate.isInstanceOf(this.getType(), theArgument);
066                //noinspection unchecked
067                return format((F) theArgument);
068        }
069
070        /**
071         * @return the type of the value bean we parse
072         */
073        Class<F> getType();
074
075        /**
076         * format the value back into a string for persistence.
077         * @param theArgument the arg value bean
078         * @return the formatted string - empty will be the empty string, not null
079         */
080        @Nonnull
081        String format(@Nonnull F theArgument);
082
083        /**
084         *
085         * @return the name of the argument format
086         */
087        default String getName(ILocalizer theLocalizer) {
088                return theLocalizer.getMessage(
089                                "permission.argument_format." + this.getType().getName() + ".name");
090        }
091
092        /**
093         *
094         * @return A description of the argument format that users can use to properly format arguments
095         */
096        default String getDescription(ILocalizer theLocalizer) {
097                return theLocalizer.getMessage(
098                                "permission.argument_format." + this.getType().getName() + ".desc");
099        }
100
101        /* -------------- */
102        /* format components */
103        /* -------------- */
104        /** Empty marker for facets of permission arguments */
105        interface IPermissionComponent {}
106
107        /** The permission is scoped to only apply to a type */
108        interface IResourceTypeRestriction extends IPermissionComponent {
109                /** A resource type - e.g. Patient */
110                String getResourceType();
111        }
112
113        /** the permission is scoped to a compartment */
114        interface ICompartmentRestriction extends IPermissionComponent {
115                /** Get compartment owner */
116                IIdType getOwner();
117
118                /** The type of the compartment - i.e. the type of the compartment owner.  E.g Patient, Device, etc. */
119                default String getCompartmentType() {
120                        return getOwner().getResourceType();
121                }
122        }
123
124        interface ICompartmentsRestriction extends IPermissionComponent {
125                /** Get compartment owners */
126                Collection<IIdType> getOwners();
127        }
128
129        /** The permission is scoped to a single instance */
130        interface IInstanceRestriction extends IPermissionComponent, IResourceTypeRestriction {
131                /** The scope instance */
132                IIdType getInstanceId();
133
134                /** The type of the instance */
135                @Override
136                default String getResourceType() {
137                        return getInstanceId().getResourceType();
138                }
139        }
140
141        /** The permission has an optional FHIR filter */
142        interface IOptionalFilterRestriction extends IPermissionComponent {
143                /** The fhir query - optional */
144                Optional<String> getFilter();
145                /** Is this permission scoped by a fhir query? */
146                default boolean hasFilter() {
147                        return getFilter().isPresent();
148                }
149        }
150
151        interface ICodeInValueSetRestriction extends IPermissionComponent {
152                /** A resource type - e.g., Patient */
153                String getResourceType();
154
155                /** a search parameter */
156                String getSearchParamName();
157
158                /** a URL identifying a value set */
159                String getValueSetUrl();
160        }
161
162        interface IOperationRestriction extends IPermissionComponent {
163                /** the name of an operation */
164                String getOperationName();
165        }
166
167        interface IExportResourceTypesRestriction extends IPermissionComponent {
168                /** indicates that the user has permission to export all types */
169                boolean canExportAllResourceTypes();
170
171                /** the names of the types that the user has permission to export */
172                Collection<String> getResourceTypes();
173        }
174
175        interface ICompartmentWithExportResourceTypesRestriction extends IPermissionComponent, ICompartmentRestriction {
176
177                /** the names of the types that the user has permission to export */
178                Collection<String> getResourceTypes();
179        }
180
181        interface ICompartmentsWithExportResourceTypesRestriction
182                        extends IPermissionComponent, ICompartmentsRestriction, IExportResourceTypesRestriction {}
183
184        interface INodeAndModuleIdRestriction extends IPermissionComponent {
185                /** the nodeId that the user has permission for */
186                String getNodeId();
187                /** the moduleId that the user has permission for */
188                String getModuleId();
189        }
190}