001/*
002 *  Copyright 2016 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.authentication;
017
018import java.util.HashMap;
019import java.util.LinkedHashMap;
020import java.util.Map;
021import java.util.Set;
022
023import org.apache.avalon.framework.activity.Disposable;
024import org.apache.avalon.framework.activity.Initializable;
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.context.Context;
029import org.apache.avalon.framework.context.ContextException;
030import org.apache.avalon.framework.context.Contextualizable;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.avalon.framework.thread.ThreadSafe;
035import org.apache.cocoon.components.LifecycleHelper;
036import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040import org.ametys.runtime.i18n.I18nizableText;
041import org.ametys.runtime.parameter.AbstractParameterParser;
042import org.ametys.runtime.parameter.Enumerator;
043import org.ametys.runtime.parameter.Parameter;
044import org.ametys.runtime.parameter.ParameterChecker;
045import org.ametys.runtime.parameter.ParameterCheckerDescriptor;
046import org.ametys.runtime.parameter.ParameterCheckerParser;
047import org.ametys.runtime.parameter.ParameterHelper;
048import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
049import org.ametys.runtime.parameter.Validator;
050import org.ametys.runtime.plugin.ExtensionPoint;
051import org.ametys.runtime.plugin.component.AbstractLogEnabled;
052import org.ametys.runtime.plugin.component.LogEnabled;
053import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
054
055/**
056 * This extension point handles a list of {@link CredentialProvider} handled by the plugins.
057 */
058public class CredentialProviderFactory extends AbstractLogEnabled implements ExtensionPoint<CredentialProviderModel>, Initializable, ThreadSafe, Component, Serviceable, Contextualizable, Disposable
059{
060    /** The avalon role */
061    public static final String ROLE = CredentialProviderFactory.class.getName();
062    
063    private Map<String, CredentialProviderModel> _cpModels;
064
065    private ServiceManager _smanager;
066
067    private Context _context;
068    
069    @Override
070    public void initialize() throws Exception
071    {
072        _cpModels = new HashMap<>();
073    }
074    
075    @Override
076    public void dispose()
077    {
078        _cpModels.clear();
079    }
080    
081    @Override
082    public void service(ServiceManager smanager) throws ServiceException
083    {
084        _smanager = smanager;
085    }
086    
087    @Override
088    public void contextualize(Context context) throws ContextException
089    {
090        _context = context;
091    }
092    
093    /**
094     * Creates a instance of {@link CredentialProvider}
095     * @param id The unique id of this credential provider instance
096     * @param modelId The id of the credential provider model
097     * @param paramsValues the parameters's values
098     * @param label The optional label
099     * @return a credential provider
100     */
101    public CredentialProvider createCredentialProvider (String id, String modelId, Map<String, Object> paramsValues, String label)
102    {
103        if (_cpModels.containsKey(modelId))
104        {
105            CredentialProviderModel credentialProviderModel = _cpModels.get(modelId);
106            
107            CredentialProvider cp = null;
108            Class<CredentialProvider> cpClass = credentialProviderModel.getCredentialProviderClass();
109            
110            try
111            {
112                cp = credentialProviderModel.getCredentialProviderClass().newInstance();
113            }
114            catch (InstantiationException | IllegalAccessException e)
115            {
116                throw new IllegalArgumentException("Cannot instanciate the class " + cpClass.getCanonicalName() + ". Check that there is a public constructor with no arguments.");
117            }
118            
119            Logger logger = LoggerFactory.getLogger(cpClass);
120            try
121            {
122                if (cp instanceof LogEnabled)
123                {
124                    ((LogEnabled) cp).setLogger(logger);
125                }
126                
127                LifecycleHelper.setupComponent(cp, new SLF4JLoggerAdapter(logger), _context, _smanager, credentialProviderModel.getCredentialProviderConfiguration());
128                
129                cp.init(id, modelId, paramsValues, label);
130            }
131            catch (Exception e)
132            {
133                getLogger().error("An error occured during the initialization of the CredentialProvider " + id + " [" + modelId + "]", e);
134                return null;
135            }
136            
137            return cp;
138        }
139        
140        return null;
141    }
142    
143    @Override
144    public void addExtension(String id, String pluginName, String featureName, Configuration configuration) throws ConfigurationException
145    {
146        if (getLogger().isDebugEnabled())
147        {
148            getLogger().debug("Adding credential provider model from plugin " + pluginName + "/" + featureName);
149        }
150
151        try
152        {
153            addCredentialProviderModel(pluginName, configuration);
154        }
155        catch (ConfigurationException e)
156        {
157            if (getLogger().isWarnEnabled())
158            {
159                getLogger().warn("The plugin '" + pluginName + "." + featureName + "' has a credential provider model extension but has an incorrect configuration", e);
160            }
161        }
162    }
163    
164    /**
165     * Add a credential provider model
166     * @param pluginName The plugin name
167     * @param configuration The configuration
168     * @throws ConfigurationException when a configuration problem occurs
169     */
170    protected void addCredentialProviderModel (String pluginName, Configuration configuration) throws ConfigurationException
171    {
172        String id = configuration.getAttribute("id");
173        I18nizableText label = I18nizableText.parseI18nizableText(configuration.getChild("label"), "plugin." + pluginName);
174        I18nizableText description = I18nizableText.parseI18nizableText(configuration.getChild("description"), "plugin." + pluginName);
175        I18nizableText connectionLabel;
176        if (configuration.getChild("label-connection", false) == null)
177        {
178            connectionLabel = null;
179        }
180        else
181        {
182            connectionLabel = I18nizableText.parseI18nizableText(configuration.getChild("label-connection"), "plugin." + pluginName); 
183        }
184        String iconGlyph = configuration.getChild("icon-glyph").getValue("");
185        String iconDecorator = configuration.getChild("icon-decorator").getValue("");
186        String iconSmallPath = configuration.getChild("icon-small").getValue("");
187        String iconMediumPath = configuration.getChild("icon-medium").getValue("");
188        String iconLargePath = configuration.getChild("icon-large").getValue("");
189        String connectionColor = configuration.getChild("color").getValue("");
190        
191        String className = null;
192        Class<?> cpClass = null;
193        Configuration classConfig = null;
194        try
195        {
196            className = configuration.getChild("class").getAttribute("name");
197            cpClass = Class.forName(className);
198            classConfig = configuration.getChild("class");
199        }
200        catch (ClassNotFoundException | ConfigurationException e)
201        {
202            throw new ConfigurationException("Credential provider model with id '" + id + "' has an invalid configuration for class name '" + (className != null ? className + " <class not found>" : "<missing tag <class>") + "'", e);
203        }
204        
205        if (!CredentialProvider.class.isAssignableFrom(cpClass))
206        {
207            throw new ConfigurationException("Credential provider model with id '" + id + "' has an invalid configuration: '" + className + "' is not an instance of CredentialProvider");
208        }
209        
210        Map<String, Parameter<ParameterType>> parameters = new LinkedHashMap<>();
211        
212        ThreadSafeComponentManager<Validator> validatorManager = new ThreadSafeComponentManager<>();
213        validatorManager.setLogger(getLogger());
214        validatorManager.contextualize(_context);
215        validatorManager.service(_smanager);
216        
217        ThreadSafeComponentManager<Enumerator> enumeratorManager = new ThreadSafeComponentManager<>();
218        enumeratorManager.setLogger(getLogger());
219        enumeratorManager.contextualize(_context);
220        enumeratorManager.service(_smanager);
221        
222        CredentialProviderModelParameterParser cpParser = new CredentialProviderModelParameterParser(enumeratorManager, validatorManager);
223        
224        Configuration[] paramsConfiguration = configuration.getChild("parameters").getChildren("param");
225        for (Configuration paramConfiguration : paramsConfiguration)
226        {
227            configureParameters(cpParser, paramConfiguration, pluginName, parameters);
228        }
229        
230        // Parse parameter checkers
231        Map<String, ParameterCheckerDescriptor> parameterCheckers = new LinkedHashMap<>();
232        
233        ThreadSafeComponentManager<ParameterChecker> parameterCheckerManager = new ThreadSafeComponentManager<>();
234        parameterCheckerManager.setLogger(getLogger());
235        parameterCheckerManager.contextualize(_context);
236        parameterCheckerManager.service(_smanager);
237        
238        ParameterCheckerParser parameterCheckerParser = new ParameterCheckerParser(parameterCheckerManager);
239        
240        Configuration[] paramCheckersConfiguration = configuration.getChild("parameters").getChildren("param-checker");
241        for (Configuration paramCheckerConfiguration : paramCheckersConfiguration)
242        {
243            configureParamChecker(parameterCheckerParser, paramCheckerConfiguration, pluginName, parameterCheckers);
244        }
245        
246        try
247        {
248            cpParser.lookupComponents();
249            parameterCheckerParser.lookupComponents();
250        }
251        catch (Exception e)
252        {
253            throw new ConfigurationException("Unable to lookup parameter local components", configuration, e);
254        }
255        
256        @SuppressWarnings("unchecked")
257        CredentialProviderModel cpModel = new DefaultCredentialProviderModel(id, (Class<CredentialProvider>) cpClass, classConfig, label, description, connectionLabel, iconGlyph, iconDecorator, iconSmallPath, iconMediumPath, iconLargePath, connectionColor, parameters, parameterCheckers, pluginName);
258        if (_cpModels.containsKey(id))
259        {
260            CredentialProviderModel oldCPModel = _cpModels.get(id);
261            throw new IllegalArgumentException("Credential provider model with id '" + id + "' is already declared in plugin '" + oldCPModel.getPluginName() + "'. This second declaration is ignored.");
262        }
263        
264        _cpModels.put(id, cpModel);
265    }
266    
267    /**
268     * Configure a parameter to access the credential provider
269     * @param paramParser the parameter parser.
270     * @param configuration The parameter configuration.
271     * @param pluginName The plugin name
272     * @param parameters The model's parameters
273     * @throws ConfigurationException if configuration is incomplete or invalid.
274     */
275    protected void configureParameters(CredentialProviderModelParameterParser paramParser, Configuration configuration, String pluginName, Map<String, Parameter<ParameterType>> parameters) throws ConfigurationException
276    {
277        Parameter<ParameterType> parameter = paramParser.parseParameter(_smanager, pluginName, configuration);
278        String id = parameter.getId();
279        
280        if (parameters.containsKey(id))
281        {
282            throw new ConfigurationException("The parameter '" + id + "' is already declared. IDs must be unique.", configuration);
283        }
284        
285        parameters.put(id, parameter);
286    }
287    
288    /**
289     * Configure a parameter checker of a user directory
290     * @param parser the parameter checker parser.
291     * @param configuration The parameter checker configuration.
292     * @param pluginName The plugin name
293     * @param parameterCheckers The model's parameter checkers
294     * @throws ConfigurationException if configuration is incomplete or invalid.
295     */
296    protected void configureParamChecker(ParameterCheckerParser parser, Configuration configuration, String pluginName, Map<String, ParameterCheckerDescriptor> parameterCheckers) throws ConfigurationException
297    {
298        ParameterCheckerDescriptor parameterChecker = parser.parseParameterChecker(pluginName, configuration);
299        String id = parameterChecker.getId();
300        
301        if (parameterCheckers.containsKey(id))
302        {
303            throw new ConfigurationException("The parameter checker '" + id + "' is already declared. IDs must be unique.", configuration);
304        }
305        
306        parameterCheckers.put(id, parameterChecker);
307    }
308    
309    @Override
310    public void initializeExtensions() throws Exception
311    {
312        // Nothing to do
313    }
314
315    @Override
316    public boolean hasExtension(String id)
317    {
318        return _cpModels.containsKey(id);
319    }
320
321    @Override
322    public CredentialProviderModel getExtension(String id)
323    {
324        return _cpModels.get(id);
325    }
326
327    @Override
328    public Set<String> getExtensionsIds()
329    {
330        return _cpModels.keySet();
331    }
332    
333    /**
334     * Class for parsing parameters of a {@link CredentialProviderModel}
335     */
336    public class CredentialProviderModelParameterParser extends AbstractParameterParser<Parameter<ParameterType>, ParameterType>
337    {
338        /**
339         * Constructor
340         * @param enumeratorManager The manager for enumeration
341         * @param validatorManager The manager for validation
342         */
343        public CredentialProviderModelParameterParser(ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager)
344        {
345            super(enumeratorManager, validatorManager);
346        }
347        
348        @Override
349        protected Parameter<ParameterType> _createParameter(Configuration parameterConfig) throws ConfigurationException
350        {
351            return new Parameter<>();
352        }
353        
354        @Override
355        protected String _parseId(Configuration parameterConfig) throws ConfigurationException
356        {
357            return parameterConfig.getAttribute("id");
358        }
359        
360        @Override
361        protected ParameterType _parseType(Configuration parameterConfig) throws ConfigurationException
362        {
363            try
364            {
365                return ParameterType.valueOf(parameterConfig.getAttribute("type").toUpperCase());
366            }
367            catch (IllegalArgumentException e)
368            {
369                throw new ConfigurationException("Invalid parameter type", parameterConfig, e);
370            }
371        }
372        
373        @Override
374        protected Object _parseDefaultValue(Configuration parameterConfig, Parameter<ParameterType> parameter) throws ConfigurationException
375        {
376            String defaultValue = parameterConfig.getChild("default-value").getValue(null);
377            return ParameterHelper.castValue(defaultValue, parameter.getType());
378        }
379        
380        @Override
381        protected void _additionalParsing(ServiceManager manager, String pluginName, Configuration parameterConfig, String parameterId, Parameter<ParameterType> parameter)
382                throws ConfigurationException
383        {
384            super._additionalParsing(manager, pluginName, parameterConfig, parameterId, parameter);
385            parameter.setId(parameterId);
386        }
387    }
388}