Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@
<version>2.12.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-dbcp-service-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-resource-transfer</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/*
* 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.gcp.cloudsql;

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ImpersonatedCredentials;
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.ConfigVerificationResult;
import org.apache.nifi.components.ConfigVerificationResult.Outcome;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.AbstractControllerService;
import org.apache.nifi.controller.ConfigurationContext;
import org.apache.nifi.controller.VerifiableControllerService;
import org.apache.nifi.dbcp.api.DatabasePasswordProvider;
import org.apache.nifi.dbcp.api.DatabasePasswordRequestContext;
import org.apache.nifi.gcp.credentials.service.GCPCredentialsService;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.reporting.InitializationException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Tags({"gcp", "cloud sql", "postgresql", "mysql", "iam", "jdbc", "password"})
@CapabilityDescription("""
Generates Google Cloud SQL IAM authentication tokens for Cloud SQL database connections.
PostgreSQL and MySQL are supported.
The generated access token replaces the database user password so that NiFi does not need to store long-lived credentials inside DBCP services.
""")
public class GcpCloudSqlIamDatabasePasswordProvider extends AbstractControllerService implements DatabasePasswordProvider, VerifiableControllerService {

static final String SQLSERVICE_LOGIN_SCOPE = "https://www.googleapis.com/auth/sqlservice.login";
static final String FAILED_PASSWORD_MESSAGE = "Failed to generate Cloud SQL IAM database password";
static final String VERIFY_SCOPE_STEP = "Resolve GCP credentials";
static final String VERIFY_TOKEN_STEP = "Acquire Cloud SQL IAM access token";
static final String VERIFY_CREDENTIALS_UNAVAILABLE = "Configured GCP Credentials Provider Service did not return Google credentials.";
static final String VERIFY_SCOPED_CREDENTIALS_UNAVAILABLE = "Failed to apply the Cloud SQL login scope to the configured Google credentials.";
static final String VERIFY_TOKEN_ACQUISITION_FAILED = "Failed to acquire a Cloud SQL IAM access token.";

static final PropertyDescriptor GCP_CREDENTIALS_PROVIDER_SERVICE = new PropertyDescriptor.Builder()
.name("GCP Credentials Provider Service")
.description("Controller Service that provides the Google credentials used to request Cloud SQL IAM authentication tokens.")
.identifiesControllerService(GCPCredentialsService.class)
.required(true)
.build();

private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
GCP_CREDENTIALS_PROVIDER_SERVICE
);

private volatile GoogleCredentials scopedCredentials;

@Override
protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
return PROPERTY_DESCRIPTORS;
}

@OnEnabled
public void onEnabled(final ConfigurationContext context) throws InitializationException {
scopedCredentials = requireScopedCredentials(context);
}

@OnDisabled
public void onDisabled() {
scopedCredentials = null;
}

@Override
public char[] getPassword(final DatabasePasswordRequestContext requestContext) {
Objects.requireNonNull(requestContext, "Database Password Request Context required");

final GoogleCredentials credentials = scopedCredentials;
if (credentials == null) {
throw new ProcessException(FAILED_PASSWORD_MESSAGE);
}

final AccessToken accessToken = refreshAccessToken(credentials);
if (!hasTokenValue(accessToken)) {
throw new ProcessException(FAILED_PASSWORD_MESSAGE);
}

return accessToken.getTokenValue().toCharArray();
}

@Override
public List<ConfigVerificationResult> verify(final ConfigurationContext context, final ComponentLog verificationLogger,
Comment thread
pvillard31 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the verificationLogger passed in but not used, it would be helpful to pass it down to some of the nested methods and log the exceptions thrown, instead of just returning null for verification.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

final Map<String, String> attributes) {
final List<ConfigVerificationResult> results = new ArrayList<>(2);
final GoogleCredentials scopedVerificationCredentials = resolveVerificationCredentials(context, verificationLogger);
if (scopedVerificationCredentials == null) {
results.add(buildVerificationResult(VERIFY_SCOPE_STEP, Outcome.FAILED, VERIFY_CREDENTIALS_UNAVAILABLE));
} else {
final ConfigVerificationResult scopedCredentialResult = describeScopedCredential(scopedVerificationCredentials);
results.add(scopedCredentialResult);
if (scopedCredentialResult.getOutcome() == Outcome.SUCCESSFUL) {
results.add(verifyAccessToken(scopedVerificationCredentials, verificationLogger));
}
}
return results;
}

private GoogleCredentials resolveVerificationCredentials(final ConfigurationContext context, final ComponentLog verificationLogger) {
try {
return createSqlLoginScopedCredentials(resolveGoogleCredentials(context));
} catch (final RuntimeException e) {
verificationLogger.error("Failed to resolve scoped Google credentials", e);
return null;
}
}

private ConfigVerificationResult verifyAccessToken(final GoogleCredentials credentials, final ComponentLog verificationLogger) {
try {
final AccessToken accessToken = credentials.refreshAccessToken();
return hasTokenValue(accessToken)
? buildTokenVerificationResult()
: buildVerificationResult(VERIFY_TOKEN_STEP, Outcome.FAILED, VERIFY_TOKEN_ACQUISITION_FAILED);
} catch (final IOException | RuntimeException e) {
verificationLogger.error("Failed to acquire Cloud SQL IAM access token", e);
return buildVerificationResult(VERIFY_TOKEN_STEP, Outcome.FAILED, VERIFY_TOKEN_ACQUISITION_FAILED);
}
}

private GoogleCredentials requireScopedCredentials(final ConfigurationContext context) throws InitializationException {
final GoogleCredentials googleCredentials;
try {
googleCredentials = resolveGoogleCredentials(context);
} catch (final RuntimeException e) {
throw new InitializationException(VERIFY_CREDENTIALS_UNAVAILABLE, e);
}

if (googleCredentials == null) {
throw new InitializationException(VERIFY_CREDENTIALS_UNAVAILABLE);
}

final GoogleCredentials credentials;
try {
credentials = createSqlLoginScopedCredentials(googleCredentials);
} catch (final RuntimeException e) {
throw new InitializationException(VERIFY_SCOPED_CREDENTIALS_UNAVAILABLE, e);
}

if (credentials == null) {
throw new InitializationException(VERIFY_SCOPED_CREDENTIALS_UNAVAILABLE);
}

return credentials;
}

private AccessToken refreshAccessToken(final GoogleCredentials credentials) {
try {
credentials.refreshIfExpired();
} catch (final IOException | RuntimeException e) {
throw new ProcessException(FAILED_PASSWORD_MESSAGE);
}

return credentials.getAccessToken();
}

private GoogleCredentials resolveGoogleCredentials(final ConfigurationContext context) {
final GCPCredentialsService credentialsService = context.getProperty(GCP_CREDENTIALS_PROVIDER_SERVICE)
.asControllerService(GCPCredentialsService.class);
if (credentialsService == null) {
return null;
}

return credentialsService.getGoogleCredentials();
}

private GoogleCredentials createSqlLoginScopedCredentials(final GoogleCredentials googleCredentials) {
if (googleCredentials == null) {
return null;
}

return googleCredentials.createScoped(List.of(SQLSERVICE_LOGIN_SCOPE));
}

private ConfigVerificationResult describeScopedCredential(final GoogleCredentials scopedVerificationCredentials) {
if (scopedVerificationCredentials instanceof ImpersonatedCredentials) {
return buildVerificationResult(
VERIFY_SCOPE_STEP,
Outcome.SUCCESSFUL,
"Resolved GCP credentials and Cloud SQL login scope. Target service account impersonation is active."
);
}

return buildVerificationResult(
VERIFY_SCOPE_STEP,
Outcome.SUCCESSFUL,
"Resolved GCP credentials and Cloud SQL login scope."
);
}

private ConfigVerificationResult buildTokenVerificationResult() {
return buildVerificationResult(
VERIFY_TOKEN_STEP,
Outcome.SUCCESSFUL,
"Acquired a Cloud SQL IAM access token. Use DBCP Verify to validate the database connection."
);
}

private boolean hasTokenValue(final AccessToken accessToken) {
return accessToken != null && StringUtils.isNotBlank(accessToken.getTokenValue());
}

private ConfigVerificationResult buildVerificationResult(final String stepName, final Outcome outcome, final String explanation) {
return new ConfigVerificationResult.Builder()
.verificationStepName(stepName)
.outcome(outcome)
.explanation(explanation)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ private CredentialPropertyDescriptors() { }
.dependsOn(AUTHENTICATION_STRATEGY, AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
.build();

public static final PropertyDescriptor TARGET_SERVICE_ACCOUNT = new PropertyDescriptor.Builder()
.name("Target Service Account")
.description("Target Google service account email to impersonate after Workload Identity Federation token exchange.")
.addValidator(StandardValidators.NON_BLANK_VALIDATOR)
.dependsOn(AUTHENTICATION_STRATEGY, AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION.getValue())
.build();

public static final PropertyDescriptor DELEGATION_STRATEGY = new PropertyDescriptor.Builder()
.name("Delegation Strategy")
.required(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdentityPoolCredentials;
import com.google.auth.oauth2.IdentityPoolSubjectTokenSupplier;
import com.google.auth.oauth2.ImpersonatedCredentials;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.ConfigurationContext;
Expand Down Expand Up @@ -60,6 +61,7 @@ public GoogleCredentials getGoogleCredentials(final ConfigurationContext context
final String scopeValue = context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SCOPE).getValue();
final String tokenEndpoint = context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_TOKEN_ENDPOINT).getValue();
final String subjectTokenType = context.getProperty(CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE).getValue();
final String targetServiceAccount = context.getProperty(CredentialPropertyDescriptors.TARGET_SERVICE_ACCOUNT).getValue();

final List<String> scopes = parseScopes(scopeValue);
final IdentityPoolSubjectTokenSupplier tokenSupplier = createSubjectTokenSupplier(subjectTokenProvider);
Expand All @@ -77,7 +79,20 @@ public GoogleCredentials getGoogleCredentials(final ConfigurationContext context
builder.setHttpTransportFactory(transportFactory);
}

return builder.build();
final IdentityPoolCredentials sourceCredentials = builder.build();
if (StringUtils.isBlank(targetServiceAccount)) {
return sourceCredentials;
}

final ImpersonatedCredentials.Builder impersonatedCredentialsBuilder = ImpersonatedCredentials.newBuilder()
.setSourceCredentials(sourceCredentials)
.setTargetPrincipal(targetServiceAccount)
.setScopes(Collections.emptyList());
if (transportFactory != null) {
impersonatedCredentialsBuilder.setHttpTransportFactory(transportFactory);
}

return impersonatedCredentialsBuilder.build();
}

private IdentityPoolSubjectTokenSupplier createSubjectTokenSupplier(final OAuth2AccessTokenProvider tokenProvider) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.GoogleCredentials;
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.OnEnabled;
Expand Down Expand Up @@ -55,6 +56,7 @@
import static org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.LEGACY_USE_COMPUTE_ENGINE_CREDENTIALS;
import static org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON;
import static org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.SERVICE_ACCOUNT_JSON_FILE;
import static org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.TARGET_SERVICE_ACCOUNT;
import static org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_AUDIENCE;
import static org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SCOPE;
import static org.apache.nifi.processors.gcp.credentials.factory.CredentialPropertyDescriptors.WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER;
Expand Down Expand Up @@ -84,6 +86,7 @@ public class GCPCredentialsControllerService extends AbstractControllerService i
WORKLOAD_IDENTITY_TOKEN_ENDPOINT,
WORKLOAD_IDENTITY_SUBJECT_TOKEN_PROVIDER,
WORKLOAD_IDENTITY_SUBJECT_TOKEN_TYPE,
TARGET_SERVICE_ACCOUNT,
ProxyConfiguration.createProxyConfigPropertyDescriptor(ProxyAwareTransportFactory.PROXY_SPECS),
DELEGATION_STRATEGY,
DELEGATION_USER
Expand All @@ -106,6 +109,20 @@ public GoogleCredentials getGoogleCredentials() throws ProcessException {
protected Collection<ValidationResult> customValidate(final ValidationContext validationContext) {
final List<ValidationResult> results = new ArrayList<>();
ProxyConfiguration.validateProxySpec(validationContext, results, ProxyAwareTransportFactory.PROXY_SPECS);

final String targetServiceAccount = validationContext.getProperty(TARGET_SERVICE_ACCOUNT).getValue();
final AuthenticationStrategy authenticationStrategy = validationContext.getProperty(AUTHENTICATION_STRATEGY)
.asAllowableValue(AuthenticationStrategy.class);
if (StringUtils.isNotBlank(targetServiceAccount)
&& authenticationStrategy != AuthenticationStrategy.WORKLOAD_IDENTITY_FEDERATION) {
results.add(new ValidationResult.Builder()
.subject(TARGET_SERVICE_ACCOUNT.getDisplayName())
.input(targetServiceAccount)
.valid(false)
.explanation("Target Service Account requires Workload Identity Federation")
.build());
}

return results;
}

Expand All @@ -117,7 +134,7 @@ public List<ConfigVerificationResult> verify(final ConfigurationContext context,
result = new ConfigVerificationResult.Builder()
.verificationStepName("Provide Google Credentials")
.outcome(Outcome.SUCCESSFUL)
.explanation(String.format("Successfully provided [%s] as Google Credentials", credentials.getClass().getSimpleName()))
.explanation("Successfully provided [%s] as Google Credentials".formatted(credentials.getClass().getSimpleName()))
.build();
} catch (final IOException e) {
result = new ConfigVerificationResult.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
# 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.
org.apache.nifi.processors.gcp.cloudsql.GcpCloudSqlIamDatabasePasswordProvider
org.apache.nifi.processors.gcp.credentials.service.GCPCredentialsControllerService
org.apache.nifi.processors.gcp.storage.GCSFileResourceService
Loading
Loading