Merge pull request #89 from sismics/master

Push to production
This commit is contained in:
Benjamin Gamard 2016-03-24 00:06:52 +01:00
commit 767099b7ea
72 changed files with 3669 additions and 122 deletions

View File

@ -23,11 +23,11 @@ Features
- Optical character recognition - Optical character recognition
- Support image, PDF, ODT and DOCX files - Support image, PDF, ODT and DOCX files
- Flexible search engine - Flexible search engine
- Full text search in image and PDF - Full text search in all supported files
- All [Dublin Core](http://dublincore.org/) metadata - All [Dublin Core](http://dublincore.org/) metadata
- 256-bit AES encryption - 256-bit AES encryption of stored files
- Tag system with relations - Tag system with nesting
- Multi-users ACL system - User/group permission system
- Hierarchical groups - Hierarchical groups
- Audit log - Audit log
- Comments - Comments

View File

@ -77,7 +77,6 @@
<artifactId>jbcrypt</artifactId> <artifactId>jbcrypt</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.lucene</groupId> <groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId> <artifactId>lucene-core</artifactId>

View File

@ -36,9 +36,9 @@ public class UserDao {
* *
* @param username User login * @param username User login
* @param password User password * @param password User password
* @return ID of the authenticated user or null * @return The authenticated user or null
*/ */
public String authenticate(String username, String password) { public User authenticate(String username, String password) {
EntityManager em = ThreadLocalContext.get().getEntityManager(); EntityManager em = ThreadLocalContext.get().getEntityManager();
Query q = em.createQuery("select u from User u where u.username = :username and u.deleteDate is null"); Query q = em.createQuery("select u from User u where u.username = :username and u.deleteDate is null");
q.setParameter("username", username); q.setParameter("username", username);
@ -47,7 +47,7 @@ public class UserDao {
if (!BCrypt.checkpw(password, user.getPassword())) { if (!BCrypt.checkpw(password, user.getPassword())) {
return null; return null;
} }
return user.getId(); return user;
} catch (NoResultException e) { } catch (NoResultException e) {
return null; return null;
} }
@ -104,6 +104,7 @@ public class UserDao {
userFromDb.setEmail(user.getEmail()); userFromDb.setEmail(user.getEmail());
userFromDb.setStorageQuota(user.getStorageQuota()); userFromDb.setStorageQuota(user.getStorageQuota());
userFromDb.setStorageCurrent(user.getStorageCurrent()); userFromDb.setStorageCurrent(user.getStorageCurrent());
userFromDb.setTotpKey(user.getTotpKey());
// Create audit log // Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId); AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId);

View File

@ -64,56 +64,63 @@ public class AuthenticationToken {
return id; return id;
} }
public void setId(String id) { public AuthenticationToken setId(String id) {
this.id = id; this.id = id;
return this;
} }
public String getUserId() { public String getUserId() {
return userId; return userId;
} }
public void setUserId(String userId) { public AuthenticationToken setUserId(String userId) {
this.userId = userId; this.userId = userId;
return this;
} }
public boolean isLongLasted() { public boolean isLongLasted() {
return longLasted; return longLasted;
} }
public void setLongLasted(boolean longLasted) { public AuthenticationToken setLongLasted(boolean longLasted) {
this.longLasted = longLasted; this.longLasted = longLasted;
return this;
} }
public String getIp() { public String getIp() {
return ip; return ip;
} }
public void setIp(String ip) { public AuthenticationToken setIp(String ip) {
this.ip = ip; this.ip = ip;
return this;
} }
public String getUserAgent() { public String getUserAgent() {
return userAgent; return userAgent;
} }
public void setUserAgent(String userAgent) { public AuthenticationToken setUserAgent(String userAgent) {
this.userAgent = userAgent; this.userAgent = userAgent;
return this;
} }
public Date getCreationDate() { public Date getCreationDate() {
return creationDate; return creationDate;
} }
public void setCreationDate(Date creationDate) { public AuthenticationToken setCreationDate(Date creationDate) {
this.creationDate = creationDate; this.creationDate = creationDate;
return this;
} }
public Date getLastConnectionDate() { public Date getLastConnectionDate() {
return lastConnectionDate; return lastConnectionDate;
} }
public void setLastConnectionDate(Date lastConnectionDate) { public AuthenticationToken setLastConnectionDate(Date lastConnectionDate) {
this.lastConnectionDate = lastConnectionDate; this.lastConnectionDate = lastConnectionDate;
return this;
} }
@Override @Override

View File

@ -48,6 +48,12 @@ public class User implements Loggable {
@Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100) @Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100)
private String privateKey; private String privateKey;
/**
* TOTP secret key.
*/
@Column(name = "USE_TOTPKEY_C", length = 100)
private String totpKey;
/** /**
* Email address. * Email address.
*/ */
@ -82,48 +88,54 @@ public class User implements Loggable {
return id; return id;
} }
public void setId(String id) { public User setId(String id) {
this.id = id; this.id = id;
return this;
} }
public String getRoleId() { public String getRoleId() {
return roleId; return roleId;
} }
public void setRoleId(String roleId) { public User setRoleId(String roleId) {
this.roleId = roleId; this.roleId = roleId;
return this;
} }
public String getUsername() { public String getUsername() {
return username; return username;
} }
public void setUsername(String username) { public User setUsername(String username) {
this.username = username; this.username = username;
return this;
} }
public String getPassword() { public String getPassword() {
return password; return password;
} }
public void setPassword(String password) { public User setPassword(String password) {
this.password = password; this.password = password;
return this;
} }
public String getEmail() { public String getEmail() {
return email; return email;
} }
public void setEmail(String email) { public User setEmail(String email) {
this.email = email; this.email = email;
return this;
} }
public Date getCreateDate() { public Date getCreateDate() {
return createDate; return createDate;
} }
public void setCreateDate(Date createDate) { public User setCreateDate(Date createDate) {
this.createDate = createDate; this.createDate = createDate;
return this;
} }
@Override @Override
@ -131,32 +143,45 @@ public class User implements Loggable {
return deleteDate; return deleteDate;
} }
public void setDeleteDate(Date deleteDate) { public User setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate; this.deleteDate = deleteDate;
return this;
} }
public String getPrivateKey() { public String getPrivateKey() {
return privateKey; return privateKey;
} }
public void setPrivateKey(String privateKey) { public User setPrivateKey(String privateKey) {
this.privateKey = privateKey; this.privateKey = privateKey;
return this;
} }
public Long getStorageQuota() { public Long getStorageQuota() {
return storageQuota; return storageQuota;
} }
public void setStorageQuota(Long storageQuota) { public User setStorageQuota(Long storageQuota) {
this.storageQuota = storageQuota; this.storageQuota = storageQuota;
return this;
} }
public Long getStorageCurrent() { public Long getStorageCurrent() {
return storageCurrent; return storageCurrent;
} }
public void setStorageCurrent(Long storageCurrent) { public User setStorageCurrent(Long storageCurrent) {
this.storageCurrent = storageCurrent; this.storageCurrent = storageCurrent;
return this;
}
public String getTotpKey() {
return totpKey;
}
public User setTotpKey(String totpKey) {
this.totpKey = totpKey;
return this;
} }
@Override @Override

View File

