Skip to content

Commit

Permalink
tenant-manager: audit logging (#1891)
Browse files Browse the repository at this point in the history
* tenant-manager: audit logging

* configure access log

* tenant manager: audit logs for authentication errors
  • Loading branch information
Fabian Martinez authored Oct 5, 2021
1 parent d2df3ba commit c37a5a7
Show file tree
Hide file tree
Showing 14 changed files with 675 additions and 5 deletions.
6 changes: 6 additions & 0 deletions app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ quarkus.log.console.format=%d{YYYY-MM-DD HH:mm:ss} %p <%X{tenantId}> [%C] (%t) %
%prod.quarkus.log.console.enable=true
%prod.quarkus.log.category."io.apicurio".level=${REGISTRY_LOG_LEVEL:INFO}

# Access logs
quarkus.http.access-log.enabled=${ENABLE_ACCESS_LOG:false}
quarkus.http.access-log.pattern="apicurio-registry.access method="%{METHOD}" path="%{REQUEST_URL}" response_code="%{RESPONSE_CODE}" response_time="%{RESPONSE_TIME}" remote_ip="%{REMOTE_IP}" remote_user="%{REMOTE_USER}" user_agent="%{i,User-Agent}""
#this property will be used by Quarkus 2.X
quarkus.http.access-log.exclude-pattern=/health/.*

# Sentry - the rest of the sentry configuration is picked from sentry own env vars
registry.enable.sentry=${ENABLE_SENTRY:false}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ objects:
value: ${REGISTRY_QUARKUS_LOG_LEVEL}
- name: QUARKUS_PROFILE
value: prod
- name: ENABLE_ACCESS_LOG
value: ${ENABLE_ACCESS_LOG}

- name: ENABLE_SENTRY
value: ${ENABLE_SENTRY}
Expand Down Expand Up @@ -318,6 +320,8 @@ objects:
value: ${TENANT_MANAGER_QUARKUS_LOG_LEVEL}
- name: TENANT_MANAGER_LOG_LEVEL
value: ${TENANT_MANAGER_LOG_LEVEL}
- name: ENABLE_ACCESS_LOG
value: ${ENABLE_ACCESS_LOG}

- name: ENABLE_SENTRY
value: ${ENABLE_SENTRY}
Expand Down Expand Up @@ -431,6 +435,9 @@ parameters:
- name: TENANT_MANAGER_QUARKUS_LOG_LEVEL
value: INFO

- name: ENABLE_ACCESS_LOG
value: false

- name: SERVICE_ACCOUNT_NAME
displayName: Service Account to use for the deployment
required: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import io.apicurio.multitenant.api.datamodel.TenantStatusValue;
import io.apicurio.multitenant.api.datamodel.UpdateRegistryTenantRequest;
import io.apicurio.multitenant.api.dto.DtoMappers;
import io.apicurio.multitenant.logging.audit.AuditLogService;
import io.apicurio.multitenant.logging.audit.Audited;
import io.apicurio.multitenant.storage.RegistryTenantStorage;
import io.apicurio.multitenant.storage.TenantNotFoundException;
import io.apicurio.multitenant.storage.dto.RegistryTenantDto;
Expand All @@ -58,6 +60,9 @@ public class TenantsResourceImpl implements TenantsResource {
@Inject
RegistryTenantStorage tenantsRepository;

@Inject
AuditLogService auditLog;

@Override
public RegistryTenantList getTenants(@QueryParam("status") String status,
@QueryParam("offset") @Min(0) Integer offset, @QueryParam("limit") @Min(1) @Max(500) Integer limit,
Expand Down Expand Up @@ -88,6 +93,7 @@ public RegistryTenantList getTenants(@QueryParam("status") String status,

@Override
@Transactional
@Audited
public Response createTenant(NewRegistryTenantRequest tenantRequest) {

required(tenantRequest.getTenantId(), "TenantId is mandatory");
Expand Down Expand Up @@ -136,6 +142,7 @@ public RegistryTenant getTenant(@PathParam("tenantId") String tenantId) {
*/
@Override
@Transactional
@Audited
public void updateTenant(String tenantId, UpdateRegistryTenantRequest tenantRequest) {
RegistryTenantDto tenant = tenantsRepository.findByTenantId(tenantId).orElseThrow(() -> TenantNotFoundException.create(tenantId));
if (tenantRequest.getName() != null) {
Expand Down Expand Up @@ -176,6 +183,7 @@ public void updateTenant(String tenantId, UpdateRegistryTenantRequest tenantRequ

@Override
@Transactional
@Audited
public void deleteTenant(@PathParam("tenantId") String tenantId) {
RegistryTenantDto tenant = tenantsRepository.findByTenantId(tenantId).orElseThrow(() -> TenantNotFoundException.create(tenantId));
tenant.setStatus(TenantStatusValue.TO_BE_DELETED.value());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@

/**
* This service provides information about the registry deployment paired with this tenant manager.
* The tenant manager can know what registry deployment is paired with via different ways.
* Via "registry.route.url" config property or via "registry.route.name", with the latter the tenant manager
* will use the OpenshiftClient to query the Openshift cluster to get the registry deployment url.
* The tenant manager can know what registry deployment is paired with via "registry.route.url" config property.
*
* @author Fabian Martinez
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2021 Red Hat
*
* 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 io.apicurio.multitenant.auth;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

import javax.annotation.Priority;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;
import javax.inject.Inject;

import org.slf4j.Logger;

import io.apicurio.multitenant.logging.audit.AuditHttpRequestContext;
import io.apicurio.multitenant.logging.audit.AuditHttpRequestInfo;
import io.apicurio.multitenant.logging.audit.AuditLogService;
import io.quarkus.oidc.runtime.BearerAuthenticationMechanism;
import io.quarkus.oidc.runtime.OidcAuthenticationMechanism;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

/**
* Custom HttpAuthenticationMechanism that simply wraps OidcAuthenticationMechanism.
* The only purpose of this HttpAuthenticationMechanism is to handle authentication errors in order to generate audit logs.
*
* @author Fabian Martinez
*/
@Alternative
@Priority(1)
@ApplicationScoped
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {

@Inject
Logger log;

@Inject
OidcAuthenticationMechanism oidcAuthenticationMechanism;

@Inject
AuditHttpRequestContext auditContext;
@Inject
AuditLogService auditLog;

private final BearerAuthenticationMechanism bearerAuth = new BearerAuthenticationMechanism();;

@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {


BiConsumer<RoutingContext, Throwable> failureHandler = context.get(QuarkusHttpUser.AUTH_FAILURE_HANDLER);
BiConsumer<RoutingContext, Throwable> auditWrapper = (ctx, ex) -> {
//this sends the http response
failureHandler.accept(ctx, ex);
//if it was an error response log it
if (ctx.response().getStatusCode() >= 400) {
Map<String, String> metadata = new HashMap<>();
metadata.put("method", ctx.request().method().name());
metadata.put("path", ctx.request().path());
metadata.put("response_code", String.valueOf(ctx.response().getStatusCode()));
if (ex != null) {
metadata.put("error_msg", ex.getMessage());
}

//request context for AuditHttpRequestContext does not exist at this point
auditLog.log("authenticate", AuditHttpRequestContext.FAILURE, metadata, new AuditHttpRequestInfo() {
@Override
public String getSourceIp() {
return ctx.request().remoteAddress().toString();
}
@Override
public String getForwardedFor() {
return ctx.request().getHeader(AuditHttpRequestContext.X_FORWARDED_FOR_HEADER);
}
});
}
};
log.info("Setting audit wrapper {}", context.statusCode());
context.put(QuarkusHttpUser.AUTH_FAILURE_HANDLER, auditWrapper);

return oidcAuthenticationMechanism.authenticate(context, identityProviderManager);
}

@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return bearerAuth.getChallenge(context);
}

@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return Collections.singleton(TokenAuthenticationRequest.class);
}

@Override
public HttpCredentialTransport getCredentialTransport() {
return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, "bearer");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2021 Red Hat
*
* 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 io.apicurio.multitenant.logging.audit;

import javax.enterprise.context.RequestScoped;

/**
* @author Fabian Martinez
*/
@RequestScoped
public class AuditHttpRequestContext implements AuditHttpRequestInfo {

public static final String X_FORWARDED_FOR_HEADER = "x-forwarded-for";
public static final String FAILURE = "failure";
public static final String SUCCESS = "success";

private String sourceIp;
private String forwardedFor;
private boolean auditEntryGenerated = false;

@Override
public String getSourceIp() {
return sourceIp;
}

public void setSourceIp(String sourceIp) {
this.sourceIp = sourceIp;
}

@Override
public String getForwardedFor() {
return forwardedFor;
}

public void setForwardedFor(String forwardedFor) {
this.forwardedFor = forwardedFor;
}

public boolean isAuditEntryGenerated() {
return auditEntryGenerated;
}

public void setAuditEntryGenerated(boolean auditEntryGenerated) {
this.auditEntryGenerated = auditEntryGenerated;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2021 Red Hat
*
* 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 io.apicurio.multitenant.logging.audit;

/**
* @author Fabian Martinez
*/
public interface AuditHttpRequestInfo {

/**
* @return the sourceIp
*/
String getSourceIp();

/**
* @return the forwardedFor
*/
String getForwardedFor();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2021 Red Hat
*
* 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 io.apicurio.multitenant.logging.audit;

import java.util.Map;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.control.ActivateRequestContext;
import javax.inject.Inject;
import org.slf4j.Logger;

/**
* @author Fabian Martinez
*/
@ApplicationScoped
public class AuditLogService {

@Inject
Logger log;

@Inject
AuditHttpRequestContext context;

@ActivateRequestContext
public void log(String action, String result, Map<String, String> metadata, AuditHttpRequestInfo requestInfo) {

String remoteAddress;
String forwardedRemoteAddress;
if (requestInfo != null) {
remoteAddress = requestInfo.getSourceIp();
forwardedRemoteAddress = requestInfo.getForwardedFor();
} else {
remoteAddress = context.getSourceIp();
forwardedRemoteAddress = context.getForwardedFor();
}

StringBuilder m = new StringBuilder();
m.append("tenant-manager.audit")
.append(" ")
.append("action=\"").append(action).append("\" ")
.append("result=\"").append(result).append("\" ")
.append("src_ip=\"").append(remoteAddress).append("\" ");
if (forwardedRemoteAddress != null) {
m.append("x_forwarded_for=\"").append(forwardedRemoteAddress).append("\" ");
}
for (Map.Entry<String, String> e : metadata.entrySet()) {
m.append(e.getKey()).append("=\"").append(e.getValue()).append("\" ");
}
log.info(m.toString());
//mark in the context that we already generated an audit entry for this request
context.setAuditEntryGenerated(true);
}

}
Loading

0 comments on commit c37a5a7

Please sign in to comment.