diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerClientCreationOptionsTest.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerClientCreationOptionsTest.cs index 4e8b4b78d024..900ba8143039 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerClientCreationOptionsTest.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerClientCreationOptionsTest.cs @@ -131,6 +131,22 @@ public async Task CredentialFileRelative() Assert.NotNull(await options.CreateSpannerClientAsync(new Spanner.V1.SpannerSettings())); } + [Fact] + public async Task CredentialFile_WithWrongCredentialType_Fails() + { + var builder = new SpannerConnectionStringBuilder("CredentialFile=SpannerEF-8dfc036f6000.json;CredentialType=authorized_user"); + var options = new SpannerClientCreationOptions(builder); + await Assert.ThrowsAsync(() => options.CreateSpannerClientAsync(new Spanner.V1.SpannerSettings())); + } + + [Fact] + public async Task CredentialFile_WithCorrectCredentialType_Succeeds() + { + var builder = new SpannerConnectionStringBuilder("CredentialFile=SpannerEF-8dfc036f6000.json;CredentialType=service_account"); + var options = new SpannerClientCreationOptions(builder); + Assert.NotNull(await options.CreateSpannerClientAsync(new Spanner.V1.SpannerSettings())); + } + [Fact] public async Task CredentialFileP12Error() { diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerConnectionStringBuilderTests.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerConnectionStringBuilderTests.cs index 4a227a207a12..6271f60fb255 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerConnectionStringBuilderTests.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerConnectionStringBuilderTests.cs @@ -373,5 +373,33 @@ public void IsolationLevelIsConvertedToEnum() Assert.Equal(System.Data.IsolationLevel.RepeatableRead, builder.IsolationLevel); } + + [Fact] + public void CredentialType_Default() + { + var builder = new SpannerConnectionStringBuilder(); + Assert.Equal(JsonCredentialParameters.ServiceAccountCredentialType, builder.CredentialType); + } + + [Fact] + public void CredentialType_Explicit() + { + var builder = new SpannerConnectionStringBuilder("CredentialType=authorized_user"); + Assert.Equal("authorized_user", builder.CredentialType); + builder.CredentialType = "service_account"; + Assert.Equal("service_account", builder.CredentialType); + // DbConnectionStringBuilder lower-cases keywords, annoyingly. + Assert.Equal("credentialtype=service_account", builder.ToString()); + } + + [Fact] + public void CredentialType_NullSet_RestoresDefault() + { + var builder = new SpannerConnectionStringBuilder(); + builder.CredentialType = "authorized_user"; + Assert.Equal("authorized_user", builder.CredentialType); + builder.CredentialType = null; + Assert.Equal(JsonCredentialParameters.ServiceAccountCredentialType, builder.CredentialType); + } } } diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerClientCreationOptions.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerClientCreationOptions.cs index d50af9e37180..a797db2d7fd3 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerClientCreationOptions.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerClientCreationOptions.cs @@ -13,6 +13,7 @@ // limitations under the License. using Google.Api.Gax.Grpc.Gcp; +using Google.Apis.Auth.OAuth2; using Google.Cloud.Spanner.Admin.Database.V1; using Google.Cloud.Spanner.V1; using System; @@ -32,6 +33,10 @@ internal sealed class SpannerClientCreationOptions : IEquatable _effectiveGoogleCredential; + internal SpannerClientCreationOptions(SpannerConnectionStringBuilder builder) { var clientBuilder = new SpannerClientBuilder @@ -39,9 +44,6 @@ internal SpannerClientCreationOptions(SpannerConnectionStringBuilder builder) EmulatorDetection = builder.EmulatorDetection, EnvironmentVariableProvider = builder.EnvironmentVariableProvider, Endpoint = builder.ContainsKey(nameof(builder.Host)) || builder.ContainsKey(nameof(builder.Port)) ? builder.EndPoint : null, -#pragma warning disable CS0618 // Temporarily disable warnings for obsolete methods. See b/453009677 for more details. - CredentialsPath = builder.CredentialFile == "" ? null: builder.CredentialFile, -#pragma warning restore CS0618 ChannelCredentials = builder.CredentialOverride, GoogleCredential = builder.GoogleCredential, AffinityChannelPoolConfiguration = new ChannelPoolConfig @@ -59,6 +61,11 @@ internal SpannerClientCreationOptions(SpannerConnectionStringBuilder builder) UsesEmulator = emulatorBuilder is not null; ClientBuilder = emulatorBuilder ?? clientBuilder; + _credentialFile = builder.CredentialFile; + _credentialType = builder.CredentialType; + // Use Lazy to avoid throwing in the constructor if the credential file is missing. + _effectiveGoogleCredential = new Lazy(() => + ClientBuilder.GoogleCredential ?? (string.IsNullOrEmpty(_credentialFile) ? null : CredentialFactory.FromFile(_credentialFile, _credentialType))); } internal Task CreateSpannerClientAsync(SpannerSettings settings) => @@ -68,11 +75,8 @@ internal Task CreateSpannerClientAsync(SpannerSettings settings) // Note we don't copy emulator detection properties because we already took care // of emulator detection on the constructor. Endpoint = ClientBuilder.Endpoint, -#pragma warning disable CS0618 // Temporarily disable warnings for obsolete methods. See b/453009677 for more details. - CredentialsPath = ClientBuilder.CredentialsPath, -#pragma warning disable CS0618 ChannelCredentials = ClientBuilder.ChannelCredentials, - GoogleCredential = ClientBuilder.GoogleCredential, + GoogleCredential = _effectiveGoogleCredential.Value, AffinityChannelPoolConfiguration = ClientBuilder.AffinityChannelPoolConfiguration, LeaderRoutingEnabled = ClientBuilder.LeaderRoutingEnabled, DirectedReadOptions = ClientBuilder.DirectedReadOptions, @@ -88,11 +92,8 @@ internal DatabaseAdminClientBuilder CreateDatabaseAdminClientBuilder() // Note we don't copy emulator detection properties because we already took care // of emulator detection on the constructor. Endpoint = ClientBuilder.Endpoint, -#pragma warning disable CS0618 // Temporarily disable warnings for obsolete methods. See b/453009677 for more details. - CredentialsPath = ClientBuilder.CredentialsPath, -#pragma warning restore CS0618 ChannelCredentials = ClientBuilder.ChannelCredentials, - GoogleCredential = ClientBuilder.GoogleCredential, + GoogleCredential = _effectiveGoogleCredential.Value, // If we ever have settings of our own, we need to merge those with these. Settings = CreateDatabaseAdminSettings(), UniverseDomain = ClientBuilder.UniverseDomain, @@ -113,11 +114,10 @@ other is not null && UsesEmulator == other.UsesEmulator && // TODO: Consider overriding ClientBuilderBase and SpannerClientBuilder Equals, etc. Equals(ClientBuilder.Endpoint, other.ClientBuilder.Endpoint) && -#pragma warning disable CS0618 // Temporarily disable warnings for obsolete methods. See b/453009677 for more details. - Equals(ClientBuilder.CredentialsPath, other.ClientBuilder.CredentialsPath) && -#pragma warning restore CS0618 Equals(ClientBuilder.ChannelCredentials, other.ClientBuilder.ChannelCredentials) && Equals(ClientBuilder.GoogleCredential, other.ClientBuilder.GoogleCredential) && + _credentialFile == other._credentialFile && + _credentialType == other._credentialType && Equals(ClientBuilder.AffinityChannelPoolConfiguration, other.ClientBuilder.AffinityChannelPoolConfiguration) && ClientBuilder.LeaderRoutingEnabled == other.ClientBuilder.LeaderRoutingEnabled && Equals(ClientBuilder.DirectedReadOptions, other.ClientBuilder.DirectedReadOptions) && @@ -129,11 +129,10 @@ public override int GetHashCode() { int hash = 31; hash = hash * 23 + (ClientBuilder.Endpoint?.GetHashCode() ?? 0); -#pragma warning disable CS0618 // Temporarily disable warnings for obsolete methods. See b/453009677 for more details. - hash = hash * 23 + (ClientBuilder.CredentialsPath?.GetHashCode() ?? 0); -#pragma warning restore CS0618 hash = hash * 23 + (ClientBuilder.ChannelCredentials?.GetHashCode() ?? 0); hash = hash * 23 + (ClientBuilder.GoogleCredential?.GetHashCode() ?? 0); + hash = hash * 23 + (_credentialFile?.GetHashCode() ?? 0); + hash = hash * 23 + (_credentialType?.GetHashCode() ?? 0); hash = hash * 23 + UsesEmulator.GetHashCode(); hash = hash * 23 + (ClientBuilder.AffinityChannelPoolConfiguration?.GetHashCode() ?? 0); hash = hash * 23 + (ClientBuilder.LeaderRoutingEnabled.GetHashCode()); @@ -150,12 +149,14 @@ public override int GetHashCode() public override string ToString() { var builder = new StringBuilder($"EndPoint: {ClientBuilder.Endpoint ?? "Default"}"); -#pragma warning disable CS0618 // Temporarily disable warnings for obsolete methods. See b/453009677 for more details. - if (!string.IsNullOrEmpty(ClientBuilder.CredentialsPath)) + if (!string.IsNullOrEmpty(_credentialFile)) + { + builder.Append($"; CredentialsFile: {_credentialFile}"); + } + if (!string.IsNullOrEmpty(_credentialType)) { - builder.Append($"; CredentialsFile: {ClientBuilder.CredentialsPath}"); + builder.Append($"; CredentialType: {_credentialType}"); } -#pragma warning restore CS0618 if (ClientBuilder.ChannelCredentials is not null) { builder.Append($"; CredentialsOverride: True"); diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerConnectionStringBuilder.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerConnectionStringBuilder.cs index 1f167953a44d..7d242169db5a 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerConnectionStringBuilder.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerConnectionStringBuilder.cs @@ -51,6 +51,7 @@ public sealed class SpannerConnectionStringBuilder : DbConnectionStringBuilder internal const int DefaultMaxConcurrentStreamsLowWatermark = 20; private const string CredentialFileKeyword = "CredentialFile"; + private const string CredentialTypeKeyword = "CredentialType"; private const string DataSourceKeyword = "Data Source"; private const string UseClrDefaultForNullKeyword = "UseClrDefaultForNull"; private const string EnableGetSchemaTableKeyword = "EnableGetSchemaTable"; @@ -94,6 +95,17 @@ public string CredentialFile set => this[CredentialFileKeyword] = value; } + /// + /// The expected type of the credential provided via . + /// Accepted values can be found in . + /// Defaults to . + /// + public string CredentialType + { + get => GetValueOrDefault(CredentialTypeKeyword, JsonCredentialParameters.ServiceAccountCredentialType); + set => this[CredentialTypeKeyword] = value; + } + /// /// Option to change between the default handling of null database values (return DBNull.Value) or /// the non-standard handling (return the default value for whatever type is requested). diff --git a/apis/Google.Cloud.Spanner.Data/docs/connection_string.md b/apis/Google.Cloud.Spanner.Data/docs/connection_string.md index a41110348698..53feddf81e24 100644 --- a/apis/Google.Cloud.Spanner.Data/docs/connection_string.md +++ b/apis/Google.Cloud.Spanner.Data/docs/connection_string.md @@ -25,6 +25,15 @@ Example: - CredentialFile=/path/to/credentials.json +## CredentialType + +The expected type of the credential provided via `CredentialFile`. +Defaults to `service_account`. + +Example: + +- CredentialType=authorized_user + ## EnableGetSchemaTable When set to true (and when targeting .NET 4.5 or .NET Standard 2.0;