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}