Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audit logging #116

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 101 additions & 41 deletions src/java/org/jivesoftware/openfire/plugin/rest/AuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;

import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.admin.AdminManager;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.ConnectionException;
import org.jivesoftware.openfire.auth.InternalUnauthenticatedException;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.security.Principal;

/**
* The Class AuthFilter.
Expand All @@ -55,57 +58,35 @@ public class AuthFilter implements ContainerRequestFilter {
private RESTServicePlugin plugin = (RESTServicePlugin) XMPPServer.getInstance().getPluginManager()
.getPlugin("restapi");

@Override
public void filter(ContainerRequestContext containerRequest) throws IOException {
if (containerRequest.getUriInfo().getRequestUri().getPath().equals("/plugins/restapi/v1/openapi.yaml")) {
LOG.debug("Authentication was bypassed for openapi.yaml file (documentation)");
return;
}
public static final String SHARED_SECRET_AUTHENTICATION_SCHEME = "SharedSecret";

if (isStatusEndpoint(containerRequest.getUriInfo().getRequestUri().getPath())) {
LOG.debug("Authentication was bypassed for a status endpoint");
return;
}
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {

if (!plugin.isEnabled()) {
LOG.debug("REST API Plugin is not enabled");
throw new WebApplicationException(Status.FORBIDDEN);
}

// Let the preflight request through the authentication
if ("OPTIONS".equals(containerRequest.getMethod())) {
LOG.debug("Authentication was bypassed because of OPTIONS request");
return;
}

// To be backwards compatible to userservice 1.*
if (containerRequest.getUriInfo().getRequestUri().getPath().contains("restapi/v1/userservice")) {
LOG.info("Deprecated 'userservice' endpoint was used. Please switch to the new endpoints");

if (!authRequired(requestContext)){
return;
}

if (!plugin.getAllowedIPs().isEmpty()) {
// Get client's IP address
String ipAddress = httpRequest.getHeader("x-forwarded-for");
if (ipAddress == null) {
ipAddress = httpRequest.getHeader("X_FORWARDED_FOR");
if (ipAddress == null) {
ipAddress = httpRequest.getHeader("X-Forward-For");
if (ipAddress == null) {
ipAddress = httpRequest.getRemoteAddr();
}
}
}
String ipAddress = getClientIPAddressForRequest(httpRequest);
if (!plugin.getAllowedIPs().contains(ipAddress)) {
LOG.warn("REST API rejected service for IP address: " + ipAddress);
throw new WebApplicationException(Status.UNAUTHORIZED);
}
}

// Get the authentication passed in HTTP headers parameters
String auth = containerRequest.getHeaderString("authorization");
String auth = requestContext.getHeaderString("authorization");

if (auth == null) {
LOG.warn("REST API request with no Authorization header rejected. [Request IP: {}, Request URI: {}]",
getClientIPAddressForRequest(httpRequest), requestContext.getUriInfo().getRequestUri().getPath());
throw new WebApplicationException(Status.UNAUTHORIZED);
}

Expand All @@ -115,41 +96,120 @@ public void filter(ContainerRequestContext containerRequest) throws IOException

// If username or password fail
if (usernameAndPassword == null || usernameAndPassword.length != 2) {
LOG.warn("Username or password is not set");
throw new WebApplicationException(Status.UNAUTHORIZED);
LOG.warn("Basic authentication failed. Username or password is not set. [Request IP: {}, Request URI: {}]",
getClientIPAddressForRequest(httpRequest), requestContext.getUriInfo().getRequestUri().getPath());
throw new WebApplicationException("Username or password is not set", Status.UNAUTHORIZED);
}

boolean userAdmin = AdminManager.getInstance().isUserAdmin(usernameAndPassword[0], true);

if (!userAdmin) {
LOG.warn("Provided User is not an admin");
throw new WebApplicationException(Status.UNAUTHORIZED);
throw new WebApplicationException("User is not authorised", Status.UNAUTHORIZED);
}

try {
AuthFactory.authenticate(usernameAndPassword[0], usernameAndPassword[1]);
setSecurityForContext(requestContext, usernameAndPassword[0], SecurityContext.BASIC_AUTH);
if (JiveGlobals.getBooleanProperty(RESTServicePlugin.SERVICE_LOGGING_ENABLED, false)) {
LOG.info("Authentication - successfully authenticated user. [Request IP: {}, Request URI: {}, Username: {}]",
getClientIPAddressForRequest(httpRequest), requestContext.getUriInfo().getRequestUri().getPath(), usernameAndPassword[0]);
}
} catch (UnauthorizedException e) {
LOG.warn("Wrong HTTP Basic Auth authorization", e);
throw new WebApplicationException(Status.UNAUTHORIZED);
} catch (ConnectionException e) {
LOG.error("Authentication went wrong", e);
throw new WebApplicationException(Status.UNAUTHORIZED);
} catch (InternalUnauthenticatedException e) {
LOG.warn("Basic authentication failed. Username or password is incorrect. [Request IP: {}, Request URI: {}]",
getClientIPAddressForRequest(httpRequest), requestContext.getUriInfo().getRequestUri().getPath());
LOG.warn("Authentication error", e);
throw new WebApplicationException("Username or password is incorrect", Status.UNAUTHORIZED);
} catch (ConnectionException | InternalUnauthenticatedException e) {
LOG.error("Authentication went wrong", e);
throw new WebApplicationException(Status.UNAUTHORIZED);
}
} else {
if (!auth.equals(plugin.getSecret())) {
LOG.warn("Wrong secret key authorization. Provided key: " + auth);
LOG.warn("Wrong secret key authorization. [Request IP: {}, Request URI: {}, Request auth: {}]",
getClientIPAddressForRequest(httpRequest), requestContext.getUriInfo().getRequestUri().getPath(), auth);
throw new WebApplicationException(Status.UNAUTHORIZED);
} else {
//For shared secret, use the authentication scheme to indicate that a username is unknown
setSecurityForContext(requestContext, SHARED_SECRET_AUTHENTICATION_SCHEME, SHARED_SECRET_AUTHENTICATION_SCHEME);
if (JiveGlobals.getBooleanProperty(RESTServicePlugin.SERVICE_LOGGING_ENABLED, false)) {
LOG.info("Authentication - successfully authenticated by secret key. [Request IP: {}, Request URI: {}]",
getClientIPAddressForRequest(httpRequest), requestContext.getUriInfo().getRequestUri().getPath());
}
}
}
}

private boolean authRequired(ContainerRequestContext requestContext){
if (requestContext.getUriInfo().getRequestUri().getPath().equals("/plugins/restapi/v1/openapi.yaml")) {
LOG.debug("Authentication was bypassed for openapi.yaml file (documentation)");
return false;
}

if (isStatusEndpoint(requestContext.getUriInfo().getRequestUri().getPath())) {
LOG.debug("Authentication was bypassed for a status endpoint");
return false;
}

// Let the preflight request through the authentication
if ("OPTIONS".equals(requestContext.getMethod())) {
LOG.debug("Authentication was bypassed because of OPTIONS request");
return false;
}

// To be backwards compatible to userservice 1.*
if (requestContext.getUriInfo().getRequestUri().getPath().contains("restapi/v1/userservice")) {
LOG.info("Deprecated 'userservice' endpoint was used. Please switch to the new endpoints");
return false;
}

return true;
}

private void setSecurityForContext(ContainerRequestContext requestContext, String username, String authScheme){
final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {

@Override
public Principal getUserPrincipal() {
return () -> username;
}

@Override
public boolean isUserInRole(String role) {
return true;
}

@Override
public boolean isSecure() {
return currentSecurityContext.isSecure();
}

@Override
public String getAuthenticationScheme() {
return authScheme;
}
});
}

private boolean isStatusEndpoint(String path){
return path.equals("/plugins/restapi/v1/system/liveness") ||
path.startsWith("/plugins/restapi/v1/system/liveness/") ||
path.equals("/plugins/restapi/v1/system/readiness") ||
path.startsWith("/plugins/restapi/v1/system/readiness/");
}

private String getClientIPAddressForRequest(HttpServletRequest request) {
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null) {
ipAddress = request.getHeader("X_FORWARDED_FOR");
if (ipAddress == null) {
ipAddress = request.getHeader("X-Forward-For");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
}
}
return ipAddress;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,17 @@
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.cluster.ClusterNodeInfo;
import org.jivesoftware.openfire.cluster.NodeID;
import org.jivesoftware.openfire.plugin.rest.RESTServicePlugin;
import org.jivesoftware.openfire.plugin.rest.entity.ClusterNodeEntities;
import org.jivesoftware.openfire.plugin.rest.entity.ClusterNodeEntity;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jivesoftware.openfire.plugin.rest.utils.LoggingUtils;
import org.jivesoftware.openfire.plugin.rest.utils.LoggingUtils.AuditEvent;

import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;

public class ClusteringController {
private static final Logger LOG = LoggerFactory.getLogger(ClusteringController.class);

private static ClusteringController INSTANCE = null;

/**
Expand All @@ -56,13 +52,8 @@ public static void setInstance(final ClusteringController instance) {
ClusteringController.INSTANCE = instance;
}

public static void log(String logMessage) {
if (JiveGlobals.getBooleanProperty(RESTServicePlugin.SERVICE_LOGGING_ENABLED, false)) {
LOG.info(logMessage);
}
}

public String getClusterStatus() {
LoggingUtils.auditEvent(AuditEvent.CLUSTERING_GET_STATUS);
if (ClusterManager.isClusteringEnabled()) {
if (ClusterManager.isClusteringStarted()) {
if (ClusterManager.isSeniorClusterMember()) {
Expand All @@ -83,11 +74,13 @@ public String getClusterStatus() {
}

public Optional<ClusterNodeEntity> getNodeEntity(String nodeId) {
LoggingUtils.auditEvent(AuditEvent.CLUSTERING_GET_NODE, nodeId);
final Optional<ClusterNodeInfo> nodeInfo = ClusterManager.getNodeInfo(NodeID.getInstance(nodeId.getBytes(StandardCharsets.UTF_8)));
return nodeInfo.map(ClusterNodeEntity::from);
}

public ClusterNodeEntities getNodeEntities() {
LoggingUtils.auditEvent(AuditEvent.CLUSTERING_GET_NODES);
final Collection<ClusterNodeInfo> nodesInfo = ClusterManager.getNodesInfo();
return new ClusterNodeEntities(nodesInfo.stream().map(ClusterNodeEntity::from).collect(Collectors.toList()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.jivesoftware.openfire.plugin.rest.entity.GroupEntity;
import org.jivesoftware.openfire.plugin.rest.exceptions.ExceptionType;
import org.jivesoftware.openfire.plugin.rest.exceptions.ServiceException;
import org.jivesoftware.openfire.plugin.rest.utils.LoggingUtils;
import org.jivesoftware.openfire.plugin.rest.utils.MUCRoomUtils;
import org.xmpp.packet.JID;

Expand Down Expand Up @@ -55,6 +56,7 @@ public static GroupController getInstance() {
* the service exception
*/
public List<GroupEntity> getGroups() throws ServiceException {
LoggingUtils.auditEvent(LoggingUtils.AuditEvent.GROUPS_LIST);
Collection<Group> groups = GroupManager.getInstance().getGroups();
List<GroupEntity> groupEntities = new ArrayList<>();
for (Group group : groups) {
Expand All @@ -75,6 +77,7 @@ public List<GroupEntity> getGroups() throws ServiceException {
* the service exception
*/
public GroupEntity getGroup(String groupName) throws ServiceException {
LoggingUtils.auditEvent(LoggingUtils.AuditEvent.GROUPS_GET_BY_NAME, groupName);
Group group;
try {
group = GroupManager.getInstance().getGroup(groupName);
Expand All @@ -101,6 +104,7 @@ public GroupEntity getGroup(String groupName) throws ServiceException {
* the service exception
*/
public Group createGroup(GroupEntity groupEntity) throws ServiceException {
LoggingUtils.auditEvent(LoggingUtils.AuditEvent.GROUPS_CREATE, groupEntity);
Group group;
if (groupEntity != null && !groupEntity.getName().isEmpty()) {
try {
Expand Down Expand Up @@ -163,6 +167,7 @@ public Group createGroup(GroupEntity groupEntity) throws ServiceException {
* @throws ServiceException the service exception
*/
public Group updateGroup(String groupName, GroupEntity groupEntity) throws ServiceException {
LoggingUtils.auditEvent(LoggingUtils.AuditEvent.GROUPS_UPDATE_BY_NAME, groupName, groupEntity);
Group group;
if (groupEntity != null && !groupEntity.getName().isEmpty()) {
if (groupName.equals(groupEntity.getName())) {
Expand Down Expand Up @@ -251,6 +256,7 @@ public Group updateGroup(String groupName, GroupEntity groupEntity) throws Servi
* the service exception
*/
public void deleteGroup(String groupName) throws ServiceException {
LoggingUtils.auditEvent(LoggingUtils.AuditEvent.GROUPS_DELETE, groupName);
try {
Group group = GroupManager.getInstance().getGroup(groupName);
GroupManager.getInstance().deleteGroup(group);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.plugin.rest.exceptions.ExceptionType;
import org.jivesoftware.openfire.plugin.rest.exceptions.ServiceException;
import org.jivesoftware.openfire.plugin.rest.utils.LoggingUtils;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.session.ClientSession;
Expand Down Expand Up @@ -66,6 +67,13 @@ public static boolean changeName(String currentUserName, String newUserName, boo
String newEmail, String newRealName) throws ServiceException {
UserManager userManager = UserManager.getInstance();

LoggingUtils.auditEvent(LoggingUtils.AuditEvent.USER_CHANGE_NAME,
"currentUserName", currentUserName,
"newUserName", newUserName,
"deleteOldUser", deleteOldUser,
"newEmail", newEmail,
"newRealName", newRealName);

try {
User currentUser = userManager.getUser(currentUserName);
// Old user found, create new one
Expand Down
Loading