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