@ -0,0 +1,456 @@
/*
* Copyright (c) 2014-2016 Enrico M. Crisostomo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the author nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.sismics.util.totp;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Random;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;
/**
* This class implements the functionality described in RFC 6238 (TOTP: Time
* based one-time password algorithm) and has been tested again Google's
* implementation of such algorithm in its Google Authenticator application.
* <p/>
* This class lets users create a new 16-bit base32-encoded secret key with the
* validation code calculated at {@code time = 0} (the UNIX epoch) and the URL
* of a Google-provided QR barcode to let an user load the generated information
* into Google Authenticator.
* <p/>
* The random number generator used by this class uses the default algorithm and
* provider. Users can override them by setting the following system properties
* to the algorithm and provider name of their choice:
* <ul>
* <li>{@link #RNG_ALGORITHM}.</li>
* <li>{@link #RNG_ALGORITHM_PROVIDER}.</li>
* </ul>
* <p/>
* This class does not store in any way either the generated keys nor the keys
* passed during the authorization process.
* <p/>
* Java Server side class for Google Authenticator's TOTP generator was inspired
* by an author's blog post.
*
* @author Enrico M. Crisostomo
* @author Warren Strange
* @version 0.5.0
* @see <a href=
* "http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html"
* />
* @see <a href="http://code.google.com/p/google-authenticator" />
* @see <a href="http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt" />
* @since 0.3.0
*/
public final class GoogleAuthenticator {
/**
* The system property to specify the random number generator algorithm to
* use.
*
* @since 0.5.0
*/
public static final String RNG_ALGORITHM = "com.warrenstrange.googleauth.rng.algorithm";
/**
* The system property to specify the random number generator provider to
* use.
*
* @since 0.5.0
*/
public static final String RNG_ALGORITHM_PROVIDER = "com.warrenstrange.googleauth.rng.algorithmProvider";
/**
* The number of bits of a secret key in binary form. Since the Base32
* encoding with 8 bit characters introduces an 160% overhead, we just need
* 80 bits (10 bytes) to generate a 16 bytes Base32-encoded secret key.
*/
private static final int SECRET_BITS = 80;
/**
* Number of scratch codes to generate during the key generation. We are
* using Google's default of providing 5 scratch codes.
*/
private static final int SCRATCH_CODES = 5;
/**
* Number of digits of a scratch code represented as a decimal integer.
*/
private static final int SCRATCH_CODE_LENGTH = 8;
/**
* Modulus used to truncate the scratch code.
*/
public static final int SCRATCH_CODE_MODULUS = (int) Math.pow(10, SCRATCH_CODE_LENGTH);
/**
* Magic number representing an invalid scratch code.
*/
private static final int SCRATCH_CODE_INVALID = -1;
/**
* Length in bytes of each scratch code. We're using Google's default of
* using 4 bytes per scratch code.
*/
private static final int BYTES_PER_SCRATCH_CODE = 4;
/**
* The default SecureRandom algorithm to use if none is specified.
*
* @see java.security.SecureRandom#getInstance(String)
* @since 0.5.0
*/
private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
/**
* The default random number algorithm provider to use if none is specified.
*
* @see java.security.SecureRandom#getInstance(String)
* @since 0.5.0
*/
private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER = "SUN";
/**
* Cryptographic hash function used to calculate the HMAC (Hash-based
* Message Authentication Code). This implementation uses the SHA1 hash
* function.
*/
private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
/**
* The configuration used by the current instance.
*/
private final GoogleAuthenticatorConfig config;
/**
* The internal SecureRandom instance used by this class. Since Java 7
* {@link Random} instances are required to be thread-safe, no
* synchronisation is required in the methods of this class using this
* instance. Thread-safety of this class was a de-facto standard in previous
* versions of Java so that it is expected to work correctly in previous
* versions of the Java platform as well.
*/
private ReseedingSecureRandom secureRandom = new ReseedingSecureRandom(getRandomNumberAlgorithm(), getRandomNumberAlgorithmProvider());
public GoogleAuthenticator() {
config = new GoogleAuthenticatorConfig();
}
public GoogleAuthenticator(GoogleAuthenticatorConfig config) {
if (config == null) {
throw new IllegalArgumentException("Configuration cannot be null.");
}
this.config = config;
}
/**
* @return the random number generator algorithm.
* @since 0.5.0
*/
private String getRandomNumberAlgorithm() {
return System.getProperty(RNG_ALGORITHM, DEFAULT_RANDOM_NUMBER_ALGORITHM);
}
/**
* @return the random number generator algorithm provider.
* @since 0.5.0
*/
private String getRandomNumberAlgorithmProvider() {
return System.getProperty(RNG_ALGORITHM_PROVIDER, DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER);
}
public int calculateCode(String secret, long tm) {
return calculateCode(decodeKey(secret), tm);
}
/**
* Decode a secret key in raw bytes.
*
* @param secret Secret key
* @return Raw bytes
*/
private byte[] decodeKey(String secret) {
switch (config.getKeyRepresentation()) {
case BASE32:
Base32 codec32 = new Base32();
return codec32.decode(secret);
case BASE64:
Base64 codec64 = new Base64();
return codec64.decode(secret);
default:
throw new IllegalArgumentException("Unknown key representation type.");
}
}
/**
* Calculates the verification code of the provided key at the specified
* instant of time using the algorithm specified in RFC 6238.
*
* @param key the secret key in binary format.
* @param tm the instant of time.
* @return the validation code for the provided key at the specified instant
* of time.
*/
int calculateCode(byte[] key, long tm) {
// Allocating an array of bytes to represent the specified instant
// of time.
byte[] data = new byte[8];
long value = tm;
// Converting the instant of time from the long representation to a
// big-endian array of bytes (RFC4226, 5.2. Description).
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
// Building the secret key specification for the HmacSHA1 algorithm.
SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);
try {
// Getting an HmacSHA1 algorithm implementation from the JCE.
Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);
// Initializing the MAC algorithm.
mac.init(signKey);
// Processing the instant of time and getting the encrypted data.
byte[] hash = mac.doFinal(data);
// Building the validation code performing dynamic truncation
// (RFC4226, 5.3. Generating an HOTP value)
int offset = hash[hash.length - 1] & 0xF;
// We are using a long because Java hasn't got an unsigned integer
// type
// and we need 32 unsigned bits).
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
// Java bytes are signed but we need an unsigned integer:
// cleaning off all but the LSB.
truncatedHash |= (hash[offset + i] & 0xFF);
}
// Clean bits higher than the 32nd (inclusive) and calculate the
// module with the maximum validation code value.
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= config.getKeyModulus();
// Returning the validation code to the caller.
return (int) truncatedHash;
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
// We're not disclosing internal error details to our clients.
throw new GoogleAuthenticatorException("The operation cannot be performed now.", ex);
}
}
/**
* This method implements the algorithm specified in RFC 6238 to check if a
* validation code is valid in a given instant of time for the given secret
* key.
*
* @param secret the Base32 encoded secret key.
* @param code the code to validate.
* @param timestamp the instant of time to use during the validation process.
* @param window the window size to use during the validation process.
* @return <code>true</code> if the validation code is valid,
* <code>false</code> otherwise.
*/
private boolean checkCode(String secret, long code, long timestamp, int window) {
// Decoding the secret key to get its raw byte representation.
byte[] decodedKey = decodeKey(secret);
// convert unix time into a 30 second "window" as specified by the
// TOTP specification. Using Google's default interval of 30 seconds.
final long timeWindow = timestamp / this.config.getTimeStepSizeInMillis();
// Calculating the verification code of the given key in each of the
// time intervals and returning true if the provided code is equal to
// one of them.
for (int i = -((window - 1) / 2); i <= window / 2; ++i) {
// Calculating the verification code for the current time interval.
long hash = calculateCode(decodedKey, timeWindow + i);
// Checking if the provided code is equal to the calculated one.
if (hash == code) {
// The verification code is valid.
return true;
}
}
// The verification code is invalid.
return false;
}
public GoogleAuthenticatorKey createCredentials() {
// Allocating a buffer sufficiently large to hold the bytes required by
// the secret key and the scratch codes.
byte[] buffer = new byte[SECRET_BITS / 8 + SCRATCH_CODES * BYTES_PER_SCRATCH_CODE];
secureRandom.nextBytes(buffer);
// Extracting the bytes making up the secret key.
byte[] secretKey = Arrays.copyOf(buffer, SECRET_BITS / 8);
String generatedKey = calculateSecretKey(secretKey);
// Generating the verification code at time = 0.
int validationCode = calculateValidationCode(secretKey);
// Calculate scratch codes
List<Integer> scratchCodes = calculateScratchCodes(buffer);
return new GoogleAuthenticatorKey(generatedKey, validationCode, scratchCodes);
}
private List<Integer> calculateScratchCodes(byte[] buffer) {
List<Integer> scratchCodes = new ArrayList<>();
while (scratchCodes.size() < SCRATCH_CODES) {
byte[] scratchCodeBuffer = Arrays.copyOfRange(buffer, SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size(), SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size() + BYTES_PER_SCRATCH_CODE);
int scratchCode = calculateScratchCode(scratchCodeBuffer);
if (scratchCode != SCRATCH_CODE_INVALID) {
scratchCodes.add(scratchCode);
} else {
scratchCodes.add(generateScratchCode());
}
}
return scratchCodes;
}
/**
* This method calculates a scratch code from a random byte buffer of
* suitable size <code>#BYTES_PER_SCRATCH_CODE</code>.
*
* @param scratchCodeBuffer a random byte buffer whose minimum size is <code>#BYTES_PER_SCRATCH_CODE</code>.
* @return the scratch code.
*/
private int calculateScratchCode(byte[] scratchCodeBuffer) {
if (scratchCodeBuffer.length < BYTES_PER_SCRATCH_CODE) {
throw new IllegalArgumentException(String.format("The provided random byte buffer is too small: %d.", scratchCodeBuffer.length));
}
int scratchCode = 0;
for (int i = 0; i < BYTES_PER_SCRATCH_CODE; ++i) {
scratchCode = (scratchCode << 8) + (scratchCodeBuffer[i] & 0xff);
}
scratchCode = (scratchCode & 0x7FFFFFFF) % SCRATCH_CODE_MODULUS;
// Accept the scratch code only if it has exactly
// SCRATCH_CODE_LENGTH digits.
if (scratchCode >= SCRATCH_CODE_MODULUS / 10) {
return scratchCode;
} else {
return SCRATCH_CODE_INVALID;
}
}
/**
* This method creates a new random byte buffer from which a new scratch
* code is generated. This function is invoked if a scratch code generated
* from the main buffer is invalid because it does not satisfy the scratch
* code restrictions.
*
* @return A valid scratch code.
*/
private int generateScratchCode() {
while (true) {
byte[] scratchCodeBuffer = new byte[BYTES_PER_SCRATCH_CODE];
secureRandom.nextBytes(scratchCodeBuffer);
int scratchCode = calculateScratchCode(scratchCodeBuffer);
if (scratchCode != SCRATCH_CODE_INVALID) {
return scratchCode;
}
}
}
/**
* This method calculates the validation code at time 0.
*
* @param secretKey The secret key to use.
* @return the validation code at time 0.
*/
private int calculateValidationCode(byte[] secretKey) {
return calculateCode(secretKey, 0);
}
/**
* This method calculates the secret key given a random byte buffer.
*
* @param secretKey a random byte buffer.
* @return the secret key.
*/
private String calculateSecretKey(byte[] secretKey) {
switch (config.getKeyRepresentation()) {
case BASE32:
return new Base32().encodeToString(secretKey);
case BASE64:
return new Base64().encodeToString(secretKey);
default:
throw new IllegalArgumentException("Unknown key representation type.");
}
}
public boolean authorize(String secret, int verificationCode) throws GoogleAuthenticatorException {
return authorize(secret, verificationCode, new Date().getTime());
}
public boolean authorize(String secret, int verificationCode, long time) throws GoogleAuthenticatorException {
// Checking user input and failing if the secret key was not provided.
if (secret == null) {
throw new IllegalArgumentException("Secret cannot be null.");
}
// Checking if the verification code is between the legal bounds.
if (verificationCode <= 0 || verificationCode >= this.config.getKeyModulus()) {
return false;
}
// Checking the validation code using the current UNIX time.
return checkCode(secret, verificationCode, time, this.config.getWindowSize());
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) 2014-2015 Enrico M. Crisostomo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the author nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.sismics.util.totp;
import java.util.concurrent.TimeUnit;
public class GoogleAuthenticatorConfig {
private long timeStepSizeInMillis = TimeUnit.SECONDS.toMillis(30);
private int windowSize = 3;
private int codeDigits = 6;
private int keyModulus = (int) Math.pow(10, codeDigits);
private KeyRepresentation keyRepresentation = KeyRepresentation.BASE32;
/**
* Returns the key module.
*
* @return the key module.
*/
public int getKeyModulus() {
return keyModulus;
}
/**
* Returns the key representation.
*
* @return the key representation.
*/
public KeyRepresentation getKeyRepresentation() {
return keyRepresentation;
}
/**
* Returns the number of digits in the generated code.
*
* @return the number of digits in the generated code.
*/
public int getCodeDigits() {
return codeDigits;
}
/**
* Returns the time step size, in milliseconds, as specified by RFC 6238.
* The default value is 30.000.
*
* @return the time step size in milliseconds.
*/
public long getTimeStepSizeInMillis() {
return timeStepSizeInMillis;
}
/**
* Returns an integer value representing the number of windows of size
* timeStepSizeInMillis that are checked during the validation process, to
* account for differences between the server and the client clocks. The
* bigger the window, the more tolerant the library code is about clock
* skews.
* <p/>
* We are using Google's default behaviour of using a window size equal to
* 3. The limit on the maximum window size, present in older versions of
* this library, has been removed.
*
* @return the window size.
* @see #timeStepSizeInMillis
*/
public int getWindowSize() {
return windowSize;
}
public static class GoogleAuthenticatorConfigBuilder {
private GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfig();
public GoogleAuthenticatorConfig build() {
return config;
}
public GoogleAuthenticatorConfigBuilder setCodeDigits(int codeDigits) {
if (codeDigits <= 0) {
throw new IllegalArgumentException("Code digits must be positive.");
}
if (codeDigits < 6) {
throw new IllegalArgumentException("The minimum number of digits is 6.");
}
if (codeDigits > 8) {
throw new IllegalArgumentException("The maximum number of digits is 8.");
}
config.codeDigits = codeDigits;
config.keyModulus = (int) Math.pow(10, codeDigits);
return this;
}
public GoogleAuthenticatorConfigBuilder setTimeStepSizeInMillis(long timeStepSizeInMillis) {
if (timeStepSizeInMillis <= 0) {
throw new IllegalArgumentException("Time step size must be positive.");
}
config.timeStepSizeInMillis = timeStepSizeInMillis;
return this;
}
public GoogleAuthenticatorConfigBuilder setWindowSize(int windowSize) {
if (windowSize <= 0) {
throw new IllegalArgumentException("Window number must be positive.");
}
config.windowSize = windowSize;
return this;
}
public GoogleAuthenticatorConfigBuilder setKeyRepresentation(KeyRepresentation keyRepresentation) {
if (keyRepresentation == null) {
throw new IllegalArgumentException("Key representation cannot be null.");
}
config.keyRepresentation = keyRepresentation;
return this;
}
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2014-2015 Enrico M. Crisostomo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the author nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.sismics.util.totp;
/**
* Date: 12/02/14
* Time: 13:36
*
* @author Enrico M. Crisostomo
*/
public class GoogleAuthenticatorException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* Builds an exception with the provided error message.
*
* @param message the error message.
*/
public GoogleAuthenticatorException(String message) {
super(message);
}
/**
* Builds an exception with the provided error mesasge and
* the provided cuase.
*
* @param message the error message.
* @param cause the cause.
*/
public GoogleAuthenticatorException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,105 @@
/*
* Copyright (c) 2014-2015 Enrico M. Crisostomo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the author nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.sismics.util.totp;
import java.util.ArrayList;
import java.util.List;
/**
* This class is a JavaBean used by the GoogleAuthenticator library to represent
* a secret key.
* <p/>
* This class is immutable.
* <p/>
* Instance of this class should only be constructed by the GoogleAuthenticator
* library.
*
* @author Enrico M. Crisostomo
* @version 1.0
* @see GoogleAuthenticator
* @since 1.0
*/
public final class GoogleAuthenticatorKey {
/**
* The secret key in Base32 encoding.
*/
private final String key;
/**
* The verification code at time = 0 (the UNIX epoch).
*/
private final int verificationCode;
/**
* The list of scratch codes.
*/
private final List<Integer> scratchCodes;
/**
* The constructor with package visibility.
*
* @param secretKey the secret key in Base32 encoding.
* @param code the verification code at time = 0 (the UNIX epoch).
* @param scratchCodes the list of scratch codes.
*/
GoogleAuthenticatorKey(String secretKey, int code, List<Integer> scratchCodes) {
key = secretKey;
verificationCode = code;
this.scratchCodes = new ArrayList<>(scratchCodes);
}
/**
* Get the list of scratch codes.
*
* @return the list of scratch codes.
*/
public List<Integer> getScratchCodes() {
return scratchCodes;
}
/**
* Returns the secret key in Base32 encoding.
*
* @return the secret key in Base32 encoding.
*/
public String getKey() {
return key;
}
/**
* Returns the verification code at time = 0 (the UNIX epoch).
*
* @return the verificationCode at time = 0 (the UNIX epoch).
*/
public int getVerificationCode() {
return verificationCode;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2014-2015 Enrico M. Crisostomo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the author nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.sismics.util.totp;
public enum KeyRepresentation {
BASE32,
BASE64
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) 2014-2015 Enrico M. Crisostomo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the author nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.sismics.util.totp;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Date: 08/04/14 Time: 15:21
*
* @author Enrico M. Crisostomo
*/
class ReseedingSecureRandom {
private static final int MAX_OPERATIONS = 1_000_000;
private final String provider;
private final String algorithm;
private final AtomicInteger count = new AtomicInteger(0);
private SecureRandom secureRandom;
ReseedingSecureRandom() {
this.algorithm = null;
this.provider = null;
buildSecureRandom();
}
ReseedingSecureRandom(String algorithm) {
if (algorithm == null) {
throw new IllegalArgumentException("Algorithm cannot be null.");
}
this.algorithm = algorithm;
this.provider = null;
buildSecureRandom();
}
ReseedingSecureRandom(String algorithm, String provider) {
if (algorithm == null) {
throw new IllegalArgumentException("Algorithm cannot be null.");
}
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null.");
}
this.algorithm = algorithm;
this.provider = provider;
buildSecureRandom();
}
private void buildSecureRandom() {
try {
if (this.algorithm == null && this.provider == null) {
this.secureRandom = new SecureRandom();
} else if (this.provider == null) {
this.secureRandom = SecureRandom.getInstance(this.algorithm);
} else {
this.secureRandom = SecureRandom.getInstance(this.algorithm, this.provider);
}
} catch (NoSuchAlgorithmException e) {
throw new GoogleAuthenticatorException(String.format("Could not initialise SecureRandom with the specified algorithm: %s. "
+ "Another provider can be chosen setting the %s system property.", this.algorithm, GoogleAuthenticator.RNG_ALGORITHM), e);
} catch (NoSuchProviderException e) {
throw new GoogleAuthenticatorException(String.format("Could not initialise SecureRandom with the specified provider: %s. "
+ "Another provider can be chosen setting the %s system property.", this.provider, GoogleAuthenticator.RNG_ALGORITHM_PROVIDER), e);
}
}
void nextBytes(byte[] bytes) {
if (count.incrementAndGet() > MAX_OPERATIONS) {
synchronized (this) {
if (count.get() > MAX_OPERATIONS) {
buildSecureRandom();
count.set(0);
}
}
}
this.secureRandom.nextBytes(bytes);
}
}

View File

@ -1 +1 @@
db.version=8 db.version=9

View File

@ -0,0 +1,2 @@
alter table T_USER add column USE_TOTPKEY_C varchar(100);
update T_CONFIG set CFG_VALUE_C = '9' where CFG_ID_C = 'DB_VERSION';

View File

@ -0,0 +1,26 @@
package com.sismics.util;
import java.util.Date;
import org.junit.Assert;
import org.junit.Test;
import com.sismics.util.totp.GoogleAuthenticator;
import com.sismics.util.totp.GoogleAuthenticatorKey;
/**
* Test of {@link GoogleAuthenticator}
*
* @author bgamard
*/
public class TestGoogleAuthenticator {
@Test
public void testGoogleAuthenticator() {
GoogleAuthenticator gAuth = new GoogleAuthenticator();
GoogleAuthenticatorKey key = gAuth.createCredentials();
Assert.assertNotNull(key.getVerificationCode());
Assert.assertEquals(5, key.getScratchCodes().size());
int validationCode = gAuth.calculateCode(key.getKey(), new Date().getTime() / 30000);
Assert.assertTrue(gAuth.authorize(key.getKey(), validationCode));
}
}

View File

@ -395,13 +395,15 @@
<version>${com.twelvemonkeys.imageio.version}</version> <version>${com.twelvemonkeys.imageio.version}</version>
</dependency> </dependency>
<dependency><!-- Only JBIG2 --> <!-- Only JBIG2 -->
<dependency>
<groupId>com.levigo.jbig2</groupId> <groupId>com.levigo.jbig2</groupId>
<artifactId>levigo-jbig2-imageio</artifactId> <artifactId>levigo-jbig2-imageio</artifactId>
<version>${com.levigo.jbig2.levigo-jbig2-imageio.version}</version> <version>${com.levigo.jbig2.levigo-jbig2-imageio.version}</version>
</dependency> </dependency>
<dependency><!-- Essentially TIFF (for OCR) --> <!-- Essentially TIFF (for OCR) -->
<dependency>
<groupId>com.github.jai-imageio</groupId> <groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-core</artifactId> <artifactId>jai-imageio-core</artifactId>
<version>${com.github.jai-imageio.jai-imageio-core.version}</version> <version>${com.github.jai-imageio.jai-imageio-core.version}</version>

View File

@ -95,12 +95,11 @@ public class ClientUtil {
* @return Authentication token * @return Authentication token
*/ */
public String login(String username, String password, Boolean remember) { public String login(String username, String password, Boolean remember) {
Form form = new Form();
form.param("username", username);
form.param("password", password);
form.param("remember", remember.toString());
Response response = resource.path("/user/login").request() Response response = resource.path("/user/login").request()
.post(Entity.form(form)); .post(Entity.form(new Form()
.param("username", username)
.param("password", password)
.param("remember", remember.toString())));
return getAuthenticationCookie(response); return getAuthenticationCookie(response);
} }

View File

@ -1,3 +1,3 @@
api.current_version=${project.version} api.current_version=${project.version}
api.min_version=1.0 api.min_version=1.0
db.version=8 db.version=9

View File

@ -55,6 +55,8 @@ import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import com.sismics.security.UserPrincipal; import com.sismics.security.UserPrincipal;
import com.sismics.util.filter.TokenBasedSecurityFilter; import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sismics.util.totp.GoogleAuthenticator;
import com.sismics.util.totp.GoogleAuthenticatorKey;
/** /**
* User REST resources. * User REST resources.
@ -253,6 +255,7 @@ public class UserResource extends BaseResource {
public Response login( public Response login(
@FormParam("username") String username, @FormParam("username") String username,
@FormParam("password") String password, @FormParam("password") String password,
@FormParam("code") String validationCodeStr,
@FormParam("remember") boolean longLasted) { @FormParam("remember") boolean longLasted) {
// Validate the input data // Validate the input data
username = StringUtils.strip(username); username = StringUtils.strip(username);
@ -260,11 +263,26 @@ public class UserResource extends BaseResource {
// Get the user // Get the user
UserDao userDao = new UserDao(); UserDao userDao = new UserDao();
String userId = userDao.authenticate(username, password); User user = userDao.authenticate(username, password);
if (userId == null) { if (user == null) {
throw new ForbiddenClientException(); throw new ForbiddenClientException();
} }
// Two factor authentication
if (user.getTotpKey() != null) {
// If TOTP is enabled, ask a validation code
if (Strings.isNullOrEmpty(validationCodeStr)) {
throw new ClientException("ValidationCodeRequired", "An OTP validation code is required");
}
// Check the validation code
int validationCode = ValidationUtil.validateInteger(validationCodeStr, "code");
GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
if (!googleAuthenticator.authorize(user.getTotpKey(), validationCode)) {
throw new ForbiddenClientException();
}
}
// Get the remote IP // Get the remote IP
String ip = request.getHeader("x-forwarded-for"); String ip = request.getHeader("x-forwarded-for");
if (Strings.isNullOrEmpty(ip)) { if (Strings.isNullOrEmpty(ip)) {
@ -273,15 +291,15 @@ public class UserResource extends BaseResource {
// Create a new session token // Create a new session token
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
AuthenticationToken authenticationToken = new AuthenticationToken(); AuthenticationToken authenticationToken = new AuthenticationToken()
authenticationToken.setUserId(userId); .setUserId(user.getId())
authenticationToken.setLongLasted(longLasted); .setLongLasted(longLasted)
authenticationToken.setIp(ip); .setIp(ip)
authenticationToken.setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000)); .setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000));
String token = authenticationTokenDao.create(authenticationToken); String token = authenticationTokenDao.create(authenticationToken);
// Cleanup old session tokens // Cleanup old session tokens
authenticationTokenDao.deleteOldSessionToken(userId); authenticationTokenDao.deleteOldSessionToken(user.getId());
JsonObjectBuilder response = Json.createObjectBuilder(); JsonObjectBuilder response = Json.createObjectBuilder();
int maxAge = longLasted ? TokenBasedSecurityFilter.TOKEN_LONG_LIFETIME : -1; int maxAge = longLasted ? TokenBasedSecurityFilter.TOKEN_LONG_LIFETIME : -1;
@ -470,7 +488,8 @@ public class UserResource extends BaseResource {
response.add("username", user.getUsername()) response.add("username", user.getUsername())
.add("email", user.getEmail()) .add("email", user.getEmail())
.add("storage_quota", user.getStorageQuota()) .add("storage_quota", user.getStorageQuota())
.add("storage_current", user.getStorageCurrent()); .add("storage_current", user.getStorageCurrent())
.add("totp_enabled", user.getTotpKey() != null);
// Base functions // Base functions
JsonArrayBuilder baseFunctions = Json.createArrayBuilder(); JsonArrayBuilder baseFunctions = Json.createArrayBuilder();
@ -639,6 +658,66 @@ public class UserResource extends BaseResource {
return Response.ok().entity(response.build()).build(); return Response.ok().entity(response.build()).build();
} }
/**
* Enable time-based one-time password.
*
* @return Response
*/
@POST
@Path("enable_totp")
public Response enableTotp() {
if (!authenticate()) {
throw new ForbiddenClientException();
}
// Create a new TOTP key
GoogleAuthenticator gAuth = new GoogleAuthenticator();
final GoogleAuthenticatorKey key = gAuth.createCredentials();
// Save it
UserDao userDao = new UserDao();
User user = userDao.getActiveByUsername(principal.getName());
user.setTotpKey(key.getKey());
user = userDao.update(user, principal.getId());
JsonObjectBuilder response = Json.createObjectBuilder()
.add("secret", key.getKey());
return Response.ok().entity(response.build()).build();
}
/**
* Disable time-based one-time password.
*
* @param password Password
* @return Response
*/
@POST
@Path("disable_totp")
public Response disableTotp(@FormParam("password") String password) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
// Validate the input data
password = ValidationUtil.validateLength(password, "password", 1, 100, false);
// Check the password and get the user
UserDao userDao = new UserDao();
User user = userDao.authenticate(principal.getName(), password);
if (user == null) {
throw new ForbiddenClientException();
}
// Remove the TOTP key
user.setTotpKey(null);
userDao.update(user, principal.getId());
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
return Response.ok().entity(response.build()).build();
}
/** /**
* Returns the authentication token value. * Returns the authentication token value.
* *

View File

@ -22,7 +22,7 @@ module.exports = function(grunt) {
separator: ';' separator: ';'
}, },
src: ['src/lib/jquery.js','src/lib/jquery.ui.js','src/lib/underscore.js','src/lib/colorpicker.js', 'src/lib/angular.js', 'src/lib/angular.*.js', src: ['src/lib/jquery.js','src/lib/jquery.ui.js','src/lib/underscore.js','src/lib/colorpicker.js', 'src/lib/angular.js', 'src/lib/angular.*.js',
'dist/app/docs/app.js', 'dist/app/docs/controller/*.js', 'dist/app/docs/directive/*.js', 'dist/app/docs/filter/*.js', 'dist/app/docs/service/*.js'], 'dist/app/docs/app.js', 'dist/app/docs/controller/**/*.js', 'dist/app/docs/directive/*.js', 'dist/app/docs/filter/*.js', 'dist/app/docs/service/*.js'],
dest: 'dist/docs.js' dest: 'dist/docs.js'
}, },
share: { share: {

View File

@ -5,7 +5,7 @@
*/ */
angular.module('docs', angular.module('docs',
// Dependencies // Dependencies
['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog', ['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog', 'ngProgress', 'monospaced.qrcode',
'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload'] 'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload']
) )
@ -61,6 +61,15 @@ angular.module('docs',
} }
} }
}) })
.state('settings.security', {
url: '/security',
views: {
'settings': {
templateUrl: 'partial/docs/settings.security.html',
controller: 'SettingsSecurity'
}
}
})
.state('settings.session', { .state('settings.session', {
url: '/session', url: '/session',
views: { views: {
@ -345,4 +354,21 @@ angular.module('docs',
$state.go(redirect, toParams); $state.go(redirect, toParams);
} }
}); });
})
/**
* Initialize ngProgress.
*/
.run(function($rootScope, ngProgressFactory, $http) {
$rootScope.ngProgress = ngProgressFactory.createInstance();
// Watch for the number of XHR running
$rootScope.$watch(function() {
return $http.pendingRequests.length > 0
}, function(count) {
if (count == 0) {
$rootScope.ngProgress.complete();
} else {
$rootScope.ngProgress.start();
}
});
}); });

