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.User; 026import org.ametys.core.user.UserIdentity; 027import org.ametys.core.user.directory.NotUniqueUserException; 028import org.ametys.core.user.directory.UserDirectory; 029import org.ametys.plugins.core.impl.user.directory.AbstractCachingUserDirectory; 030 031import com.azure.identity.ClientSecretCredential; 032import com.azure.identity.ClientSecretCredentialBuilder; 033import com.microsoft.graph.authentication.TokenCredentialAuthProvider; 034import com.microsoft.graph.http.GraphServiceException; 035import com.microsoft.graph.options.HeaderOption; 036import com.microsoft.graph.options.Option; 037import com.microsoft.graph.options.QueryOption; 038import com.microsoft.graph.requests.GraphServiceClient; 039import com.microsoft.graph.requests.UserCollectionPage; 040import com.microsoft.graph.requests.UserCollectionRequest; 041import com.microsoft.graph.requests.UserCollectionRequestBuilder; 042 043/** 044 * {@link UserDirectory} listing users in Azure Active Directory. 045 */ 046public class AADUserDirectory extends AbstractCachingUserDirectory 047{ 048 private GraphServiceClient _graphClient; 049 private String _filter; 050 051 @Override 052 public void init(String id, String udModelId, Map<String, Object> paramValues, String label) throws Exception 053 { 054 super.init(id, udModelId, paramValues, label); 055 056 String clientID = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.appid"); 057 String clientSecret = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.clientsecret"); 058 String tenant = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.tenant"); 059 _filter = (String) paramValues.get("org.ametys.plugins.extrausermgt.users.aad.filter"); 060 061 ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder().clientId(clientID) 062 .clientSecret(clientSecret) 063 .tenantId(tenant) 064 .build(); 065 066 TokenCredentialAuthProvider tokenCredentialAuthProvider = new TokenCredentialAuthProvider(clientSecretCredential); 067 _graphClient = GraphServiceClient.builder() 068 .authenticationProvider(tokenCredentialAuthProvider) 069 .buildClient(); 070 071 createCaches(); 072 } 073 074 @Override 075 protected String getCacheTypeLabel() 076 { 077 return "AzureAD"; 078 } 079 080 public Collection<User> getUsers() 081 { 082 return getUsers(-1, 0, null); 083 } 084 085 public List<User> getUsers(int count, int offset, Map<String, Object> parameters) 086 { 087 List<Option> options = new ArrayList<>(); 088 options.add(new HeaderOption("ConsistencyLevel", "eventual")); 089 090 String pattern = parameters != null ? (String) parameters.get("pattern") : null; 091 092 if (StringUtils.isNotEmpty(pattern)) 093 { 094 options.add(new QueryOption("$search", "\"givenName:" + pattern + "\" OR \"surname:" + pattern + "\" OR \"userPrincipalName:" + pattern + "\"")); 095 } 096 097 UserCollectionRequest userCollectionRequest = _graphClient.users().buildRequest(options); 098 099 int maxUsers = -1; 100 if (count > 0 && count < Integer.MAX_VALUE) 101 { 102 maxUsers = count; 103 userCollectionRequest.top(count + offset); 104 } 105 106 if (StringUtils.isNotEmpty(_filter)) 107 { 108 userCollectionRequest.filter(_filter); 109 } 110 111 UserCollectionPage userCollectionPage = userCollectionRequest.count().get(); 112 113 List<User> result = new ArrayList<>(); 114 _handlePage(userCollectionPage, result, maxUsers, offset); 115 116 return result; 117 } 118 119 private void _handlePage(UserCollectionPage userCollectionPage, List<User> users, int maxUsers, int offset) 120 { 121 int currentOffset = offset; 122 int currentCount = maxUsers; 123 for (com.microsoft.graph.models.User u : userCollectionPage.getCurrentPage()) 124 { 125 if (currentOffset > 0) 126 { 127 currentOffset--; 128 } 129 else 130 { 131 User user = new User(new UserIdentity(u.userPrincipalName, getPopulationId()), u.surname, u.givenName, u.mail, this); 132 users.add(user); 133 134 if (isCachingEnabled()) 135 { 136 getCacheByLogin().put(user.getIdentity().getLogin(), user); 137 } 138 139 if (currentCount > 0) 140 { 141 currentCount--; 142 if (currentCount == 0) 143 { 144 break; 145 } 146 } 147 } 148 } 149 150 if (currentCount != 0) 151 { 152 UserCollectionRequestBuilder nextPage = userCollectionPage.getNextPage(); 153 if (nextPage != null) 154 { 155 _handlePage(nextPage.buildRequest(new HeaderOption("ConsistencyLevel", "eventual")).get(), users, currentCount, currentOffset); 156 } 157 } 158 } 159 160 public User getUser(String login) 161 { 162 if (isCachingEnabled() && getCacheByLogin().hasKey(login)) 163 { 164 User user = getCacheByLogin().get(login); 165 return user; 166 } 167 168 User user = null; 169 try 170 { 171 com.microsoft.graph.models.User u = _graphClient.users(login).buildRequest().select("userPrincipalName, surname, givenName, mail").get(); 172 173 user = new User(new UserIdentity(u.userPrincipalName, getPopulationId()), u.surname, u.givenName, u.mail, this); 174 175 if (isCachingEnabled()) 176 { 177 getCacheByLogin().put(user.getIdentity().getLogin(), user); 178 } 179 } 180 catch (GraphServiceException e) 181 { 182 getLogger().warn("Unable to retrieve user '{}' from AzureAD", login, e); 183 } 184 185 return user; 186 } 187 188 /* 189 * As we do not know how to search for email in a "case insensitive" way, we also fill the cache "case sensitively" 190 */ 191 public User getUserByEmail(String email) throws NotUniqueUserException 192 { 193 if (StringUtils.isBlank(email)) 194 { 195 return null; 196 } 197 198 if (isCachingEnabled() && getCacheByMail().hasKey(email)) 199 { 200 User user = getCacheByMail().get(email); 201 return user; 202 } 203 204 List<com.microsoft.graph.models.User> users = _graphClient.users().buildRequest(new HeaderOption("ConsistencyLevel", "eventual")) 205 .filter("mail eq '" + email + "'") 206 .select("userPrincipalName, surname, givenName, mail") 207 .get() 208 .getCurrentPage(); 209 210 if (users.size() == 1) 211 { 212 com.microsoft.graph.models.User u = users.get(0); 213 User user = new User(new UserIdentity(u.userPrincipalName, getPopulationId()), u.surname, u.givenName, u.mail, this); 214 215 if (isCachingEnabled()) 216 { 217 getCacheByMail().put(user.getEmail(), user); 218 } 219 220 return user; 221 } 222 else if (users.isEmpty()) 223 { 224 return null; 225 } 226 else 227 { 228 throw new NotUniqueUserException("Find " + users.size() + " users matching the email " + email); 229 } 230 } 231 232 public CredentialsResult checkCredentials(String login, String password) 233 { 234 throw new UnsupportedOperationException("The AADUserDirectory cannot authenticate users"); 235 } 236}