added authentication via api key for some limited api methods (fix #64)

This commit is contained in:
Athou
2013-05-01 21:56:59 +02:00
parent 52a8ea1920
commit 5dcc923cf1
14 changed files with 148 additions and 40 deletions

View File

@@ -29,4 +29,19 @@ public class UserDAO extends GenericDAO<User> {
return user;
}
public User findByApiKey(String key) {
CriteriaQuery<User> query = builder.createQuery(getType());
Root<User> root = query.from(getType());
query.where(builder.equal(root.get(User_.apiKey), key));
TypedQuery<User> q = em.createQuery(query);
User user = null;
try {
user = q.getSingleResult();
} catch (NoResultException e) {
user = null;
}
return user;
}
}

View File

@@ -31,6 +31,9 @@ public class User extends AbstractModel {
@Column(length = 256, nullable = false)
private byte[] password;
@Column(length = 40, unique = true)
private String apiKey;
@Column(length = 8, nullable = false)
private byte[] salt;
@@ -99,4 +102,12 @@ public class User extends AbstractModel {
this.lastLogin = lastLogin;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}

View File

@@ -36,10 +36,6 @@ public class CommaFeedSession extends AuthenticatedWebSession {
return user;
}
public void setUser(User user) {
this.user = user;
}
public static CommaFeedSession get() {
return (CommaFeedSession) Session.get();
}
@@ -52,6 +48,11 @@ public class CommaFeedSession extends AuthenticatedWebSession {
@Override
public boolean authenticate(String userName, String password) {
User user = userService.login(userName, password);
setUser(user);
return user != null;
}
public void setUser(User user) {
if (user == null) {
this.user = null;
this.roles = new Roles();
@@ -64,7 +65,6 @@ public class CommaFeedSession extends AuthenticatedWebSession {
this.user = user;
this.roles = new Roles(roleSet.toArray(new String[0]));
}
return user != null;
}
}

View File

@@ -22,4 +22,7 @@ public @interface SecurityCheck {
*/
@Nonbinding
Role value() default Role.USER;
@Nonbinding
boolean apiKeyAllowed() default false;
}

View File

@@ -24,6 +24,9 @@ public class UserModel implements Serializable {
@ApiProperty("user email, if any")
private String email;
@ApiProperty("api key")
private String apiKey;
@ApiProperty(value = "user password, never returned by the api")
private String password;
@@ -81,4 +84,12 @@ public class UserModel implements Serializable {
this.email = email;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}

View File

@@ -18,6 +18,9 @@ public class ProfileModificationRequest {
@ApiProperty(value = "changes password of the user, if specified")
private String password;
@ApiProperty(value = "generate a new api key")
private boolean newApiKey;
public String getEmail() {
return email;
}
@@ -34,4 +37,12 @@ public class ProfileModificationRequest {
this.password = password;
}
public boolean isNewApiKey() {
return newApiKey;
}
public void setNewApiKey(boolean newApiKey) {
this.newApiKey = newApiKey;
}
}

View File

@@ -97,7 +97,7 @@ public abstract class AbstractREST {
@Inject
OPMLImporter opmlImporter;
@Inject
OPMLExporter opmlExporter;
@@ -124,44 +124,61 @@ public abstract class AbstractREST {
.fetchCreateAndSetSession(cycle);
if (session.getUser() == null) {
IAuthenticationStrategy authenticationStrategy = app
.getSecuritySettings().getAuthenticationStrategy();
String[] data = authenticationStrategy.load();
cookieLogin(app, session);
}
if (session.getUser() == null) {
basicHttpLogin(swreq, session);
}
}
private void cookieLogin(CommaFeedApplication app, CommaFeedSession session) {
IAuthenticationStrategy authenticationStrategy = app
.getSecuritySettings().getAuthenticationStrategy();
String[] data = authenticationStrategy.load();
if (data != null && data.length > 1) {
session.signIn(data[0], data[1]);
}
}
private void basicHttpLogin(ServletWebRequest req, CommaFeedSession session) {
String value = req.getHeader(HttpHeaders.AUTHORIZATION);
if (value != null && value.startsWith("Basic ")) {
value = value.substring(6);
String decoded = new String(Base64.decodeBase64(value));
String[] data = decoded.split(":");
if (data != null && data.length > 1) {
session.signIn(data[0], data[1]);
} else {
String value = swreq.getHeader(HttpHeaders.AUTHORIZATION);
if (value != null && value.startsWith("Basic ")) {
value = value.substring(6);
String decoded = new String(Base64.decodeBase64(value));
data = decoded.split(":");
if (data != null && data.length > 1) {
session.signIn(data[0], data[1]);
}
}
}
}
}
private void apiKeyLogin() {
String apiKey = request.getParameter("apiKey");
User user = userDAO.findByApiKey(apiKey);
CommaFeedSession.get().setUser(user);
}
protected User getUser() {
return CommaFeedSession.get().getUser();
}
@AroundInvoke
public Object checkSecurity(InvocationContext context) throws Exception {
User user = getUser();
boolean allowed = true;
User user = null;
Method method = context.getMethod();
SecurityCheck check = method.isAnnotationPresent(SecurityCheck.class) ? method
.getAnnotation(SecurityCheck.class) : method
.getDeclaringClass().getAnnotation(SecurityCheck.class);
if (method.isAnnotationPresent(SecurityCheck.class)) {
allowed = checkRole(user, method.getAnnotation(SecurityCheck.class));
} else if (method.getDeclaringClass().isAnnotationPresent(
SecurityCheck.class)) {
allowed = checkRole(
user,
method.getDeclaringClass().getAnnotation(
SecurityCheck.class));
if (check != null) {
user = getUser();
if (user == null && check.apiKeyAllowed()) {
apiKeyLogin();
user = getUser();
}
allowed = checkRole(check.value());
}
if (!allowed) {
if (user == null) {
@@ -181,8 +198,7 @@ public abstract class AbstractREST {
return context.proceed();
}
private boolean checkRole(User user, SecurityCheck annotation) {
Role requiredRole = annotation.value();
private boolean checkRole(Role requiredRole) {
if (requiredRole == Role.NONE) {
return true;
}

View File

@@ -26,7 +26,9 @@ import org.slf4j.LoggerFactory;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Category;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
@@ -111,6 +113,7 @@ public class CategoryREST extends AbstractResourceREST {
@GET
@ApiOperation(value = "Get category entries as feed", notes = "Get a feed of category entries")
@Produces(MediaType.APPLICATION_XML)
@SecurityCheck(value = Role.USER, apiKeyAllowed = true)
public String getCategoryEntriesAsFeed(
@ApiParam(value = "id of the category, 'all' or 'starred'", required = true) @QueryParam("id") String id) {

View File

@@ -31,14 +31,16 @@ import com.commafeed.backend.feeds.FetchedFeed;
import com.commafeed.backend.model.FeedCategory;
import com.commafeed.backend.model.FeedEntryStatus;
import com.commafeed.backend.model.FeedSubscription;
import com.commafeed.backend.model.UserRole.Role;
import com.commafeed.backend.model.UserSettings.ReadingOrder;
import com.commafeed.frontend.SecurityCheck;
import com.commafeed.frontend.model.Entries;
import com.commafeed.frontend.model.Entry;
import com.commafeed.frontend.model.FeedInfo;
import com.commafeed.frontend.model.Subscription;
import com.commafeed.frontend.model.request.FeedModificationRequest;
import com.commafeed.frontend.model.request.IDRequest;
import com.commafeed.frontend.model.request.MarkRequest;
import com.commafeed.frontend.model.request.FeedModificationRequest;
import com.commafeed.frontend.model.request.SubscribeRequest;
import com.commafeed.frontend.rest.Enums.ReadType;
import com.google.common.base.Preconditions;
@@ -98,6 +100,7 @@ public class FeedREST extends AbstractResourceREST {
@GET
@ApiOperation(value = "Get feed entries as a feed", notes = "Get a feed of feed entries")
@Produces(MediaType.APPLICATION_XML)
@SecurityCheck(value = Role.USER, apiKeyAllowed = true)
public String getFeedEntriesAsFeed(
@ApiParam(value = "id of the feed", required = true) @QueryParam("id") String id) {

View File

@@ -1,11 +1,14 @@
package com.commafeed.frontend.rest.resources;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
@@ -78,6 +81,7 @@ public class UserREST extends AbstractResourceREST {
userModel.setName(user.getName());
userModel.setEmail(user.getEmail());
userModel.setEnabled(!user.isDisabled());
userModel.setApiKey(user.getApiKey());
for (UserRole role : userRoleDAO.findAll(user)) {
if (role.getRole() == Role.ADMIN) {
userModel.setAdmin(true);
@@ -95,13 +99,24 @@ public class UserREST extends AbstractResourceREST {
if (StartupBean.USERNAME_DEMO.equals(user.getName())) {
return Response.status(Status.UNAUTHORIZED).build();
}
user.setEmail(request.getEmail());
if (StringUtils.isNotBlank(request.getPassword())) {
byte[] password = encryptionService.getEncryptedPassword(
request.getPassword(), user.getSalt());
user.setPassword(password);
user.setApiKey(generateKey(user));
}
if (request.isNewApiKey()) {
user.setApiKey(generateKey(user));
}
userDAO.update(user);
return Response.ok().build();
}
private String generateKey(User user) {
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID()
.toString(), user.getSalt());
return DigestUtils.sha1Hex(key);
}
}

View File

@@ -184,10 +184,11 @@ function($scope, $timeout, $stateParams, $window, $location, $state, $route, Cat
});
}]);
module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', '$dialog',
function($scope, $state, $stateParams, FeedService, CategoryService, $dialog) {
module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', 'ProfileService', '$dialog',
function($scope, $state, $stateParams, FeedService, CategoryService, ProfileService, $dialog) {
$scope.CategoryService = CategoryService;
$scope.user = ProfileService.get();
$scope.sub = FeedService.get({
id : $stateParams._id
@@ -250,9 +251,10 @@ module.controller('FeedDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedS
};
}]);
module.controller('CategoryDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', '$dialog',
function($scope, $state, $stateParams, FeedService, CategoryService, $dialog) {
module.controller('CategoryDetailsCtrl', ['$scope', '$state', '$stateParams', 'FeedService', 'CategoryService', 'ProfileService', '$dialog',
function($scope, $state, $stateParams, FeedService, CategoryService, ProfileService, $dialog) {
$scope.CategoryService = CategoryService;
$scope.user = ProfileService.get();
$scope.isMeta = function() {
return parseInt($stateParams._id, 10) != $stateParams._id;
@@ -264,7 +266,7 @@ module.controller('CategoryDetailsCtrl', ['$scope', '$state', '$stateParams', 'F
return elem.id != $scope.category.id;
};
CategoryService.get(function() {
CategoryService.init(function() {
if ($scope.isMeta()) {
$scope.category = {
id : $stateParams._id,
@@ -785,7 +787,8 @@ function($scope, $location, ProfileService, AnalyticsService) {
}
var o = {
email : $scope.user.email,
password : $scope.user.password
password : $scope.user.password,
newApiKey : $scope.newApiKey
};
ProfileService.save(o, function() {

View File

@@ -24,7 +24,8 @@
<div class="control-group">
<label class="control-label">Feed URL</label>
<div class="controls horizontal-align">
<a href="{{'rest/category/entriesAsFeed?id=' + category.id}}" target="_blank">{{'rest/category/entriesAsFeed?id=' + category.id}}</a>
<a ng-show="user.apiKey" href="{{'rest/category/entriesAsFeed?id=' + category.id + '&apiKey=' + user.apiKey}}" target="_blank">link</a>
<span ng-show="!user.apiKey">Generate an API key in your profile first.</span>
</div>
</div>

View File

@@ -37,7 +37,8 @@
<div class="control-group">
<label class="control-label">Feed URL</label>
<div class="controls horizontal-align">
<a href="{{'rest/feed/entriesAsFeed?id=' + sub.id}}" target="_blank">{{'rest/feed/entriesAsFeed?id=' + sub.id}}</a>
<a ng-show="user.apiKey" href="{{'rest/feed/entriesAsFeed?id=' + sub.id + '&apiKey=' + user.apiKey}}" target="_blank">link</a>
<span ng-show="!user.apiKey">Generate an API key in your profile first.</span>
</div>
</div>

View File

@@ -31,6 +31,21 @@
<span class="help-inline" ng-show="profileForm.password_c.$error.validator">Passwords do not match</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">API key</label>
<div class="controls horizontal-align">
<span ng-show="user.apiKey">{{user.apiKey}}</span>
<span ng-show="!user.apiKey">Not generated yet</span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="password">Generate new API key</label>
<div class="controls">
<input type="checkbox" name="newApiKey" id="newApiKey" ng-model="newApiKey">
<span class="help-block">Changing password will generate a new API key</span>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>