View File

@ -4,18 +4,28 @@
* Login controller. * Login controller.
*/ */
angular.module('docs').controller('Login', function($scope, $rootScope, $state, $dialog, User) { angular.module('docs').controller('Login', function($scope, $rootScope, $state, $dialog, User) {
$scope.codeRequired = false;
/**
* Login.
*/
$scope.login = function() { $scope.login = function() {
User.login($scope.user).then(function() { User.login($scope.user).then(function() {
User.userInfo(true).then(function(data) { User.userInfo(true).then(function(data) {
$rootScope.userInfo = data; $rootScope.userInfo = data;
}); });
$state.go('document.default'); $state.go('document.default');
}, function() { }, function(data) {
if (data.data.type == 'ValidationCodeRequired') {
// A TOTP validation code is required to login
$scope.codeRequired = true;
} else {
// Login truly failed
var title = 'Login failed'; var title = 'Login failed';
var msg = 'Username or password invalid'; var msg = 'Username or password invalid';
var btns = [{result: 'ok', label: 'OK', cssClass: 'btn-primary'}]; var btns = [{result: 'ok', label: 'OK', cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns); $dialog.messageBox(title, msg, btns);
}
}); });
}; };
}); });

View File

@ -56,11 +56,4 @@ angular.module('docs').controller('Navigation', function($scope, $http, $state,
}); });
$event.preventDefault(); $event.preventDefault();
}; };
/**
* Returns true if at least an asynchronous request is in progress.
*/
$scope.isLoading = function() {
return $http.pendingRequests.length > 0;
};
}); });

