Skip to content

Commit

Permalink
Add an auth filter for PinDeploy pipeline (#1719)
Browse files Browse the repository at this point in the history
This PR implements an auth filter that will replace the principal if both of the conditions are met
1. Request is sent from a list of spiffe ids
2. Request contains a special header.
3. Because the principal name has a SPIFFE prefix, it's more likely to exceed the DB column limit. Introduced a method to trim the string to avoid updating the DB. 

## Test and validations
test coverage and some manual tests
1. Start the service with updated configuration
2. Craft a request with the spiffe ID and special header
4. Verify in the debugger that the principal is replaced.
  • Loading branch information
tylerwowen authored Oct 2, 2024
1 parent 948ba02 commit d6258b0
Show file tree
Hide file tree
Showing 20 changed files with 327 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

public class AgentBean implements Updatable {
public class AgentBean extends BaseBean implements Updatable {

@JsonProperty("hostId")
private String host_id;
Expand Down Expand Up @@ -161,7 +161,7 @@ public String getLast_operator() {
}

public void setLast_operator(String last_operator) {
this.last_operator = last_operator;
this.last_operator = getStringWithinSizeLimit(last_operator, 64);
}

public Boolean getFirst_deploy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.pinterest.deployservice.bean;

public class BaseBean {

/**
* Trims the input string to the specified size limit. If the input string's length
* exceeds the limit, the method returns the substring from the end of the string
* with the specified limit. Otherwise returns the original string.
*
* @param value the input string to be trimmed
* @param limit the maximum length of the returned string
* @return the trimmed string if the input string's length exceeds the limit,
* otherwise the original string
*/
protected String getStringWithinSizeLimit(String value, int limit) {
if (value != null && value.length() > limit) {
return value.substring(value.length() - limit, value.length());
}
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

public class ConfigHistoryBean implements Updatable {
public class ConfigHistoryBean extends BaseBean implements Updatable {
@JsonProperty("id")
private String config_id;

Expand Down Expand Up @@ -62,7 +62,7 @@ public Long getCreation_time() {
}

public void setOperator(String operator) {
this.operator = operator;
this.operator = getStringWithinSizeLimit(operator, 64);
}

public String getOperator() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

public class DataBean implements Updatable {
public class DataBean extends BaseBean implements Updatable {
private String data_id;
// TODO deprecate data_kind, we should use json all the time
private String data_kind;
Expand Down Expand Up @@ -46,7 +46,7 @@ public String getOperator() {
}

public void setOperator(String operator) {
this.operator = operator;
this.operator = getStringWithinSizeLimit(operator, 64);
}

public String getData() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

public class DeployBean implements Updatable {
public class DeployBean extends BaseBean implements Updatable {
@JsonProperty("id")
private String deploy_id;

Expand Down Expand Up @@ -107,7 +107,7 @@ public String getOperator() {
}

public void setOperator(String operator) {
this.operator = operator;
this.operator = getStringWithinSizeLimit(operator, 64);
}

public Long getLast_update() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import org.apache.commons.text.StringEscapeUtils;
import org.hibernate.validator.constraints.Range;

public class EnvironBean implements Updatable, Serializable {
public class EnvironBean extends BaseBean implements Updatable, Serializable {
@JsonProperty("id")
private String env_id;

Expand Down Expand Up @@ -309,7 +309,7 @@ public String getLast_operator() {
}

public void setLast_operator(String last_operator) {
this.last_operator = last_operator;
this.last_operator = getStringWithinSizeLimit(last_operator, 64);
}

public Long getLast_update() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import javax.validation.constraints.NotEmpty;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

public class HotfixBean implements Updatable {
public class HotfixBean extends BaseBean implements Updatable {
private String id;

@NotEmpty
Expand Down Expand Up @@ -145,7 +145,7 @@ public String getOperator() {
}

public void setOperator(String operator) {
this.operator = operator;
this.operator = getStringWithinSizeLimit(operator, 32);
}

public Long getStart_time() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.quartz.CronExpression;

public class PromoteBean implements Updatable, Serializable {
public class PromoteBean extends BaseBean implements Updatable, Serializable {
@JsonProperty("envId")
private String env_id;

Expand Down Expand Up @@ -71,7 +71,7 @@ public String getLast_operator() {
}

public void setLast_operator(String last_operator) {
this.last_operator = last_operator;
this.last_operator = getStringWithinSizeLimit(last_operator, 64);
}

public Long getLast_update() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import java.beans.Transient;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

public class TagBean implements Updatable {
public class TagBean extends BaseBean implements Updatable {

private static final ObjectMapper mapper = new ObjectMapper();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.pinterest.deployservice.bean;


import static org.junit.Assert.assertSame;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.Test;

public class BaseBeanTest {
@Test
void testGetStringWithinSizeLimitInputNull() {
BaseBean baseBean = new BaseBean();
String result = baseBean.getStringWithinSizeLimit(null, 10);
assertNull(result);
}

@Test
void testGetStringWithinSizeLimitInputWithinLimit() {
BaseBean baseBean = new BaseBean();
String input = "test";
String result = baseBean.getStringWithinSizeLimit(input, 10);
assertSame(input, result);
}
@Test
void testGetStringWithinSizeLimitInputExceedsLimit() {
BaseBean baseBean = new BaseBean();
String result = baseBean.getStringWithinSizeLimit("0123456789", 5);
assertEquals("56789", result);
}
}
10 changes: 9 additions & 1 deletion deploy-service/teletraanservice/bin/server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ db:
# (Optional)OAuth token cache
#tokenCacheSpec: maximumSize=1000,expireAfterWrite=10m

# pinDeploySpiffeIds:
# - spiffe://pin220.com/teletraan/spin-orca/dev
# - spiffe://pin220.com/teletraan/spin-orca/staging
# - spiffe://pin220.com/teletraan/spin-orca/prod
# - spiffe://pin220.com/teletraan/spin-keel/dev
# - spiffe://pin220.com/teletraan/spin-keel/staging
# - spiffe://pin220.com/teletraan/spin-keel/prod

#
# Token based Authorization: by default is disabled.
#
Expand Down Expand Up @@ -218,4 +226,4 @@ health:
checkInterval: 5s
downtimeInterval: 10s
failureAttempts: 2
successAttempts: 1
successAttempts: 1
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.ImmutableList;
import com.pinterest.teletraan.TeletraanServiceContext;
import com.pinterest.teletraan.universal.security.EnvoyAuthFilter;
import com.pinterest.teletraan.universal.security.EnvoyAuthenticator;
import com.pinterest.teletraan.universal.security.PinDeployPipelinePrincipalReplacer;
import com.pinterest.teletraan.universal.security.bean.EnvoyCredentials;
import com.pinterest.teletraan.universal.security.bean.TeletraanPrincipal;
import io.dropwizard.auth.AuthFilter;
Expand All @@ -30,16 +33,27 @@
import io.dropwizard.auth.JSONUnauthorizedHandler;
import io.dropwizard.auth.chained.ChainedAuthFilter;
import java.util.Arrays;
import java.util.List;
import javax.ws.rs.container.ContainerRequestFilter;
import org.apache.commons.lang3.StringUtils;

@JsonTypeName("composite")
public class CompositeAuthenticationFactory extends TokenAuthenticationFactory {
@JsonProperty private List<String> pinDeploySpiffeIds;

@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public ContainerRequestFilter create(TeletraanServiceContext context) throws Exception {
Authenticator<EnvoyCredentials, TeletraanPrincipal> authenticator =
new EnvoyAuthenticator();
Authenticator<EnvoyCredentials, TeletraanPrincipal> authenticator;
if (pinDeploySpiffeIds != null && !pinDeploySpiffeIds.isEmpty()) {
authenticator =
new EnvoyAuthenticator(
ImmutableList.of(
new PinDeployPipelinePrincipalReplacer(
ImmutableList.copyOf(pinDeploySpiffeIds))));
} else {
authenticator = new EnvoyAuthenticator();
}

if (StringUtils.isNotBlank(getTokenCacheSpec())) {
MetricRegistry registry = SharedMetricRegistries.getDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ public final class Constants {
public static final String USER_HEADER = "x-forwarded-user";
public static final String GROUPS_HEADER = "x-forwarded-groups";
public static final String CLIENT_CERT_HEADER = "x-forwarded-client-cert";
public static final String PINDEPLOY_PIPELINE_HEADER = "Pindeploy-Pipeline-Identifier";

public static final String AUTHZ_ATTR_REQ_CXT_KEY = "AuthZAttributes";
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.SecurityContext;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -58,17 +59,17 @@ public void filter(final ContainerRequestContext requestContext) throws IOExcept
*/
@Nullable
private EnvoyCredentials getCredentials(ContainerRequestContext requestContext) {
String user = requestContext.getHeaders().getFirst(Constants.USER_HEADER);
String spiffeId =
getSpiffeId(requestContext.getHeaders().getFirst(Constants.CLIENT_CERT_HEADER));
List<String> groups =
getGroups(requestContext.getHeaders().getFirst(Constants.GROUPS_HEADER));
MultivaluedMap<String, String> headers = requestContext.getHeaders();
String user = headers.getFirst(Constants.USER_HEADER);
String spiffeId = getSpiffeId(headers.getFirst(Constants.CLIENT_CERT_HEADER));
List<String> groups = getGroups(headers.getFirst(Constants.GROUPS_HEADER));
String pipelineId = headers.getFirst(Constants.PINDEPLOY_PIPELINE_HEADER);

if (StringUtils.isBlank(spiffeId) && StringUtils.isBlank(user)) {
return null;
}

return new EnvoyCredentials(user, spiffeId, groups);
return new EnvoyCredentials(user, spiffeId, groups, pipelineId);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.pinterest.teletraan.universal.security;

import com.google.common.collect.ImmutableList;
import com.pinterest.teletraan.universal.security.AuthMetricsFactory.PrincipalType;
import com.pinterest.teletraan.universal.security.bean.EnvoyCredentials;
import com.pinterest.teletraan.universal.security.bean.ServicePrincipal;
Expand All @@ -24,38 +25,48 @@
import io.dropwizard.auth.Authenticator;
import io.micrometer.core.instrument.Counter;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;

/** An authenticator for Envoy credentials. */
@NoArgsConstructor
@AllArgsConstructor
public class EnvoyAuthenticator implements Authenticator<EnvoyCredentials, TeletraanPrincipal> {
private final Counter envoyAuthUserSuccessCounter;
private final Counter envoyAuthServiceSuccessCounter;
private final Counter envoyAuthFailureCounter;

public EnvoyAuthenticator() {
envoyAuthUserSuccessCounter =
AuthMetricsFactory.createAuthNCounter(
EnvoyAuthenticator.class, true, PrincipalType.USER);
envoyAuthServiceSuccessCounter =
AuthMetricsFactory.createAuthNCounter(
EnvoyAuthenticator.class, true, PrincipalType.SERVICE);
envoyAuthFailureCounter =
AuthMetricsFactory.createAuthNCounter(
EnvoyAuthenticator.class, false, PrincipalType.NA);
}
private final Counter envoyAuthUserSuccessCounter =
AuthMetricsFactory.createAuthNCounter(
EnvoyAuthenticator.class, true, PrincipalType.USER);
private final Counter envoyAuthServiceSuccessCounter =
AuthMetricsFactory.createAuthNCounter(
EnvoyAuthenticator.class, true, PrincipalType.SERVICE);
private final Counter envoyAuthFailureCounter =
AuthMetricsFactory.createAuthNCounter(
EnvoyAuthenticator.class, false, PrincipalType.NA);
/**
* List of principal replacers to be applied to the authenticated principal.
*
* <p>The default value is an empty list. Note that the order of the replacers matters.
*/
private ImmutableList<PrincipalReplacer> principalReplacers = ImmutableList.of();

@Override
public Optional<TeletraanPrincipal> authenticate(EnvoyCredentials credentials)
throws AuthenticationException {
TeletraanPrincipal principal = null;
if (StringUtils.isNotBlank(credentials.getUser())) {
envoyAuthUserSuccessCounter.increment();
return Optional.of(new UserPrincipal(credentials.getUser(), credentials.getGroups()));
}
if (StringUtils.isNotBlank(credentials.getSpiffeId())) {
principal = new UserPrincipal(credentials.getUser(), credentials.getGroups());
} else if (StringUtils.isNotBlank(credentials.getSpiffeId())) {
envoyAuthServiceSuccessCounter.increment();
return Optional.of(new ServicePrincipal(credentials.getSpiffeId()));
principal = new ServicePrincipal(credentials.getSpiffeId());
} else {
envoyAuthFailureCounter.increment();
}
envoyAuthFailureCounter.increment();
return Optional.empty();

for (PrincipalReplacer replacer : principalReplacers) {
principal = replacer.replace(principal, credentials);
}

return Optional.ofNullable(principal);
}
}
Loading

0 comments on commit d6258b0

Please sign in to comment.