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}