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}