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