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.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)
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<User> getUsers()
080    {
081        return getUsers(-1, 0, null);
082    }
083
084    public List<User> getUsers(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<User> result = new ArrayList<>();
113        _handlePage(userCollectionPage, result, maxUsers, offset);
114        
115        return result;
116    }
117    
118    private void _handlePage(UserCollectionPage userCollectionPage, List<User> users, 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                User user = new User(new UserIdentity(u.userPrincipalName, getPopulationId()), u.surname, u.givenName, u.mail, this);
131                users.add(user);
132                
133                if (isCachingEnabled())
134                {
135                    getCacheByLogin().put(user.getIdentity().getLogin(), user);
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(), users, currentCount, currentOffset);
155            }
156        }
157    }
158
159    public User getUser(String login)
160    {
161        if (isCachingEnabled() && getCacheByLogin().hasKey(login))
162        {
163            User user = getCacheByLogin().get(login);
164            return user;
165        }
166        
167        com.microsoft.graph.models.User u = _graphClient.users(login).buildRequest().select("userPrincipalName, surname, givenName, mail").get();
168        
169        User user =  new User(new UserIdentity(u.userPrincipalName, getPopulationId()), u.surname, u.givenName, u.mail, this);
170        
171        if (isCachingEnabled())
172        {
173            getCacheByLogin().put(user.getIdentity().getLogin(), user);
174        }
175
176        return user;
177    }
178
179    /*
180     * As we do not know how to search for email in a "case insensitive" way, we also fill the cache "case sensitively"
181     */
182    public User getUserByEmail(String email) throws NotUniqueUserException
183    {
184        if (StringUtils.isBlank(email))
185        {
186            return null;
187        }
188        
189        if (isCachingEnabled() && getCacheByMail().hasKey(email))
190        {
191            User user = getCacheByMail().get(email);
192            return user;
193        }
194        
195        List<com.microsoft.graph.models.User> users = _graphClient.users().buildRequest(new HeaderOption("ConsistencyLevel", "eventual"))
196                                                                          .filter("mail eq '" + email + "'")
197                                                                          .select("userPrincipalName, surname, givenName, mail")
198                                                                          .get()
199                                                                          .getCurrentPage();
200        
201        if (users.size() == 1)
202        {
203            com.microsoft.graph.models.User u = users.get(0);
204            User user =  new User(new UserIdentity(u.userPrincipalName, getPopulationId()), u.surname, u.givenName, u.mail, this);
205            
206            if (isCachingEnabled())
207            {
208                getCacheByMail().put(user.getEmail(), user);
209            }
210            
211            return user;
212        }
213        else if (users.isEmpty())
214        {
215            return null;
216        }
217        else
218        {
219            throw new NotUniqueUserException("Find " + users.size() + " users matching the email " + email);
220        }
221    }
222
223    public boolean checkCredentials(String login, String password)
224    {
225        throw new UnsupportedOperationException("The AADUserDirectory cannot authenticate users");
226    }
227}