001/* 002 * Copyright 2021 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.extrausermgt.users.aad; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.List; 021import java.util.Map; 022 023import org.apache.commons.lang3.StringUtils; 024 025import org.ametys.core.user.directory.NotUniqueUserException; 026import org.ametys.core.user.directory.StoredUser; 027import org.ametys.core.user.directory.UserDirectory; 028import org.ametys.plugins.core.impl.user.directory.AbstractCachingUserDirectory; 029 030import com.azure.identity.ClientSecretCredential; 031import com.azure.identity.ClientSecretCredentialBuilder; 032import com.microsoft.graph.authentication.TokenCredentialAuthProvider; 033import com.microsoft.graph.http.GraphServiceException; 034import com.microsoft.graph.options.HeaderOption; 035import com.microsoft.graph.options.Option; 036import com.microsoft.graph.options.QueryOption; 037import com.microsoft.graph.requests.GraphServiceClient; 038import com.microsoft.graph.requests.UserCollectionPage; 039import com.microsoft.graph.requests.UserCollectionRequest; 040import com.microsoft.graph.requests.UserCollectionRequestBuilder; 041 042/** 043 * {@link UserDirectory} listing users in Azure Active Directory. 044 */ 045public class AADUserDirectory extends AbstractCachingUserDirectory 046{ 047 private GraphServiceClient _graphClient; 048 private String _filter; 049 050 @Override 051 public void init(String id, String udModelId, Map<String, Object> paramValues, String label) throws Exception 052 { 053 super.init(id, udModelId, paramValues, label); 054 055 String clientID = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.appid"); 056 String clientSecret = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.clientsecret"); 057 String tenant = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.tenant"); 058 _filter = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.filter"); 059 060 ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder().clientId(clientID) 061 .clientSecret(clientSecret) 062 .tenantId(tenant) 063 .build(); 064 065 TokenCredentialAuthProvider tokenCredentialAuthProvider = new TokenCredentialAuthProvider(clientSecretCredential); 066 _graphClient = GraphServiceClient.builder() 067 .authenticationProvider(tokenCredentialAuthProvider) 068 .buildClient(); 069 070 createCaches(); 071 } 072 073 @Override 074 protected String getCacheTypeLabel() 075 { 076 return "AzureAD"; 077 } 078 079 public Collection<StoredUser> getStoredUsers() 080 { 081 return getStoredUsers(-1, 0, null); 082 } 083 084 public List<StoredUser> getStoredUsers(int count, int offset, Map<String, Object> parameters) 085 { 086 List<Option> options = new ArrayList<>(); 087 options.add(new HeaderOption("ConsistencyLevel", "eventual")); 088 089 String pattern = parameters != null ? (String) parameters.get("pattern") : null; 090 091 if (StringUtils.isNotEmpty(pattern)) 092 { 093 options.add(new QueryOption("$search", "\"givenName:" + pattern + "\" OR \"surname:" + pattern + "\" OR \"userPrincipalName:" + pattern + "\"")); 094 } 095 096 UserCollectionRequest userCollectionRequest = _graphClient.users().buildRequest(options); 097 098 int maxUsers = -1; 099 if (count > 0 && count < Integer.MAX_VALUE) 100 { 101 maxUsers = count; 102 userCollectionRequest.top(count + offset); 103 } 104 105 if (StringUtils.isNotEmpty(_filter)) 106 { 107 userCollectionRequest.filter(_filter); 108 } 109 110 UserCollectionPage userCollectionPage = userCollectionRequest.count().get(); 111 112 List<StoredUser> result = new ArrayList<>(); 113 _handlePage(userCollectionPage, result, maxUsers, offset); 114 115 return result; 116 } 117 118 private void _handlePage(UserCollectionPage userCollectionPage, List<StoredUser> storedUsers, int maxUsers, int offset) 119 { 120 int currentOffset = offset; 121 int currentCount = maxUsers; 122 for (com.microsoft.graph.models.User u : userCollectionPage.getCurrentPage()) 123 { 124 if (currentOffset > 0) 125 { 126 currentOffset--; 127 } 128 else 129 { 130 StoredUser storedUser = new StoredUser(u.userPrincipalName, u.surname, u.givenName, u.mail); 131 storedUsers.add(storedUser); 132 133 if (isCachingEnabled()) 134 { 135 getCacheByLogin().put(storedUser.getIdentifier(), storedUser); 136 } 137 138 if (currentCount > 0) 139 { 140 currentCount--; 141 if (currentCount == 0) 142 { 143 break; 144 } 145 } 146 } 147 } 148 149 if (currentCount != 0) 150 { 151 UserCollectionRequestBuilder nextPage = userCollectionPage.getNextPage(); 152 if (nextPage != null) 153 { 154 _handlePage(nextPage.buildRequest(new HeaderOption("ConsistencyLevel", "eventual")).get(), storedUsers, currentCount, currentOffset); 155 } 156 } 157 } 158 159 public StoredUser getStoredUser(String login) 160 { 161 if (isCachingEnabled() && getCacheByLogin().hasKey(login)) 162 { 163 StoredUser storedUser = getCacheByLogin().get(login); 164 return storedUser; 165 } 166 167 StoredUser storedUser = null; 168 try 169 { 170 com.microsoft.graph.models.User u = _graphClient.users(login).buildRequest().select("userPrincipalName, surname, givenName, mail").get(); 171 172 storedUser = new StoredUser(u.userPrincipalName, u.surname, u.givenName, u.mail); 173 174 if (isCachingEnabled()) 175 { 176 getCacheByLogin().put(storedUser.getIdentifier(), storedUser); 177 } 178 } 179 catch (GraphServiceException e) 180 { 181 getLogger().warn("Unable to retrieve user '{}' from AzureAD", login, e); 182 } 183 184 return storedUser; 185 } 186 187 /* 188 * As we do not know how to search for email in a "case insensitive" way, we also fill the cache "case sensitively" 189 */ 190 public StoredUser getStoredUserByEmail(String email) throws NotUniqueUserException 191 { 192 if (StringUtils.isBlank(email)) 193 { 194 return null; 195 } 196 197 if (isCachingEnabled() && getCacheByMail().hasKey(email)) 198 { 199 StoredUser storedUser = getCacheByMail().get(email); 200 return storedUser; 201 } 202 203 List<com.microsoft.graph.models.User> users = _graphClient.users().buildRequest(new HeaderOption("ConsistencyLevel", "eventual")) 204 .filter("mail eq '" + email + "'") 205 .select("userPrincipalName, surname, givenName, mail") 206 .get() 207 .getCurrentPage(); 208 209 if (users.size() == 1) 210 { 211 com.microsoft.graph.models.User u = users.get(0); 212 StoredUser storedUser = new StoredUser(u.userPrincipalName, u.surname, u.givenName, u.mail); 213 214 if (isCachingEnabled()) 215 { 216 getCacheByMail().put(storedUser.getEmail(), storedUser); 217 } 218 219 return storedUser; 220 } 221 else if (users.isEmpty()) 222 { 223 return null; 224 } 225 else 226 { 227 throw new NotUniqueUserException("Find " + users.size() + " users matching the email " + email); 228 } 229 } 230 231 public CredentialsResult checkCredentials(String login, String password) 232 { 233 throw new UnsupportedOperationException("The AADUserDirectory cannot authenticate users"); 234 } 235}