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}