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}