diff options
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); -} | 
