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.Collections; 021import java.util.List; 022import java.util.Map; 023import java.util.concurrent.atomic.AtomicInteger; 024 025import org.apache.commons.lang3.StringUtils; 026 027import org.ametys.core.user.directory.NotUniqueUserException; 028import org.ametys.core.user.directory.StoredUser; 029import org.ametys.core.user.directory.UserDirectory; 030import org.ametys.plugins.core.impl.user.directory.AbstractCachingUserDirectory; 031 032import com.azure.identity.ClientSecretCredential; 033import com.azure.identity.ClientSecretCredentialBuilder; 034import com.microsoft.graph.core.tasks.PageIterator; 035import com.microsoft.graph.models.User; 036import com.microsoft.graph.models.UserCollectionResponse; 037import com.microsoft.graph.serviceclient.GraphServiceClient; 038 039/** 040 * {@link UserDirectory} listing users in Azure Active Directory. 041 */ 042public class AADUserDirectory extends AbstractCachingUserDirectory 043{ 044 private static final String[] __USER_ATTRIBUTES_SELECT = new String[]{"userPrincipalName", "surname", "givenName", "mail"}; 045 046 private GraphServiceClient _graphClient; 047 private String _filter; 048 049 @Override 050 public void init(String id, String udModelId, Map<String, Object> paramValues, String label) throws Exception 051 { 052 super.init(id, udModelId, paramValues, label); 053 054 String clientID = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.appid"); 055 String clientSecret = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.clientsecret"); 056 String tenant = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.tenant"); 057 _filter = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.filter"); 058 059 ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder().clientId(clientID) 060 .clientSecret(clientSecret) 061 .tenantId(tenant) 062 .build(); 063 064 _graphClient = new GraphServiceClient(clientSecretCredential); 065 066 createCaches(); 067 } 068 069 @Override 070 protected String getCacheTypeLabel() 071 { 072 return "AzureAD"; 073 } 074 075 public Collection<StoredUser> getStoredUsers() 076 { 077 return getStoredUsers(-1, 0, null); 078 } 079 080 public List<StoredUser> getStoredUsers(int count, int offset, Map<String, Object> parameters) 081 { 082 UserCollectionResponse userCollectionResponse = _graphClient.users().get(requestConfiguration -> { 083 requestConfiguration.headers.add("ConsistencyLevel", "eventual"); 084 085 String pattern = parameters != null ? (String) parameters.get("pattern") : null; 086 087 if (StringUtils.isNotEmpty(pattern)) 088 { 089 requestConfiguration.queryParameters.search = "\"givenName:" + pattern + "\" OR \"surname:" + pattern + "\" OR \"userPrincipalName:" + pattern + "\""; 090 } 091 092 if (count > 0 && count < Integer.MAX_VALUE) 093 { 094 requestConfiguration.queryParameters.top = Math.min(count + offset, 999); // try to do only one request to Graph API 095 } 096 097 if (StringUtils.isNotEmpty(_filter)) 098 { 099 requestConfiguration.queryParameters.filter = _filter; 100 } 101 102 requestConfiguration.queryParameters.select = __USER_ATTRIBUTES_SELECT; 103 }); 104 105 List<StoredUser> result = new ArrayList<>(); 106 AtomicInteger offsetCounter = new AtomicInteger(offset); // use AtomicInteger to be able to decrement directly in the below lambda 107 108 try 109 { 110 new PageIterator.Builder<User, UserCollectionResponse>() 111 .client(_graphClient) 112 .collectionPage(userCollectionResponse) 113 .collectionPageFactory(UserCollectionResponse::createFromDiscriminatorValue) 114 .processPageItemCallback(user -> { 115 if (offsetCounter.decrementAndGet() <= 0) 116 { 117 _handleUser(user, result); 118 } 119 120 return count <= 0 || result.size() < count; 121 }) 122 .build() 123 .iterate(); 124 } 125 catch (Exception e) 126 { 127 getLogger().error("Error while fetching users from Entra ID", e); 128 return Collections.emptyList(); 129 } 130 131 return result; 132 } 133 134 private void _handleUser(User user, List<StoredUser> storedUsers) 135 { 136 StoredUser storedUser = new StoredUser(user.getUserPrincipalName(), user.getSurname(), user.getGivenName(), user.getMail()); 137 storedUsers.add(storedUser); 138 139 if (isCachingEnabled()) 140 { 141 getCacheByLogin().put(storedUser.getIdentifier(), storedUser); 142 } 143 } 144 145 public StoredUser getStoredUser(String login) 146 { 147 if (isCachingEnabled() && getCacheByLogin().hasKey(login)) 148 { 149 StoredUser storedUser = getCacheByLogin().get(login); 150 return storedUser; 151 } 152 153 StoredUser storedUser = null; 154 try 155 { 156 User user = _graphClient.users().byUserId(login).get(requestConfiguration -> { 157 requestConfiguration.queryParameters.select = __USER_ATTRIBUTES_SELECT; 158 }); 159 160 storedUser = new StoredUser(user.getUserPrincipalName(), user.getSurname(), user.getGivenName(), user.getMail()); 161 162 if (isCachingEnabled()) 163 { 164 getCacheByLogin().put(storedUser.getIdentifier(), storedUser); 165 } 166 } 167 catch (Exception e) 168 { 169 getLogger().warn("Unable to retrieve user '{}' from AzureAD", login, e); 170 } 171 172 return storedUser; 173 } 174 175 /* 176 * As we do not know how to search for email in a "case insensitive" way, we also fill the cache "case sensitively" 177 */ 178 public StoredUser getStoredUserByEmail(String email) throws NotUniqueUserException 179 { 180 if (StringUtils.isBlank(email)) 181 { 182 return null; 183 } 184 185 if (isCachingEnabled() && getCacheByMail().hasKey(email)) 186 { 187 StoredUser storedUser = getCacheByMail().get(email); 188 return storedUser; 189 } 190 191 List<User> users = _graphClient.users().get(requestConfiguration -> { 192 requestConfiguration.headers.add("ConsistencyLevel", "eventual"); 193 requestConfiguration.queryParameters.filter = "mail eq '" + email + "'"; 194 requestConfiguration.queryParameters.select = __USER_ATTRIBUTES_SELECT; 195 }).getValue(); 196 197 if (users.size() == 1) 198 { 199 User u = users.get(0); 200 StoredUser storedUser = new StoredUser(u.getUserPrincipalName(), u.getSurname(), u.getGivenName(), u.getMail()); 201 202 if (isCachingEnabled()) 203 { 204 getCacheByMail().put(storedUser.getEmail(), storedUser); 205 } 206 207 return storedUser; 208 } 209 else if (users.isEmpty()) 210 { 211 return null; 212 } 213 else 214 { 215 throw new NotUniqueUserException("Find " + users.size() + " users matching the email " + email); 216 } 217 } 218 219 public CredentialsResult checkCredentials(String login, String password) 220 { 221 throw new UnsupportedOperationException("The AADUserDirectory cannot authenticate users"); 222 } 223}