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}