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