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}