diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/control/AccountControl.java b/account/src/main/java/de/muehlencord/shared/account/business/account/control/AccountControl.java index 4edcc89..0f98dc9 100644 --- a/account/src/main/java/de/muehlencord/shared/account/business/account/control/AccountControl.java +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/control/AccountControl.java @@ -129,7 +129,7 @@ public class AccountControl implements Serializable { Subject currentUser = SecurityUtils.getSubject(); String currentLoggedInUser = currentUser.getPrincipal().toString(); - account.setLastUpdatedBy(currentLoggedInUser); + account.setLastUpdatedBy(currentLoggedInUser); // FIXME - should be done via updateable account.setLastUpdatedOn(now); boolean newAccount = (account.getCreatedOn() == null); diff --git a/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyEntity.java b/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyEntity.java index c00dc23..90bfa8b 100644 --- a/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyEntity.java +++ b/account/src/main/java/de/muehlencord/shared/account/business/account/entity/ApiKeyEntity.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.Date; import java.util.UUID; import javax.persistence.Basic; +import javax.persistence.Cacheable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; @@ -27,6 +28,7 @@ import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; +import javax.persistence.QueryHint; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; @@ -41,13 +43,18 @@ import org.hibernate.annotations.Type; * @author Joern Muehlencord */ @Entity +@Cacheable @Table(name = "api_key") @XmlRootElement @NamedQueries({ @NamedQuery(name = "ApiKeyEntity.findAll", query = "SELECT a FROM ApiKeyEntity a"), - @NamedQuery(name = "ApiKeyEntity.findByApiKey", query = "SELECT a FROM ApiKeyEntity a WHERE a.apiKey = :apiKey"), + @NamedQuery(name = "ApiKeyEntity.findByApiKey", query = "SELECT a FROM ApiKeyEntity a WHERE a.apiKey = :apiKey", hints = { + @QueryHint(name = "org.hibernate.cacheable", value = "true"), + @QueryHint(name = "org.hibernate.cacheRegion", value = "Queries")}), @NamedQuery(name = "ApiKeyEntity.findByIssuedOn", query = "SELECT a FROM ApiKeyEntity a WHERE a.issuedOn = :issuedOn"), - @NamedQuery(name = "ApiKeyEntity.findByAccount", query = "SELECT a FROM ApiKeyEntity a WHERE a.account = :account"), + @NamedQuery(name = "ApiKeyEntity.findByAccount", query = "SELECT a FROM ApiKeyEntity a WHERE a.account = :account", hints = { + @QueryHint(name = "org.hibernate.cacheable", value = "true"), + @QueryHint(name = "org.hibernate.cacheRegion", value = "Queries")}), @NamedQuery(name = "ApiKeyEntity.findByExpiration", query = "SELECT a FROM ApiKeyEntity a WHERE a.expiration = :expiration")}) public class ApiKeyEntity implements Serializable { @@ -72,7 +79,7 @@ public class ApiKeyEntity implements Serializable { @Temporal(TemporalType.TIMESTAMP) private Date issuedOn; @Column(name = "expiration") - private Short expiration; + private Short expiration; @JoinColumn(name = "account", referencedColumnName = "id") @ManyToOne(optional = false) private AccountEntity account; diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java b/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java index 2cc7a21..31fda59 100644 --- a/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/filter/JWTAuthenticationFilter.java @@ -94,8 +94,14 @@ public final class JWTAuthenticationFilter extends AuthenticatingFilter { if (isLoggedAttempt(request, response)) { String jwtToken = getAuthzHeader(request); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("found jwtToke in header = {}", jwtToken); + } + if (jwtToken != null) { - return createToken(jwtToken); + JWTObject jwtObject = apiKeyService.getJWTObject(jwtToken); + return new JWTAuthenticationToken(jwtObject.getUserName(), jwtToken); +// return createToken(jwtToken); } } @@ -142,13 +148,13 @@ public final class JWTAuthenticationFilter extends AuthenticatingFilter { httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } else if ((existing != null) && (existing.getClass().isAssignableFrom(APIException.class))) { APIException apiException = (APIException) existing; - + HttpServletResponse httpResponse = WebUtils.toHttp(response); httpResponse.setStatus(apiException.getHttpResponse().getStatus()); httpResponse.addHeader(APIException.HTTP_HEADER_X_ERROR, apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ERROR)); httpResponse.addHeader(APIException.HTTP_HEADER_X_ERROR_CODE, apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ERROR_CODE)); - - if(apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ROOT_CAUSE) != null) { + + if (apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ROOT_CAUSE) != null) { httpResponse.addHeader(APIException.HTTP_HEADER_X_ROOT_CAUSE, apiException.getHttpResponse().getHeaderString(APIException.HTTP_HEADER_X_ROOT_CAUSE)); } } else { diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/pam/AllSupportedSuccessfulStrategy.java b/account/src/main/java/de/muehlencord/shared/account/shiro/pam/AllSupportedSuccessfulStrategy.java new file mode 100644 index 0000000..2768393 --- /dev/null +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/pam/AllSupportedSuccessfulStrategy.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 joern.muehlencord. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.muehlencord.shared.account.shiro.pam; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UnknownAccountException; +import org.apache.shiro.authc.pam.AbstractAuthenticationStrategy; +import org.apache.shiro.realm.Realm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author joern.muehlencord + */ +public class AllSupportedSuccessfulStrategy extends AbstractAuthenticationStrategy { + + private static final Logger LOGGER = LoggerFactory.getLogger(AllSupportedSuccessfulStrategy.class); + + public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { + if (!realm.supports(token)) { + return info; + } + + return info; + } + + public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t) + throws AuthenticationException { + if (t != null) { + if (t instanceof AuthenticationException) { + //propagate: + throw ((AuthenticationException) t); + } else { + String msg = "Unable to acquire account data from realm [" + realm + "]. The [" + + getClass().getName() + " implementation requires all configured realm(s) to operate successfully " + + "for a successful authentication."; + throw new AuthenticationException(msg, t); + } + } + if (info == null) { + if (realm.supports(token)) { + String msg = "Realm [" + realm + "] could not find any associated account data for the submitted " + + "AuthenticationToken [" + token + "]. The [" + getClass().getName() + "] implementation requires " + + "all configured realm(s) to acquire valid account data for a submitted token during the " + + "log-in process."; + throw new UnknownAccountException(msg); + } else { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Realm [{}] does not support token of type [{}], skipped realm", realm, token); + } + } + } else { + LOGGER.debug("Account successfully authenticated using realm [{}]", realm); + } + + // If non-null account is returned, then the realm was able to authenticate the + // user - so merge the account with any accumulated before + // if the realm does not support the token, null is returned + if (realm.supports(token)) { + merge(info, aggregate); + } + + return aggregate; + } + +} diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/realm/AccountRealm.java b/account/src/main/java/de/muehlencord/shared/account/shiro/realm/AccountRealm.java index af64a7e..1f8238a 100644 --- a/account/src/main/java/de/muehlencord/shared/account/shiro/realm/AccountRealm.java +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/realm/AccountRealm.java @@ -54,17 +54,100 @@ public class AccountRealm extends JdbcRealm { public AccountRealm() { this.authenticationQuery = "select al.account_password from account a, account_login al where al.account = a.id and a.username = ? and status not in ('LOCKED','DELETED','DISABLED')"; - this.userRolesQuery = "select r.role_name from application_role r, account_role ar, account a WHERE a.username = ? AND a.id = ar.account AND ar.account_role = r.id"; + this.userRolesQuery = "select r.role_name from application_role r, account_role ar, account a WHERE a.username = ? AND a.id = ar.account AND ar.account_role = r.id AND r.application = ?"; this.permissionsQuery = "select permission_name from application_role appr, role_permission rp, application_permission appp WHERE appr.role_name = ? AND appr.application = ? AND rp.application_role = appr.id AND rp.role_permission = appp.id"; this.permissionsLookupEnabled = true; } - + @Override public boolean supports(AuthenticationToken token) { - super.supports(token); - return token != null && (token instanceof JWTAuthenticationToken || token instanceof UsernamePasswordToken); + return (token != null && ((JWTAuthenticationToken.class.isAssignableFrom(token.getClass())) || (UsernamePasswordToken.class.isAssignableFrom(token.getClass())))); + } + + @Override + protected Set getRoleNamesForUser(Connection conn, String username) throws SQLException { + PreparedStatement ps = null; + ResultSet rs = null; + Set roleNames = new LinkedHashSet<>(); + try { + ps = conn.prepareStatement(userRolesQuery); + ps.setString(1, username); + ps.setObject(2, UUID.fromString(applicationId)); // this is the changed line - rest is the same as in JDBCRealm + + // Execute query + rs = ps.executeQuery(); + + // Loop over results and add each returned role to a set + while (rs.next()) { + + String roleName = rs.getString(1); + + // Add the role to the list of names if it isn't null + if (roleName != null) { + roleNames.add(roleName); + } else { + LOGGER.error("Null role name found while retrieving role names for user [{}]", username); + } + } + } finally { + JdbcUtils.closeResultSet(rs); + JdbcUtils.closeStatement(ps); + } + return roleNames; } + /** + * overwritten getPermissions. Only change is to inject the applicationId + * into the the query + * + * @param conn the connection to use + * @param username the user to lookup + * @param roleNames the users roles + * @return a list of permissions + * @throws SQLException if the SQL query fails + */ + @Override + protected Set getPermissions(Connection conn, String username, Collection roleNames) throws SQLException { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("user {} has the following roles: {}", username, roleNames.toString()); + LOGGER.debug("looking up permissions for user"); + } + + PreparedStatement ps = null; + Set permissions = new LinkedHashSet<>(); + try { + ps = conn.prepareStatement(permissionsQuery); + for (String roleName : roleNames) { + + ps.setString(1, roleName); + ps.setObject(2, UUID.fromString(applicationId)); // this is the changed line - rest is the same as in JDBCRealm + + ResultSet rs = null; + try { + // Execute query + rs = ps.executeQuery(); + // Loop over results and add each returned role to a set + while (rs.next()) { + String permissionString = rs.getString(1); + // Add the permission to the set of permissions + permissions.add(permissionString); + } + } finally { + JdbcUtils.closeResultSet(rs); + } + + } + } finally { + JdbcUtils.closeStatement(ps); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("user {} has the following permissions: {}", username, permissions.toString()); + } + + return permissions; + } + public boolean isJwtAuthentication(AuthenticationToken token) { if (token == null) { throw new AuthenticationException("empty tokens are not supported by this realm"); @@ -83,54 +166,7 @@ public class AccountRealm extends JdbcRealm { } else { throw new AuthenticationException("Handling of " + token.getClass() + " not supported by this realm"); } - } - - /** - * overwritten getPermissions. Only change is to inject the applicationId - * into the the query - * - * @param conn the connection to use - * @param username the user to lookup - * @param roleNames the users roles - * @return a list of permissions - * @throws SQLException if the SQL query fails - */ - @Override - protected Set getPermissions(Connection conn, String username, Collection roleNames) throws SQLException { - PreparedStatement ps = null; - Set permissions = new LinkedHashSet<>(); - try { - ps = conn.prepareStatement(permissionsQuery); - for (String roleName : roleNames) { - - ps.setString(1, roleName); - ps.setObject(2, UUID.fromString(applicationId)); // this is the changed line - rest is the same as in JDBCRealm - - ResultSet rs = null; - - try { - // Execute query - rs = ps.executeQuery(); - - // Loop over results and add each returned role to a set - while (rs.next()) { - - String permissionString = rs.getString(1); - - // Add the permission to the set of permissions - permissions.add(permissionString); - } - } finally { - JdbcUtils.closeResultSet(rs); - } - - } - } finally { - JdbcUtils.closeStatement(ps); - } - - return permissions; - } + } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { diff --git a/account/src/main/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealm.java b/account/src/main/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealm.java index 5f49770..97b0dbc 100644 --- a/account/src/main/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealm.java +++ b/account/src/main/java/de/muehlencord/shared/account/shiro/realm/UserNameActiveDirectoryRealm.java @@ -40,6 +40,11 @@ public class UserNameActiveDirectoryRealm extends ActiveDirectoryRealm { private boolean permissionsLookupEnabled = true; protected String fallbackPrincipalSuffix = null; + + @Override + public boolean supports(AuthenticationToken token) { + return (token != null && (UsernamePasswordToken.class.isAssignableFrom(token.getClass()))); + } @Override protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException { diff --git a/jeeutil/src/main/java/de/muehlencord/shared/jeeutil/restexfw/APIExceptionInterceptor.java b/jeeutil/src/main/java/de/muehlencord/shared/jeeutil/restexfw/APIExceptionInterceptor.java index 39d992c..409f875 100644 --- a/jeeutil/src/main/java/de/muehlencord/shared/jeeutil/restexfw/APIExceptionInterceptor.java +++ b/jeeutil/src/main/java/de/muehlencord/shared/jeeutil/restexfw/APIExceptionInterceptor.java @@ -43,6 +43,10 @@ public class APIExceptionInterceptor { // if an exception is thrown during processing, this is passed in to the catch block below proceedResponse = context.proceed(); } catch (Exception ex) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Detailed stacktrace", new Object[]{ex}); + } + Response errorResponse; if (ex instanceof APIException) { errorResponse = ((APIException) ex).getHttpResponse(); @@ -56,12 +60,7 @@ public class APIExceptionInterceptor { throw (ConstraintViolationException) ex.getCause(); } else { errorResponse = new APIException(ex, locale).getHttpResponse(); - } - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(ex.toString(), ex); - } - + } return errorResponse; } return proceedResponse; diff --git a/jeeutil/src/main/java/de/muehlencord/shared/jeeutil/restexfw/ConstraintViolationMapper.java b/jeeutil/src/main/java/de/muehlencord/shared/jeeutil/restexfw/ConstraintViolationMapper.java index f09bedf..6d647af 100644 --- a/jeeutil/src/main/java/de/muehlencord/shared/jeeutil/restexfw/ConstraintViolationMapper.java +++ b/jeeutil/src/main/java/de/muehlencord/shared/jeeutil/restexfw/ConstraintViolationMapper.java @@ -1,63 +1,63 @@ -/* - * Copyright 2018 jomu. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.muehlencord.shared.jeeutil.restexfw; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Variant; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * - * @author jomu - */ -@Provider -public class ConstraintViolationMapper implements ExceptionMapper { - - private static List acceptableMediaTypes = Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE).build(); - - @Context - protected Request request; - - @Override - public Response toResponse(ConstraintViolationException ex) { - Set> constViolations = ex.getConstraintViolations(); - List errorList = new ArrayList<>(); - for (ConstraintViolation constraintViolation : constViolations) { - errorList.add(new ConstraintViolationEntry(constraintViolation)); - } - GenericEntity> entity = new GenericEntity>(errorList) {}; - return Response.status(Response.Status.BAD_REQUEST).entity(entity).type(getNegotiatedMediaType()).build(); - } - - protected MediaType getNegotiatedMediaType() { - final Variant selectedMediaType = request.selectVariant(acceptableMediaTypes); - if (selectedMediaType == null) { - return MediaType.APPLICATION_JSON_TYPE; - } - return selectedMediaType.getMediaType(); - } - -} +/* + * Copyright 2018 jomu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.muehlencord.shared.jeeutil.restexfw; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Variant; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * + * @author jomu + */ +@Provider +public class ConstraintViolationMapper implements ExceptionMapper { + + private static final List ACCEPTABLE_MEDIA_TYPES = Variant.mediaTypes(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE).build(); + + @Context + protected Request request; + + @Override + public Response toResponse(ConstraintViolationException ex) { + Set> constViolations = ex.getConstraintViolations(); + List errorList = new ArrayList<>(); + constViolations.forEach((constraintViolation) -> { + errorList.add(new ConstraintViolationEntry(constraintViolation)); + }); + GenericEntity> entity = new GenericEntity>(errorList) {}; + return Response.status(Response.Status.BAD_REQUEST).entity(entity).type(getNegotiatedMediaType()).build(); + } + + protected MediaType getNegotiatedMediaType() { + final Variant selectedMediaType = request.selectVariant(ACCEPTABLE_MEDIA_TYPES); + if (selectedMediaType == null) { + return MediaType.APPLICATION_JSON_TYPE; + } + return selectedMediaType.getMediaType(); + } + +}