Skip to content

Commit 9d6c395

Browse files
author
M
authored
Support to get transitive groups for authorization (Azure#17162)
* Fix spring readme broken link; Support to get transitive groups for authorization, https://github.com/MicrosoftDocs/azure-dev-docs/issues/329
1 parent 6a8a57a commit 9d6c395

File tree

6 files changed

+114
-4
lines changed

6 files changed

+114
-4
lines changed

sdk/spring/azure-spring-boot-starter-active-directory/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ The authorization flow is composed of 3 phrases:
3434
* Get On-Behalf-Of token and membership info from Azure AD Graph API
3535
* Evaluate the permission based on membership info to grant or deny access
3636

37+
### Group membership
38+
The way to obtain group relationship that will determine which graph api will be used. You can change it using the `azure.activedirectory.user-group.group-relationship` configuration.
39+
* **direct**: the default value, get groups that the user is a direct member of. For details, see [list memberOf][graph-api-list-member-of] api.
40+
* **transitive**: Get groups that the user is a member of, and will also return all groups the user is a nested member of. For details, see [list transitive memberOf][graph-api-list-transitive-member-of] api.
41+
3742
### Authenticate in frontend
3843
Sends bearer authorization code to backend, in backend a Spring Security filter `AADAuthenticationFilter` validates the Jwt token from Azure AD and save authentication. The Jwt token is also used to acquire a On-Behalf-Of token for Azure AD Graph API so that authenticated user's membership information is available for authorization of access of API resources.
3944
Below is a diagram that shows the layers and typical flow for Single Page Application with Spring Boot web API backend that uses the filter for Authentication and Authorization.
@@ -278,3 +283,6 @@ Please follow [instructions here](https://github.com/Azure/azure-sdk-for-java/bl
278283
[logging]: https://github.com/Azure/azure-sdk-for-java/wiki/Logging-with-Azure-SDK#use-logback-logging-framework-in-a-spring-boot-application
279284
[azure_subscription]: https://azure.microsoft.com/free
280285
[jdk_link]: https://docs.microsoft.com/java/azure/jdk/?view=azure-java-stable
286+
287+
[graph-api-list-member-of]: https://docs.microsoft.com/graph/api/user-list-memberof?view=graph-rest-1.0
288+
[graph-api-list-transitive-member-of]: https://docs.microsoft.com/graph/api/user-list-transitivememberof?view=graph-rest-1.0

sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public class AADAuthenticationProperties {
3030
private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationProperties.class);
3131
private static final String DEFAULT_SERVICE_ENVIRONMENT = "global";
3232
private static final long DEFAULT_JWK_SET_CACHE_LIFESPAN = TimeUnit.MINUTES.toMillis(5);
33+
private static final String GROUP_RELATIONSHIP_DIRECT = "direct";
34+
private static final String GROUP_RELATIONSHIP_TRANSITIVE = "transitive";
3335

3436
/**
3537
* Default UserGroup configuration.
@@ -112,6 +114,7 @@ public class AADAuthenticationProperties {
112114
public List<String> getActiveDirectoryGroups() {
113115
return userGroup.getAllowedGroups();
114116
}
117+
115118
/**
116119
* Properties dedicated to changing the behavior of how the groups are mapped from the Azure AD response. Depending
117120
* on the graph API used the object will not be the same.
@@ -144,6 +147,16 @@ public static class UserGroupProperties {
144147
@NotEmpty
145148
private String objectIDKey = "objectId";
146149

150+
151+
/**
152+
* The way to obtain group relationship.<br/>
153+
* direct: the default value, get groups that the user is a direct member of;<br/>
154+
* transitive: Get groups that the user is a member of, and will also return all
155+
* groups the user is a nested member of;
156+
*/
157+
@NotEmpty
158+
private String groupRelationship = GROUP_RELATIONSHIP_DIRECT;
159+
147160
public List<String> getAllowedGroups() {
148161
return allowedGroups;
149162
}
@@ -176,13 +189,22 @@ public void setObjectIDKey(String objectIDKey) {
176189
this.objectIDKey = objectIDKey;
177190
}
178191

192+
public String getGroupRelationship() {
193+
return groupRelationship;
194+
}
195+
196+
public void setGroupRelationship(String groupRelationship) {
197+
this.groupRelationship = groupRelationship;
198+
}
199+
179200
@Override
180201
public String toString() {
181202
return "UserGroupProperties{"
182203
+ "allowedGroups=" + allowedGroups
183204
+ ", key='" + key + '\''
184205
+ ", value='" + value + '\''
185206
+ ", objectIDKey='" + objectIDKey + '\''
207+
+ ", groupRelationship='" + groupRelationship + '\''
186208
+ '}';
187209
}
188210

@@ -198,7 +220,8 @@ public boolean equals(Object o) {
198220
return Objects.equals(allowedGroups, that.allowedGroups)
199221
&& Objects.equals(key, that.key)
200222
&& Objects.equals(value, that.value)
201-
&& Objects.equals(objectIDKey, that.objectIDKey);
223+
&& Objects.equals(objectIDKey, that.objectIDKey)
224+
&& Objects.equals(groupRelationship, that.groupRelationship);
202225
}
203226

204227
@Override
@@ -230,6 +253,11 @@ public void validateUserGroupProperties() {
230253
throw new IllegalArgumentException("One of the User Group Properties must be populated. "
231254
+ "Please populate azure.activedirectory.user-group.allowed-groups");
232255
}
256+
if (!GROUP_RELATIONSHIP_DIRECT.equalsIgnoreCase(userGroup.groupRelationship)
257+
&& !GROUP_RELATIONSHIP_TRANSITIVE.equalsIgnoreCase(userGroup.groupRelationship)) {
258+
throw new IllegalArgumentException("Configuration 'azure.activedirectory.user-group.group-relationship' "
259+
+ "should be 'direct' or 'transitive'.");
260+
}
233261
}
234262

235263
public UserGroupProperties getUserGroup() {
@@ -349,6 +377,14 @@ public void setSessionStateless(Boolean sessionStateless) {
349377
this.sessionStateless = sessionStateless;
350378
}
351379

380+
public static String getDirectGroupRelationship() {
381+
return GROUP_RELATIONSHIP_DIRECT;
382+
}
383+
384+
public static String getTransitiveGroupRelationship() {
385+
return GROUP_RELATIONSHIP_TRANSITIVE;
386+
}
387+
352388
public boolean isAllowedGroup(String group) {
353389
return Optional.ofNullable(getUserGroup())
354390
.map(UserGroupProperties::getAllowedGroups)

sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureADGraphClient.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private static String getResponseString(HttpURLConnection connection) throws IOE
119119
public Set<String> getGroups(String graphApiToken) throws IOException {
120120
final Set<String> groups = new LinkedHashSet<>();
121121
final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance();
122-
String aadMembershipRestUri = serviceEndpoints.getAadMembershipRestUri();
122+
String aadMembershipRestUri = getAadMembershipRestUri();
123123
while (aadMembershipRestUri != null) {
124124
String membershipsJson = getUserMemberships(graphApiToken, aadMembershipRestUri);
125125
Memberships memberships = objectMapper.readValue(membershipsJson, Memberships.class);
@@ -136,6 +136,20 @@ public Set<String> getGroups(String graphApiToken) throws IOException {
136136
return groups;
137137
}
138138

139+
/**
140+
* Get the rest url to get the groups that the user is a member of.
141+
* @return rest url
142+
*/
143+
private String getAadMembershipRestUri() {
144+
if (AADAuthenticationProperties.getDirectGroupRelationship()
145+
.equalsIgnoreCase(aadAuthenticationProperties
146+
.getUserGroup().getGroupRelationship())) {
147+
return serviceEndpoints.getAadMembershipRestUri();
148+
} else {
149+
return serviceEndpoints.getAadTransitiveMemberRestUri();
150+
}
151+
}
152+
139153
private boolean isGroupObject(final Membership membership) {
140154
return membership.getObjectType().equals(aadAuthenticationProperties.getUserGroup().getValue());
141155
}

sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/ServiceEndpoints.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class ServiceEndpoints {
1212
private String aadGraphApiUri;
1313
private String aadKeyDiscoveryUri;
1414
private String aadMembershipRestUri;
15+
private String aadTransitiveMemberRestUri;
1516

1617
public String getAadSigninUri() {
1718
return aadSigninUri;
@@ -44,4 +45,12 @@ public String getAadMembershipRestUri() {
4445
public void setAadMembershipRestUri(String aadMembershipRestUri) {
4546
this.aadMembershipRestUri = aadMembershipRestUri;
4647
}
48+
49+
public String getAadTransitiveMemberRestUri() {
50+
return aadTransitiveMemberRestUri;
51+
}
52+
53+
public void setAadTransitiveMemberRestUri(String aadTransitiveMemberRestUri) {
54+
this.aadTransitiveMemberRestUri = aadTransitiveMemberRestUri;
55+
}
4756
}

sdk/spring/azure-spring-boot/src/main/resources/service-endpoints.properties

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@ azure.service.endpoints.cn.aadSigninUri=https://login.partner.microsoftonline.cn
22
azure.service.endpoints.cn.aadGraphApiUri=https://graph.chinacloudapi.cn/
33
azure.service.endpoints.cn.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys
44
azure.service.endpoints.cn.aadMembershipRestUri=https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6
5+
azure.service.endpoints.cn.aadTransitiveMemberRestUri=https://graph.chinacloudapi.cn\
6+
/me/transitiveMemberOf?api-version=1.6
57
azure.service.endpoints.cn-v2-graph.aadSigninUri=https://login.partner.microsoftonline.cn/
68
azure.service.endpoints.cn-v2-graph.aadGraphApiUri=https://microsoftgraph.chinacloudapi.cn/
79
azure.service.endpoints.cn-v2-graph.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys
810
azure.service.endpoints.cn-v2-graph.aadMembershipRestUri=https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf
11+
azure.service.endpoints.cn-v2-graph.aadTransitiveMemberRestUri=https://microsoftgraph.chinacloudapi.cn\
12+
/v1.0/me/transitiveMemberOf
913
azure.service.endpoints.global.aadSigninUri=https://login.microsoftonline.com/
1014
azure.service.endpoints.global.aadGraphApiUri=https://graph.windows.net/
1115
azure.service.endpoints.global.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/
1216
azure.service.endpoints.global.aadMembershipRestUri=https://graph.windows.net/me/memberOf?api-version=1.6
17+
azure.service.endpoints.global.aadTransitiveMemberRestUri=https://graph.windows.net\
18+
/me/transitiveMemberOf?api-version=1.6
1319
azure.service.endpoints.global-v2-graph.aadSigninUri=https://login.microsoftonline.com/
1420
azure.service.endpoints.global-v2-graph.aadGraphApiUri=https://graph.microsoft.com/
1521
azure.service.endpoints.global-v2-graph.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/
1622
azure.service.endpoints.global-v2-graph.aadMembershipRestUri=https://graph.microsoft.com/v1.0/me/memberOf
23+
azure.service.endpoints.global-v2-graph.aadTransitiveMemberRestUri=https://graph.microsoft.com\
24+
/v1.0/me/transitiveMemberOf

sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,16 @@ public void setup() {
7878
serviceEndpointsProperties = new ServiceEndpointsProperties();
7979
final ServiceEndpoints serviceEndpoints = new ServiceEndpoints();
8080
serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf");
81+
serviceEndpoints.setAadTransitiveMemberRestUri("http://localhost:9519/transitiveMemberOf");
8182
serviceEndpointsProperties.getEndpoints().put("global-v2-graph", serviceEndpoints);
8283
}
8384

8485
@Test
8586
public void getAuthoritiesByUserGroups() throws Exception {
87+
aadAuthenticationProperties.getUserGroup().setGroupRelationship("direct");
8688
aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("group1"));
89+
serviceEndpointsProperties.getServiceEndpoints("global-v2-graph")
90+
.setAadMembershipRestUri("http://localhost:9519/memberOf");
8791
this.graphClientMock = new AzureADGraphClient(aadAuthenticationProperties, serviceEndpointsProperties);
8892

8993
stubFor(get(urlEqualTo("/memberOf"))
@@ -104,8 +108,11 @@ public void getAuthoritiesByUserGroups() throws Exception {
104108
}
105109

106110
@Test
107-
public void getGroups() throws Exception {
108-
aadAuthenticationProperties.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3"));
111+
public void getDirectGroups() throws Exception {
112+
aadAuthenticationProperties.getUserGroup().setGroupRelationship("direct");
113+
AADAuthenticationProperties.UserGroupProperties userGroupProperties = aadAuthenticationProperties.getUserGroup();
114+
userGroupProperties.setAllowedGroups(Arrays.asList("group1", "group2", "group3"));
115+
aadAuthenticationProperties.setUserGroup(userGroupProperties);
109116
this.graphClientMock = new AzureADGraphClient(aadAuthenticationProperties, serviceEndpointsProperties);
110117

111118
stubFor(get(urlEqualTo("/memberOf"))
@@ -128,6 +135,34 @@ public void getGroups() throws Exception {
128135
.withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)));
129136
}
130137

138+
@Test
139+
public void getTransitiveGroups() throws Exception {
140+
aadAuthenticationProperties.getUserGroup().setGroupRelationship("transitive");
141+
AADAuthenticationProperties.UserGroupProperties userGroupProperties = aadAuthenticationProperties.getUserGroup();
142+
userGroupProperties.setAllowedGroups(Arrays.asList("group1", "group2", "group3"));
143+
aadAuthenticationProperties.setUserGroup(userGroupProperties);
144+
this.graphClientMock = new AzureADGraphClient(aadAuthenticationProperties, serviceEndpointsProperties);
145+
146+
stubFor(get(urlEqualTo("/transitiveMemberOf"))
147+
.withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))
148+
.willReturn(aResponse()
149+
.withStatus(200)
150+
.withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
151+
.withBody(userGroupsJson)));
152+
153+
final Collection<? extends GrantedAuthority> authorities = graphClientMock
154+
.getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN);
155+
156+
assertThat(authorities)
157+
.isNotEmpty()
158+
.extracting(GrantedAuthority::getAuthority)
159+
.containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3");
160+
161+
verify(getRequestedFor(urlMatching("/transitiveMemberOf"))
162+
.withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken)))
163+
.withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)));
164+
}
165+
131166
@Test
132167
public void userPrincipalIsSerializable() throws ParseException, IOException, ClassNotFoundException {
133168
final File tmpOutputFile = File.createTempFile("test-user-principal", "txt");

0 commit comments

Comments
 (0)