001/*
002 * (C) Copyright 2006-2013 Nuxeo SA (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 *     André Justo
019 */
020package org.nuxeo.ecm.platform.oauth2.tokens;
021
022import java.io.IOException;
023import java.io.Serializable;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentModelList;
037import org.nuxeo.ecm.directory.DirectoryException;
038import org.nuxeo.ecm.directory.Session;
039import org.nuxeo.ecm.directory.api.DirectoryService;
040import org.nuxeo.runtime.api.Framework;
041
042import com.google.api.client.auth.oauth2.StoredCredential;
043import com.google.api.client.util.store.DataStore;
044import com.google.api.client.util.store.DataStoreFactory;
045
046/**
047 * {@link DataStore} backed by a Nuxeo Directory
048 *
049 * @since 7.3
050 */
051public class OAuth2TokenStore implements DataStore<StoredCredential> {
052
053    protected static final Log log = LogFactory.getLog(OAuth2TokenStore.class);
054
055    public static final String DIRECTORY_NAME = "oauth2Tokens";
056
057    public static final String ENTRY_ID = "id";
058
059    private String serviceName;
060
061    public OAuth2TokenStore(String serviceName) {
062        this.serviceName = serviceName;
063    }
064
065    @Override
066    public DataStore<StoredCredential> set(String key, StoredCredential credential) throws IOException {
067        Map<String, Serializable> filter = new HashMap<>();
068        filter.put(ENTRY_ID, key);
069        DocumentModel entry = find(filter);
070
071        if (entry == null) {
072            store(key, new NuxeoOAuth2Token(credential));
073        } else {
074            refresh(entry, new NuxeoOAuth2Token(credential));
075        }
076        return this;
077    }
078
079    @Override
080    public DataStore<StoredCredential> delete(String key) throws IOException {
081        DirectoryService ds = Framework.getService(DirectoryService.class);
082        try (Session session = ds.open(DIRECTORY_NAME)) {
083            Map<String, Serializable> filter = new HashMap<>();
084            filter.put("serviceName", serviceName);
085            filter.put(ENTRY_ID, key);
086
087            DocumentModelList entries = session.query(filter);
088            for (DocumentModel entry : entries) {
089                session.deleteEntry(entry);
090            }
091        }
092        return this;
093    }
094
095    @Override
096    public StoredCredential get(String key) throws IOException {
097        Map<String, Serializable> filter = new HashMap<>();
098        filter.put(ENTRY_ID, key);
099        DocumentModel entry = find(filter);
100        return entry != null ? NuxeoOAuth2Token.asCredential(entry) : null;
101    }
102
103    @Override
104    public DataStoreFactory getDataStoreFactory() {
105        return null;
106    }
107
108    @Override
109    public final String getId() {
110        return this.serviceName;
111    }
112
113    @Override
114    public boolean containsKey(String key) throws IOException {
115        return this.get(key) != null;
116    }
117
118    @Override
119    public boolean containsValue(StoredCredential value) throws IOException {
120        return this.values().contains(value);
121    }
122
123    @Override
124    public boolean isEmpty() throws IOException {
125        return this.size() == 0;
126    }
127
128    @Override
129    public int size() throws IOException {
130        return this.keySet().size();
131    }
132
133    @Override
134    public Set<String> keySet() throws IOException {
135        Set<String> keys = new HashSet<>();
136        DocumentModelList entries = query();
137        for (DocumentModel entry : entries) {
138            keys.add((String) entry.getProperty(NuxeoOAuth2Token.SCHEMA, ENTRY_ID));
139        }
140        return keys;
141    }
142
143    @Override
144    public Collection<StoredCredential> values() throws IOException {
145        List<StoredCredential> results = new ArrayList<>();
146        DocumentModelList entries = query();
147        for (DocumentModel entry : entries) {
148            results.add(NuxeoOAuth2Token.asCredential(entry));
149        }
150        return results;
151    }
152
153    @Override
154    public DataStore<StoredCredential> clear() throws IOException {
155        return null;
156    }
157
158    /*
159     * Methods used by Nuxeo when acting as OAuth2 provider
160     */
161    public void store(String userId, NuxeoOAuth2Token token) {
162        token.setServiceName(serviceName);
163        token.setNuxeoLogin(userId);
164        try {
165            storeTokenAsDirectoryEntry(token);
166        } catch (DirectoryException e) {
167            log.error("Error during token storage", e);
168        }
169    }
170
171    public NuxeoOAuth2Token refresh(String refreshToken, String clientId) {
172        Map<String, Serializable> filter = new HashMap<>();
173        filter.put("clientId", clientId);
174        filter.put("refreshToken", refreshToken);
175        filter.put("serviceName", serviceName);
176
177        DocumentModel entry = find(filter);
178        if (entry != null) {
179            NuxeoOAuth2Token token = getTokenFromDirectoryEntry(entry);
180            delete(token.getAccessToken(), clientId);
181            token.refresh();
182            return storeTokenAsDirectoryEntry(token);
183        }
184        return null;
185    }
186
187    public NuxeoOAuth2Token refresh(DocumentModel entry, NuxeoOAuth2Token token) {
188        DirectoryService ds = Framework.getService(DirectoryService.class);
189        return Framework.doPrivileged(() -> {
190            try (Session session = ds.open(DIRECTORY_NAME)) {
191                entry.setProperty(NuxeoOAuth2Token.SCHEMA, "accessToken", token.getAccessToken());
192                entry.setProperty(NuxeoOAuth2Token.SCHEMA, "refreshToken", token.getRefreshToken());
193                entry.setProperty(NuxeoOAuth2Token.SCHEMA, "creationDate", token.getCreationDate());
194                entry.setProperty(NuxeoOAuth2Token.SCHEMA, "expirationTimeMilliseconds",
195                        token.getExpirationTimeMilliseconds());
196                session.updateEntry(entry);
197                return getTokenFromDirectoryEntry(entry);
198            }
199        });
200    }
201
202    /**
203     * @since 9.10
204     */
205    public boolean update(NuxeoOAuth2Token token) {
206        DirectoryService ds = Framework.getService(DirectoryService.class);
207        return Framework.doPrivileged(() -> {
208            try (Session session = ds.open(DIRECTORY_NAME)) {
209                DocumentModel entry = session.getEntry(String.valueOf(token.getId()));
210                if (entry == null) {
211                    return false;
212                }
213                entry.setProperties(NuxeoOAuth2Token.SCHEMA, token.toMap());
214                session.updateEntry(entry);
215                return true;
216            }
217        });
218    }
219
220    public void delete(String token, String clientId) {
221        DirectoryService ds = Framework.getService(DirectoryService.class);
222        Framework.doPrivileged(() -> {
223            try (Session session = ds.open(DIRECTORY_NAME)) {
224                Map<String, Serializable> filter = new HashMap<String, Serializable>();
225                filter.put("serviceName", serviceName);
226                filter.put("clientId", clientId);
227                filter.put("accessToken", token);
228
229                DocumentModelList entries = session.query(filter);
230                for (DocumentModel entry : entries) {
231                    session.deleteEntry(entry);
232                }
233            }
234        });
235    }
236
237    /**
238     * Retrieves an entry by its {@code accessToken}.
239     */
240    public NuxeoOAuth2Token getToken(String token) {
241        Map<String, Serializable> filter = new HashMap<>();
242        filter.put("accessToken", token);
243
244        DocumentModelList entries = query(filter);
245        if (entries.size() == 0) {
246            return null;
247        }
248        if (entries.size() > 1) {
249            log.error("Found several tokens");
250        }
251        return getTokenFromDirectoryEntry(entries.get(0));
252    }
253
254    /**
255     * Retrieves an entry by its {@code clientId} and {@code nuxeoLogin}.
256     *
257     * @since 9.10
258     */
259    public NuxeoOAuth2Token getToken(String clientId, String userId) {
260        Map<String, Serializable> filter = new HashMap<>();
261        filter.put("clientId", clientId);
262        filter.put("nuxeoLogin", userId);
263        Map<String, String> orderBy = Collections.singletonMap("creationDate", "desc");
264        DocumentModelList entries = query(filter, null, orderBy);
265        if (entries.isEmpty()) {
266            return null;
267        }
268        return getTokenFromDirectoryEntry(entries.get(0));
269    }
270
271    public DocumentModelList query() {
272        return query(new HashMap<>());
273    }
274
275    public DocumentModelList query(Map<String, Serializable> filter) {
276        return query(filter, null, null);
277    }
278
279    /**
280     * @since 9.10
281     */
282    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext,
283            Map<String, String> orderBy) {
284        DirectoryService ds = Framework.getService(DirectoryService.class);
285        return Framework.doPrivileged(() -> {
286            try (Session session = ds.open(DIRECTORY_NAME)) {
287                filter.put("serviceName", serviceName);
288                return session.query(filter, fulltext, orderBy);
289            }
290        });
291    }
292
293    protected NuxeoOAuth2Token getTokenFromDirectoryEntry(DocumentModel entry) {
294        return new NuxeoOAuth2Token(entry);
295    }
296
297    protected NuxeoOAuth2Token storeTokenAsDirectoryEntry(NuxeoOAuth2Token aToken) {
298        DirectoryService ds = Framework.getService(DirectoryService.class);
299        return Framework.doPrivileged(() -> {
300            try (Session session = ds.open(DIRECTORY_NAME)) {
301                DocumentModel entry = session.createEntry(aToken.toMap());
302                return getTokenFromDirectoryEntry(entry);
303            }
304        });
305    }
306
307    protected DocumentModel find(Map<String, Serializable> filter) {
308        DocumentModelList entries = query(filter);
309        if (entries.size() == 0) {
310            return null;
311        }
312        if (entries.size() > 1) {
313            log.error("Found several tokens");
314        }
315        return entries.get(0);
316    }
317}