View File

@ -0,0 +1,53 @@
'use strict';
/**
* Settings security controller.
*/
angular.module('docs').controller('SettingsSecurity', function($scope, User, $dialog, $modal, Restangular) {
User.userInfo().then(function(data) {
$scope.user = data;
});
/**
* Enable TOTP.
*/
$scope.enableTotp = function() {
var title = 'Enable two-factor authentication';
var msg = 'Make sure you have a TOTP-compatible application on your phone ready to add a new account';
var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns, function(result) {
if (result == 'ok') {
Restangular.one('user/enable_totp').post().then(function(data) {
$scope.secret = data.secret;
User.userInfo(true).then(function(data) {
$scope.user = data;
})
});
}
});
};
/**
* Disable TOTP.
*/
$scope.disableTotp = function() {
$modal.open({
templateUrl: 'partial/docs/settings.security.disabletotp.html',
controller: 'SettingsSecurityModalDisableTotp'
}).result.then(function (password) {
if (password == null) {
return;
}
// Disable TOTP
Restangular.one('user/disable_totp').post('', {
password: password
}).then(function() {
User.userInfo(true).then(function(data) {
$scope.user = data;
})
});
});
};
});

View File

@ -0,0 +1,11 @@
'use strict';
/**
* Settings modal disable TOTP controller.
*/
angular.module('docs').controller('SettingsSecurityModalDisableTotp', function ($scope, $modalInstance) {
$scope.password = '';
$scope.close = function(password) {
$modalInstance.close(password);
}
});

