001/* 002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * Nelson Silva 018 */ 019package org.nuxeo.ecm.platform.oauth2.providers; 020 021import java.io.Serializable; 022import java.util.Arrays; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import org.apache.commons.lang3.StringUtils; 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.ecm.core.api.DocumentModel; 034import org.nuxeo.ecm.directory.BaseSession; 035import org.nuxeo.ecm.directory.DirectoryException; 036import org.nuxeo.ecm.directory.Session; 037import org.nuxeo.ecm.directory.api.DirectoryService; 038import org.nuxeo.runtime.api.Framework; 039import org.nuxeo.runtime.model.ComponentContext; 040import org.nuxeo.runtime.model.ComponentInstance; 041import org.nuxeo.runtime.model.DefaultComponent; 042 043/** 044 * Implementation of the {@link OAuth2ServiceProviderRegistry}. The storage backend is a SQL Directory. 045 */ 046public class OAuth2ServiceProviderRegistryImpl extends DefaultComponent implements OAuth2ServiceProviderRegistry { 047 048 protected static final Log log = LogFactory.getLog(OAuth2ServiceProviderRegistryImpl.class); 049 050 public static final String PROVIDER_EP = "providers"; 051 052 public static final String DIRECTORY_NAME = "oauth2ServiceProviders"; 053 054 public static final String SCHEMA = "oauth2ServiceProvider"; 055 056 /** 057 * Registry of contributed providers. These providers can extend and/or override the default provider class. 058 */ 059 protected OAuth2ServiceProviderContributionRegistry registry = new OAuth2ServiceProviderContributionRegistry(); 060 061 protected DocumentModel getProviderDocModel(String serviceName) { 062 try { 063 if (StringUtils.isBlank(serviceName)) { 064 log.warn("Can not find provider without a serviceName!"); 065 return null; 066 } 067 068 Map<String, Serializable> filter = new HashMap<>(); 069 filter.put("serviceName", serviceName); 070 071 List<DocumentModel> providers = queryProviders(filter, 1); 072 return providers.isEmpty() ? null : providers.get(0); 073 } catch (DirectoryException e) { 074 log.error("Unable to read provider from Directory backend", e); 075 return null; 076 } 077 } 078 079 @Override 080 public OAuth2ServiceProvider getProvider(String serviceName) { 081 DocumentModel model = getProviderDocModel(serviceName); 082 return model == null ? null : buildProvider(model); 083 } 084 085 @Override 086 public List<OAuth2ServiceProvider> getProviders() { 087 List<DocumentModel> providers = queryProviders(Collections.emptyMap(), 0); 088 return providers.stream().map(this::buildProvider).collect(Collectors.toList()); 089 } 090 091 @Override 092 public OAuth2ServiceProvider addProvider(String serviceName, String description, String tokenServerURL, 093 String authorizationServerURL, String clientId, String clientSecret, List<String> scopes) { 094 return addProvider(serviceName, description, tokenServerURL, authorizationServerURL, null, clientId, 095 clientSecret, scopes, Boolean.TRUE); 096 } 097 098 @Override 099 public OAuth2ServiceProvider addProvider(String serviceName, String description, String tokenServerURL, 100 String authorizationServerURL, String userAuthorizationURL, String clientId, String clientSecret, 101 List<String> scopes, Boolean isEnabled) { 102 103 DirectoryService ds = Framework.getService(DirectoryService.class); 104 try (Session session = ds.open(DIRECTORY_NAME)) { 105 DocumentModel creationEntry = BaseSession.createEntryModel(null, SCHEMA, null, null); 106 DocumentModel entry = Framework.doPrivileged(() -> session.createEntry(creationEntry)); 107 entry.setProperty(SCHEMA, "serviceName", serviceName); 108 entry.setProperty(SCHEMA, "description", description); 109 entry.setProperty(SCHEMA, "authorizationServerURL", authorizationServerURL); 110 entry.setProperty(SCHEMA, "tokenServerURL", tokenServerURL); 111 entry.setProperty(SCHEMA, "userAuthorizationURL", userAuthorizationURL); 112 entry.setProperty(SCHEMA, "clientId", clientId); 113 entry.setProperty(SCHEMA, "clientSecret", clientSecret); 114 entry.setProperty(SCHEMA, "scopes", String.join(",", scopes)); 115 boolean enabled = (clientId != null && clientSecret != null); 116 entry.setProperty(SCHEMA, "enabled", Boolean.valueOf(enabled && (isEnabled == null ? false : isEnabled))); 117 if (!enabled) { 118 log.info("OAuth2 provider for " + serviceName 119 + " is disabled because clientId and/or clientSecret are empty"); 120 } 121 Framework.doPrivileged(() -> session.updateEntry(entry)); 122 return getProvider(serviceName); 123 } 124 } 125 126 @Override 127 public OAuth2ServiceProvider updateProvider(String serviceName, OAuth2ServiceProvider provider) { 128 DirectoryService ds = Framework.getService(DirectoryService.class); 129 try (Session session = ds.open(DIRECTORY_NAME)) { 130 DocumentModel entry = getProviderDocModel(serviceName); 131 entry.setProperty(SCHEMA, "serviceName", provider.getServiceName()); 132 entry.setProperty(SCHEMA, "description", provider.getDescription()); 133 entry.setProperty(SCHEMA, "authorizationServerURL", provider.getAuthorizationServerURL()); 134 entry.setProperty(SCHEMA, "tokenServerURL", provider.getTokenServerURL()); 135 entry.setProperty(SCHEMA, "userAuthorizationURL", provider.getUserAuthorizationURL()); 136 entry.setProperty(SCHEMA, "clientId", provider.getClientId()); 137 entry.setProperty(SCHEMA, "clientSecret", provider.getClientSecret()); 138 entry.setProperty(SCHEMA, "scopes", String.join(",", provider.getScopes())); 139 boolean enabled = provider.getClientId() != null && provider.getClientSecret() != null; 140 entry.setProperty(SCHEMA, "enabled", Boolean.valueOf(enabled && provider.isEnabled())); 141 if (!enabled) { 142 log.info("OAuth2 provider for " + serviceName 143 + " is disabled because clientId and/or clientSecret are empty"); 144 } 145 session.updateEntry(entry); 146 return getProvider(serviceName); 147 } 148 } 149 150 @Override 151 public void deleteProvider(String serviceName) { 152 DirectoryService ds = Framework.getService(DirectoryService.class); 153 try (Session session = ds.open(DIRECTORY_NAME)) { 154 DocumentModel entry = getProviderDocModel(serviceName); 155 session.deleteEntry(entry); 156 } 157 } 158 159 protected List<DocumentModel> queryProviders(Map<String, Serializable> filter, int limit) { 160 DirectoryService ds = Framework.getService(DirectoryService.class); 161 return Framework.doPrivileged(() -> { 162 try (Session session = ds.open(DIRECTORY_NAME)) { 163 Set<String> fulltext = Collections.emptySet(); 164 Map<String, String> orderBy = Collections.emptyMap(); 165 return session.query(filter, fulltext, orderBy, true, limit, 0); 166 } catch (DirectoryException e) { 167 log.error("Error while fetching provider directory", e); 168 return Collections.emptyList(); 169 } 170 }); 171 } 172 173 /** 174 * Instantiates the provider merging the contribution and the directory entry 175 */ 176 protected OAuth2ServiceProvider buildProvider(DocumentModel entry) { 177 String serviceName = (String) entry.getProperty(SCHEMA, "serviceName"); 178 OAuth2ServiceProvider provider = registry.getProvider(serviceName); 179 if (provider == null) { 180 provider = new NuxeoOAuth2ServiceProvider(); 181 provider.setServiceName(serviceName); 182 } 183 provider.setId((Long) entry.getProperty(SCHEMA, "id")); 184 provider.setDescription((String) entry.getProperty(SCHEMA, "description")); 185 provider.setAuthorizationServerURL((String) entry.getProperty(SCHEMA, "authorizationServerURL")); 186 provider.setTokenServerURL((String) entry.getProperty(SCHEMA, "tokenServerURL")); 187 provider.setUserAuthorizationURL((String) entry.getProperty(SCHEMA, "userAuthorizationURL")); 188 provider.setClientId((String) entry.getProperty(SCHEMA, "clientId")); 189 provider.setClientSecret((String) entry.getProperty(SCHEMA, "clientSecret")); 190 String scopes = (String) entry.getProperty(SCHEMA, "scopes"); 191 provider.setScopes(StringUtils.split(scopes, ",")); 192 provider.setEnabled((Boolean) entry.getProperty(SCHEMA, "enabled")); 193 return provider; 194 } 195 196 @Override 197 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 198 if (PROVIDER_EP.equals(extensionPoint)) { 199 OAuth2ServiceProviderDescriptor provider = (OAuth2ServiceProviderDescriptor) contribution; 200 log.info("OAuth2 provider for " + provider.getName() + " will be registered at application startup"); 201 // delay registration because data sources may not be available 202 // at this point 203 registry.addContribution(provider); 204 } 205 } 206 207 @Override 208 public void start(ComponentContext context) { 209 registerCustomProviders(); 210 } 211 212 protected void registerCustomProviders() { 213 for (OAuth2ServiceProviderDescriptor provider : registry.getContribs()) { 214 if (getProvider(provider.getName()) == null) { 215 addProvider(provider.getName(), provider.getDescription(), provider.getTokenServerURL(), 216 provider.getAuthorizationServerURL(), provider.getClientId(), provider.getClientSecret(), 217 Arrays.asList(provider.getScopes())); 218 } else { 219 log.info("Provider " + provider.getName() 220 + " is already in the Database, XML contribution won't overwrite it"); 221 } 222 } 223 } 224}