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