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