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.directory.NotUniqueUserException;
026import org.ametys.core.user.directory.StoredUser;
027import org.ametys.core.user.directory.UserDirectory;
028import org.ametys.plugins.core.impl.user.directory.AbstractCachingUserDirectory;
029
030import com.azure.identity.ClientSecretCredential;
031import com.azure.identity.ClientSecretCredentialBuilder;
032import com.microsoft.graph.authentication.TokenCredentialAuthProvider;
033import com.microsoft.graph.http.GraphServiceException;
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) throws Exception
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<StoredUser> getStoredUsers()
080    {
081        return getStoredUsers(-1, 0, null);
082    }
083
084    public List<StoredUser> getStoredUsers(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<StoredUser> result = new ArrayList<>();
113        _handlePage(userCollectionPage, result, maxUsers, offset);
114        
115        return result;
116    }
117    
118    private void _handlePage(UserCollectionPage userCollectionPage, List<StoredUser> storedUsers, 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                StoredUser storedUser = new StoredUser(u.userPrincipalName, u.surname, u.givenName, u.mail);
131                storedUsers.add(storedUser);
132                
133                if (isCachingEnabled())
134                {
135                    getCacheByLogin().put(storedUser.getIdentifier(), storedUser);
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(), storedUsers, currentCount, currentOffset);
155            }
156        }
157    }
158
159    public StoredUser getStoredUser(String login)
160    {
161        if (isCachingEnabled() && getCacheByLogin().hasKey(login))
162        {
163            StoredUser storedUser = getCacheByLogin().get(login);
164            return storedUser;
165        }
166        
167        StoredUser storedUser = null;
168        try
169        {
170            com.microsoft.graph.models.User u = _graphClient.users(login).buildRequest().select("userPrincipalName, surname, givenName, mail").get();
171
172            storedUser =  new StoredUser(u.userPrincipalName, u.surname, u.givenName, u.mail);
173            
174            if (isCachingEnabled())
175            {
176                getCacheByLogin().put(storedUser.getIdentifier(), storedUser);
177            }
178        }
179        catch (GraphServiceException e)
180        {
181            getLogger().warn("Unable to retrieve user '{}' from AzureAD", login, e);
182        }
183
184        return storedUser;
185    }
186
187    /*
188     * As we do not know how to search for email in a "case insensitive" way, we also fill the cache "case sensitively"
189     */
190    public StoredUser getStoredUserByEmail(String email) throws NotUniqueUserException
191    {
192        if (StringUtils.isBlank(email))
193        {
194            return null;
195        }
196        
197        if (isCachingEnabled() && getCacheByMail().hasKey(email))
198        {
199            StoredUser storedUser = getCacheByMail().get(email);
200            return storedUser;
201        }
202        
203        List<com.microsoft.graph.models.User> users = _graphClient.users().buildRequest(new HeaderOption("ConsistencyLevel", "eventual"))
204                                                                          .filter("mail eq '" + email + "'")
205                                                                          .select("userPrincipalName, surname, givenName, mail")
206                                                                          .get()
207                                                                          .getCurrentPage();
208        
209        if (users.size() == 1)
210        {
211            com.microsoft.graph.models.User u = users.get(0);
212            StoredUser storedUser =  new StoredUser(u.userPrincipalName, u.surname, u.givenName, u.mail);
213            
214            if (isCachingEnabled())
215            {
216                getCacheByMail().put(storedUser.getEmail(), storedUser);
217            }
218            
219            return storedUser;
220        }
221        else if (users.isEmpty())
222        {
223            return null;
224        }
225        else
226        {
227            throw new NotUniqueUserException("Find " + users.size() + " users matching the email " + email);
228        }
229    }
230
231    public CredentialsResult checkCredentials(String login, String password)
232    {
233        throw new UnsupportedOperationException("The AADUserDirectory cannot authenticate users");
234    }
235}