View File

@ -34,8 +34,8 @@ angular.module('docs').controller('SettingsVocabulary', function($scope, Restang
// Add an entry // Add an entry
$scope.addEntry = function(entry) { $scope.addEntry = function(entry) {
entry.name = $scope.vocabulary; entry.name = $scope.vocabulary;
Restangular.one('vocabulary').put(entry).then(function() { Restangular.one('vocabulary').put(entry).then(function(data) {
$scope.entries.push(entry); $scope.entries.push(data);
$scope.entry = {}; $scope.entry = {};
}); });
}; };

View File

@ -37,35 +37,39 @@
<script src="lib/angular.restangular.js" type="text/javascript"></script> <script src="lib/angular.restangular.js" type="text/javascript"></script>
<script src="lib/angular.colorpicker.js" type="text/javascript"></script> <script src="lib/angular.colorpicker.js" type="text/javascript"></script>
<script src="lib/angular.file-upload.js" type="text/javascript"></script> <script src="lib/angular.file-upload.js" type="text/javascript"></script>
<script src="lib/angular.ngprogress.js" type="text/javascript"></script>
<script src="lib/angular.qrcode.js" type="text/javascript"></script>
<script src="app/docs/app.js" type="text/javascript"></script> <script src="app/docs/app.js" type="text/javascript"></script>
<script src="app/docs/controller/Main.js" type="text/javascript"></script> <script src="app/docs/controller/Main.js" type="text/javascript"></script>
<script src="app/docs/controller/Document.js" type="text/javascript"></script> <script src="app/docs/controller/document/Document.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentDefault.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentDefault.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentEdit.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentEdit.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentView.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentView.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentViewContent.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentViewContent.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentViewPermissions.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentViewPermissions.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentViewActivity.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentViewActivity.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentModalShare.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentModalShare.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentModalPdf.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentModalPdf.js" type="text/javascript"></script>
<script src="app/docs/controller/FileView.js" type="text/javascript"></script> <script src="app/docs/controller/document/FileView.js" type="text/javascript"></script>
<script src="app/docs/controller/FileModalView.js" type="text/javascript"></script> <script src="app/docs/controller/document/FileModalView.js" type="text/javascript"></script>
<script src="app/docs/controller/Login.js" type="text/javascript"></script> <script src="app/docs/controller/Login.js" type="text/javascript"></script>
<script src="app/docs/controller/Tag.js" type="text/javascript"></script> <script src="app/docs/controller/tag/Tag.js" type="text/javascript"></script>
<script src="app/docs/controller/Navigation.js" type="text/javascript"></script> <script src="app/docs/controller/Navigation.js" type="text/javascript"></script>
<script src="app/docs/controller/Settings.js" type="text/javascript"></script> <script src="app/docs/controller/settings/Settings.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsDefault.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsDefault.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsAccount.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsAccount.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsSession.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsSecurity.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsLog.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsSecurityModalDisableTotp.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsUser.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsSession.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsUserEdit.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsLog.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsGroup.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsUser.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsGroupEdit.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsUserEdit.js" type="text/javascript"></script>
<script src="app/docs/controller/SettingsVocabulary.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsGroup.js" type="text/javascript"></script>
<script src="app/docs/controller/UserGroup.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsGroupEdit.js" type="text/javascript"></script>
<script src="app/docs/controller/UserProfile.js" type="text/javascript"></script> <script src="app/docs/controller/settings/SettingsVocabulary.js" type="text/javascript"></script>
<script src="app/docs/controller/GroupProfile.js" type="text/javascript"></script> <script src="app/docs/controller/usergroup/UserGroup.js" type="text/javascript"></script>
<script src="app/docs/controller/usergroup/UserProfile.js" type="text/javascript"></script>
<script src="app/docs/controller/usergroup/GroupProfile.js" type="text/javascript"></script>
<script src="app/docs/service/User.js" type="text/javascript"></script> <script src="app/docs/service/User.js" type="text/javascript"></script>
<script src="app/docs/service/Tag.js" type="text/javascript"></script> <script src="app/docs/service/Tag.js" type="text/javascript"></script>
<script src="app/docs/filter/Newline.js" type="text/javascript"></script> <script src="app/docs/filter/Newline.js" type="text/javascript"></script>
@ -92,10 +96,6 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<div class="navbar-brand loader" ng-class="{'loader-hide': !isLoading() }">
<img src="img/loader.gif" />
</div>
<div class="hidden-xs navbar-text navbar-logo"> <div class="hidden-xs navbar-text navbar-logo">
<img src="favicon.png" /> <img src="favicon.png" />
</div> </div>

View File

@ -0,0 +1,220 @@
/*
ngprogress 1.1.2 - slim, site-wide progressbar for AngularJS
(C) 2013 - Victor Bjelkholm
License: MIT
Source: https://github.com/VictorBjelkholm/ngProgress
Date Compiled: 2015-07-27
*/
angular.module('ngProgress.provider', ['ngProgress.directive'])
.service('ngProgress', function () {
'use strict';
return ['$document', '$window', '$compile', '$rootScope', '$timeout', function($document, $window, $compile, $rootScope, $timeout) {
this.autoStyle = true;
this.count = 0;
this.height = '2px';
this.$scope = $rootScope.$new();
this.color = 'white';
this.parent = $document.find('body')[0];
this.count = 0;
// Compile the directive
this.progressbarEl = $compile('<ng-progress></ng-progress>')(this.$scope);
// Add the element to body
this.parent.appendChild(this.progressbarEl[0]);
// Set the initial height
this.$scope.count = this.count;
// If height or color isn't undefined, set the height, background-color and color.
if (this.height !== undefined) {
this.progressbarEl.eq(0).children().css('height', this.height);
}
if (this.color !== undefined) {
this.progressbarEl.eq(0).children().css('background-color', this.color);
this.progressbarEl.eq(0).children().css('color', this.color);
}
// The ID for the interval controlling start()
this.intervalCounterId = 0;
// Starts the animation and adds between 0 - 5 percent to loading
// each 400 milliseconds. Should always be finished with progressbar.complete()
// to hide it
this.start = function () {
// TODO Use requestAnimationFrame instead of setInterval
// https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame
this.show();
var self = this;
clearInterval(this.intervalCounterId);
this.intervalCounterId = setInterval(function () {
if (isNaN(self.count)) {
clearInterval(self.intervalCounterId);
self.count = 0;
self.hide();
} else {
self.remaining = 100 - self.count;
self.count = self.count + (0.15 * Math.pow(1 - Math.sqrt(self.remaining), 2));
self.updateCount(self.count);
}
}, 200);
};
this.updateCount = function (new_count) {
this.$scope.count = new_count;
if(!this.$scope.$$phase) {
this.$scope.$apply();
}
};
// Sets the height of the progressbar. Use any valid CSS value
// Eg '10px', '1em' or '1%'
this.setHeight = function (new_height) {
if (new_height !== undefined) {
this.height = new_height;
this.$scope.height = this.height;
if(!this.$scope.$$phase) {
this.$scope.$apply();
}
}
return this.height;
};
// Sets the color of the progressbar and it's shadow. Use any valid HTML
// color
this.setColor = function(new_color) {
if (new_color !== undefined) {
this.color = new_color;
this.$scope.color = this.color;
if(!this.$scope.$$phase) {
this.$scope.$apply();
}
}
return this.color;
};
this.hide = function() {
this.progressbarEl.children().css('opacity', '0');
var self = this;
self.animate(function () {
self.progressbarEl.children().css('width', '0%');
self.animate(function () {
self.show();
}, 500);
}, 500);
};
this.show = function () {
var self = this;
self.animate(function () {
self.progressbarEl.children().css('opacity', '1');
}, 100);
};
// Cancel any prior animations before running new ones.
// Multiple simultaneous animations just look weird.
this.animate = function(fn, time) {
if(this.animation !== undefined) { $timeout.cancel(this.animation); }
this.animation = $timeout(fn, time);
};
// Returns on how many percent the progressbar is at. Should'nt be needed
this.status = function () {
return this.count;
};
// Stops the progressbar at it's current location
this.stop = function () {
clearInterval(this.intervalCounterId);
};
// Set's the progressbar percentage. Use a number between 0 - 100.
// If 100 is provided, complete will be called.
this.set = function (new_count) {
this.show();
this.updateCount(new_count);
this.count = new_count;
clearInterval(this.intervalCounterId);
return this.count;
};
this.css = function (args) {
return this.progressbarEl.children().css(args);
};
// Resets the progressbar to percetage 0 and therefore will be hided after
// it's rollbacked
this.reset = function () {
clearInterval(this.intervalCounterId);
this.count = 0;
this.updateCount(this.count);
return 0;
};
// Jumps to 100% progress and fades away progressbar.
this.complete = function () {
this.count = 100;
this.updateCount(this.count);
var self = this;
clearInterval(this.intervalCounterId);
$timeout(function () {
self.hide();
$timeout(function () {
self.count = 0;
self.updateCount(self.count);
}, 200);
}, 500);
return this.count;
};
// Set the parent of the directive, sometimes body is not sufficient
this.setParent = function(newParent) {
if(newParent === null || newParent === undefined) {
throw new Error('Provide a valid parent of type HTMLElement');
}
if(this.parent !== null && this.parent !== undefined) {
this.parent.removeChild(this.progressbarEl[0]);
}
this.parent = newParent;
this.parent.appendChild(this.progressbarEl[0]);
};
// Gets the current element the progressbar is attached to
this.getDomElement = function () {
return this.progressbarEl;
};
this.setAbsolute = function() {
this.progressbarEl.css('position', 'absolute');
};
}];
})
.factory('ngProgressFactory', ['$injector', 'ngProgress', function($injector, ngProgress) {
var service = {
createInstance: function () {
return $injector.instantiate(ngProgress);
}
};
return service;
}]);
angular.module('ngProgress.directive', [])
.directive('ngProgress', ["$window", "$rootScope", function ($window, $rootScope) {
var directiveObj = {
// Replace the directive
replace: true,
// Only use as a element
restrict: 'E',
link: function ($scope, $element, $attrs, $controller) {
// Watch the count on the $rootScope. As soon as count changes to something that
// isn't undefined or null, change the counter on $scope and also the width of
// the progressbar. The same goes for color and height on the $rootScope
$scope.$watch('count', function (newVal) {
if (newVal !== undefined || newVal !== null) {
$scope.counter = newVal;
$element.eq(0).children().css('width', newVal + '%');
}
});
$scope.$watch('color', function (newVal) {
if (newVal !== undefined || newVal !== null) {
$scope.color = newVal;
$element.eq(0).children().css('background-color', newVal);
$element.eq(0).children().css('color', newVal);
}
});
$scope.$watch('height', function (newVal) {
if (newVal !== undefined || newVal !== null) {
$scope.height = newVal;
$element.eq(0).children().css('height', newVal);
}
});
},
// The actual html that will be used
template: '<div id="ngProgress-container"><div id="ngProgress"></div></div>'
};
return directiveObj;
}]);
angular.module('ngProgress', ['ngProgress.directive', 'ngProgress.provider']);

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,15 @@
<input class="form-control" type="password" id="inputPassword" placeholder="Password" ng-model="user.password" /> <input class="form-control" type="password" id="inputPassword" placeholder="Password" ng-model="user.password" />
</div> </div>
<span class="help-block" ng-if="codeRequired">
A validation code is required
<span class="glyphicon glyphicon-question-sign" title="You have activated the two-factor authentication on your account. Please enter a validation code generated by the phone app your configured."></span>
</span>
<div class="form-group" ng-if="codeRequired">
<label class="sr-only" for="inputCode">Validation code</label>
<input class="form-control" type="text" id="inputCode" placeholder="Validation code" ng-model="user.code" />
</div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="user.remember" /> Remember me <input type="checkbox" ng-model="user.remember" /> Remember me

View File

@ -1,9 +1,10 @@
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-3">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Personal settings</strong></div> <div class="panel-heading"><strong>Personal settings</strong></div>
<ul class="list-group"> <ul class="list-group">
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/account" href="#/settings/account">User account</a> <a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/account" href="#/settings/account">User account</a>
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/security" href="#/settings/security">Two-factor authentication</a>
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/session" href="#/settings/session">Opened sessions</a> <a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/session" href="#/settings/session">Opened sessions</a>
</ul> </ul>
</div> </div>
@ -19,7 +20,7 @@
</div> </div>
</div> </div>
<div class="col-md-8"> <div class="col-md-9">
<div ui-view="settings"></div> <div ui-view="settings"></div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,18 @@
<div class="modal-header">
<h3>Disable two-factor authentication</h3>
</div>
<div class="modal-body">
<p class="text-danger">
Your account will not be protected by the two-factor authentication anymore.
</p>
<p>
<label for="password">Confirm your password</label>
<input class="form-control" type="password" id="password" ng-model="password" />
</p>
</div>
<div class="modal-footer">
<button ng-click="close(password)" class="btn btn-warning">
Disable two-factor authentication
</button>
<button ng-click="close(null)" class="btn btn-default">Cancel</button>
</div>

View File

@ -0,0 +1,43 @@
<h1>
Two-factor <small>authentication</small>
<span class="label" ng-class="{ 'label-success': user.totp_enabled, 'label-danger': !user.totp_enabled }">
{{ user.totp_enabled ? 'Enabled' : 'Disabled' }}
</span>
</h1>
<div ng-if="!user.totp_enabled">
<p>
Two-factor authentication allows you to add a layer of security on your <strong>Sismics Docs</strong> account.<br/>
Before activating this feature, make sure you have a TOTP-compatible app on your phone:
</p>
<ul>
<li>For Android, iOS, and Blackberry: <a href="https://support.google.com/accounts/answer/1066447" target="_blank">Google Authenticator</a></li>
<li>For Android and iOS: <a href="https://guide.duo.com/third-party-accounts" target="_blank">Duo Mobile</a></li>
<li>For Windows Phone: <a href="https://www.microsoft.com/en-US/store/apps/Authenticator/9WZDNCRFJ3RJ" target="_blank">Authenticator</a></li>
</ul>
<p>
Those applications automatically generate a validation code that changes after a certain period of time.<br/>
You will be required to enter this validation code each time you login on <strong>Sismics Docs</strong>.
</p>
<p>
<button class="btn btn-primary" ng-click="enableTotp()">Enable two-factor authentication</button>
</p>
</div>
<div ng-if="user.totp_enabled">
<div ng-if="secret">
<p>Your secret key is: <strong>{{ secret }}</strong></p>
<qrcode data="otpauth://totp/Sismics%20Docs?secret={{ secret }}" size="200"></qrcode>
<p class="text-danger">
<strong>Configure your TOTP app on your phone with this secret key now, you will not be able to access it later.</strong>
</p>
</div>
<p>
Two-factor authentication is enabled on your account.<br/>
Each time you login on <strong>Sismics Docs</strong>, you will be asked a validation code from your configured phone app.<br/>
If you loose your phone, you will not be able to login into your account but active sessions will allow you to regenerate a secrey key.
</p>
<p>
<button class="btn btn-warning" ng-click="disableTotp()">Disable two-factor authentication</button>
</p>
</div>

View File

@ -240,5 +240,38 @@ input[readonly].share-link {
.login-box { .login-box {
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
padding: 20px; padding: 20px;
border-radius: 4px border-radius: 4px;
.help-block, .checkbox {
color: white;
}
}
/* Styling for the ngProgress itself */
#ngProgress {
margin: 0;
padding: 0;
z-index: 99998;
background-color: green;
color: green;
box-shadow: 0 0 10px 0; /* Inherits the font color */
height: 2px;
opacity: 0;
/* Add CSS3 styles for transition smoothing */
-webkit-transition: all 0.5s ease-in-out;
-moz-transition: all 0.5s ease-in-out;
-o-transition: all 0.5s ease-in-out;
transition: all 0.5s ease-in-out;
}
/* Styling for the ngProgress-container */
#ngProgress-container {
position: fixed;
margin: 0;
padding: 0;
top: 0;
left: 0;
right: 0;
z-index: 99999;
} }

View File

@ -1,3 +1,3 @@
api.current_version=${project.version} api.current_version=${project.version}
api.min_version=1.0 api.min_version=1.0
db.version=8 db.version=9

View File

@ -1,3 +1,3 @@
api.current_version=${project.version} api.current_version=${project.version}
api.min_version=1.0 api.min_version=1.0
db.version=8 db.version=9

View File

@ -23,8 +23,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestAclResource extends BaseJerseyTest { public class TestAclResource extends BaseJerseyTest {
/** /**
* Test the ACL resource. * Test the ACL resource.
*
* @throws JSONException
*/ */
@Test @Test
public void testAclResource() { public void testAclResource() {

View File

@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestAppResource extends BaseJerseyTest { public class TestAppResource extends BaseJerseyTest {
/** /**
* Test the API resource. * Test the API resource.
*
* @throws JSONException
*/ */
@Test @Test
public void testAppResource() { public void testAppResource() {
@ -63,8 +61,6 @@ public class TestAppResource extends BaseJerseyTest {
/** /**
* Test the log resource. * Test the log resource.
*
* @throws JSONException
*/ */
@Test @Test
public void testLogResource() { public void testLogResource() {

View File

@ -20,8 +20,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestAuditLogResource extends BaseJerseyTest { public class TestAuditLogResource extends BaseJerseyTest {
/** /**
* Test the audit log resource. * Test the audit log resource.
*
* @throws JSONException
*/ */
@Test @Test
public void testAuditLogResource() { public void testAuditLogResource() {

View File

@ -21,11 +21,9 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestCommentResource extends BaseJerseyTest { public class TestCommentResource extends BaseJerseyTest {
/** /**
* Test the comment resource. * Test the comment resource.
*
* @throws Exception
*/ */
@Test @Test
public void testCommentResource() throws Exception { public void testCommentResource() {
// Login comment1 // Login comment1
clientUtil.createUser("comment1"); clientUtil.createUser("comment1");
String comment1Token = clientUtil.login("comment1"); String comment1Token = clientUtil.login("comment1");

View File

@ -359,9 +359,8 @@ public class TestDocumentResource extends BaseJerseyTest {
* @param query Search query * @param query Search query
* @param token Authentication token * @param token Authentication token
* @return Number of documents found * @return Number of documents found
* @throws Exception
*/ */
private int searchDocuments(String query, String token) throws Exception { private int searchDocuments(String query, String token) {
JsonObject json = target().path("/document/list") JsonObject json = target().path("/document/list")
.queryParam("search", query) .queryParam("search", query)
.request() .request()

View File

@ -198,6 +198,11 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals(1, files.size()); Assert.assertEquals(1, files.size());
} }
/**
* Test orphan files (without linked document).
*
* @throws Exception
*/
@Test @Test
public void testOrphanFile() throws Exception { public void testOrphanFile() throws Exception {
// Login file2 // Login file2
@ -283,6 +288,11 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals("ok", json.getString("status")); Assert.assertEquals("ok", json.getString("status"));
} }
/**
* Test user quota.
*
* @throws Exception
*/
@Test @Test
public void testQuota() throws Exception { public void testQuota() throws Exception {
// Login file_quota // Login file_quota

View File

@ -22,8 +22,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestGroupResource extends BaseJerseyTest { public class TestGroupResource extends BaseJerseyTest {
/** /**
* Test the group resource. * Test the group resource.
*
* @throws JSONException
*/ */
@Test @Test
public void testGroupResource() { public void testGroupResource() {

View File

@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestSecurity extends BaseJerseyTest { public class TestSecurity extends BaseJerseyTest {
/** /**
* Test of the security layer. * Test of the security layer.
*
* @throws JSONException
*/ */
@Test @Test
public void testSecurity() { public void testSecurity() {

View File

@ -28,7 +28,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestShareResource extends BaseJerseyTest { public class TestShareResource extends BaseJerseyTest {
/** /**
* Test the share resource. * Test the share resource.
* @throws Exception
* *
* @throws Exception * @throws Exception
*/ */

View File

@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestTagResource extends BaseJerseyTest { public class TestTagResource extends BaseJerseyTest {
/** /**
* Test the tag resource. * Test the tag resource.
*
* @throws JSONException
*/ */
@Test @Test
public void testTagResource() { public void testTagResource() {

View File

@ -1,5 +1,6 @@
package com.sismics.docs.rest; package com.sismics.docs.rest;
import java.util.Date;
import java.util.Locale; import java.util.Locale;
import javax.json.JsonArray; import javax.json.JsonArray;
@ -13,6 +14,7 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import com.sismics.util.filter.TokenBasedSecurityFilter; import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sismics.util.totp.GoogleAuthenticator;
/** /**
* Exhaustive test of the user resource. * Exhaustive test of the user resource.
@ -22,8 +24,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestUserResource extends BaseJerseyTest { public class TestUserResource extends BaseJerseyTest {
/** /**
* Test the user resource. * Test the user resource.
*
* @throws JSONException
*/ */
@Test @Test
public void testUserResource() { public void testUserResource() {
@ -229,8 +229,6 @@ public class TestUserResource extends BaseJerseyTest {
/** /**
* Test the user resource admin functions. * Test the user resource admin functions.
*
* @throws JSONException
*/ */
@Test @Test
public void testUserResourceAdmin() { public void testUserResourceAdmin() {
@ -290,4 +288,71 @@ public class TestUserResource extends BaseJerseyTest {
json = response.readEntity(JsonObject.class); json = response.readEntity(JsonObject.class);
Assert.assertEquals("UserNotFound", json.getString("type")); Assert.assertEquals("UserNotFound", json.getString("type"));
} }
@Test
public void testTotp() {
// Create totp1 user
clientUtil.createUser("totp1");
String totp1Token = clientUtil.login("totp1");
// Check TOTP enablement
JsonObject json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
.get(JsonObject.class);
Assert.assertFalse(json.getBoolean("totp_enabled"));
// Enable TOTP for totp1
json = target().path("/user/enable_totp").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
.post(Entity.form(new Form()), JsonObject.class);
String secret = json.getString("secret");
Assert.assertNotNull(secret);
// Try to login with totp1 without a validation code
Response response = target().path("/user/login").request()
.post(Entity.form(new Form()
.param("username", "totp1")
.param("password", "12345678")
.param("remember", "false")));
Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
json = response.readEntity(JsonObject.class);
Assert.assertEquals("ValidationCodeRequired", json.getString("type"));
// Generate a OTP
GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
int validationCode = googleAuthenticator.calculateCode(secret, new Date().getTime() / 30000);
// Login with totp1 with a validation code
json = target().path("/user/login").request()
.post(Entity.form(new Form()
.param("username", "totp1")
.param("password", "12345678")
.param("code", Integer.toString(validationCode))
.param("remember", "false")), JsonObject.class);
// Check TOTP enablement
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
.get(JsonObject.class);
Assert.assertTrue(json.getBoolean("totp_enabled"));
// Disable TOTP for totp1
json = target().path("/user/disable_totp").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
.post(Entity.form(new Form()
.param("password", "12345678")), JsonObject.class);
// Login with totp1 without a validation code
json = target().path("/user/login").request()
.post(Entity.form(new Form()
.param("username", "totp1")
.param("password", "12345678")
.param("remember", "false")), JsonObject.class);
// Check TOTP enablement
json = target().path("/user").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token)
.get(JsonObject.class);
Assert.assertFalse(json.getBoolean("totp_enabled"));
}
} }

View File

@ -19,11 +19,9 @@ import com.sismics.util.filter.TokenBasedSecurityFilter;
public class TestVocabularyResource extends BaseJerseyTest { public class TestVocabularyResource extends BaseJerseyTest {
/** /**
* Test the vocabulary resource. * Test the vocabulary resource.
*
* @throws Exception
*/ */
@Test @Test
public void testVocabularyResource() throws Exception { public void testVocabularyResource() {
// Login vocabulary1 // Login vocabulary1
clientUtil.createUser("vocabulary1"); clientUtil.createUser("vocabulary1");
String vocabulary1Token = clientUtil.login("vocabulary1"); String vocabulary1Token = clientUtil.login("vocabulary1");