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}