
com.google.cloud.spanner.pgadapter.wireprotocol.PasswordMessage Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of google-cloud-spanner-pgadapter Show documentation
Show all versions of google-cloud-spanner-pgadapter Show documentation
The PGAdapter server implements the PostgreSQL wire-protocol, but sends all received statements
to a Cloud Spanner database instead of a PostgreSQL database. The Cloud Spanner database must
have been created to use the PostgreSQL dialect. See https://cloud.google.com/spanner/docs/quickstart-console#postgresql
for more information on how to create PostgreSQL dialect databases on Cloud Spanner.
The newest version!
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.cloud.spanner.pgadapter.wireprotocol;
import static com.google.cloud.spanner.pgadapter.wireprotocol.StartupMessage.DATABASE_KEY;
import static com.google.cloud.spanner.pgadapter.wireprotocol.StartupMessage.createConnectionAndSendStartupMessage;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.util.PemReader;
import com.google.api.client.util.PemReader.Section;
import com.google.api.client.util.Strings;
import com.google.api.core.InternalApi;
import com.google.auth.Credentials;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.OAuth2Credentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.spanner.pgadapter.ConnectionHandler;
import com.google.cloud.spanner.pgadapter.error.PGException;
import com.google.cloud.spanner.pgadapter.error.SQLState;
import com.google.cloud.spanner.pgadapter.error.Severity;
import com.google.cloud.spanner.pgadapter.wireoutput.ErrorResponse;
import com.google.cloud.spanner.pgadapter.wireoutput.TerminateResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Objects;
/**
* PGAdapter will convert a password message into gRPC authentication in the following ways:
*
*
* - If the username is 'oauth2' the password will be interpreted as an OAuth2 token.
*
- If the username is an email address and the password contains private key section,
* PGAdapter will construct a service account from the email address and private key.
*
- Otherwise, PGAdapter will try to construct a Google credentials instance from the string in
* the password message. The username will be ignored.
*
*/
@InternalApi
public class PasswordMessage extends ControlMessage {
private static final String USER_KEY = "user";
protected static final char IDENTIFIER = 'p';
private final Map parameters;
private final String username;
private final String password;
public PasswordMessage(ConnectionHandler connection, Map parameters)
throws Exception {
super(connection);
this.parameters = parameters;
this.username = parameters.get(USER_KEY);
this.password = this.readAll();
}
protected void sendPayload() throws Exception {
if (!useAuthentication()) {
new ErrorResponse(
this.connection,
PGException.newBuilder("Received PasswordMessage while authentication is disabled.")
.setSQLState(SQLState.ProtocolViolation)
.setSeverity(Severity.ERROR)
.build())
.send(false);
new TerminateResponse(this.outputStream).send();
return;
}
Credentials credentials = checkCredentials(this.username, this.password);
if (credentials == null) {
new ErrorResponse(
this.connection,
PGException.newBuilder("Invalid credentials received.")
.setHints(
"PGAdapter expects credentials to be one of the following:\n"
+ "1. Username contains the fixed string 'oauth2' and the password field contains a valid OAuth2 token.\n"
+ "2. Username contains any string and the password field contains the JSON payload of a service account or user account credentials file. Note: Only user accounts and service accounts are supported.\n"
+ "3. Username contains the email address of a service account and the password contains the corresponding private key for the service account.")
.setSQLState(SQLState.InvalidPassword)
.setSeverity(Severity.ERROR)
.build())
.send(false);
new TerminateResponse(this.outputStream).send();
} else {
createConnectionAndSendStartupMessage(
this.connection, this.parameters.get(DATABASE_KEY), this.parameters, credentials);
}
}
private boolean useAuthentication() {
return this.connection.getServer().getOptions().shouldAuthenticate();
}
private Credentials checkCredentials(String username, String password) {
if (Strings.isNullOrEmpty(password)) {
return null;
}
// Verify that the password is either a JSON credentials file or a private key.
// A private key is only allowed in combination with a username that is the email address of a
// service account.
if (!Strings.isNullOrEmpty(username) && username.indexOf('@') > -1) {
// The username is potentially an email address. That means that the password could be a
// private key. Try to parse it as such.
try {
String privateKeyText = password.replace("\\n", "\n");
Section privateKeySection =
PemReader.readFirstSectionAndClose(new StringReader(privateKeyText), "PRIVATE KEY");
if (privateKeySection != null) {
// Successfully identified as a private key. Manually create a ServiceAccountCredentials
// instance and try to use this when connecting to Spanner.
return ServiceAccountCredentials.fromPkcs8(
/*clientId=*/ null,
username,
privateKeyText,
/*privateKeyId=*/ null,
/*scopes=*/ null);
}
} catch (IOException ioException) {
// Ignore and try to parse it as a credentials file.
}
}
if (!Strings.isNullOrEmpty(username) && username.equalsIgnoreCase("oauth2")) {
// Interpret the password as an OAuth2 token.
return OAuth2Credentials.create(new AccessToken(password, null));
}
// Try to parse the password field as a JSON string that contains a credentials object.
try {
// Only try to parse credentials that we know and trust.
if (isValidCredentialsType(password)) {
return GoogleCredentials.fromStream(
new ByteArrayInputStream(password.getBytes(StandardCharsets.UTF_8)));
}
} catch (IOException ioException) {
// Ignore and fallthrough.
}
return null;
}
static boolean isValidCredentialsType(String credentials) throws IOException {
JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
JsonObjectParser parser = new JsonObjectParser(jsonFactory);
GenericJson fileContents =
parser.parseAndClose(new StringReader(credentials), GenericJson.class);
String fileType = (String) fileContents.get("type");
if (fileType == null) {
throw new IOException("Error reading credentials from password, 'type' field not specified.");
}
return Objects.equals("authorized_user", fileType)
|| Objects.equals("service_account", fileType);
}
@Override
protected String getMessageName() {
return "Password Exchange";
}
@Override
protected String getPayloadString() {
return new MessageFormat("Length: {0}, " + "Username: {1}, " + "Password: {2}")
.format(new Object[] {this.length, this.username, this.password});
}
@Override
public String getIdentifier() {
return String.valueOf(IDENTIFIER);
}
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy