wwauth/Google.Solutions.WWAuth/Data/CredentialConfiguration.cs (242 lines of code) (raw):

// // Copyright 2022 Google LLC // // 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. // using Google.Apis.Util; using Google.Solutions.WWAuth.Adapters; using Google.Solutions.WWAuth.Util; using Newtonsoft.Json; using System; using System.Reflection; using System.Text.RegularExpressions; namespace Google.Solutions.WWAuth.Data { /// <summary> /// Application-specific representation of a: /// /// - workload identity federation credential configuration /// - workforce identity federation credential configuration /// (for completeness' sake, we can't do anything with that yet) /// /// </summary> internal class CredentialConfiguration { internal static readonly string[] DefaultScopes = new[] { "https://www.googleapis.com/auth/cloud-platform" }; /// <summary> /// Workload or workforce identity pool configuration. /// </summary> public IdentityPoolConfiguration PoolConfiguration { get; } /// <summary> /// Service account to impersonate, optional. /// Only applies to workload identity federation. /// </summary> public string ServiceAccountEmail { get; set; } /// <summary> /// Command line options for for executable command. /// </summary> public UnattendedCommandLineOptions Options { get; } /// <summary> /// Timeout for executable command. /// </summary> public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); internal CredentialConfiguration( IdentityPoolConfiguration poolConfiguration, UnattendedCommandLineOptions options) { this.PoolConfiguration = poolConfiguration.ThrowIfNull(nameof(poolConfiguration)); this.Options = options.ThrowIfNull(nameof(options)); } public void Validate() { if (string.IsNullOrEmpty(this.Options.IssuerUrl)) { throw new InvalidCredentialConfigurationException("AD FS Issuer URL must be specified."); } if (string.IsNullOrEmpty(this.Options.RelyingPartyId)) { throw new InvalidCredentialConfigurationException("Relying party ID must be specified."); } if (!string.IsNullOrEmpty(this.ServiceAccountEmail) && !this.ServiceAccountEmail.EndsWith(".iam.gserviceaccount.com")) { throw new InvalidCredentialConfigurationException( $"{this.ServiceAccountEmail} is not a valid service account email address"); } this.PoolConfiguration.Validate(); } public void ResetExecutable() { this.Options.Executable = Assembly.GetExecutingAssembly().Location; } //--------------------------------------------------------------------- // Factory methods. //--------------------------------------------------------------------- public static CredentialConfiguration NewWorkloadIdentityConfiguration() { return new CredentialConfiguration( new WorkloadIdentityPoolConfiguration(), new UnattendedCommandLineOptions() { Executable = Assembly.GetExecutingAssembly().Location }); } public static CredentialConfiguration NewWorkforceIdentityConfiguration() { return new CredentialConfiguration( new WorkforceIdentityPoolConfiguration(), new UnattendedCommandLineOptions() { Executable = Assembly.GetExecutingAssembly().Location }); } //--------------------------------------------------------------------- // JSON serialization to/from the credential configuration file format. //--------------------------------------------------------------------- internal CredentialConfigurationInfo ToJsonStructure() { Validate(); var tokenType = this.Options.Protocol == UnattendedCommandLineOptions.AuthenticationProtocol.AdfsOidc ? Data.SubjectTokenType.Jwt : Data.SubjectTokenType.Saml2; var userProjectNumber = (this.PoolConfiguration as WorkforceIdentityPoolConfiguration)?.UserProjectNumber; var serviceAccountImpersonationUrl = this.PoolConfiguration is WorkloadIdentityPoolConfiguration && !string.IsNullOrEmpty(this.ServiceAccountEmail) ? "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + $"{this.ServiceAccountEmail}:generateAccessToken" : null; return new CredentialConfigurationInfo( "external_account", StsAdapter.DefaultTokenUrl, this.PoolConfiguration.Audience, userProjectNumber, serviceAccountImpersonationUrl, tokenType.GetDescription(), new CredentialSourceInfo(new ExecutableInfo( this.Options.ToString(), (ulong)this.Timeout.TotalMilliseconds))); } internal static CredentialConfiguration FromJsonStructure(CredentialConfigurationInfo info) { if (info?.Type != CredentialConfigurationInfo.ExternalAccount) { throw new UnknownCredentialConfigurationException( "Unsupported configuration type: " + info?.Type); } if (info.CredentialSource?.Executable?.Command == null) { throw new InvalidCredentialConfigurationException( "Missing credential source or command"); } IdentityPoolConfiguration poolConfig; if (string.IsNullOrEmpty(info.Audience)) { throw new InvalidCredentialConfigurationException("Audience missing"); } else if (WorkloadIdentityPoolConfiguration.TryParse( info.Audience, out var workloadPoolConfig)) { poolConfig = workloadPoolConfig; } else if (WorkforceIdentityPoolConfiguration.TryParse( info.Audience, out var workforcePoolConfig)) { if (info.WorkforcePoolUserProject == null || info.WorkforcePoolUserProject == 0) { throw new InvalidCredentialConfigurationException( "Missing user project number for workforce identity."); } workforcePoolConfig.UserProjectNumber = info.WorkforcePoolUserProject; poolConfig = workforcePoolConfig; } else { throw new InvalidCredentialConfigurationException( "Malformed audience: " + info.Audience); } var configuration = new CredentialConfiguration( poolConfig, UnattendedCommandLineOptions.Parse(info.CredentialSource.Executable.Command)); if (info.CredentialSource.Executable.TimeoutMillis != null) { configuration.Timeout = TimeSpan.FromMilliseconds( info.CredentialSource.Executable.TimeoutMillis.Value); } if (!string.IsNullOrEmpty(info.ServiceAccountImpersonationUrl)) { var serviceAccountImpersonationUrlMatch = new Regex( "^https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*):generateAccessToken$") .Match(info.ServiceAccountImpersonationUrl); if (!serviceAccountImpersonationUrlMatch.Success) { throw new ArgumentException("Malformed service account impersonation URL: " + info.ServiceAccountImpersonationUrl); } configuration.ServiceAccountEmail = serviceAccountImpersonationUrlMatch.Groups[1].Value; } return configuration; } public override string ToString() { return JsonConvert.SerializeObject( ToJsonStructure(), Formatting.Indented); } public static CredentialConfiguration FromJson(string json) { return FromJsonStructure( JsonConvert.DeserializeObject<CredentialConfigurationInfo>(json)); } internal class CredentialConfigurationInfo { internal const string ExternalAccount = "external_account"; [JsonProperty("type")] public string Type { get; } [JsonProperty("token_url")] public string TokenUrl { get; } [JsonProperty("audience")] public string Audience { get; } [JsonProperty("workforce_pool_user_project")] public ulong? WorkforcePoolUserProject { get; } [JsonProperty("service_account_impersonation_url")] public string ServiceAccountImpersonationUrl { get; } [JsonProperty("subject_token_type")] public string SubjectTokenType { get; } [JsonProperty("credential_source")] public CredentialSourceInfo CredentialSource { get; } [JsonConstructor] public CredentialConfigurationInfo( [JsonProperty("type")] string type, [JsonProperty("token_url")] string tokenUrl, [JsonProperty("audience")] string audience, [JsonProperty("workforce_pool_user_project")] ulong? WorkforcePoolUserProject, [JsonProperty("service_account_impersonation_url")] string saImpersonationUrl, [JsonProperty("subject_token_type")] string subjectTokenType, [JsonProperty("credential_source")] CredentialSourceInfo credentialSource) { this.Type = type; this.TokenUrl = tokenUrl; this.Audience = audience; this.WorkforcePoolUserProject = WorkforcePoolUserProject; this.ServiceAccountImpersonationUrl = saImpersonationUrl; this.SubjectTokenType = subjectTokenType; this.CredentialSource = credentialSource; } } internal class CredentialSourceInfo { [JsonProperty("executable")] public ExecutableInfo Executable { get; set; } [JsonConstructor] public CredentialSourceInfo( [JsonProperty("executable")] ExecutableInfo executable) { this.Executable = executable; } } internal class ExecutableInfo { [JsonProperty("command")] public string Command { get; } [JsonProperty("timeout_millis")] public ulong? TimeoutMillis { get; } [JsonConstructor] public ExecutableInfo( [JsonProperty("command")] string command, [JsonProperty("timeout_millis")] ulong? timeoutMillis) { this.Command = command; this.TimeoutMillis = timeoutMillis; } } } public class UnknownCredentialConfigurationException : Exception { public UnknownCredentialConfigurationException(string message) : base(message) { } } public class InvalidCredentialConfigurationException : Exception { public InvalidCredentialConfigurationException(string message) : base(message) { } } }