summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2014-03-15 10:19:35 -0700
committerJesse Morgan <jesse@jesterpm.net>2014-03-15 10:19:35 -0700
commit2f736d013550b36cd96ba8688846919f7da8f461 (patch)
tree415a39ba2a6ce2fa8769dea1e59ba537caa3a1cf
parent486fe2ecd48f1eae31160fc6d6765341e87d8678 (diff)
Adding authenticated outside access to backend.
-rw-r--r--build.xml8
-rw-r--r--src/com/p4square/grow/GrowProcessComponent.java87
-rw-r--r--src/com/p4square/grow/backend/BackendVerifier.java92
-rw-r--r--src/com/p4square/grow/backend/GrowBackend.java32
-rw-r--r--src/com/p4square/grow/backend/resources/AccountResource.java40
-rw-r--r--src/com/p4square/grow/model/UserRecord.java82
-rw-r--r--src/com/p4square/grow/provider/DelegateProvider.java40
-rw-r--r--src/com/p4square/grow/provider/ProvidesUserRecords.java19
-rw-r--r--src/com/p4square/grow/provider/QuestionProvider.java41
-rw-r--r--src/grow.properties1
10 files changed, 378 insertions, 64 deletions
diff --git a/build.xml b/build.xml
index 75e514d..ac675ae 100644
--- a/build.xml
+++ b/build.xml
@@ -14,6 +14,14 @@
<!-- Targets to get this project ready -->
<target name="bootstrap" depends="setup-tomcat" />
+ <target name="server" depends="compile">
+ <java classname="com.p4square.grow.GrowProcessComponent"
+ classpathref="classpath.run" fork="true">
+
+ <arg file="devfiles/grow-server.properties" />
+ </java>
+ </target>
+
<target name="frontend" depends="compile">
<java classname="com.p4square.grow.frontend.GrowFrontend"
classpathref="classpath.run" fork="true">
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);
-}
diff --git a/src/grow.properties b/src/grow.properties
index d9d3f29..941cc0e 100644
--- a/src/grow.properties
+++ b/src/grow.properties
@@ -1,5 +1,4 @@
# Frontend Settings
-dev.backendUri = http://localhost:9095
dev.staticRoot =
dev.dynamicRoot =