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