mirror of
https://github.com/apache/nifi.git
synced 2025-02-09 11:35:05 +00:00
NIFI-11780 Added Certificate SAN Attributes to HandleHttpRequest
Signed-off-by: Pierre Villard <pierre.villard.fr@gmail.com> This closes #7521.
This commit is contained in:
parent
72618b1817
commit
3a078c0c31
@ -41,6 +41,9 @@ import org.apache.nifi.processor.ProcessSession;
|
||||
import org.apache.nifi.processor.Relationship;
|
||||
import org.apache.nifi.processor.exception.ProcessException;
|
||||
import org.apache.nifi.processor.util.StandardValidators;
|
||||
import org.apache.nifi.processors.standard.http.CertificateAttribute;
|
||||
import org.apache.nifi.processors.standard.http.CertificateAttributesProvider;
|
||||
import org.apache.nifi.processors.standard.http.HandleHttpRequestCertificateAttributesProvider;
|
||||
import org.apache.nifi.processors.standard.http.HttpProtocolStrategy;
|
||||
import org.apache.nifi.processors.standard.util.HTTPUtils;
|
||||
import org.apache.nifi.scheduling.ExecutionNode;
|
||||
@ -69,7 +72,6 @@ import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
@ -122,6 +124,10 @@ import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
+ "unless the Processor is configured to use an SSLContext Service"),
|
||||
@WritesAttribute(attribute = "http.issuer.dn", description = "The Distinguished Name of the entity that issued the Subject's certificate. "
|
||||
+ "This value will not be populated unless the Processor is configured to use an SSLContext Service"),
|
||||
@WritesAttribute(attribute = "http.certificate.sans.N.name", description = "X.509 Client Certificate Subject Alternative Name value from mutual TLS authentication. "
|
||||
+ "The attribute name has a zero-based index ordered according to the content of Client Certificate"),
|
||||
@WritesAttribute(attribute = "http.certificate.sans.N.nameType", description = "X.509 Client Certificate Subject Alternative Name type from mutual TLS authentication. "
|
||||
+ "The attribute name has a zero-based index ordered according to the content of Client Certificate. The attribute value is one of the General Names from RFC 3280 Section 4.1.2.7"),
|
||||
@WritesAttribute(attribute = "http.headers.XXX", description = "Each of the HTTP Headers that is received in the request will be added as an "
|
||||
+ "attribute, prefixed with \"http.headers.\" For example, if the request contains an HTTP Header named \"x-my-header\", then the value "
|
||||
+ "will be added to an attribute named \"http.headers.x-my-header\""),
|
||||
@ -343,6 +349,7 @@ public class HandleHttpRequest extends AbstractProcessor {
|
||||
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
||||
private final AtomicBoolean runOnPrimary = new AtomicBoolean(false);
|
||||
private final AtomicReference<Set<String>> parameterToAttributesReference = new AtomicReference<>(null);
|
||||
private final CertificateAttributesProvider certificateAttributesProvider = new HandleHttpRequestCertificateAttributesProvider();
|
||||
|
||||
@Override
|
||||
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
|
||||
@ -756,22 +763,15 @@ public class HandleHttpRequest extends AbstractProcessor {
|
||||
putAttribute(attributes, "http.principal.name", principal.getName());
|
||||
}
|
||||
|
||||
final X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
|
||||
if (certs != null && certs.length > 0) {
|
||||
final X509Certificate cert = certs[0];
|
||||
final String subjectDn = cert.getSubjectDN().getName();
|
||||
final String issuerDn = cert.getIssuerDN().getName();
|
||||
|
||||
putAttribute(attributes, HTTPUtils.HTTP_SSL_CERT, subjectDn);
|
||||
putAttribute(attributes, "http.issuer.dn", issuerDn);
|
||||
}
|
||||
final Map<String, String> certificateAttributes = certificateAttributesProvider.getCertificateAttributes(request);
|
||||
attributes.putAll(certificateAttributes);
|
||||
|
||||
return session.putAllAttributes(flowFile, attributes);
|
||||
}
|
||||
|
||||
private void forwardFlowFile(final ProcessSession session, final long start, final HttpServletRequest request, final FlowFile flowFile) {
|
||||
final long receiveMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
|
||||
final String subjectDn = flowFile.getAttribute(HTTPUtils.HTTP_SSL_CERT);
|
||||
final String subjectDn = flowFile.getAttribute(CertificateAttribute.HTTP_SUBJECT_DN.getName());
|
||||
session.getProvenanceReporter().receive(flowFile, HTTPUtils.getURI(flowFile.getAttributes()),
|
||||
"Received from " + request.getRemoteAddr() + (subjectDn == null ? "" : " with DN=" + subjectDn), receiveMillis);
|
||||
session.transfer(flowFile, REL_SUCCESS);
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.processors.standard.http;
|
||||
|
||||
/**
|
||||
* X.509 Client Certificate FlowFile Attribute Names
|
||||
*/
|
||||
public enum CertificateAttribute {
|
||||
/** Certificate Subject Distinguished Name */
|
||||
HTTP_SUBJECT_DN("http.subject.dn"),
|
||||
|
||||
/** Certificate Issuer Distinguished Name */
|
||||
HTTP_ISSUER_DN("http.issuer.dn"),
|
||||
|
||||
/** Certificate Subject Distinguished Name */
|
||||
HTTP_CERTIFICATE_PARSING_EXCEPTION("http.certificate.parsing.exception"),
|
||||
|
||||
/** Certificate Subject Alternative Names */
|
||||
HTTP_CERTIFICATE_SANS("http.certificate.sans");
|
||||
|
||||
private final String name;
|
||||
|
||||
CertificateAttribute(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.processors.standard.http;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Provider abstraction for reading X.509 Client Certificates from an HTTP Servlet Request and returning FlowFile attributes
|
||||
*/
|
||||
public interface CertificateAttributesProvider {
|
||||
/**
|
||||
* Get X.509 Client Certificate Attributes
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @return Map of Client Certificate Attributes or empty when no client certificate found
|
||||
*/
|
||||
Map<String, String> getCertificateAttributes(HttpServletRequest request);
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.processors.standard.http;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Certificate Attributes Provider for HandleHttpRequest reads the first X.509 Certificate presented
|
||||
*/
|
||||
public class HandleHttpRequestCertificateAttributesProvider implements CertificateAttributesProvider {
|
||||
protected static final String REQUEST_CERTIFICATES_ATTRIBUTE_NAME = "javax.servlet.request.X509Certificate";
|
||||
|
||||
private static final String SAN_NAME_TYPE_FORMAT = "%s.%d.nameType";
|
||||
|
||||
private static final String SAN_NAME_FORMAT = "%s.%d.name";
|
||||
|
||||
private static final Map<String, String> GENERAL_NAME_TYPES = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
// General Name types defined in RFC 3280 Section 4.2.1.7 */
|
||||
GENERAL_NAME_TYPES.put("0", "otherName");
|
||||
GENERAL_NAME_TYPES.put("1", "rfc822Name");
|
||||
GENERAL_NAME_TYPES.put("2", "dNSName");
|
||||
GENERAL_NAME_TYPES.put("3", "x400Address");
|
||||
GENERAL_NAME_TYPES.put("4", "directoryName");
|
||||
GENERAL_NAME_TYPES.put("5", "ediPartyName");
|
||||
GENERAL_NAME_TYPES.put("6", "uniformResourceIdentifier");
|
||||
GENERAL_NAME_TYPES.put("7", "iPAddress");
|
||||
GENERAL_NAME_TYPES.put("8", "registeredID");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getCertificateAttributes(final HttpServletRequest request) {
|
||||
Objects.requireNonNull(request, "HTTP Servlet Request required");
|
||||
|
||||
final Map<String, String> attributes;
|
||||
|
||||
final Object requestCertificates = request.getAttribute(REQUEST_CERTIFICATES_ATTRIBUTE_NAME);
|
||||
if (requestCertificates instanceof X509Certificate[]) {
|
||||
final X509Certificate[] certificates = (X509Certificate[]) requestCertificates;
|
||||
if (certificates.length == 0) {
|
||||
attributes = Collections.emptyMap();
|
||||
} else {
|
||||
final X509Certificate clientCertificate = certificates[0];
|
||||
attributes = getCertificateAttributes(clientCertificate);
|
||||
}
|
||||
} else {
|
||||
attributes = Collections.emptyMap();
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private Map<String, String> getCertificateAttributes(final X509Certificate certificate) {
|
||||
final Map<String, String> attributes = new LinkedHashMap<>();
|
||||
|
||||
final String subjectPrincipal = certificate.getSubjectX500Principal().getName();
|
||||
final String issuerPrincipal = certificate.getIssuerX500Principal().getName();
|
||||
|
||||
attributes.put(CertificateAttribute.HTTP_SUBJECT_DN.getName(), subjectPrincipal);
|
||||
attributes.put(CertificateAttribute.HTTP_ISSUER_DN.getName(), issuerPrincipal);
|
||||
|
||||
try {
|
||||
final Collection<List<?>> subjectAlternativeNames = certificate.getSubjectAlternativeNames();
|
||||
|
||||
if (subjectAlternativeNames != null) {
|
||||
final Map<String, String> subjectAlternativeNameAttributes = getSubjectAlternativeNameAttributes(subjectAlternativeNames);
|
||||
attributes.putAll(subjectAlternativeNameAttributes);
|
||||
}
|
||||
} catch (final CertificateParsingException e) {
|
||||
attributes.put(CertificateAttribute.HTTP_CERTIFICATE_PARSING_EXCEPTION.getName(), e.getMessage());
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private Map<String, String> getSubjectAlternativeNameAttributes(final Collection<List<?>> subjectAlternativeNames) {
|
||||
final Map<String, String> attributes = new LinkedHashMap<>();
|
||||
|
||||
int subjectAlternativeNameIndex = 0;
|
||||
for (final List<?> subjectAlternativeTypeName : subjectAlternativeNames) {
|
||||
final String nameTypeAttributeKey = String.format(SAN_NAME_TYPE_FORMAT, CertificateAttribute.HTTP_CERTIFICATE_SANS.getName(), subjectAlternativeNameIndex);
|
||||
final String nameType = subjectAlternativeTypeName.get(0).toString();
|
||||
final String generalNameType = GENERAL_NAME_TYPES.getOrDefault(nameType, nameType);
|
||||
attributes.put(nameTypeAttributeKey, generalNameType);
|
||||
|
||||
final String nameAttributeKey = String.format(SAN_NAME_FORMAT, CertificateAttribute.HTTP_CERTIFICATE_SANS.getName(), subjectAlternativeNameIndex);
|
||||
final Object name = subjectAlternativeTypeName.get(1);
|
||||
final String serializedName = getSerializedName(name);
|
||||
attributes.put(nameAttributeKey, serializedName);
|
||||
|
||||
subjectAlternativeNameIndex++;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private String getSerializedName(final Object name) {
|
||||
final String serializedName;
|
||||
if (name instanceof byte[]) {
|
||||
final byte[] encodedName = (byte[]) name;
|
||||
serializedName = Base64.getEncoder().encodeToString(encodedName);
|
||||
} else {
|
||||
serializedName = name.toString();
|
||||
}
|
||||
return serializedName;
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.processors.standard.http;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HandleHttpRequestCertificateAttributesProviderTest {
|
||||
private static final X500Principal SUBJECT_PRINCIPAL = new X500Principal("CN=subject, OU=users");
|
||||
|
||||
private static final X500Principal ISSUER_PRINCIPAL = new X500Principal("CN=issuer, OU=authorities");
|
||||
|
||||
private static final String RFC_822_NAME_GENERAL_NAME = "rfc822Name";
|
||||
|
||||
private static final String DNS_NAME_GENERAL_NAME = "dNSName";
|
||||
|
||||
private static final Integer RFC_822_NAME_TYPE = 1;
|
||||
|
||||
private static final String EMAIL_ADDRESS = "username@localhost.localdomain";
|
||||
|
||||
private static final Integer DNS_NAME_TYPE = 2;
|
||||
|
||||
private static final String DNS_NAME = "localhost.localdomain";
|
||||
|
||||
private static final String FIRST_SAN_NAME_ATTRIBUTE_KEY = "http.certificate.sans.0.name";
|
||||
|
||||
private static final String FIRST_SAN_NAME_TYPE_ATTRIBUTE_KEY = "http.certificate.sans.0.nameType";
|
||||
|
||||
private static final String SECOND_SAN_NAME_ATTRIBUTE_KEY = "http.certificate.sans.1.name";
|
||||
|
||||
private static final String SECOND_SAN_NAME_TYPE_ATTRIBUTE_KEY = "http.certificate.sans.1.nameType";
|
||||
|
||||
private static final String PARSING_EXCEPTION_MESSAGE = "SAN parsing failed";
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Mock
|
||||
private X509Certificate certificate;
|
||||
|
||||
private HandleHttpRequestCertificateAttributesProvider provider;
|
||||
|
||||
@BeforeEach
|
||||
void setProvider() {
|
||||
provider = new HandleHttpRequestCertificateAttributesProvider();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCertificatesNotFound() {
|
||||
final Map<String, String> attributes = provider.getCertificateAttributes(request);
|
||||
|
||||
assertTrue(attributes.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCertificatesFound() {
|
||||
final X509Certificate[] certificates = new X509Certificate[]{certificate};
|
||||
when(request.getAttribute(eq(HandleHttpRequestCertificateAttributesProvider.REQUEST_CERTIFICATES_ATTRIBUTE_NAME))).thenReturn(certificates);
|
||||
|
||||
when(certificate.getSubjectX500Principal()).thenReturn(SUBJECT_PRINCIPAL);
|
||||
when(certificate.getIssuerX500Principal()).thenReturn(ISSUER_PRINCIPAL);
|
||||
|
||||
final Map<String, String> attributes = provider.getCertificateAttributes(request);
|
||||
|
||||
assertSubjectIssuerFound(attributes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCertificatesFoundParsingException() throws CertificateParsingException {
|
||||
final X509Certificate[] certificates = new X509Certificate[]{certificate};
|
||||
when(request.getAttribute(eq(HandleHttpRequestCertificateAttributesProvider.REQUEST_CERTIFICATES_ATTRIBUTE_NAME))).thenReturn(certificates);
|
||||
|
||||
when(certificate.getSubjectX500Principal()).thenReturn(SUBJECT_PRINCIPAL);
|
||||
when(certificate.getIssuerX500Principal()).thenReturn(ISSUER_PRINCIPAL);
|
||||
|
||||
when(certificate.getSubjectAlternativeNames()).thenThrow(new CertificateParsingException(PARSING_EXCEPTION_MESSAGE));
|
||||
|
||||
final Map<String, String> attributes = provider.getCertificateAttributes(request);
|
||||
|
||||
assertSubjectIssuerFound(attributes);
|
||||
|
||||
assertEquals(attributes.get(CertificateAttribute.HTTP_CERTIFICATE_PARSING_EXCEPTION.getName()), PARSING_EXCEPTION_MESSAGE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCertificateSubjectAlternativeNamesFound() throws CertificateParsingException {
|
||||
final X509Certificate[] certificates = new X509Certificate[]{certificate};
|
||||
when(request.getAttribute(eq(HandleHttpRequestCertificateAttributesProvider.REQUEST_CERTIFICATES_ATTRIBUTE_NAME))).thenReturn(certificates);
|
||||
|
||||
when(certificate.getSubjectX500Principal()).thenReturn(SUBJECT_PRINCIPAL);
|
||||
when(certificate.getIssuerX500Principal()).thenReturn(ISSUER_PRINCIPAL);
|
||||
|
||||
final Collection<List<?>> subjectAlternativeNames = new ArrayList<>();
|
||||
|
||||
final List<?> emailAddressName = Arrays.asList(RFC_822_NAME_TYPE, EMAIL_ADDRESS);
|
||||
subjectAlternativeNames.add(emailAddressName);
|
||||
|
||||
final List<?> dnsName = Arrays.asList(DNS_NAME_TYPE, DNS_NAME);
|
||||
subjectAlternativeNames.add(dnsName);
|
||||
|
||||
when(certificate.getSubjectAlternativeNames()).thenReturn(subjectAlternativeNames);
|
||||
|
||||
final Map<String, String> attributes = provider.getCertificateAttributes(request);
|
||||
|
||||
assertSubjectIssuerFound(attributes);
|
||||
|
||||
assertEquals(attributes.get(FIRST_SAN_NAME_ATTRIBUTE_KEY), EMAIL_ADDRESS);
|
||||
assertEquals(attributes.get(FIRST_SAN_NAME_TYPE_ATTRIBUTE_KEY), RFC_822_NAME_GENERAL_NAME);
|
||||
|
||||
assertEquals(attributes.get(SECOND_SAN_NAME_ATTRIBUTE_KEY), DNS_NAME);
|
||||
assertEquals(attributes.get(SECOND_SAN_NAME_TYPE_ATTRIBUTE_KEY), DNS_NAME_GENERAL_NAME);
|
||||
}
|
||||
|
||||
private void assertSubjectIssuerFound(final Map<String, String> attributes) {
|
||||
assertEquals(SUBJECT_PRINCIPAL.getName(), attributes.get(CertificateAttribute.HTTP_SUBJECT_DN.getName()));
|
||||
assertEquals(ISSUER_PRINCIPAL.getName(), attributes.get(CertificateAttribute.HTTP_ISSUER_DN.getName()));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user