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