diff options
author | Jesse Morgan <jesse@jesterpm.net> | 2014-03-15 10:19:35 -0700 |
---|---|---|
committer | Jesse Morgan <jesse@jesterpm.net> | 2014-03-15 10:19:35 -0700 |
commit | 1494010751e89a6ed748ed90d702d5574ffbdd9b (patch) | |
tree | 626f0229e4309df4b7fc23b21aab3b38824faa95 /src/com/p4square | |
parent | 21f1ddafde8a552e4671d2bc574aa97fa86f95a3 (diff) |
Adding authenticated outside access to backend.
Diffstat (limited to 'src/com/p4square')
-rw-r--r-- | src/com/p4square/grow/GrowProcessComponent.java | 87 | ||||
-rw-r--r-- | src/com/p4square/grow/backend/BackendVerifier.java | 92 | ||||
-rw-r--r-- | src/com/p4square/grow/backend/GrowBackend.java | 32 | ||||
-rw-r--r-- | src/com/p4square/grow/backend/resources/AccountResource.java | 40 | ||||
-rw-r--r-- | src/com/p4square/grow/model/UserRecord.java | 82 | ||||
-rw-r--r-- | src/com/p4square/grow/provider/DelegateProvider.java | 40 | ||||
-rw-r--r-- | src/com/p4square/grow/provider/ProvidesUserRecords.java | 19 | ||||
-rw-r--r-- | src/com/p4square/grow/provider/QuestionProvider.java | 41 |
8 files changed, 370 insertions, 63 deletions
diff --git a/src/com/p4square/grow/GrowProcessComponent.java b/src/com/p4square/grow/GrowProcessComponent.java index 4196a5e..eb92840 100644 --- a/src/com/p4square/grow/GrowProcessComponent.java +++ b/src/com/p4square/grow/GrowProcessComponent.java @@ -4,9 +4,21 @@ package com.p4square.grow; +import java.io.File; +import java.io.IOException; + +import org.apache.log4j.Logger; + +import org.restlet.Application; +import org.restlet.Client; import org.restlet.Component; +import org.restlet.Restlet; +import org.restlet.data.ChallengeScheme; import org.restlet.data.Protocol; +import org.restlet.resource.Directory; +import org.restlet.security.ChallengeAuthenticator; +import com.p4square.grow.backend.BackendVerifier; import com.p4square.grow.backend.GrowBackend; import com.p4square.grow.config.Config; import com.p4square.grow.frontend.GrowFrontend; @@ -16,6 +28,10 @@ import com.p4square.grow.frontend.GrowFrontend; * @author Jesse Morgan <jesse@jesterpm.net> */ public class GrowProcessComponent extends Component { + private static Logger LOG = Logger.getLogger(GrowProcessComponent.class); + + private static final String BACKEND_REALM = "Grow Backend"; + private final Config mConfig; /** @@ -37,6 +53,13 @@ public class GrowProcessComponent extends Component { // Backend GrowBackend backend = new GrowBackend(mConfig); getInternalRouter().attach("/backend", backend); + + // Authenticated access to the backend + BackendVerifier verifier = new BackendVerifier(backend.getUserRecordProvider()); + ChallengeAuthenticator auth = new ChallengeAuthenticator(getContext(), false, + ChallengeScheme.HTTP_BASIC, BACKEND_REALM, verifier); + auth.setNext(backend); + getDefaultHost().attach("/backend", auth); } @Override @@ -56,4 +79,68 @@ public class GrowProcessComponent extends Component { super.start(); } + + /** + * Stand-alone main for testing. + */ + public static void main(String[] args) throws Exception { + // Start the HTTP Server + final GrowProcessComponent component = new GrowProcessComponent(); + component.getServers().add(Protocol.HTTP, 8085); + component.getClients().add(Protocol.HTTP); + component.getClients().add(Protocol.HTTPS); + component.getClients().add(Protocol.FILE); + //component.getClients().add(new Client(null, Arrays.asList(Protocol.HTTPS), "org.restlet.ext.httpclient.HttpClientHelper")); + + // Static content + try { + component.getDefaultHost().attach("/images/", new FileServingApp("./build/root/images/")); + component.getDefaultHost().attach("/scripts", new FileServingApp("./build/root/scripts")); + component.getDefaultHost().attach("/style.css", new FileServingApp("./build/root/style.css")); + component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./build/root/favicon.ico")); + component.getDefaultHost().attach("/notfound.html", new FileServingApp("./build/root/notfound.html")); + component.getDefaultHost().attach("/error.html", new FileServingApp("./build/root/error.html")); + } catch (IOException e) { + LOG.error("Could not create directory for static resources: " + + e.getMessage(), e); + } + + // Load an optional config file from the first argument. + component.mConfig.setDomain("dev"); + if (args.length == 1) { + component.mConfig.updateConfig(args[0]); + } + + // Setup shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread() { + public void run() { + try { + component.stop(); + } catch (Exception e) { + LOG.error("Exception during cleanup", e); + } + } + }); + + LOG.info("Starting server..."); + + try { + component.start(); + } catch (Exception e) { + LOG.fatal("Could not start: " + e.getMessage(), e); + } + } + + private static class FileServingApp extends Application { + private final String mPath; + + public FileServingApp(String path) throws IOException { + mPath = new File(path).getAbsolutePath(); + } + + @Override + public Restlet createInboundRoot() { + return new Directory(getContext(), "file://" + mPath); + } + } } diff --git a/src/com/p4square/grow/backend/BackendVerifier.java b/src/com/p4square/grow/backend/BackendVerifier.java new file mode 100644 index 0000000..83160a9 --- /dev/null +++ b/src/com/p4square/grow/backend/BackendVerifier.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.backend; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.binary.Hex; + +import org.restlet.security.SecretVerifier; + +import com.p4square.grow.model.UserRecord; +import com.p4square.grow.provider.Provider; + +/** + * Verify the given credentials against the users with backend access. + */ +public class BackendVerifier extends SecretVerifier { + + private final Provider<String, UserRecord> mUserProvider; + + public BackendVerifier(Provider<String, UserRecord> userProvider) { + mUserProvider = userProvider; + } + + @Override + public int verify(String identifier, char[] secret) { + if (identifier == null) { + throw new IllegalArgumentException("Null identifier"); + } + + if (secret == null) { + throw new IllegalArgumentException("Null secret"); + } + + // Does the user exist? + UserRecord user; + try { + user = mUserProvider.get(identifier); + if (user == null) { + return RESULT_UNKNOWN; + } + + } catch (IOException e) { + return RESULT_UNKNOWN; + } + + // Does the user have a backend password? + String storedHash = user.getBackendPasswordHash(); + if (storedHash == null) { + // This user doesn't have access + return RESULT_INVALID; + } + + // Validate the password. + try { + String hashedInput = hashPassword(secret); + if (hashedInput.equals(storedHash)) { + return RESULT_VALID; + } + + } catch (NoSuchAlgorithmException e) { + return RESULT_UNSUPPORTED; + } + + // If all else fails, fail. + return RESULT_INVALID; + } + + /** + * Hash the given secret. + */ + public static String hashPassword(char[] secret) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + // Convert the char[] to byte[] + // FIXME This approach is incorrectly truncating multibyte + // characters. + byte[] b = new byte[secret.length]; + for (int i = 0; i < secret.length; i++) { + b[i] = (byte) secret[i]; + } + + md.update(b); + + byte[] hash = md.digest(); + return new String(Hex.encodeHex(hash)); + } +} diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java index f844feb..683c99b 100644 --- a/src/com/p4square/grow/backend/GrowBackend.java +++ b/src/com/p4square/grow/backend/GrowBackend.java @@ -22,17 +22,19 @@ import com.p4square.grow.backend.db.CassandraProviderImpl; import com.p4square.grow.backend.db.CassandraCollectionProvider; import com.p4square.grow.backend.db.CassandraTrainingRecordProvider; +import com.p4square.grow.model.Message; +import com.p4square.grow.model.MessageThread; +import com.p4square.grow.model.Playlist; import com.p4square.grow.model.Question; import com.p4square.grow.model.TrainingRecord; -import com.p4square.grow.model.Playlist; -import com.p4square.grow.model.MessageThread; -import com.p4square.grow.model.Message; +import com.p4square.grow.model.UserRecord; import com.p4square.grow.provider.CollectionProvider; +import com.p4square.grow.provider.DelegateProvider; import com.p4square.grow.provider.Provider; import com.p4square.grow.provider.ProvidesQuestions; import com.p4square.grow.provider.ProvidesTrainingRecords; -import com.p4square.grow.provider.QuestionProvider; +import com.p4square.grow.provider.ProvidesUserRecords; import com.p4square.grow.backend.resources.AccountResource; import com.p4square.grow.backend.resources.BannerResource; @@ -51,7 +53,8 @@ import com.p4square.grow.backend.feed.TopicResource; * @author Jesse Morgan <jesse@jesterpm.net> */ public class GrowBackend extends Application - implements ProvidesQuestions, ProvidesTrainingRecords, FeedDataProvider { + implements ProvidesQuestions, ProvidesTrainingRecords, FeedDataProvider, + ProvidesUserRecords { private static final String DEFAULT_COLUMN = "value"; private final static Logger LOG = Logger.getLogger(GrowBackend.class); @@ -59,6 +62,8 @@ public class GrowBackend extends Application private final Config mConfig; private final CassandraDatabase mDatabase; + private final Provider<String, UserRecord> mUserRecordProvider; + private final Provider<String, Question> mQuestionProvider; private final CassandraTrainingRecordProvider mTrainingRecordProvider; @@ -73,7 +78,16 @@ public class GrowBackend extends Application mConfig = config; mDatabase = new CassandraDatabase(); - mQuestionProvider = new QuestionProvider<CassandraKey>(new CassandraProviderImpl<Question>(mDatabase, Question.class)) { + mUserRecordProvider = new DelegateProvider<String, CassandraKey, UserRecord>( + new CassandraProviderImpl<UserRecord>(mDatabase, UserRecord.class)) { + @Override + public CassandraKey makeKey(String userid) { + return new CassandraKey("accounts", userid, DEFAULT_COLUMN); + } + }; + + mQuestionProvider = new DelegateProvider<String, CassandraKey, Question>( + new CassandraProviderImpl<Question>(mDatabase, Question.class)) { @Override public CassandraKey makeKey(String questionId) { return new CassandraKey("strings", "/questions/" + questionId, DEFAULT_COLUMN); @@ -93,6 +107,7 @@ public class GrowBackend extends Application Router router = new Router(getContext()); // Account API + router.attach("/accounts", AccountResource.class); router.attach("/accounts/{userId}", AccountResource.class); // Survey API @@ -153,6 +168,11 @@ public class GrowBackend extends Application } @Override + public Provider<String, UserRecord> getUserRecordProvider() { + return mUserRecordProvider; + } + + @Override public Provider<String, Question> getQuestionProvider() { return mQuestionProvider; } diff --git a/src/com/p4square/grow/backend/resources/AccountResource.java b/src/com/p4square/grow/backend/resources/AccountResource.java index f3404c0..2ac7061 100644 --- a/src/com/p4square/grow/backend/resources/AccountResource.java +++ b/src/com/p4square/grow/backend/resources/AccountResource.java @@ -9,12 +9,15 @@ import java.io.IOException; import org.restlet.data.Status; import org.restlet.resource.ServerResource; import org.restlet.representation.Representation; -import org.restlet.representation.StringRepresentation; + +import org.restlet.ext.jackson.JacksonRepresentation; import org.apache.log4j.Logger; -import com.p4square.grow.backend.GrowBackend; -import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.model.UserRecord; +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.ProvidesUserRecords; +import com.p4square.grow.provider.JsonEncodedProvider; /** * Stores a document about a user. @@ -24,7 +27,7 @@ import com.p4square.grow.backend.db.CassandraDatabase; public class AccountResource extends ServerResource { private static final Logger LOG = Logger.getLogger(AccountResource.class); - private CassandraDatabase mDb; + private Provider<String, UserRecord> mUserRecordProvider; private String mUserId; @@ -32,8 +35,8 @@ public class AccountResource extends ServerResource { public void doInit() { super.doInit(); - final GrowBackend backend = (GrowBackend) getApplication(); - mDb = backend.getDatabase(); + final ProvidesUserRecords backend = (ProvidesUserRecords) getApplication(); + mUserRecordProvider = backend.getUserRecordProvider(); mUserId = getAttribute("userId"); } @@ -43,14 +46,22 @@ public class AccountResource extends ServerResource { */ @Override protected Representation get() { - String result = mDb.getKey("accounts", mUserId); + try { + UserRecord result = mUserRecordProvider.get(mUserId); + + if (result == null) { + setStatus(Status.CLIENT_ERROR_NOT_FOUND); + return null; + } + + JacksonRepresentation<UserRecord> rep = new JacksonRepresentation<UserRecord>(result); + rep.setObjectMapper(JsonEncodedProvider.MAPPER); + return rep; - if (result == null) { - setStatus(Status.CLIENT_ERROR_NOT_FOUND); + } catch (IOException e) { + setStatus(Status.SERVER_ERROR_INTERNAL); return null; } - - return new StringRepresentation(result); } /** @@ -59,7 +70,12 @@ public class AccountResource extends ServerResource { @Override protected Representation put(Representation entity) { try { - mDb.putKey("accounts", mUserId, entity.getText()); + JacksonRepresentation<UserRecord> representation = + new JacksonRepresentation<>(entity, UserRecord.class); + representation.setObjectMapper(JsonEncodedProvider.MAPPER); + UserRecord record = representation.getObject(); + + mUserRecordProvider.put(mUserId, record); setStatus(Status.SUCCESS_NO_CONTENT); } catch (IOException e) { diff --git a/src/com/p4square/grow/model/UserRecord.java b/src/com/p4square/grow/model/UserRecord.java index 0702eb1..b5aaa4e 100644 --- a/src/com/p4square/grow/model/UserRecord.java +++ b/src/com/p4square/grow/model/UserRecord.java @@ -4,6 +4,12 @@ package com.p4square.grow.model; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.binary.Hex; + import org.restlet.security.User; /** @@ -14,6 +20,10 @@ public class UserRecord { private String mFirstName; private String mLastName; private String mEmail; + private String mLanding; + + // Backend Access + private String mBackendPasswordHash; /** * Create an empty UserRecord. @@ -42,7 +52,7 @@ public class UserRecord { * Set the user's identifier. * @param value The new id. */ - public void setId (final String value) { + public void setId(final String value) { mId = value; } @@ -57,7 +67,7 @@ public class UserRecord { * Set the user's email. * @param value The new email. */ - public void setEmail (final String value) { + public void setEmail(final String value) { mEmail = value; } @@ -72,7 +82,7 @@ public class UserRecord { * Set the user's first name. * @param value The new first name. */ - public void setFirstName (final String value) { + public void setFirstName(final String value) { mFirstName = value; } @@ -87,7 +97,71 @@ public class UserRecord { * Set the user's last name. * @param value The new last name. */ - public void setLastName (final String value) { + public void setLastName(final String value) { mLastName = value; } + + /** + * @return The user's landing page. + */ + public String getLanding() { + return mLanding; + } + + /** + * Set the user's landing page. + * @param value The new landing page. + */ + public void setLanding(final String value) { + mLanding = value; + } + + /** + * @return The user's backend password hash, null if he doesn't have + * access. + */ + public String getBackendPasswordHash() { + return mBackendPasswordHash; + } + + /** + * Set the user's backend password hash. + * @param value The new backend password hash or null to remove + * access. + */ + public void setBackendPasswordHash(final String value) { + mBackendPasswordHash = value; + } + + /** + * Set the user's backend password to the clear-text value given. + * @param value The new backend password. + */ + public void setBackendPassword(final String value) { + try { + mBackendPasswordHash = hashPassword(value); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Hash the given secret. + */ + public static String hashPassword(final String secret) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + // Convert the char[] to byte[] + // FIXME This approach is incorrectly truncating multibyte + // characters. + byte[] b = new byte[secret.length()]; + for (int i = 0; i < secret.length(); i++) { + b[i] = (byte) secret.charAt(i); + } + + md.update(b); + + byte[] hash = md.digest(); + return new String(Hex.encodeHex(hash)); + } } diff --git a/src/com/p4square/grow/provider/DelegateProvider.java b/src/com/p4square/grow/provider/DelegateProvider.java new file mode 100644 index 0000000..66c5666 --- /dev/null +++ b/src/com/p4square/grow/provider/DelegateProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +/** + * DelegateProvider wraps an existing Provider an transforms the key from + * type K to type D. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public abstract class DelegateProvider<K, D, V> implements Provider<K, V> { + + private Provider<D, V> mProvider; + + public DelegateProvider(final Provider<D, V> provider) { + mProvider = provider; + } + + @Override + public V get(final K key) throws IOException { + return mProvider.get(makeKey(key)); + } + + @Override + public void put(final K key, final V obj) throws IOException { + mProvider.put(makeKey(key), obj); + } + + /** + * Make a Key for questionId. + * + * @param questionId The question id. + * @return a key for questionId. + */ + protected abstract D makeKey(final K input); +} diff --git a/src/com/p4square/grow/provider/ProvidesUserRecords.java b/src/com/p4square/grow/provider/ProvidesUserRecords.java new file mode 100644 index 0000000..d77c878 --- /dev/null +++ b/src/com/p4square/grow/provider/ProvidesUserRecords.java @@ -0,0 +1,19 @@ +/* + * Copyright 2014 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import com.p4square.grow.model.UserRecord; + +/** + * Indicates the ability to provide a UserRecord Provider. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public interface ProvidesUserRecords { + /** + * @return A Provider of Questions keyed by question id. + */ + Provider<String, UserRecord> getUserRecordProvider(); +} diff --git a/src/com/p4square/grow/provider/QuestionProvider.java b/src/com/p4square/grow/provider/QuestionProvider.java deleted file mode 100644 index b569dc8..0000000 --- a/src/com/p4square/grow/provider/QuestionProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2013 Jesse Morgan - */ - -package com.p4square.grow.provider; - -import java.io.IOException; - -import com.p4square.grow.model.Question; - -/** - * QuestionProvider wraps an existing Provider to get and put Questions. - * - * @author Jesse Morgan <jesse@jesterpm.net> - */ -public abstract class QuestionProvider<K> implements Provider<String, Question> { - - private Provider<K, Question> mProvider; - - public QuestionProvider(Provider<K, Question> provider) { - mProvider = provider; - } - - @Override - public Question get(String key) throws IOException { - return mProvider.get(makeKey(key)); - } - - @Override - public void put(String key, Question obj) throws IOException { - mProvider.put(makeKey(key), obj); - } - - /** - * Make a Key for questionId. - * - * @param questionId The question id. - * @return a key for questionId. - */ - protected abstract K makeKey(String questionId); -} |