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}