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