001/*
002 *  Copyright 2024 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.net.URI;
019import java.util.Map;
020
021import javax.annotation.Nonnull;
022
023import org.apache.avalon.framework.activity.Initializable;
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.avalon.framework.service.Serviceable;
028import org.apache.commons.lang3.StringUtils;
029
030import org.ametys.core.user.CurrentUserProvider;
031import org.ametys.core.user.User;
032import org.ametys.core.user.UserIdentity;
033import org.ametys.core.user.UserManager;
034import org.ametys.core.util.SessionAttributeProvider;
035import org.ametys.plugins.extrausermgt.authentication.msal.AbstractMSALCredentialProvider;
036import org.ametys.runtime.config.Config;
037import org.ametys.runtime.plugin.component.AbstractLogEnabled;
038
039import com.azure.identity.ClientSecretCredential;
040import com.azure.identity.ClientSecretCredentialBuilder;
041import com.microsoft.graph.serviceclient.GraphServiceClient;
042import com.microsoft.graph.users.item.UserItemRequestBuilder;
043import com.microsoft.kiota.authentication.AccessTokenProvider;
044import com.microsoft.kiota.authentication.AllowedHostsValidator;
045import com.microsoft.kiota.authentication.BaseBearerTokenAuthenticationProvider;
046
047/**
048 * Provide a Graph client for a user.
049 * Depending on the configuration, the client can either be authenticated
050 * using an application credentials or the current user Entra id token
051 */
052public class GraphClientProvider extends AbstractLogEnabled implements Initializable, Serviceable, Component
053{
054    /** The avalon role */
055    public static final String ROLE = GraphClientProvider.class.getName();
056    
057    private static final String __SCOPE = "https://graph.microsoft.com/.default";
058
059    private GraphServiceClient _graphClient;
060    
061    private CurrentUserProvider _currentUserProvider;
062    private SessionAttributeProvider _sessionAttributeProvider;
063    private UserManager _userManager;
064    
065    public void service(ServiceManager manager) throws ServiceException
066    {
067        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
068        _sessionAttributeProvider = (SessionAttributeProvider) manager.lookup(SessionAttributeProvider.ROLE);
069        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
070    }
071    
072    public void initialize()
073    {
074        if ((boolean) Config.getInstance().getValue("org.ametys.plugins.extra-user-management.graph.useadmin"))
075        {
076            String appId = Config.getInstance().getValue("org.ametys.plugins.extra-user-management.graph.appid");
077            String clientSecret = Config.getInstance().getValue("org.ametys.plugins.extra-user-management.graph.clientsecret");
078            String tenant = Config.getInstance().getValue("org.ametys.plugins.extra-user-management.graph.tenant");
079            
080            ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder()
081                    .clientId(appId)
082                    .clientSecret(clientSecret)
083                    .tenantId(tenant)
084                    .build();
085            
086            _graphClient = new GraphServiceClient(clientSecretCredential, __SCOPE);
087        }
088    }
089
090    /**
091     * Get a graph client for the user.
092     * No check is performed to ensure that the user is actually a valid graph user
093     * @param userIdentity the user identity
094     * @return the client
095     * @throws GraphClientException when its not possible to retrieve a client for the user
096     */
097    public UserItemRequestBuilder getUserRequestBuilder(UserIdentity userIdentity) throws GraphClientException
098    {
099        if (_graphClient != null)
100        {
101            return _graphClient.users().byUserId(_getUserPrincipalName(userIdentity));
102        }
103        else
104        {
105            // This mode can only provide a client for the current user
106            // check that the requested user is the current user before getting the client
107            if (!userIdentity.equals(_currentUserProvider.getUser()))
108            {
109                throw new GraphClientException(userIdentity.toString() + " is not the current user. A graph client can only be retrieved for the current user");
110            }
111            return _sessionAttributeProvider.getSessionAttribute(AbstractMSALCredentialProvider.ACCESS_TOKEN_SESSION_ATTRIBUTE)
112                .filter(String.class::isInstance)
113                .map(String.class::cast)
114                .map(this::_getClientFromToken)
115                .map(GraphServiceClient::me)
116                .orElseThrow(() -> new GraphClientException("The current user " + _currentUserProvider.getUser().toString() + "is not logged with Entra ID."));
117        }
118    }
119    
120    /**
121     * Get an authenticated Graph client
122     * @return a Graph client
123     * @throws GraphClientException when its not possible to retrieve a client for the user
124     */
125    public GraphServiceClient getGraphClient() throws GraphClientException
126    {
127        if (_graphClient != null)
128        {
129            return _graphClient;
130        }
131        else
132        {
133            return _sessionAttributeProvider.getSessionAttribute(AbstractMSALCredentialProvider.ACCESS_TOKEN_SESSION_ATTRIBUTE)
134                .filter(String.class::isInstance)
135                .map(String.class::cast)
136                .map(this::_getClientFromToken)
137                .orElseThrow(() -> new GraphClientException("The current user " + _currentUserProvider.getUser().toString() + "is not logged with Entra ID."));
138        }
139    }
140    
141    private GraphServiceClient _getClientFromToken(String token)
142    {
143        AccessTokenAuthenticationProvider accessTokenAuthenticationProvider = new AccessTokenAuthenticationProvider(token);
144        return new GraphServiceClient(new BaseBearerTokenAuthenticationProvider(accessTokenAuthenticationProvider));
145    }
146    
147    private String _getUserPrincipalName(UserIdentity userIdentity) throws GraphClientException
148    {
149        if ("email".equals(Config.getInstance().getValue("org.ametys.plugins.extra-user-management.graph.authmethod")))
150        {
151            User user = _userManager.getUser(userIdentity);
152            String email = user.getEmail();
153            if (StringUtils.isBlank(email))
154            {
155                throw new GraphClientException("The user '" + userIdentity.toString() + "' has no email address set, thus exchange cannot be contacted using 'email' authentication method");
156            }
157            
158            return email;
159        }
160        else
161        {
162            return userIdentity.getLogin();
163        }
164    }
165    
166    /**
167     * Exception related to the Graph client
168     */
169    public static class GraphClientException extends Exception
170    {
171        /**
172         * Constructs a new exception with the specified detail message.
173         * @param message the detail message.
174         */
175        public GraphClientException(String message)
176        {
177            super(message);
178        }
179    }
180    
181    private static class AccessTokenAuthenticationProvider implements AccessTokenProvider
182    {
183        private String _accessToken;
184
185        public AccessTokenAuthenticationProvider(@Nonnull String accessToken)
186        {
187            _accessToken = accessToken;
188        }
189
190        public String getAuthorizationToken(URI uri, Map<String, Object> additionalAuthenticationContext)
191        {
192            return _accessToken;
193        }
194
195        public AllowedHostsValidator getAllowedHostsValidator()
196        {
197            return null;
198        }
199    }
200}