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}