19
19
import com .google .common .base .Suppliers ;
20
20
import com .microsoft .graph .http .GraphServiceException ;
21
21
import com .microsoft .graph .models .Group ;
22
+ import com .microsoft .graph .models .ProfilePhoto ;
22
23
import com .microsoft .graph .options .Option ;
23
24
import com .microsoft .graph .options .QueryOption ;
24
25
import com .microsoft .graph .requests .GraphServiceClient ;
25
26
import com .microsoft .graph .requests .GroupCollectionPage ;
27
+ import com .microsoft .graph .requests .ProfilePhotoRequestBuilder ;
28
+ import com .microsoft .jenkins .azuread .avatar .EntraAvatarProperty ;
26
29
import com .microsoft .jenkins .azuread .scribe .AzureAdApi ;
27
30
import com .microsoft .jenkins .azuread .utils .UUIDValidator ;
28
31
import com .thoughtworks .xstream .converters .Converter ;
47
50
import hudson .util .Secret ;
48
51
import io .jenkins .plugins .azuresdk .HttpClientRetriever ;
49
52
53
+ import java .io .File ;
54
+ import java .nio .file .Files ;
55
+ import java .nio .file .StandardCopyOption ;
50
56
import javax .servlet .http .HttpSession ;
51
57
52
58
import jenkins .model .Jenkins ;
53
59
import jenkins .security .SecurityListener ;
60
+ import jenkins .util .SystemProperties ;
54
61
import okhttp3 .Request ;
55
62
import org .apache .commons .lang3 .RandomStringUtils ;
56
63
import org .apache .commons .lang3 .StringUtils ;
@@ -146,8 +153,8 @@ public AccessToken getAccessToken() {
146
153
tokenRequestContext .setScopes (singletonList (graphResource + ".default" ));
147
154
148
155
AccessToken accessToken = ("Certificate" .equals (credentialType ) ? getClientCertificateCredential () : getClientSecretCredential ())
149
- .getToken (tokenRequestContext )
150
- .block ();
156
+ .getToken (tokenRequestContext )
157
+ .block ();
151
158
152
159
if (accessToken == null ) {
153
160
throw new IllegalStateException ("Access token null when it is required" );
@@ -185,6 +192,7 @@ ClientCertificateCredential getClientCertificateCredential() {
185
192
.httpClient (HttpClientRetriever .get ())
186
193
.build ();
187
194
}
195
+
188
196
public boolean isPromptAccount () {
189
197
return promptAccount ;
190
198
}
@@ -230,6 +238,7 @@ public String getClientCertificateSecret() {
230
238
public String getCredentialType () {
231
239
return credentialType ;
232
240
}
241
+
233
242
public String getTenantSecret () {
234
243
return tenant .getEncryptedValue ();
235
244
}
@@ -465,9 +474,14 @@ public HttpResponse doFinishLogin(StaplerRequest request)
465
474
466
475
// Enforce updating current identity
467
476
SecurityContextHolder .getContext ().setAuthentication (auth );
468
- updateIdentity (auth .getAzureAdUser (), User .current ());
477
+ User currentUser = User .current ();
478
+ updateIdentity (auth .getAzureAdUser (), currentUser );
469
479
470
480
SecurityListener .fireAuthenticated2 (userDetails );
481
+
482
+ if (!isDisableGraphIntegration ()) {
483
+ updateAvatar (userDetails , currentUser );
484
+ }
471
485
} catch (Exception ex ) {
472
486
LOGGER .log (Level .SEVERE , "error" , ex );
473
487
throw ex ;
@@ -480,6 +494,50 @@ public HttpResponse doFinishLogin(StaplerRequest request)
480
494
}
481
495
}
482
496
497
+ private void updateAvatar (AzureAdUser userDetails , User currentUser ) {
498
+ if (currentUser == null ) {
499
+ return ;
500
+ }
501
+ try {
502
+ if (SystemProperties .getBoolean (AzureSecurityRealm .class .getName () + ".disableAvatar" , false )) {
503
+ return ;
504
+ }
505
+ ProfilePhotoRequestBuilder photosRequestBuilder = getAzureClient ()
506
+ .users (userDetails .getObjectID ()).photos ("48x48" );
507
+ LOGGER .finest ("Fetching avatar metadata" );
508
+ ProfilePhoto profilePhoto = photosRequestBuilder .buildRequest ().get ();
509
+ LOGGER .finest ("Completed fetching avatar metadata" );
510
+ if (profilePhoto != null ) {
511
+ LOGGER .finest ("Fetching avatar" );
512
+ try (InputStream inputStream = photosRequestBuilder .content ().buildRequest ().get ()) {
513
+ if (inputStream != null ) {
514
+ String mediaContentType = profilePhoto .additionalDataManager ().get ("@odata.mediaContentType" )
515
+ .getAsString ();
516
+ EntraAvatarProperty .AvatarImage avatarImage = new EntraAvatarProperty .AvatarImage (
517
+ mediaContentType
518
+ );
519
+ EntraAvatarProperty entraAvatarProperty = new EntraAvatarProperty (avatarImage );
520
+ File targetFile = new File (currentUser .getUserFolder (), "entra-avatar." + avatarImage .getFilenameSuffix ());
521
+
522
+ Files .copy (
523
+ inputStream ,
524
+ targetFile .toPath (),
525
+ StandardCopyOption .REPLACE_EXISTING );
526
+ currentUser .addProperty (entraAvatarProperty );
527
+ LOGGER .finest ("Saved avatar" );
528
+ }
529
+
530
+ } catch (IOException e ) {
531
+ LOGGER .log (Level .WARNING , "Failed to save profile photo for %s" .formatted (currentUser .getId ()), e );
532
+ }
533
+ } else {
534
+ LOGGER .finest ("No avatar found" );
535
+ }
536
+ } catch (GraphServiceException e ) {
537
+ LOGGER .log (e .getResponseCode () == 404 ? Level .FINER : Level .WARNING , "Failed to get profile photo for %s" .formatted (currentUser .getId ()), e );
538
+ }
539
+ }
540
+
483
541
JwtClaims validateIdToken (String expectedNonce , String idToken ) throws InvalidJwtException {
484
542
JwtClaims claims = getJwtConsumer ().processToClaims (idToken );
485
543
final String responseNonce = (String ) claims .getClaimValue ("nonce" );
@@ -878,7 +936,7 @@ private void updateIdentity(final AzureAdUser azureAdUser, final User u) {
878
936
if (StringUtils .isNotBlank (azureAdUser .getEmail ())) {
879
937
UserProperty existing = u .getProperty (UserProperty .class );
880
938
if (existing == null || !existing .hasExplicitlyConfiguredAddress ()) {
881
- u .addProperty (new Mailer .UserProperty (azureAdUser .getEmail ()));
939
+ u .addProperty (new Mailer .UserProperty (azureAdUser .getEmail ()));
882
940
}
883
941
}
884
942
} catch (IOException e ) {
0 commit comments