001package ca.cdr.api.model.json;
002
003/*-
004 * #%L
005 * Smile CDR - CDR
006 * %%
007 * Copyright (C) 2016 - 2024 Smile CDR, Inc.
008 * %%
009 * All rights reserved.
010 * #L%
011 */
012
013import ca.cdr.api.util.SmileClaimConstants;
014import ca.uhn.fhir.util.JsonUtil;
015import com.fasterxml.jackson.annotation.JsonIgnore;
016import com.fasterxml.jackson.annotation.JsonProperty;
017import io.swagger.v3.oas.annotations.Operation;
018import io.swagger.v3.oas.annotations.Parameter;
019import io.swagger.v3.oas.annotations.media.Schema;
020import org.apache.commons.lang3.StringUtils;
021import org.apache.commons.lang3.Validate;
022import org.springframework.security.core.AuthenticatedPrincipal;
023
024import java.util.ArrayList;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Optional;
030import java.util.Set;
031
032import static org.apache.commons.lang3.StringUtils.trim;
033
034@Schema(
035                name = "UserSessionDetails",
036                description = "A user session details object contains details about a logged in user and "
037                                + "a specific session they have established with the authorization server.")
038public class UserSessionDetailsJson extends UserDetailsJson
039                implements IOAuth2Session, IModelJson, AuthenticatedPrincipal, IHasUserData {
040        public static final String FHIR_CONTEXT = "fhirContext";
041
042        @JsonProperty("launchContextParameters")
043        @Schema(
044                        description =
045                                        "Specifies the parameters that will be returned to the user as launch context if the SMART authorization flow requests a launch context",
046                        accessMode = Schema.AccessMode.READ_ONLY)
047        private List<LaunchContextParameterJson> myLaunchContextParameters;
048
049        @JsonProperty("launchResourceIds")
050        @Schema(
051                        description =
052                                        "Specifies the IDs that will be returned to the user as launch context if the SMART authorization flow requests a launch context",
053                        accessMode = Schema.AccessMode.READ_ONLY)
054        private List<LaunchResourceIdJson> myLaunchResourceIds;
055
056        /**
057         * This is for launch context, not the Hapi FhirContext.
058         * @see <a href='http://hl7.org/fhir/smart-app-launch/scopes-and-launch-context.html#launch-context-arrives-with-your-access_token'>http://hl7.org/fhir/smart-app-launch/scopes-and-launch-context.html#launch-context-arrives-with-your-access_token</a>
059         */
060        @JsonProperty("fhirContext")
061        @Schema(
062                        description =
063                                        "Specifies the components of the fhirContext, including a reference or a reference/role pair.",
064                        accessMode = Schema.AccessMode.READ_ONLY)
065        private List<SmartFhirContextEntryJson> myFhirContext;
066
067        @JsonProperty("approvedScopes")
068        @Schema(
069                        description =
070                                        "If the session is an OAuth2 session (i.e. it is accessed via a bearer token that was granted by a SMART Auth server) this field will be populated with the set of scopes that were approved for the client")
071        private Set<String> myApprovedScopes;
072
073        @JsonIgnore
074        private transient volatile String myJsonString;
075
076        @JsonProperty("oidcClientId")
077        @Schema(
078                        description =
079                                        "If the session is an OAuth2 session (i.e. it is accessed via a bearer token that was granted by a SMART Auth server) this field will be populated with the id of the client.")
080        private String myOidcClientId;
081
082        @JsonProperty("oidcClientNodeId")
083        @Schema(description = "The node ID associated with OIDC client of this user.")
084        private String myOidcClientNodeId;
085
086        @Schema(description = "The module ID associated with the OIDC client of this user account.")
087        @JsonProperty("oidcClientModuleId")
088        private String myOidcClientModuleId;
089
090        @Schema(description = "The user data for this session.")
091        @JsonProperty("userData")
092        private Map<String, Object> myUserData;
093
094        @Schema(
095                        description =
096                                        "Specifies the FHIR Resource URL associated with this user session. This value will be used to provide the `fhirUser` claim in returned ID Tokens, and is not used for other purposes.")
097        @JsonProperty("fhirUserUrl")
098        private String myFhirUserUrl;
099
100        /**
101         * Constructor
102         */
103        public UserSessionDetailsJson() {
104                super();
105        }
106
107        /**
108         * Copy Constructor
109         */
110        public UserSessionDetailsJson(UserDetailsJson theCopyObject) {
111                super(theCopyObject);
112
113                if (theCopyObject instanceof UserSessionDetailsJson) {
114                        UserSessionDetailsJson copy = (UserSessionDetailsJson) theCopyObject;
115                        getApprovedScopes().addAll(copy.getApprovedScopes());
116                        getLaunchResourceIds().addAll(copy.getLaunchResourceIds());
117                        getFhirContext().addAll(copy.getFhirContext());
118                        setOidcClientId(copy.getOidcClientId());
119                        setOidcClientModuleId(copy.getOidcClientModuleId());
120                        setOidcClientNodeId(copy.getOidcClientNodeId());
121                        if (copy.myUserData != null) {
122                                myUserData = new HashMap<>(copy.myUserData);
123                        }
124                }
125        }
126
127        /**
128         * Does this session have any approved scopes
129         */
130        @Override
131        public boolean hasApprovedScopes() {
132                return myApprovedScopes != null && !myApprovedScopes.isEmpty();
133        }
134
135        /**
136         * If the session is an OAuth2 session (i.e. it is accessed via a bearer token that
137         * was granted by a SMART Auth server) this field will be populated with the set of
138         * scopes that were approved for the client
139         */
140        @Override
141        public Set<String> getApprovedScopes() {
142                if (myApprovedScopes == null) {
143                        myApprovedScopes = new HashSet<>();
144                }
145                return myApprovedScopes;
146        }
147
148        public void setApprovedScopes(Set<String> theApprovedScopes) {
149                myApprovedScopes = theApprovedScopes;
150        }
151
152        @Operation(
153                        summary = "addUserData",
154                        description =
155                                        "Add user data to the session. Custom user data can be added for use within the system or in interceptors.")
156        public void addUserData(
157                        @Parameter(name = "theKey", description = "The user data key") String theKey,
158                        @Parameter(name = "theValue", description = "The user data value") String theValue) {
159                String key = trim(theKey);
160                String value = trim(theValue);
161                Validate.isTrue(!StringUtils.containsWhitespace(key), "Invalid user data key; must not contain any whitespace");
162                // we pull claims into the user data
163                // so we'll block the user data that has our custom claim prefix so our claims
164                // will not be overwritten
165                Validate.isTrue(
166                                !theKey.startsWith(SmileClaimConstants.SMILE_CLAIM_PREFIX),
167                                "Invalid user data key; cannot start with " + SmileClaimConstants.SMILE_CLAIM_PREFIX);
168                getClientPopulatedUserData(true).put(theKey, value);
169        }
170
171        @Operation(summary = "addApprovedScope", description = "Add an approved scope to the session")
172        public void addApprovedScope(
173                        @Parameter(name = "theScope", description = "The SMART on FHIR/OIDC scope name") String theScope) {
174                String scope = trim(theScope);
175                Validate.isTrue(!StringUtils.containsWhitespace(scope), "Invalid scope, must not contain any whitespace");
176                getApprovedScopes().add(scope);
177        }
178
179        @Operation(
180                        summary = "removeApprovedScope",
181                        description =
182                                        "Remove an approved scope to the session. This method has no effect if the given scope is not in the existing approved scope list.")
183        public void removeApprovedScope(
184                        @Parameter(name = "theScope", description = "The SMART on FHIR/OIDC scope name") String theScope) {
185                String scope = trim(theScope);
186                Validate.isTrue(!StringUtils.containsWhitespace(scope), "Invalid scope, must not contain any whitespace");
187                getApprovedScopes().remove(scope);
188        }
189
190        @Operation(summary = "addLaunchResourceId", description = "Adds a launch context resource id")
191        public void addLaunchResourceId(
192                        @Parameter(
193                                                        name = "theResourceType",
194                                                        description =
195                                                                        "The launch context resource type. Note that this value is not capitalized, e.g. `patient` or `encounter`.")
196                                        String theResourceType,
197                        @Parameter(
198                                                        name = "theResourceId",
199                                                        description = "The resource ID. This value does not include a resource type, e.g. `123`.")
200                                        String theResourceId) {
201                Validate.notBlank(theResourceType, "The resource type must not be null or empty");
202                Validate.notBlank(theResourceId, "The resource ID must not be null or empty");
203                Validate.isTrue(!theResourceId.contains("/"), "The resource ID must not contain '/'");
204
205                getLaunchResourceIds()
206                                .add(new LaunchResourceIdJson().setResourceType(theResourceType).setResourceId(theResourceId));
207        }
208
209        @Operation(
210                        summary = "addFhirContextReference",
211                        description = "Adds a Smart fhirContext entry containing only the reference")
212        public void addFhirContextReference(
213                        @Parameter(name = "theReference", description = "A reference to a FHIR resource in the fhirContext.")
214                                        String theReference) {
215                getFhirContext().add(SmartFhirContextEntryJson.withReferenceOnly(theReference));
216        }
217
218        @Operation(
219                        summary = "addFhirContextReference",
220                        description = "Adds a Smart fhirContext entry containing the reference and role")
221        public void addFhirContextReference(
222                        @Parameter(name = "theReference", description = "A reference to a FHIR resource in the fhirContext.")
223                                        String theReference,
224                        @Parameter(name = "theRole", description = "A reference to a role URI in the fhirContext") String theRole) {
225                getFhirContext().add(SmartFhirContextEntryJson.withReferenceAndRole(theReference, theRole));
226        }
227
228        @Operation(
229                        summary = "getLaunchResourceIds",
230                        description = "Provides the launch context resource IDs associated with this session")
231        public List<LaunchResourceIdJson> getLaunchResourceIds() {
232                if (myLaunchResourceIds == null) {
233                        myLaunchResourceIds = new ArrayList<>();
234                }
235                return myLaunchResourceIds;
236        }
237
238        @Operation(summary = "getFhirContext", description = "Provides the fhirContext entries with this session")
239        public List<SmartFhirContextEntryJson> getFhirContext() {
240                if (myFhirContext == null) {
241                        myFhirContext = new ArrayList<>();
242                }
243                return myFhirContext;
244        }
245
246        @Operation(
247                        summary = "getLaunchResourceIdForResourceType",
248                        description =
249                                        "Provides a single launch context resource ID associated with this session for a given resource type, returning the resource ID (e.g. `123`) or `null` if none are found.")
250        public String getLaunchResourceIdForResourceType(
251                        @Parameter(
252                                                        name = "theResourceType",
253                                                        description =
254                                                                        "The launch context resource type. Note that this value is not capitalized, e.g. `patient` or `encounter`.")
255                                        String theResourceType) {
256                for (LaunchResourceIdJson next : getLaunchResourceIds()) {
257                        if (next.getResourceType().equalsIgnoreCase(theResourceType)) {
258                                return next.getResourceId();
259                        }
260                }
261                return null;
262        }
263
264        @Operation(
265                        summary = "getLaunchResourceIdsForResourceType",
266                        description =
267                                        "Provides the launch context resource IDs associated with this session for a given resource type, returning an array of `LaunchResourceId` objects.")
268        public List<LaunchResourceIdJson> getLaunchResourceIdsForResourceType(
269                        @Parameter(
270                                                        name = "theResourceType",
271                                                        description =
272                                                                        "The launch context resource type. Note that this value is not capitalized, e.g. `patient` or `encounter`.")
273                                        String theResourceType) {
274                List<LaunchResourceIdJson> retVal = new ArrayList<>();
275                for (LaunchResourceIdJson next : getLaunchResourceIds()) {
276                        if (next.getResourceType().equalsIgnoreCase(theResourceType)) {
277                                retVal.add(next);
278                        }
279                }
280                return retVal;
281        }
282
283        @Operation(summary = "addLaunchContextParameter", description = "Adds a launch context parameter name/value pair")
284        public void addLaunchContextParameter(
285                        @Parameter(
286                                                        name = "theParameterName",
287                                                        description =
288                                                                        "The launch context parameter name,e.g. `need_patient_banner` or `smart_style_url`.")
289                                        String theParameterName,
290                        @Parameter(name = "theParameterValue", description = "The parameter value.") String theParameterValue) {
291                Validate.notBlank(theParameterName, "The parameter name must not be null or empty");
292                Validate.notBlank(theParameterValue, "The parameter value must not be null or empty");
293
294                getLaunchContextParameters()
295                                .add(new LaunchContextParameterJson()
296                                                .setParameterName(theParameterName)
297                                                .setParameterValue(theParameterValue));
298        }
299
300        @Operation(
301                        summary = "getLaunchContextParameters",
302                        description = "Provides the launch context parameters associated with this session")
303        public List<LaunchContextParameterJson> getLaunchContextParameters() {
304                if (myLaunchContextParameters == null) {
305                        myLaunchContextParameters = new ArrayList<>();
306                }
307
308                return myLaunchContextParameters;
309        }
310
311        public Optional<String> getLaunchContextParameterValueForParameterName(String theParameterName) {
312                for (LaunchContextParameterJson contextParam : getLaunchContextParameters()) {
313                        if (contextParam.getParameterName().equalsIgnoreCase(theParameterName)) {
314                                return Optional.of(contextParam.getParameterValue());
315                        }
316                }
317
318                return Optional.empty();
319        }
320
321        public void addApprovedScopes(Set<String> theScope) {
322                if (theScope != null && theScope.size() > 0) {
323                        if (myApprovedScopes == null) {
324                                myApprovedScopes = new HashSet<>();
325                        }
326                        myApprovedScopes.addAll(theScope);
327                }
328        }
329
330        /**
331         * This method caches its output!!
332         */
333        public String toJsonString() {
334                String retVal = myJsonString;
335                if (retVal == null) {
336                        retVal = JsonUtil.serialize(this);
337                        myJsonString = retVal;
338                }
339                return retVal;
340        }
341
342        // Required by Javascript
343        @Override
344        public String toString() {
345                return toJsonString();
346        }
347
348        @Override
349        public String getName() {
350                return getUsername();
351        }
352
353        public String getOidcClientId() {
354                return this.myOidcClientId;
355        }
356
357        public void setOidcClientId(String theClientId) {
358                this.myOidcClientId = theClientId;
359        }
360
361        public String getOidcClientNodeId() {
362                return myOidcClientNodeId;
363        }
364
365        public void setOidcClientNodeId(String theOidcNodeId) {
366                myOidcClientNodeId = theOidcNodeId;
367        }
368
369        public String getOidcClientModuleId() {
370                return myOidcClientModuleId;
371        }
372
373        public void setOidcClientModuleId(String theModuleId) {
374                myOidcClientModuleId = theModuleId;
375        }
376
377        public String getFhirUserUrl() {
378                return myFhirUserUrl;
379        }
380
381        public void setFhirUserUrl(String theFhirUserUrl) {
382                myFhirUserUrl = theFhirUserUrl;
383        }
384
385        @Override
386        public Map<String, Object> getClientPopulatedUserData(boolean theCreateIfNull) {
387                if (theCreateIfNull && myUserData == null) {
388                        myUserData = new HashMap<>();
389                }
390                return myUserData;
391        }
392
393        /**
394         * Adds all claims in the provided map as user data
395         */
396        public void addClaimsAsUserData(Map<String, Object> theClaims) {
397                for (Map.Entry<String, Object> claim : theClaims.entrySet()) {
398                        setUserDataInternal(false, claim.getKey(), claim.getValue());
399                }
400        }
401}