001/* 002 * (C) Copyright 2014-2017 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 * Arnaud Kervern 018 */ 019package org.nuxeo.ecm.platform.oauth2.clients; 020 021import static java.util.Objects.requireNonNull; 022import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; 023import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; 024import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; 025 026import java.io.Serializable; 027import java.util.Collections; 028import java.util.List; 029import java.util.Map; 030import java.util.function.Function; 031import java.util.stream.Collectors; 032 033import org.apache.commons.lang3.StringUtils; 034import org.nuxeo.ecm.core.api.DocumentModel; 035import org.nuxeo.ecm.core.api.DocumentModelList; 036import org.nuxeo.ecm.core.api.NuxeoException; 037import org.nuxeo.ecm.core.api.NuxeoPrincipal; 038import org.nuxeo.ecm.directory.Session; 039import org.nuxeo.ecm.directory.api.DirectoryService; 040import org.nuxeo.runtime.api.Framework; 041import org.nuxeo.runtime.model.DefaultComponent; 042 043/** 044 * OAuth2 Client service 045 * 046 * @since 9.2 047 */ 048public class OAuth2ClientServiceImpl extends DefaultComponent implements OAuth2ClientService { 049 050 @Override 051 public boolean hasClient(String clientId) { 052 OAuth2Client client = getClient(clientId); 053 return client != null && client.isEnabled(); 054 } 055 056 @Override 057 public boolean isValidClient(String clientId, String clientSecret) { 058 OAuth2Client client = getClient(clientId); 059 return client != null && client.isValidWith(clientId, clientSecret); 060 } 061 062 @Override 063 public OAuth2Client getClient(String clientId) { 064 DocumentModel doc = getClientModel(clientId); 065 if (doc == null) { 066 return null; 067 } 068 return OAuth2Client.fromDocumentModel(doc); 069 } 070 071 @Override 072 public List<OAuth2Client> getClients() { 073 return queryClients().stream().map(OAuth2Client::fromDocumentModel).collect(Collectors.toList()); 074 } 075 076 @Override 077 public OAuth2Client create(OAuth2Client oAuth2Client, NuxeoPrincipal principal) { 078 validate(oAuth2Client); 079 checkUnicity(oAuth2Client.getId()); 080 081 return execute(session -> { 082 DocumentModel documentModel = OAuth2Client.fromOAuth2Client(oAuth2Client); 083 return OAuth2Client.fromDocumentModel(session.createEntry(documentModel)); 084 }, principal); 085 } 086 087 @Override 088 public OAuth2Client update(String clientId, OAuth2Client oAuth2Client, NuxeoPrincipal principal) { 089 validate(oAuth2Client); 090 if (!oAuth2Client.getId().equals(clientId)) { 091 checkUnicity(oAuth2Client.getId()); 092 } 093 094 DocumentModel doc = getDocument(clientId); 095 return execute(session -> { 096 DocumentModel documentModel = OAuth2Client.updateDocument(doc, oAuth2Client); 097 session.updateEntry(documentModel); 098 return OAuth2Client.fromDocumentModel(documentModel); 099 }, principal); 100 } 101 102 @Override 103 public void delete(String clientId, NuxeoPrincipal principal) { 104 DocumentModel document = getDocument(clientId); 105 execute(session -> { 106 session.deleteEntry(document); 107 return null; 108 }, principal); 109 } 110 111 protected DocumentModel getClientModel(String clientId) { 112 return execute(session -> { 113 Map<String, Serializable> filter = Collections.singletonMap("clientId", clientId); 114 DocumentModelList docs = session.query(filter); 115 if (docs.size() == 1) { 116 return docs.get(0); 117 } else if (docs.size() > 1) { 118 throw new NuxeoException(String.format("More than one client registered for the '%s' id", clientId)); 119 } 120 return null; 121 }); 122 } 123 124 protected List<DocumentModel> queryClients() { 125 return execute(session -> session.query(Collections.emptyMap())); 126 } 127 128 /** 129 * @since 11.1 130 */ 131 protected <T> T execute(Function<Session, T> function) { 132 return execute(function, null); 133 } 134 135 /** 136 * @since 11.1 137 */ 138 protected <T> T execute(Function<Session, T> function, NuxeoPrincipal principal) { 139 if (principal != null) { 140 checkPermission(principal); 141 } 142 DirectoryService service = Framework.getService(DirectoryService.class); 143 return Framework.doPrivileged(() -> { 144 try (Session session = service.open(OAUTH2CLIENT_DIRECTORY_NAME)) { 145 return function.apply(session); 146 } 147 }); 148 } 149 150 protected void checkPermission(NuxeoPrincipal principal) { 151 if (!principal.isAdministrator()) { 152 throw new NuxeoException("You do not have permissions to perform this operation.", SC_FORBIDDEN); 153 } 154 } 155 156 /** 157 * Validates the {@link OAuth2Client}. An {@link OAuth2Client} is valid if and only if 158 * <ul> 159 * <li>It is not {@code null}</li> 160 * <li>The required fields are filled in: 161 * {@link OAuth2Client#getId()},{@link OAuth2Client#getName()},{@link OAuth2Client#getRedirectURIs()}</li> 162 * <li>The {@link OAuth2Client#getRedirectURIs()} is a valid URI, 163 * {@link OAuth2Client#isRedirectURIValid(String)}</li> 164 * </ul> 165 * 166 * @param oAuth2Client the {@code not null} oAuth2Client to validate 167 * @throws NullPointerException if the oAuth2Client is {@code null} 168 * @throws NuxeoException if oAuth2Client is not valid 169 * @since 11.1 170 */ 171 protected void validate(OAuth2Client oAuth2Client) { 172 requireNonNull(oAuth2Client, "oAuth2Client is required"); 173 String message; 174 if (StringUtils.isBlank(oAuth2Client.getName())) { 175 message = "Client name is required"; 176 } else if (StringUtils.isBlank(oAuth2Client.getId())) { 177 message = "Client Id is required"; 178 } else if (oAuth2Client.getRedirectURIs().isEmpty()) { 179 message = "Redirect URIs is required"; 180 } else { 181 message = oAuth2Client.getRedirectURIs() 182 .stream() 183 .filter(uri -> !OAuth2Client.isRedirectURIValid(uri)) 184 .findAny() 185 .map(uri -> String.format("'%s' is not a valid redirect URI", uri)) 186 .orElse(null); 187 } 188 189 if (StringUtils.isNotEmpty(message)) { 190 throw new NuxeoException(String.format("%s", message), SC_BAD_REQUEST); 191 } 192 } 193 194 /** 195 * Checks if a client with the {@code clientId} is unique. 196 * 197 * @param clientId the client id to check 198 * @throws NuxeoException if an oAuth2 client with the given {@code clientId} already exists 199 * @since 11.1 200 */ 201 protected void checkUnicity(String clientId) { 202 if (getClientModel(clientId) != null) { 203 throw new NuxeoException(String.format("Client with id '%s' already exists", clientId), SC_BAD_REQUEST); 204 } 205 } 206 207 /** 208 * Gets the document model from a given {@code clientId} 209 * 210 * @param clientId the oAuth client id 211 * @throws NuxeoException if there is no document model for the given {@code clientId} 212 * @since 11.1 213 */ 214 protected DocumentModel getDocument(String clientId) { 215 DocumentModel doc = getClientModel(clientId); 216 if (doc == null) { 217 throw new NuxeoException(SC_NOT_FOUND); 218 } 219 220 return doc; 221 } 222 223}