001/* 002 * Copyright 2013 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.core.ui; 017 018import java.lang.reflect.Method; 019import java.util.List; 020import java.util.Map; 021import java.util.function.Supplier; 022import java.util.stream.Stream; 023 024import org.apache.avalon.framework.parameters.Parameters; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.thread.ThreadSafe; 027import org.apache.cocoon.acting.ServiceableAction; 028import org.apache.cocoon.environment.ObjectModelHelper; 029import org.apache.cocoon.environment.Redirector; 030import org.apache.cocoon.environment.Request; 031import org.apache.cocoon.environment.SourceResolver; 032import org.apache.commons.lang3.ClassUtils; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.commons.lang3.reflect.MethodUtils; 035 036import org.ametys.core.cocoon.JSonReader; 037import org.ametys.core.right.RightAssignmentContext; 038import org.ametys.core.right.RightAssignmentContextExtensionPoint; 039import org.ametys.core.right.RightManager; 040import org.ametys.core.right.RightManager.RightResult; 041import org.ametys.core.ui.Callable.RightMode; 042import org.ametys.core.user.CurrentUserProvider; 043import org.ametys.core.user.UserIdentity; 044import org.ametys.core.util.LambdaUtils.LambdaException; 045import org.ametys.runtime.authentication.AccessDeniedException; 046import org.ametys.runtime.plugin.ExtensionPoint; 047 048/** 049 * Action executing remote method calls coming from client-side elements.<br> 050 * Called methods should be annotated with {@link Callable}.<br> 051 */ 052public class ExecuteClientCallsAction extends ServiceableAction implements ThreadSafe 053{ 054 private RightManager _rightManager; 055 private CurrentUserProvider _currentUserProvider; 056 private RightAssignmentContextExtensionPoint _rightCtxEP; 057 private CallableDecoratorsExtensionPoint _callableDecoratorsEP; 058 059 private RightManager _getRightManager() 060 { 061 if (_rightManager == null) 062 { 063 try 064 { 065 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 066 } 067 catch (ServiceException e) 068 { 069 throw new RuntimeException(e); 070 } 071 } 072 073 return _rightManager; 074 } 075 076 private CurrentUserProvider _getCurrentUserProvider() 077 { 078 if (_currentUserProvider == null) 079 { 080 try 081 { 082 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 083 } 084 catch (ServiceException e) 085 { 086 throw new RuntimeException(e); 087 } 088 } 089 return _currentUserProvider; 090 } 091 092 private CallableDecoratorsExtensionPoint _getCallableDecoratorsEP() 093 { 094 if (_callableDecoratorsEP == null) 095 { 096 try 097 { 098 _callableDecoratorsEP = (CallableDecoratorsExtensionPoint) manager.lookup(CallableDecoratorsExtensionPoint.ROLE); 099 } 100 catch (ServiceException e) 101 { 102 throw new RuntimeException(e); 103 } 104 } 105 return _callableDecoratorsEP; 106 } 107 108 109 private RightAssignmentContextExtensionPoint _getRightContextEP() 110 { 111 if (_rightCtxEP == null) 112 { 113 try 114 { 115 _rightCtxEP = (RightAssignmentContextExtensionPoint) manager.lookup(RightAssignmentContextExtensionPoint.ROLE); 116 } 117 catch (ServiceException e) 118 { 119 throw new RuntimeException(e); 120 } 121 } 122 return _rightCtxEP; 123 } 124 125 @SuppressWarnings("unchecked") 126 @Override 127 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 128 { 129 Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 130 131 // Find the corresponding object, either a component or an extension 132 String role = (String) jsParameters.get("role"); 133 134 if (role == null) 135 { 136 throw new IllegalArgumentException("Component role should be present."); 137 } 138 139 Object object; 140 141 if (!manager.hasService(role)) 142 { 143 throw new IllegalArgumentException("The role '" + role + "' does not correspond to a valid component."); 144 } 145 146 Object component = manager.lookup(role); 147 148 if (component instanceof ExtensionPoint) 149 { 150 ExtensionPoint extPoint = (ExtensionPoint) component; 151 152 String id = (String) jsParameters.get("id"); 153 154 if (id == null) 155 { 156 object = component; 157 } 158 else 159 { 160 object = extPoint.getExtension(id); 161 162 if (object == null) 163 { 164 throw new IllegalArgumentException("The id '" + id + "' does not correspond to a valid extension for point " + role); 165 } 166 } 167 } 168 else 169 { 170 object = component; 171 } 172 173 // Find the corresponding method 174 String methodName = (String) jsParameters.get("methodName"); 175 List<Object> params = (List<Object>) jsParameters.get("parameters"); 176 177 if (methodName == null) 178 { 179 throw new IllegalArgumentException("No method name present, cannot execute server side code."); 180 } 181 182 Class[] paramClass; 183 Object[] paramValues; 184 if (params == null) 185 { 186 paramClass = new Class[0]; 187 paramValues = new Object[0]; 188 } 189 else 190 { 191 paramValues = params.toArray(); 192 paramClass = ClassUtils.toClass(paramValues); 193 } 194 195 Class<? extends Object> clazz = object.getClass(); 196 Method method = MethodUtils.getMatchingAccessibleMethod(clazz, methodName, paramClass); 197 198 if (method == null) 199 { 200 throw new IllegalArgumentException("No method with signature " + methodName + "(" + StringUtils.join(paramClass, ", ").replaceAll("class ", "") + ") present in class " + clazz.getName() + "."); 201 } 202 203 Object result = _executeMethod(method, object, paramValues); 204 205 Request request = ObjectModelHelper.getRequest(objectModel); 206 request.setAttribute(JSonReader.OBJECT_TO_READ, result); 207 208 return EMPTY_MAP; 209 } 210 211 /** 212 * Execute the method set in the client call 213 * @param method The method 214 * @param object The object which has the method 215 * @param paramValues The method parameters 216 * @return The result 217 * @throws Exception If an error occurred 218 */ 219 protected Object _executeMethod(Method method, Object object, Object[] paramValues) throws Exception 220 { 221 if (method.isAnnotationPresent(Callable.class)) 222 { 223 Callable callable = method.getAnnotation(Callable.class); 224 225 _checkAccess(callable, method, paramValues); 226 227 String[] decorators = callable.decorators(); 228 if (decorators == null || decorators.length == 0) 229 { 230 return method.invoke(object, paramValues); 231 } 232 else 233 { 234 Supplier<Object> supplier = () -> { 235 try 236 { 237 return method.invoke(object, paramValues); 238 } 239 catch (Exception e) 240 { 241 throw new LambdaException(e); 242 } 243 }; 244 245 246 for (String decoratorId : decorators) 247 { 248 CallableDecorator decorator = _getCallableDecoratorsEP().getExtension(decoratorId); 249 if (decorator == null) 250 { 251 throw new IllegalArgumentException("Callable method [" + method.toGenericString() + "] refers to a unknown decorator of id " + decoratorId + "."); 252 } 253 254 supplier = decorator.decorate(supplier); 255 } 256 257 try 258 { 259 return supplier.get(); 260 } 261 catch (LambdaException le) 262 { 263 throw (Exception) le.getCause(); // You can cast to Exception since Method.invoke only throws Exception 264 } 265 } 266 267 } 268 else 269 { 270 throw new IllegalArgumentException("Trying to call a non-callable method: " + method.toGenericString() + "."); 271 } 272 } 273 274 private void _checkAccess(Callable callable, Method method, Object[] paramValues) 275 { 276 UserIdentity currentUser = _getCurrentUserProvider().getUser(); 277 if (currentUser == null && !callable.allowAnonymous()) 278 { 279 throw new AccessDeniedException("Anonymous user tried to access the authenticated callable method [" + method.toGenericString() + "]"); 280 } 281 282 List<String> actualRights = Stream.of(callable.rights()) 283 .filter(right -> !Callable.CHECKED_BY_IMPLEMENTATION.equals(right) 284 && !Callable.NO_CHECK_REQUIRED.equals(right)) 285 .toList(); 286 287 // If at least one right is not a special value, we should check rights 288 if (!actualRights.isEmpty()) 289 { 290 Object context = _getRightContext(method, callable, paramValues); 291 292 for (String rightId : actualRights) 293 { 294 if (Callable.READ_ACCESS.equals(rightId) ? _getRightManager().hasReadAccess(currentUser, context) : _getRightManager().hasRight(currentUser, rightId, context) == RightResult.RIGHT_ALLOW) 295 { 296 if (callable.rightMode() == RightMode.OR) 297 { 298 return; 299 } 300 } 301 else if (callable.rightMode() == RightMode.AND) 302 { 303 break; 304 } 305 } 306 307 // none of the right is allowed, access is refused 308 throw new AccessDeniedException("The user " + currentUser + " tried to access the callable method [" + method.toGenericString() + "] without sufficient rights"); 309 } 310 } 311 312 private Object _getRightContext(Method method, Callable callable, Object[] paramValues) 313 { 314 if (StringUtils.isNotEmpty(callable.rightContext())) 315 { 316 int index = callable.paramIndex(); 317 if (index < 0 || index > paramValues.length - 1) 318 { 319 throw new IllegalArgumentException("Callable method [" + method.toGenericString() + "] refers to a invalid 'paramIndex' " + index + "."); 320 } 321 322 Object jsContext = paramValues[index]; 323 String rightCtxId = callable.rightContext(); 324 325 RightAssignmentContext rightCtx = _getRightContextEP().getExtension(rightCtxId); 326 if (rightCtx == null) 327 { 328 throw new IllegalArgumentException("Callable method [" + method.toGenericString() + "] refers to a unknown 'rightContext' of id " + rightCtxId + "."); 329 } 330 331 Object context = rightCtx.convertJSContext(jsContext); 332 if (context == null) 333 { 334 throw new IllegalArgumentException("Right object context not found for value " + jsContext + ". Unable to check right for callable method: " + method.toGenericString() + "."); 335 } 336 return context; 337 } 338 else 339 { 340 return callable.context(); 341 } 342 } 343}