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}