forked from Archives/Athou_commafeed
added authentication via api key for some limited api methods (fix #64)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,4 +22,7 @@ public @interface SecurityCheck {
|
||||
*/
|
||||
@Nonbinding
|
||||
Role value() default Role.USER;
|
||||
|
||||
@Nonbinding
|
||||
boolean apiKeyAllowed() default false;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user