001/*
002 * (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *      Nelson Silva
016 */
017package org.nuxeo.ecm.platform.oauth2.providers;
018
019import java.io.Serializable;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.commons.lang.StringUtils;
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.nuxeo.ecm.core.api.DocumentModel;
031import org.nuxeo.ecm.directory.BaseSession;
032import org.nuxeo.ecm.directory.DirectoryException;
033import org.nuxeo.ecm.directory.Session;
034import org.nuxeo.ecm.directory.api.DirectoryService;
035import org.nuxeo.runtime.api.Framework;
036import org.nuxeo.runtime.model.ComponentContext;
037import org.nuxeo.runtime.model.ComponentInstance;
038import org.nuxeo.runtime.model.DefaultComponent;
039
040/**
041 * Implementation of the {@link OAuth2ServiceProviderRegistry}. The storage backend is a SQL Directory.
042 */
043public class OAuth2ServiceProviderRegistryImpl extends DefaultComponent implements OAuth2ServiceProviderRegistry {
044
045    protected static final Log log = LogFactory.getLog(OAuth2ServiceProviderRegistryImpl.class);
046
047    public static final String PROVIDER_EP = "providers";
048
049    public static final String DIRECTORY_NAME = "oauth2ServiceProviders";
050
051    public static final String SCHEMA = "oauth2ServiceProvider";
052
053    /**
054     * Registry of contributed providers. These providers can extend and/or override the default provider class.
055     */
056    protected OAuth2ServiceProviderContributionRegistry registry = new OAuth2ServiceProviderContributionRegistry();
057
058    @Override
059    public OAuth2ServiceProvider getProvider(String serviceName) {
060        try {
061            if (StringUtils.isBlank(serviceName)) {
062                log.warn("Can not find provider without a serviceName!");
063                return null;
064            }
065
066            Map<String, Serializable> filter = new HashMap<>();
067            filter.put("serviceName", serviceName);
068
069            List<DocumentModel> providers = queryProviders(filter, 1);
070            return providers.isEmpty() ? null : buildProvider(providers.get(0));
071        } catch (DirectoryException e) {
072            log.error("Unable to read provider from Directory backend", e);
073            return null;
074        }
075    }
076
077    @Override
078    public OAuth2ServiceProvider addProvider(String serviceName, String description, String tokenServerURL,
079            String authorizationServerURL, String clientId, String clientSecret, List<String> scopes) {
080
081        DirectoryService ds = Framework.getService(DirectoryService.class);
082        try (Session session = ds.open(DIRECTORY_NAME)) {
083            DocumentModel creationEntry = BaseSession.createEntryModel(null, SCHEMA, null, null);
084            DocumentModel entry = session.createEntry(creationEntry);
085            entry.setProperty(SCHEMA, "serviceName", serviceName);
086            entry.setProperty(SCHEMA, "description", description);
087            entry.setProperty(SCHEMA, "authorizationServerURL", authorizationServerURL);
088            entry.setProperty(SCHEMA, "tokenServerURL", tokenServerURL);
089            entry.setProperty(SCHEMA, "clientId", clientId);
090            entry.setProperty(SCHEMA, "clientSecret", clientSecret);
091            entry.setProperty(SCHEMA, "scopes", StringUtils.join(scopes, ","));
092            boolean enabled = (clientId != null && clientSecret != null);
093            entry.setProperty(SCHEMA, "enabled", enabled);
094            if (!enabled) {
095                log.info("OAuth2 provider for " + serviceName
096                        + " is disabled because clientId and/or clientSecret are empty");
097            }
098            session.updateEntry(entry);
099            return getProvider(serviceName);
100        }
101    }
102
103    protected List<DocumentModel> queryProviders(Map<String, Serializable> filter, int limit) {
104        DirectoryService ds = Framework.getService(DirectoryService.class);
105        try (Session session = ds.open(DIRECTORY_NAME)) {
106            Set<String> fulltext = Collections.emptySet();
107            Map<String, String> orderBy = Collections.emptyMap();
108            return session.query(filter, fulltext, orderBy, true, limit, 0);
109        } catch (DirectoryException e) {
110            log.error("Error while fetching provider directory", e);
111        }
112        return Collections.emptyList();
113    }
114
115    /**
116     * Instantiates the provider merging the contribution and the directory entry
117     */
118    protected OAuth2ServiceProvider buildProvider(DocumentModel entry) {
119        String serviceName = (String) entry.getProperty(SCHEMA, "serviceName");
120        OAuth2ServiceProvider provider = registry.getProvider(serviceName);
121        if (provider == null) {
122            provider = new NuxeoOAuth2ServiceProvider();
123            provider.setServiceName(serviceName);
124        }
125        provider.setId((Long) entry.getProperty(SCHEMA, "id"));
126        provider.setAuthorizationServerURL((String) entry.getProperty(SCHEMA, "authorizationServerURL"));
127        provider.setTokenServerURL((String) entry.getProperty(SCHEMA, "tokenServerURL"));
128        provider.setClientId((String) entry.getProperty(SCHEMA, "clientId"));
129        provider.setClientSecret((String) entry.getProperty(SCHEMA, "clientSecret"));
130        String scopes = (String) entry.getProperty(SCHEMA, "scopes");
131        provider.setScopes(StringUtils.split(scopes, ","));
132        provider.setEnabled((Boolean) entry.getProperty(SCHEMA, "enabled"));
133        return provider;
134    }
135
136    @Override
137    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
138        if (PROVIDER_EP.equals(extensionPoint)) {
139            OAuth2ServiceProviderDescriptor provider = (OAuth2ServiceProviderDescriptor) contribution;
140            log.info("OAuth2 provider for " + provider.getName() + " will be registered at application startup");
141            // delay registration because data sources may not be available
142            // at this point
143            registry.addContribution(provider);
144        }
145    }
146
147    @Override
148    public void applicationStarted(ComponentContext context) {
149        super.applicationStarted(context);
150        registerCustomProviders();
151    }
152
153    protected void registerCustomProviders() {
154        for (OAuth2ServiceProviderDescriptor provider : registry.getContribs()) {
155            if (getProvider(provider.getName()) == null) {
156                addProvider(provider.getName(), provider.getDescription(), provider.getTokenServerURL(),
157                        provider.getAuthorizationServerURL(), provider.getClientId(), provider.getClientSecret(),
158                        Arrays.asList(provider.getScopes()));
159            } else {
160                log.info("Provider " + provider.getName()
161                        + " is already in the Database, XML contribution  won't overwrite it");
162            }
163        }
164    }
165}