exceptionfactory commented on code in PR #10524: URL: https://github.com/apache/nifi/pull/10524#discussion_r2556757189
########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProvider.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.aws.rds; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.apache.nifi.processors.aws.region.RegionUtil.CUSTOM_REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.getRegion; +import static org.apache.nifi.processors.aws.region.RegionUtil.isDynamicRegion; + +/** + * Controller Service that generates short-lived Amazon RDS IAM authentication tokens for JDBC connections. + */ +@Tags({"aws", "rds", "iam", "jdbc", "password"}) +@CapabilityDescription("Generates Amazon RDS IAM authentication tokens each time a JDBC connection is requested. The generated token replaces the database user password so that " + + "NiFi does not need to store long-lived credentials inside DBCP services.") +public class AwsRdsIamDatabasePasswordProvider extends AbstractControllerService implements DatabasePasswordProvider { + + static final PropertyDescriptor AWS_CREDENTIALS_PROVIDER_SERVICE = new PropertyDescriptor.Builder() + .name("AWS Credentials Provider service") Review Comment: ```suggestion .name("AWS Credentials Provider Service") ``` ########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProvider.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.aws.rds; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.apache.nifi.processors.aws.region.RegionUtil.CUSTOM_REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.getRegion; +import static org.apache.nifi.processors.aws.region.RegionUtil.isDynamicRegion; + +/** + * Controller Service that generates short-lived Amazon RDS IAM authentication tokens for JDBC connections. + */ +@Tags({"aws", "rds", "iam", "jdbc", "password"}) +@CapabilityDescription("Generates Amazon RDS IAM authentication tokens each time a JDBC connection is requested. The generated token replaces the database user password so that " + + "NiFi does not need to store long-lived credentials inside DBCP services.") +public class AwsRdsIamDatabasePasswordProvider extends AbstractControllerService implements DatabasePasswordProvider { + + static final PropertyDescriptor AWS_CREDENTIALS_PROVIDER_SERVICE = new PropertyDescriptor.Builder() + .name("AWS Credentials Provider service") + .description("Controller Service that provides the AWS credentials used to sign IAM authentication requests.") + .identifiesControllerService(AwsCredentialsProviderService.class) + .required(true) + .build(); + + private static final int DEFAULT_POSTGRES_PORT = 5432; + + private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS; + + static { + final List<PropertyDescriptor> descriptors = new ArrayList<>(); + descriptors.add(AWS_CREDENTIALS_PROVIDER_SERVICE); + descriptors.add(REGION); + descriptors.add(CUSTOM_REGION); + PROPERTY_DESCRIPTORS = Collections.unmodifiableList(descriptors); + } Review Comment: This can be collapsed into a `List.of()` declaration. ########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProvider.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.aws.rds; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.apache.nifi.processors.aws.region.RegionUtil.CUSTOM_REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.getRegion; +import static org.apache.nifi.processors.aws.region.RegionUtil.isDynamicRegion; + +/** + * Controller Service that generates short-lived Amazon RDS IAM authentication tokens for JDBC connections. + */ +@Tags({"aws", "rds", "iam", "jdbc", "password"}) +@CapabilityDescription("Generates Amazon RDS IAM authentication tokens each time a JDBC connection is requested. The generated token replaces the database user password so that " + + "NiFi does not need to store long-lived credentials inside DBCP services.") +public class AwsRdsIamDatabasePasswordProvider extends AbstractControllerService implements DatabasePasswordProvider { + + static final PropertyDescriptor AWS_CREDENTIALS_PROVIDER_SERVICE = new PropertyDescriptor.Builder() + .name("AWS Credentials Provider service") + .description("Controller Service that provides the AWS credentials used to sign IAM authentication requests.") + .identifiesControllerService(AwsCredentialsProviderService.class) + .required(true) + .build(); + + private static final int DEFAULT_POSTGRES_PORT = 5432; + + private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS; + + static { + final List<PropertyDescriptor> descriptors = new ArrayList<>(); + descriptors.add(AWS_CREDENTIALS_PROVIDER_SERVICE); + descriptors.add(REGION); + descriptors.add(CUSTOM_REGION); + PROPERTY_DESCRIPTORS = Collections.unmodifiableList(descriptors); + } + + private volatile AwsCredentialsProvider awsCredentialsProvider; + private volatile RdsUtilities rdsUtilities; + private volatile Region awsRegion; + + @Override + protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { + return PROPERTY_DESCRIPTORS; + } + + @Override + protected Collection<ValidationResult> customValidate(final ValidationContext validationContext) { + final List<ValidationResult> results = new ArrayList<>(); + if (isDynamicRegion(validationContext)) { + results.add(new ValidationResult.Builder() + .subject(REGION.getDisplayName()) + .valid(false) + .explanation("FlowFile or attribute-driven regions are not supported by this Controller Service") + .build()); + } + return results; + } + + @OnEnabled + public void onEnabled(final ConfigurationContext context) { + final AwsCredentialsProviderService credentialsService = context.getProperty(AWS_CREDENTIALS_PROVIDER_SERVICE) + .asControllerService(AwsCredentialsProviderService.class); + awsCredentialsProvider = credentialsService.getAwsCredentialsProvider(); + awsRegion = getRegion(context); + rdsUtilities = createRdsUtilities(awsRegion, awsCredentialsProvider); + } + + @OnDisabled + public void onDisabled() { + awsCredentialsProvider = null; + rdsUtilities = null; + awsRegion = null; + } + + @Override + public char[] getPassword(final DatabasePasswordRequestContext requestContext) { + Objects.requireNonNull(requestContext, "Database Password Request Context required"); + + final ParsedEndpoint parsedEndpoint = parseEndpoint(requestContext.getJdbcUrl()); + final String hostname = resolveHostname(parsedEndpoint, requestContext.getJdbcUrl()); + final int port = resolvePort(parsedEndpoint); + final String username = resolveUsername(requestContext.getDatabaseUser()); + + final GenerateAuthenticationTokenRequest tokenRequest = GenerateAuthenticationTokenRequest.builder() + .hostname(hostname) + .port(port) + .username(username) + .build(); + + final String token; + try { + token = rdsUtilities.generateAuthenticationToken(tokenRequest); + } catch (final RuntimeException e) { + throw new ProcessException("Failed to generate RDS IAM authentication token", e); + } + + return token.toCharArray(); + } + + protected RdsUtilities createRdsUtilities(final Region region, final AwsCredentialsProvider credentialsProvider) { + return RdsUtilities.builder() + .region(region) + .credentialsProvider(credentialsProvider) + .build(); + } + + private String resolveHostname(final ParsedEndpoint parsedEndpoint, final String jdbcUrl) { + final String hostname = parsedEndpoint.hostname(); + if (StringUtils.isBlank(hostname)) { + throw new ProcessException(String.format("Database Endpoint not configured and JDBC URL [%s] does not contain a hostname", jdbcUrl)); Review Comment: ```suggestion throw new ProcessException("Database Endpoint not configured and JDBC URL [%s] does not contain a hostname".formatted(jdbcUrl)); ``` ########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProvider.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.aws.rds; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.apache.nifi.processors.aws.region.RegionUtil.CUSTOM_REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.getRegion; +import static org.apache.nifi.processors.aws.region.RegionUtil.isDynamicRegion; + +/** + * Controller Service that generates short-lived Amazon RDS IAM authentication tokens for JDBC connections. + */ Review Comment: Minor note, but with the Capability Description, the Javadoc comment seems less useful. ########## nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/src/main/java/org/apache/nifi/dbcp/ProviderAwareBasicDataSource.java: ########## @@ -0,0 +1,94 @@ +/* + * 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.dbcp; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.dbcp2.Constants; +import org.apache.commons.dbcp2.ConnectionFactory; +import org.apache.commons.dbcp2.DriverConnectionFactory; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; + +/** + * Extension of {@link BasicDataSource} that supports obtaining database passwords from a {@link DatabasePasswordProvider}. + */ +public class ProviderAwareBasicDataSource extends BasicDataSource { + private volatile DatabasePasswordProvider databasePasswordProvider; + private volatile DatabasePasswordRequestContext passwordRequestContext; + + public void setDatabasePasswordProvider(final DatabasePasswordProvider passwordProvider, + final DatabasePasswordRequestContext requestContext) { + if (passwordProvider != null && requestContext == null) { + throw new IllegalArgumentException("Database Password Request Context required when a provider is configured"); + } + this.databasePasswordProvider = passwordProvider; + this.passwordRequestContext = requestContext; + } + + @Override + protected ConnectionFactory createConnectionFactory() throws SQLException { + final ConnectionFactory delegate = super.createConnectionFactory(); + + if (databasePasswordProvider == null) { + return delegate; + } + + if (delegate instanceof DriverConnectionFactory driverConnectionFactory) { + return new PasswordRefreshingConnectionFactory(driverConnectionFactory, databasePasswordProvider, passwordRequestContext); + } + + throw new SQLException(String.format("Database Password Provider configured but unsupported ConnectionFactory [%s]", + delegate.getClass().getName())); + } + + private static class PasswordRefreshingConnectionFactory extends DriverConnectionFactory { + private final DatabasePasswordProvider passwordProvider; + private final DatabasePasswordRequestContext passwordRequestContext; + + PasswordRefreshingConnectionFactory(final DriverConnectionFactory delegate, + final DatabasePasswordProvider passwordProvider, + final DatabasePasswordRequestContext passwordRequestContext) { + super(delegate.getDriver(), delegate.getConnectionString(), delegate.getProperties()); + this.passwordProvider = passwordProvider; + this.passwordRequestContext = passwordRequestContext; + } + + @Override + public Connection createConnection() throws SQLException { + final char[] passwordCharacters; + try { + passwordCharacters = passwordProvider.getPassword(passwordRequestContext); + } catch (final Exception e) { + throw new SQLException("Failed to obtain database password from provider", e); + } + + if (passwordCharacters == null || passwordCharacters.length == 0) { + throw new SQLException("Database Password Provider returned an empty password"); + } + + final String password = new String(passwordCharacters); + Arrays.fill(passwordCharacters, '\0'); + + getProperties().put(Constants.KEY_PASSWORD, password); Review Comment: Is this required by the DBCP library? If so, recommend adding a comment above this line for clarity. ########## nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/src/main/java/org/apache/nifi/dbcp/ProviderAwareBasicDataSource.java: ########## @@ -0,0 +1,94 @@ +/* + * 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.dbcp; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.dbcp2.Constants; +import org.apache.commons.dbcp2.ConnectionFactory; +import org.apache.commons.dbcp2.DriverConnectionFactory; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; + +/** + * Extension of {@link BasicDataSource} that supports obtaining database passwords from a {@link DatabasePasswordProvider}. + */ +public class ProviderAwareBasicDataSource extends BasicDataSource { + private volatile DatabasePasswordProvider databasePasswordProvider; + private volatile DatabasePasswordRequestContext passwordRequestContext; + + public void setDatabasePasswordProvider(final DatabasePasswordProvider passwordProvider, + final DatabasePasswordRequestContext requestContext) { + if (passwordProvider != null && requestContext == null) { + throw new IllegalArgumentException("Database Password Request Context required when a provider is configured"); + } + this.databasePasswordProvider = passwordProvider; + this.passwordRequestContext = requestContext; + } + + @Override + protected ConnectionFactory createConnectionFactory() throws SQLException { + final ConnectionFactory delegate = super.createConnectionFactory(); + + if (databasePasswordProvider == null) { + return delegate; + } + + if (delegate instanceof DriverConnectionFactory driverConnectionFactory) { + return new PasswordRefreshingConnectionFactory(driverConnectionFactory, databasePasswordProvider, passwordRequestContext); + } + + throw new SQLException(String.format("Database Password Provider configured but unsupported ConnectionFactory [%s]", + delegate.getClass().getName())); Review Comment: ```suggestion throw new SQLException("Database Password Provider configured but unsupported ConnectionFactory [%s]".formatted( delegate.getClass().getName())); ``` ########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProvider.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.aws.rds; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.apache.nifi.processors.aws.region.RegionUtil.CUSTOM_REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.getRegion; +import static org.apache.nifi.processors.aws.region.RegionUtil.isDynamicRegion; + +/** + * Controller Service that generates short-lived Amazon RDS IAM authentication tokens for JDBC connections. + */ +@Tags({"aws", "rds", "iam", "jdbc", "password"}) +@CapabilityDescription("Generates Amazon RDS IAM authentication tokens each time a JDBC connection is requested. The generated token replaces the database user password so that " + + "NiFi does not need to store long-lived credentials inside DBCP services.") +public class AwsRdsIamDatabasePasswordProvider extends AbstractControllerService implements DatabasePasswordProvider { + + static final PropertyDescriptor AWS_CREDENTIALS_PROVIDER_SERVICE = new PropertyDescriptor.Builder() + .name("AWS Credentials Provider service") + .description("Controller Service that provides the AWS credentials used to sign IAM authentication requests.") + .identifiesControllerService(AwsCredentialsProviderService.class) + .required(true) + .build(); + + private static final int DEFAULT_POSTGRES_PORT = 5432; + + private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS; + + static { + final List<PropertyDescriptor> descriptors = new ArrayList<>(); + descriptors.add(AWS_CREDENTIALS_PROVIDER_SERVICE); + descriptors.add(REGION); + descriptors.add(CUSTOM_REGION); + PROPERTY_DESCRIPTORS = Collections.unmodifiableList(descriptors); + } + + private volatile AwsCredentialsProvider awsCredentialsProvider; + private volatile RdsUtilities rdsUtilities; + private volatile Region awsRegion; + + @Override + protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { + return PROPERTY_DESCRIPTORS; + } + + @Override + protected Collection<ValidationResult> customValidate(final ValidationContext validationContext) { + final List<ValidationResult> results = new ArrayList<>(); + if (isDynamicRegion(validationContext)) { + results.add(new ValidationResult.Builder() + .subject(REGION.getDisplayName()) + .valid(false) + .explanation("FlowFile or attribute-driven regions are not supported by this Controller Service") Review Comment: ```suggestion .explanation("FlowFile or attribute-driven regions are not supported") ``` ########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProvider.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.aws.rds; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.apache.nifi.processors.aws.region.RegionUtil.CUSTOM_REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.getRegion; +import static org.apache.nifi.processors.aws.region.RegionUtil.isDynamicRegion; + +/** + * Controller Service that generates short-lived Amazon RDS IAM authentication tokens for JDBC connections. + */ +@Tags({"aws", "rds", "iam", "jdbc", "password"}) +@CapabilityDescription("Generates Amazon RDS IAM authentication tokens each time a JDBC connection is requested. The generated token replaces the database user password so that " + + "NiFi does not need to store long-lived credentials inside DBCP services.") Review Comment: A multiline string could be used instead of concatenation. ########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProvider.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.aws.rds; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.apache.nifi.processors.aws.region.RegionUtil.CUSTOM_REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.getRegion; +import static org.apache.nifi.processors.aws.region.RegionUtil.isDynamicRegion; + +/** + * Controller Service that generates short-lived Amazon RDS IAM authentication tokens for JDBC connections. + */ +@Tags({"aws", "rds", "iam", "jdbc", "password"}) +@CapabilityDescription("Generates Amazon RDS IAM authentication tokens each time a JDBC connection is requested. The generated token replaces the database user password so that " + + "NiFi does not need to store long-lived credentials inside DBCP services.") +public class AwsRdsIamDatabasePasswordProvider extends AbstractControllerService implements DatabasePasswordProvider { + + static final PropertyDescriptor AWS_CREDENTIALS_PROVIDER_SERVICE = new PropertyDescriptor.Builder() + .name("AWS Credentials Provider service") + .description("Controller Service that provides the AWS credentials used to sign IAM authentication requests.") + .identifiesControllerService(AwsCredentialsProviderService.class) + .required(true) + .build(); + + private static final int DEFAULT_POSTGRES_PORT = 5432; + + private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS; + + static { + final List<PropertyDescriptor> descriptors = new ArrayList<>(); + descriptors.add(AWS_CREDENTIALS_PROVIDER_SERVICE); + descriptors.add(REGION); + descriptors.add(CUSTOM_REGION); + PROPERTY_DESCRIPTORS = Collections.unmodifiableList(descriptors); + } + + private volatile AwsCredentialsProvider awsCredentialsProvider; + private volatile RdsUtilities rdsUtilities; + private volatile Region awsRegion; + + @Override + protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { + return PROPERTY_DESCRIPTORS; + } + + @Override + protected Collection<ValidationResult> customValidate(final ValidationContext validationContext) { + final List<ValidationResult> results = new ArrayList<>(); + if (isDynamicRegion(validationContext)) { + results.add(new ValidationResult.Builder() + .subject(REGION.getDisplayName()) + .valid(false) + .explanation("FlowFile or attribute-driven regions are not supported by this Controller Service") + .build()); + } + return results; + } + + @OnEnabled + public void onEnabled(final ConfigurationContext context) { + final AwsCredentialsProviderService credentialsService = context.getProperty(AWS_CREDENTIALS_PROVIDER_SERVICE) + .asControllerService(AwsCredentialsProviderService.class); + awsCredentialsProvider = credentialsService.getAwsCredentialsProvider(); + awsRegion = getRegion(context); + rdsUtilities = createRdsUtilities(awsRegion, awsCredentialsProvider); + } + + @OnDisabled + public void onDisabled() { + awsCredentialsProvider = null; + rdsUtilities = null; + awsRegion = null; + } + + @Override + public char[] getPassword(final DatabasePasswordRequestContext requestContext) { + Objects.requireNonNull(requestContext, "Database Password Request Context required"); + + final ParsedEndpoint parsedEndpoint = parseEndpoint(requestContext.getJdbcUrl()); + final String hostname = resolveHostname(parsedEndpoint, requestContext.getJdbcUrl()); + final int port = resolvePort(parsedEndpoint); + final String username = resolveUsername(requestContext.getDatabaseUser()); + + final GenerateAuthenticationTokenRequest tokenRequest = GenerateAuthenticationTokenRequest.builder() + .hostname(hostname) + .port(port) + .username(username) + .build(); + + final String token; + try { + token = rdsUtilities.generateAuthenticationToken(tokenRequest); + } catch (final RuntimeException e) { + throw new ProcessException("Failed to generate RDS IAM authentication token", e); + } + + return token.toCharArray(); + } + + protected RdsUtilities createRdsUtilities(final Region region, final AwsCredentialsProvider credentialsProvider) { + return RdsUtilities.builder() + .region(region) + .credentialsProvider(credentialsProvider) + .build(); + } + + private String resolveHostname(final ParsedEndpoint parsedEndpoint, final String jdbcUrl) { + final String hostname = parsedEndpoint.hostname(); + if (StringUtils.isBlank(hostname)) { + throw new ProcessException(String.format("Database Endpoint not configured and JDBC URL [%s] does not contain a hostname", jdbcUrl)); + } + return hostname; + } + + private int resolvePort(final ParsedEndpoint parsedEndpoint) { + final Integer parsedPort = parsedEndpoint.port(); + return parsedPort != null ? parsedPort : DEFAULT_POSTGRES_PORT; Review Comment: As noted above, this seems too specific to PostgreSQL, so recommend throwing an `IllegalStateException` or similar if the port is missing. ########## nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/src/main/java/org/apache/nifi/dbcp/utils/DBCPProperties.java: ########## @@ -69,6 +70,12 @@ private DBCPProperties() { .expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT) .build(); + public static final PropertyDescriptor DB_PASSWORD_PROVIDER = new PropertyDescriptor.Builder() + .name("Database Password Provider") + .description("Controller Service that supplies database passwords on demand. When configured, the Password property is ignored.") Review Comment: What do you think about introducing a new strategy property? Such as `Password Source`? Options could include `Properties` or `Password Provider`, and then this property and the `Password` property would depend on the corresponding value. ########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/main/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProvider.java: ########## @@ -0,0 +1,188 @@ +/* + * 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.aws.rds; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnDisabled; +import org.apache.nifi.annotation.lifecycle.OnEnabled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static org.apache.nifi.processors.aws.region.RegionUtil.CUSTOM_REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.apache.nifi.processors.aws.region.RegionUtil.getRegion; +import static org.apache.nifi.processors.aws.region.RegionUtil.isDynamicRegion; + +/** + * Controller Service that generates short-lived Amazon RDS IAM authentication tokens for JDBC connections. + */ +@Tags({"aws", "rds", "iam", "jdbc", "password"}) +@CapabilityDescription("Generates Amazon RDS IAM authentication tokens each time a JDBC connection is requested. The generated token replaces the database user password so that " + + "NiFi does not need to store long-lived credentials inside DBCP services.") +public class AwsRdsIamDatabasePasswordProvider extends AbstractControllerService implements DatabasePasswordProvider { + + static final PropertyDescriptor AWS_CREDENTIALS_PROVIDER_SERVICE = new PropertyDescriptor.Builder() + .name("AWS Credentials Provider service") + .description("Controller Service that provides the AWS credentials used to sign IAM authentication requests.") + .identifiesControllerService(AwsCredentialsProviderService.class) + .required(true) + .build(); + + private static final int DEFAULT_POSTGRES_PORT = 5432; Review Comment: This seems too specific to PostgreSQL, since AWS RDS supports other types of databases. ########## nifi-extension-bundles/nifi-aws-bundle/nifi-aws-processors/src/test/java/org/apache/nifi/processors/aws/rds/AwsRdsIamDatabasePasswordProviderTest.java: ########## @@ -0,0 +1,105 @@ +/* + * 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.aws.rds; + +import org.apache.nifi.dbcp.api.DatabasePasswordProvider; +import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext; +import org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService; +import org.apache.nifi.processors.aws.s3.FetchS3Object; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.regions.Region; + +import static org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.ACCESS_KEY_ID; +import static org.apache.nifi.processors.aws.credentials.provider.service.AWSCredentialsProviderControllerService.SECRET_KEY; +import static org.apache.nifi.processors.aws.rds.AwsRdsIamDatabasePasswordProvider.AWS_CREDENTIALS_PROVIDER_SERVICE; +import static org.apache.nifi.processors.aws.region.RegionUtil.REGION; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AwsRdsIamDatabasePasswordProviderTest { + + private TestRunner runner; + private AWSCredentialsProviderControllerService credentialsService; + private AwsRdsIamDatabasePasswordProvider passwordProvider; + + @BeforeEach + void setUp() throws Exception { + runner = TestRunners.newTestRunner(FetchS3Object.class); + + credentialsService = new AWSCredentialsProviderControllerService(); + runner.addControllerService("awsCredentials", credentialsService); + runner.setProperty(credentialsService, ACCESS_KEY_ID, "accessKey"); + runner.setProperty(credentialsService, SECRET_KEY, "secretKey"); + runner.enableControllerService(credentialsService); + + passwordProvider = new AwsRdsIamDatabasePasswordProvider(); + runner.addControllerService("iamProvider", passwordProvider); + runner.setProperty(passwordProvider, AWS_CREDENTIALS_PROVIDER_SERVICE, "awsCredentials"); + runner.setProperty(passwordProvider, REGION, Region.US_WEST_2.id()); + runner.enableControllerService(passwordProvider); + } + + @Test + void testGeneratesTokenUsingRequestContext() { + final DatabasePasswordProvider service = getService(); + final DatabasePasswordRequestContext context = DatabasePasswordRequestContext.builder() + .jdbcUrl("jdbc:postgresql://example.us-west-2.rds.amazonaws.com:5432/dev") + .databaseUser("dbuser") + .driverClassName("org.postgresql.Driver") + .build(); + + final String token = new String(service.getPassword(context)); + assertTrue(token.startsWith("example.us-west-2.rds.amazonaws.com:5432/")); + assertTrue(token.contains("DBUser=dbuser")); + } + + @Test + void testGeneratesTokenWithDefaultPort() { + final DatabasePasswordProvider service = getService(); + final DatabasePasswordRequestContext context = DatabasePasswordRequestContext.builder() + .jdbcUrl("jdbc:postgresql://example.us-west-2.rds.amazonaws.com/db") + .databaseUser("dbuser") + .driverClassName("org.postgresql.Driver") + .build(); + + final String token = new String(service.getPassword(context)); + assertTrue(token.startsWith("example.us-west-2.rds.amazonaws.com:5432/")); + assertTrue(token.contains("DBUser=dbuser")); + } + + @Test + void testMissingHostnameThrowsProcessException() { + final DatabasePasswordProvider service = getService(); + final DatabasePasswordRequestContext context = DatabasePasswordRequestContext.builder() + .jdbcUrl("jdbc:postgresql:///dbname") + .databaseUser("dbuser") + .driverClassName("org.postgresql.Driver") Review Comment: Recommend declaring and reusing static variables for the JDBC Driver class name, and other shared values. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
