diff --git a/Minio.Examples/Cases/AssumeRoleProviderExample.cs b/Minio.Examples/Cases/AssumeRoleProviderExample.cs new file mode 100644 index 000000000..bc4ea0d56 --- /dev/null +++ b/Minio.Examples/Cases/AssumeRoleProviderExample.cs @@ -0,0 +1,70 @@ +// -*- coding: utf-8 -*- +// MinIO Python Library for Amazon S3 Compatible Cloud Storage, +// (C) 2022 MinIO, Inc. +// +// 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. +// + +using System; +using System.Threading.Tasks; +using Minio.Credentials; + + +namespace Minio.Examples.Cases +{ + public class AssumeRoleProviderExample + { + // Establish Authentication by assuming the role of an existing user + public async static Task Run() + { + // endpoint usually point to MinIO server. + var endpoint = "alias:port"; + + // Access key to fetch credentials from STS endpoint. + var accessKey = "access-key"; + + // Secret key to fetch credentials from STS endpoint. + var secretKey = "secret-key"; + + MinioClient minio = new MinioClient() + .WithEndpoint(endpoint) + .WithCredentials(accessKey, secretKey) + .WithSSL() + .Build(); + try + { + var provider = new AssumeRoleProvider(minio); + + var token = await provider.GetCredentialsAsync(); + // Console.WriteLine("\nToken = "); utils.Print(token); + MinioClient minioClient = new MinioClient() + .WithEndpoint(endpoint) + .WithCredentials(token.AccessKey, token.SecretKey) + .WithSessionToken(token.SessionToken) + .WithSSL() + .Build() + ; + StatObjectArgs statObjectArgs = new StatObjectArgs() + .WithBucket("bucket-name") + .WithObject("object-name"); + var result = await minio.StatObjectAsync(statObjectArgs); + // Console.WriteLine("Object Stat: \n"); utils.Print(result); + Console.WriteLine("AssumeRoleProvider test PASSed\n"); + } + catch (Exception e) + { + Console.WriteLine($"AssumeRoleProvider test exception: {e}\n"); + } + } + } +} diff --git a/Minio.Examples/Cases/CertificateIdentityProviderExample.cs b/Minio.Examples/Cases/CertificateIdentityProviderExample.cs index 2e90a13e7..7421c0ef3 100644 --- a/Minio.Examples/Cases/CertificateIdentityProviderExample.cs +++ b/Minio.Examples/Cases/CertificateIdentityProviderExample.cs @@ -28,42 +28,43 @@ namespace Minio.Examples.Cases { - public class CeritificateIdentityProviderExample + public class CertificateIdentityProviderExample { // Establish Authentication on both ways with client and server certificates public async static Task Run() { // STS endpoint - var stsEndpoint = "https://myminio:9000/"; + var stsEndpoint = "https://alias:port/"; - // Generatng pfx cert for this call. - // openssl pkcs12 -export -out client.pfx -inkey client.key -in client.crt -certfile server.crt - using(var cert = new X509Certificate2("C:\\dev\\client.pfx", "optional-password")) - { - var provider = new CertificateIdentityProvider() - .WithStsEndpoint(stsEndpoint) - .WithCertificate(cert) - .Build(); + // Generatng pfx cert for this call. + // openssl pkcs12 -export -out client.pfx -inkey client.key -in client.crt -certfile server.crt + using (var cert = new X509Certificate2("C:\\dev\\client.pfx", "optional-password")) + { + try + { + var provider = new CertificateIdentityProvider() + .WithStsEndpoint(stsEndpoint) + .WithCertificate(cert) + .Build(); - MinioClient minioClient = new MinioClient() - .WithEndpoint("myminio:9000") - .WithSSL() - .WithCredentialsProvider(provider) - .Build(); + MinioClient minioClient = new MinioClient() + .WithEndpoint("alias:port") + .WithSSL() + .WithCredentialsProvider(provider) + .Build(); - try - { - StatObjectArgs statObjectArgs = new StatObjectArgs() - .WithBucket("bucket-name") - .WithObject("object-name"); - ObjectStat result = await minioClient.StatObjectAsync(statObjectArgs); - Console.WriteLine("Object Stat: \n" + result.ToString()); - } - catch (Exception e) - { - Console.WriteLine($"CertificateIdentityExample test exception: {e}"); - } - } + StatObjectArgs statObjectArgs = new StatObjectArgs() + .WithBucket("bucket-name") + .WithObject("object-name"); + ObjectStat result = await minioClient.StatObjectAsync(statObjectArgs); + // Console.WriteLine("\nObject Stat: \n" + result.ToString()); + Console.WriteLine("\nCertificateIdentityProvider test PASSed\n"); + } + catch (Exception e) + { + Console.WriteLine($"\nCertificateIdentityProvider test exception: {e}\n"); + } + } } } } diff --git a/Minio.Functional.Tests/FunctionalTest.cs b/Minio.Functional.Tests/FunctionalTest.cs index 0d82fd4de..ff51cbb9d 100644 --- a/Minio.Functional.Tests/FunctionalTest.cs +++ b/Minio.Functional.Tests/FunctionalTest.cs @@ -669,6 +669,7 @@ internal async static Task TearDown(MinioClient minio, string bucketName) { return; }); + System.Threading.Thread.Sleep(4500); if (lockConfig != null && lockConfig.ObjectLockEnabled.Equals(ObjectLockConfiguration.LockEnabled)) { @@ -2883,11 +2884,11 @@ internal async static Task ListObjects_Test6(MinioClient minio) Assert.AreEqual(count, numObjects); }); System.Threading.Thread.Sleep(3500); - new MintLogger("ListObjects_Test6", listObjectsSignature, "Tests whether ListObjects lists all objects when number of objects == 100", TestStatus.PASS, (DateTime.Now - startTime), args: args).Log(); + new MintLogger("ListObjects_Test6", listObjectsSignature, "Tests whether ListObjects lists more than 1000 objects correctly(max-keys = 1000)", TestStatus.PASS, (DateTime.Now - startTime), args: args).Log(); } catch (Exception ex) { - new MintLogger("ListObjects_Test6", listObjectsSignature, "Tests whether ListObjects lists all objects when number of objects == 100", TestStatus.FAIL, (DateTime.Now - startTime), ex.Message, ex.ToString(), args: args).Log(); + new MintLogger("ListObjects_Test6", listObjectsSignature, "Tests whether ListObjects lists more than 1000 objects correctly(max-keys = 1000)", TestStatus.FAIL, (DateTime.Now - startTime), ex.Message, ex.ToString(), args: args).Log(); throw; } finally @@ -3774,6 +3775,7 @@ internal async static Task RemoveIncompleteUpload_Test(MinioClient minio) RemoveIncompleteUploadArgs rmArgs = new RemoveIncompleteUploadArgs() .WithBucket(bucketName) .WithObject(objectName); + await minio.RemoveIncompleteUploadAsync(rmArgs); ListIncompleteUploadsArgs listArgs = new ListIncompleteUploadsArgs() diff --git a/Minio.Tests/AuthenticatorTest.cs b/Minio.Tests/AuthenticatorTest.cs index 9a07f60fe..9aa9d684a 100644 --- a/Minio.Tests/AuthenticatorTest.cs +++ b/Minio.Tests/AuthenticatorTest.cs @@ -37,10 +37,10 @@ public void TestAnonymousInsecureRequestHeaders() var request = new HttpRequestMessageBuilder(HttpMethod.Put, "http://localhost:9000/bucketname/objectname"); request.AddJsonBody("[]"); - var authenticatorInsecure = new V4Authenticator(false, "a", "b"); + var authenticatorInsecure = new V4Authenticator(false, "a", "b"); Assert.IsFalse(authenticatorInsecure.isAnonymous); - authenticatorInsecure.Authenticate(request); + authenticatorInsecure.Authenticate(request, false); Assert.IsTrue(hasPayloadHeader(request, "x-amz-content-sha256")); } @@ -54,10 +54,10 @@ public void TestAnonymousSecureRequestHeaders() var request = new HttpRequestMessageBuilder(HttpMethod.Put, "http://localhost:9000/bucketname/objectname"); request.AddJsonBody("[]"); - var authenticatorSecure = new V4Authenticator(true, "a", "b"); + var authenticatorSecure = new V4Authenticator(true, "a", "b"); Assert.IsFalse(authenticatorSecure.isAnonymous); - authenticatorSecure.Authenticate(request); + authenticatorSecure.Authenticate(request, false); Assert.IsTrue(hasPayloadHeader(request, "x-amz-content-sha256")); } @@ -71,7 +71,7 @@ public void TestSecureRequestHeaders() var request = new HttpRequestMessageBuilder(HttpMethod.Put, "http://localhost:9000/bucketname/objectname"); request.AddJsonBody("[]"); - authenticator.Authenticate(request); + authenticator.Authenticate(request, false); Assert.IsTrue(hasPayloadHeader(request, "x-amz-content-sha256")); Tuple match = GetHeaderKV(request, "x-amz-content-sha256"); Assert.IsTrue(match != null && match.Item2.Equals("UNSIGNED-PAYLOAD")); @@ -86,7 +86,7 @@ public void TestInsecureRequestHeaders() Assert.IsFalse(authenticator.isAnonymous); var request = new HttpRequestMessageBuilder(HttpMethod.Put, "http://localhost:9000/bucketname/objectname"); request.AddJsonBody("[]"); - authenticator.Authenticate(request); + authenticator.Authenticate(request, false); Assert.IsTrue(hasPayloadHeader(request, "x-amz-content-sha256")); Assert.IsFalse(hasPayloadHeader(request, "Content-Md5")); } diff --git a/Minio/ApiEndpoints/BucketOperations.cs b/Minio/ApiEndpoints/BucketOperations.cs index 6aeb7c25d..277291395 100644 --- a/Minio/ApiEndpoints/BucketOperations.cs +++ b/Minio/ApiEndpoints/BucketOperations.cs @@ -253,63 +253,64 @@ public IObservable ListObjectsAsync(ListObjectsArgs args, CancellationToke { args.Validate(); return Observable.Create( - async (obs, ct) => - { - bool isRunning = true; - var delimiter = (args.Recursive) ? string.Empty : "/"; - string marker = string.Empty; - uint count = 0; - string versionIdMarker = string.Empty; - string nextContinuationToken = string.Empty; - using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ct)) - { - while (isRunning) - { - GetObjectListArgs goArgs = new GetObjectListArgs() - .WithBucket(args.BucketName) - .WithPrefix(args.Prefix) - .WithDelimiter(delimiter) - .WithVersions(args.Versions) - .WithContinuationToken(nextContinuationToken) - .WithMarker(marker) - .WithListObjectsV1(!args.UseV2) - .WithVersionIdMarker(versionIdMarker); - if (args.Versions) - { - Tuple> objectList = await this.GetObjectVersionsListAsync(goArgs, cts.Token).ConfigureAwait(false); - ListObjectVersionResponse listObjectsItemResponse = new ListObjectVersionResponse(args, objectList, obs); - if (objectList.Item2.Count == 0 && count == 0) - { - string name = args.BucketName; - if (!string.IsNullOrEmpty(args.Prefix)) - name += "/" + args.Prefix; - throw new EmptyBucketOperation("Bucket " + name + " is empty."); - } - obs = listObjectsItemResponse.ItemObservable; - marker = listObjectsItemResponse.NextKeyMarker; - versionIdMarker = listObjectsItemResponse.NextVerMarker; - isRunning = objectList.Item1.IsTruncated; - } - else - { - Tuple> objectList = await GetObjectListAsync(goArgs, cts.Token).ConfigureAwait(false); - if (objectList.Item2.Count == 0 && objectList.Item1.KeyCount.Equals("0") && count == 0) - { - string name = args.BucketName; - if (!string.IsNullOrEmpty(args.Prefix)) - name += "/" + args.Prefix; - throw new EmptyBucketOperation("Bucket " + name + " is empty."); - } - ListObjectsItemResponse listObjectsItemResponse = new ListObjectsItemResponse(args, objectList, obs); - marker = listObjectsItemResponse.NextMarker; - isRunning = objectList.Item1.IsTruncated; - nextContinuationToken = (objectList.Item1.IsTruncated) ? objectList.Item1.NextContinuationToken : string.Empty; - } - cts.Token.ThrowIfCancellationRequested(); - count++; - } - } - }); + async (obs, ct) => + { + bool isRunning = true; + var delimiter = (args.Recursive) ? string.Empty : "/"; + string marker = string.Empty; + uint count = 0; + string versionIdMarker = string.Empty; + string nextContinuationToken = string.Empty; + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, ct)) + { + while (isRunning) + { + GetObjectListArgs goArgs = new GetObjectListArgs() + .WithBucket(args.BucketName) + .WithPrefix(args.Prefix) + .WithDelimiter(delimiter) + .WithVersions(args.Versions) + .WithContinuationToken(nextContinuationToken) + .WithMarker(marker) + .WithListObjectsV1(!args.UseV2) + .WithVersionIdMarker(versionIdMarker); + if (args.Versions) + { + Tuple> objectList = await this.GetObjectVersionsListAsync(goArgs, cts.Token).ConfigureAwait(false); + ListObjectVersionResponse listObjectsItemResponse = new ListObjectVersionResponse(args, objectList, obs); + if (objectList.Item2.Count == 0 && count == 0) + { + string name = args.BucketName; + if (!string.IsNullOrEmpty(args.Prefix)) + name += "/" + args.Prefix; + throw new EmptyBucketOperation("Bucket " + name + " is empty."); + } + obs = listObjectsItemResponse.ItemObservable; + marker = listObjectsItemResponse.NextKeyMarker; + versionIdMarker = listObjectsItemResponse.NextVerMarker; + isRunning = objectList.Item1.IsTruncated; + } + else + { + Tuple> objectList = await GetObjectListAsync(goArgs, cts.Token).ConfigureAwait(false); + if (objectList.Item2.Count == 0 && objectList.Item1.KeyCount.Equals("0") && count == 0) + { + string name = args.BucketName; + if (!string.IsNullOrEmpty(args.Prefix)) + name += "/" + args.Prefix; + throw new EmptyBucketOperation("Bucket " + name + " is empty."); + } + ListObjectsItemResponse listObjectsItemResponse = new ListObjectsItemResponse(args, objectList, obs); + marker = listObjectsItemResponse.NextMarker; + isRunning = objectList.Item1.IsTruncated; + nextContinuationToken = (objectList.Item1.IsTruncated) ? objectList.Item1.NextContinuationToken : string.Empty; + } + cts.Token.ThrowIfCancellationRequested(); + count++; + } + } + } + ); } diff --git a/Minio/Credentials/AssumeRoleBaseProvider.cs b/Minio/Credentials/AssumeRoleBaseProvider.cs index a1b36c308..d21c82546 100644 --- a/Minio/Credentials/AssumeRoleBaseProvider.cs +++ b/Minio/Credentials/AssumeRoleBaseProvider.cs @@ -112,6 +112,7 @@ internal async virtual Task BuildRequest() throw new InvalidOperationException("MinioClient is not set in AssumeRoleBaseProvider"); } reqBuilder = await Client.CreateRequest(HttpMethod.Post); + reqBuilder.AddQueryParameter("Action", this.Action); reqBuilder.AddQueryParameter("Version", "2011-06-15"); if (!string.IsNullOrWhiteSpace(this.Policy)) { diff --git a/Minio/Credentials/AssumeRoleProvider.cs b/Minio/Credentials/AssumeRoleProvider.cs index fa2e7d568..3808385a7 100644 --- a/Minio/Credentials/AssumeRoleProvider.cs +++ b/Minio/Credentials/AssumeRoleProvider.cs @@ -16,60 +16,86 @@ */ using System; +using System.IO; using System.Net.Http; +using System.Text; using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; +using System.Collections.Generic; + +using Minio.DataModel; namespace Minio.Credentials { - public class AssumeRoleProvider : AssumeRoleBaseProvider - where T: AssumeRoleProvider + [Serializable] + [XmlRoot(ElementName = "AssumeRoleResponse", Namespace = "https://sts.amazonaws.com/doc/2011-06-15/")] + public class AssumeRoleResponse { - internal string STSEndPoint { get; set; } - internal string AccessKey { get; set; } - internal string SecretKey { get; set; } - internal string ContentSHA256 { get; set; } - internal HttpRequestMessage Request { get; set; } - internal string Url { get; set; } - private readonly uint DefaultDurationInSeconds = 1; - private readonly string AssumeRole = "AssumeRole"; - - public AssumeRoleProvider() + [XmlElement(ElementName = "AssumeRoleResult")] + public AssumeRoleResult arr { get; set; } + public string ToXML() { - } + XmlWriterSettings settings = new XmlWriterSettings + { + OmitXmlDeclaration = true + }; + using (MemoryStream ms = new MemoryStream()) + { + var xmlWriter = XmlWriter.Create(ms, settings); + XmlSerializerNamespaces names = new XmlSerializerNamespaces(); + names.Add(string.Empty, "https://sts.amazonaws.com/doc/2011-06-15/"); - public AssumeRoleProvider(MinioClient client) : base(client) - { + XmlSerializer cs = new XmlSerializer(typeof(CertificateResponse)); + cs.Serialize(xmlWriter, this, names); + + ms.Flush(); + ms.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(ms); + var xml = streamReader.ReadToEnd(); + return xml; + } } - public T WithAccessKey(string accessKey) + [Serializable] + [XmlRoot(ElementName = "AssumeRoleResult")] + public class AssumeRoleResult { - if (string.IsNullOrWhiteSpace(accessKey)) + public AssumeRoleResult() { } + + [XmlElement(ElementName = "Credentials")] + public AccessCredentials Credentials { get; set; } + public AccessCredentials GetAccessCredentials() { - throw new ArgumentNullException("The Access Key cannot be null or empty."); + return this.Credentials; } + } + } + + public class AssumeRoleProvider : AssumeRoleBaseProvider + { + internal string STSEndPoint { get; set; } + internal AccessCredentials credentials { get; set; } + internal string Url { get; set; } + private readonly uint DefaultDurationInSeconds = 3600; + private readonly string AssumeRole = "AssumeRole"; - this.AccessKey = accessKey; - return (T)this; + public AssumeRoleProvider() + { } - public T WithSecretKey(string secretKey) + public AssumeRoleProvider(MinioClient client) : base(client) { - if (string.IsNullOrWhiteSpace(secretKey)) - { - throw new ArgumentNullException("The Secret Key cannot be null or empty."); - } - this.SecretKey = secretKey; - return (T)this; } - public T WithSTSEndpoint(string endpoint) + public AssumeRoleProvider WithSTSEndpoint(string endpoint) { if (string.IsNullOrWhiteSpace(endpoint)) { throw new ArgumentNullException("The STS endpoint cannot be null or empty."); } this.STSEndPoint = endpoint; - var stsUri = utils.GetBaseUrl(endpoint); + Uri stsUri = utils.GetBaseUrl(endpoint); if ((stsUri.Scheme == "http" && stsUri.Port == 80) || (stsUri.Scheme == "https" && stsUri.Port == 443) || stsUri.Port <= 0) @@ -82,21 +108,76 @@ public T WithSTSEndpoint(string endpoint) } this.Url = stsUri.Authority; - return (T)this; + return this; + } + + public async override Task GetCredentialsAsync() + { + if (this.credentials != null && !this.credentials.AreExpired()) + { + return this.credentials; + } + + var requestBuilder = await this.BuildRequest(); + if (Client != null) + { + ResponseResult responseResult = null; + try + { + responseResult = await Client.ExecuteTaskAsync(this.NoErrorHandlers, requestBuilder, isSts: true); + + AssumeRoleResponse assumeRoleResp = null; + if (responseResult.Response.IsSuccessStatusCode) + { + var contentBytes = Encoding.UTF8.GetBytes(responseResult.Content); + + using (var stream = new MemoryStream(contentBytes)) + { + assumeRoleResp = (AssumeRoleResponse)new XmlSerializer(typeof(AssumeRoleResponse)).Deserialize(stream); + } + + } + if (this.credentials == null && + assumeRoleResp != null && + assumeRoleResp.arr != null) + { + this.credentials = assumeRoleResp.arr.Credentials; + } + return this.credentials; + } + catch (Exception) + { + throw; + } + finally + { + responseResult?.Dispose(); + } + } + throw new ArgumentNullException(nameof(Client) + " should have been assigned for the operation to continue."); } internal override async Task BuildRequest() { this.Action = this.AssumeRole; - if (this.DurationInSeconds != null && this.DurationInSeconds.Value == 0) + if (this.DurationInSeconds == null || this.DurationInSeconds.Value == 0) this.DurationInSeconds = DefaultDurationInSeconds; - var requestMessageBuilder = await base.BuildRequest(); - if (string.IsNullOrWhiteSpace(this.ExternalID)) + var requestMessageBuilder = await Client.CreateRequest(HttpMethod.Post); + + FormUrlEncodedContent formContent = new FormUrlEncodedContent(new[] { - requestMessageBuilder.AddQueryParameter("ExternalId", this.ExternalID); - } - throw new System.NotImplementedException(); + new KeyValuePair("Action", "AssumeRole"), + new KeyValuePair("DurationSeconds", this.DurationInSeconds.ToString()), + new KeyValuePair("Version", "2011-06-15"), + }); + var byteArrContent = await formContent.ReadAsByteArrayAsync(); + requestMessageBuilder.SetBody(byteArrContent); + requestMessageBuilder.AddOrUpdateHeaderParameter("Content-Type", "application/x-www-form-urlencoded"); + requestMessageBuilder.AddOrUpdateHeaderParameter("Accept-Encoding", "identity"); + await Task.Yield(); + + return requestMessageBuilder; } } } \ No newline at end of file diff --git a/Minio/HttpRequestMessageBuilder.cs b/Minio/HttpRequestMessageBuilder.cs index 399eabeae..a83f45bfd 100644 --- a/Minio/HttpRequestMessageBuilder.cs +++ b/Minio/HttpRequestMessageBuilder.cs @@ -48,9 +48,7 @@ public HttpRequestMessage Request foreach (var queryParameter in this.QueryParameters) { var query = HttpUtility.ParseQueryString(requestUriBuilder.Query); - query[queryParameter.Key] = queryParameter.Value; requestUriBuilder.Query = query.ToString(); - } var requestUri = requestUriBuilder.Uri; diff --git a/Minio/MinioClient.cs b/Minio/MinioClient.cs index 257a7a159..b69340939 100644 --- a/Minio/MinioClient.cs +++ b/Minio/MinioClient.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -524,20 +525,24 @@ public MinioClient WithCredentialsProvider(ClientProvider provider) /// List of handlers to override default handling /// The build of HttpRequestMessageBuilder /// Optional cancellation token to cancel the operation + /// boolean; if true role credentials, otherwise IAM user /// ResponseResult internal Task ExecuteTaskAsync( IEnumerable errorHandlers, HttpRequestMessageBuilder requestMessageBuilder, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default(CancellationToken), + bool isSts = false) { return ExecuteWithRetry( - () => ExecuteTaskCoreAsync(errorHandlers, requestMessageBuilder, cancellationToken)); + () => ExecuteTaskCoreAsync(errorHandlers, requestMessageBuilder, + cancellationToken, isSts)); } private async Task ExecuteTaskCoreAsync( IEnumerable errorHandlers, HttpRequestMessageBuilder requestMessageBuilder, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default(CancellationToken), + bool isSts = false) { var startTime = DateTime.Now; @@ -552,7 +557,7 @@ public MinioClient WithCredentialsProvider(ClientProvider provider) this.SessionToken); requestMessageBuilder.AddOrUpdateHeaderParameter("Authorization", - v4Authenticator.Authenticate(requestMessageBuilder)); + v4Authenticator.Authenticate(requestMessageBuilder, isSts)); HttpRequestMessage request = requestMessageBuilder.Request; @@ -728,7 +733,7 @@ private static void ParseErrorFromContent(ResponseResult response) throw new BucketNotFoundException(bucketName, "Not found."); } - var contentBytes = System.Text.Encoding.UTF8.GetBytes(response.Content); + var contentBytes = Encoding.UTF8.GetBytes(response.Content); var stream = new MemoryStream(contentBytes); ErrorResponse errResponse = (ErrorResponse)new XmlSerializer(typeof(ErrorResponse)).Deserialize(stream); diff --git a/Minio/V4Authenticator.cs b/Minio/V4Authenticator.cs index 88e24a445..c7b2a36ea 100644 --- a/Minio/V4Authenticator.cs +++ b/Minio/V4Authenticator.cs @@ -91,11 +91,12 @@ private string GetRegion(string endpoint) /// Implements Authenticate interface method for IAuthenticator. /// /// Instantiated IRestRequest object - public string Authenticate(HttpRequestMessageBuilder requestBuilder) + /// boolean; if true role credentials, otherwise IAM user + public string Authenticate(HttpRequestMessageBuilder requestBuilder, bool isSts = false) { DateTime signingDate = DateTime.UtcNow; - this.SetContentSha256(requestBuilder); + this.SetContentSha256(requestBuilder, isSts); requestBuilder.RequestUri = requestBuilder.Request.RequestUri; var requestUri = requestBuilder.RequestUri; @@ -110,35 +111,34 @@ public string Authenticate(HttpRequestMessageBuilder requestBuilder) } this.SetDateHeader(requestBuilder, signingDate); this.SetSessionTokenHeader(requestBuilder, this.sessionToken); + SortedDictionary headersToSign = this.GetHeadersToSign(requestBuilder); string signedHeaders = this.GetSignedHeaders(headersToSign); + string canonicalRequest = this.GetCanonicalRequest(requestBuilder, headersToSign); - byte[] canonicalRequestBytes = System.Text.Encoding.UTF8.GetBytes(canonicalRequest); - string canonicalRequestHash = this.BytesToHex(this.ComputeSha256(canonicalRequestBytes)); + byte[] canonicalRequestBytes = Encoding.UTF8.GetBytes(canonicalRequest); + var hash = this.ComputeSha256(canonicalRequestBytes); + string canonicalRequestHash = this.BytesToHex(hash); string region = this.GetRegion(requestUri.Host); - string stringToSign = this.GetStringToSign(region, signingDate, canonicalRequestHash); - - byte[] signingKey = this.GenerateSigningKey(region, signingDate); - - byte[] stringToSignBytes = System.Text.Encoding.UTF8.GetBytes(stringToSign); - + string stringToSign = this.GetStringToSign(region, signingDate, canonicalRequestHash, isSts); + byte[] signingKey = this.GenerateSigningKey(region, signingDate, isSts); + byte[] stringToSignBytes = Encoding.UTF8.GetBytes(stringToSign); byte[] signatureBytes = this.SignHmac(signingKey, stringToSignBytes); - string signature = this.BytesToHex(signatureBytes); - - string authorization = this.GetAuthorizationHeader(signedHeaders, signature, signingDate, region); + string authorization = this.GetAuthorizationHeader(signedHeaders, signature, signingDate, region, isSts); return authorization; } /// - /// Get credential string of form {ACCESSID}/date/region/s3/aws4_request. + /// Get credential string of form {ACCESSID}/date/region/serviceKind/aws4_request. /// /// Signature initiated date /// Region for the credential string + /// boolean; if true role credentials, otherwise IAM user /// Credential string for the authorization header - public string GetCredentialString(DateTime signingDate, string region) + public string GetCredentialString(DateTime signingDate, string region, bool isSts = false) { - var scope = this.GetScope(region, signingDate); + var scope = this.GetScope(region, signingDate, isSts); return $"{this.accessKey}/{scope}"; } @@ -149,10 +149,11 @@ public string GetCredentialString(DateTime signingDate, string region) /// Hexadecimally encoded computed signature /// Date for signature to be signed /// Requested region + /// boolean; if true role credentials, otherwise IAM user /// Fully formed authorization header - private string GetAuthorizationHeader(string signedHeaders, string signature, DateTime signingDate, string region) + private string GetAuthorizationHeader(string signedHeaders, string signature, DateTime signingDate, string region, bool isSts = false) { - var scope = this.GetScope(region, signingDate); + var scope = this.GetScope(region, signingDate, isSts); return $"AWS4-HMAC-SHA256 Credential={this.accessKey}/{scope}, SignedHeaders={signedHeaders}, Signature={signature}"; } @@ -166,25 +167,37 @@ private string GetSignedHeaders(SortedDictionary headersToSign) return string.Join(";", headersToSign.Keys); } + /// + /// Determines and returns the kind of service + /// + /// boolean; if true role credentials, otherwise IAM user + /// returns the kind of service as a string + private string getService(bool isSts) + { + return isSts ? "sts" : "s3"; + } + /// /// Generates signing key based on the region and date. /// /// Requested region /// Date for signature to be signed + /// boolean; if true role credentials, otherwise IAM user /// bytes of computed hmac - private byte[] GenerateSigningKey(string region, DateTime signingDate) + private byte[] GenerateSigningKey(string region, DateTime signingDate, bool isSts = false) { - byte[] formattedDateBytes = System.Text.Encoding.UTF8.GetBytes(signingDate.ToString("yyyyMMdd")); - byte[] formattedKeyBytes = System.Text.Encoding.UTF8.GetBytes($"AWS4{this.secretKey}"); - byte[] dateKey = this.SignHmac(formattedKeyBytes, formattedDateBytes); + byte[] dateRegionServiceKey; + byte[] requestBytes; - byte[] regionBytes = System.Text.Encoding.UTF8.GetBytes(region); + byte[] serviceBytes = Encoding.UTF8.GetBytes(getService(isSts)); + byte[] formattedDateBytes = Encoding.UTF8.GetBytes(signingDate.ToString("yyyyMMdd")); + byte[] formattedKeyBytes = Encoding.UTF8.GetBytes($"AWS4{this.secretKey}"); + byte[] dateKey = this.SignHmac(formattedKeyBytes, formattedDateBytes); + byte[] regionBytes = Encoding.UTF8.GetBytes(region); byte[] dateRegionKey = this.SignHmac(dateKey, regionBytes); - - byte[] serviceBytes = System.Text.Encoding.UTF8.GetBytes("s3"); - byte[] dateRegionServiceKey = this.SignHmac(dateRegionKey, serviceBytes); - - byte[] requestBytes = System.Text.Encoding.UTF8.GetBytes("aws4_request"); + dateRegionServiceKey = this.SignHmac(dateRegionKey, serviceBytes); + requestBytes = Encoding.UTF8.GetBytes("aws4_request"); + var signingKey = Encoding.UTF8.GetString(this.SignHmac(dateRegionServiceKey, requestBytes)); return this.SignHmac(dateRegionServiceKey, requestBytes); } @@ -207,10 +220,12 @@ private byte[] SignHmac(byte[] key, byte[] content) /// Requested region /// Date for signature to be signed /// Hexadecimal encoded sha256 checksum of canonicalRequest + /// boolean; if true role credentials, otherwise IAM user /// String to sign - private string GetStringToSign(string region, DateTime signingDate, string canonicalRequestHash) + private string GetStringToSign(string region, DateTime signingDate, + string canonicalRequestHash, bool isSts = false) { - var scope = this.GetScope(region, signingDate); + var scope = this.GetScope(region, signingDate, isSts); return $"AWS4-HMAC-SHA256\n{signingDate:yyyyMMddTHHmmssZ}\n{scope}\n{canonicalRequestHash}"; } @@ -219,10 +234,11 @@ private string GetStringToSign(string region, DateTime signingDate, string canon /// /// Requested region /// Date for signature to be signed + /// boolean; if true role credentials, otherwise IAM user /// Scope string - private string GetScope(string region, DateTime signingDate) + private string GetScope(string region, DateTime signingDate, bool isSts = false) { - return $"{signingDate:yyyyMMdd}/{region}/s3/aws4_request"; + return $"{signingDate:yyyyMMdd}/{region}/{getService(isSts)}/aws4_request"; } /// @@ -256,11 +272,9 @@ private string BytesToHex(byte[] checkSum) public string PresignPostSignature(string region, DateTime signingDate, string policyBase64) { byte[] signingKey = this.GenerateSigningKey(region, signingDate); - byte[] stringToSignBytes = System.Text.Encoding.UTF8.GetBytes(policyBase64); - + byte[] stringToSignBytes = Encoding.UTF8.GetBytes(policyBase64); byte[] signatureBytes = this.SignHmac(signingKey, stringToSignBytes); string signature = this.BytesToHex(signatureBytes); - return signature; } @@ -310,11 +324,11 @@ internal string PresignURL(HttpRequestMessageBuilder requestBuilder, int expires var presignUri = new UriBuilder(requestUri) { Query = requestQuery }.Uri; string canonicalRequest = this.GetPresignCanonicalRequest(requestBuilder.Method, presignUri, headersToSign); string headers = string.Concat(headersToSign.Select(p => $"&{p.Key}={utils.UrlEncode(p.Value)}")); - byte[] canonicalRequestBytes = System.Text.Encoding.UTF8.GetBytes(canonicalRequest); + byte[] canonicalRequestBytes = Encoding.UTF8.GetBytes(canonicalRequest); string canonicalRequestHash = this.BytesToHex(ComputeSha256(canonicalRequestBytes)); string stringToSign = this.GetStringToSign(region, signingDate, canonicalRequestHash); byte[] signingKey = this.GenerateSigningKey(region, signingDate); - byte[] stringToSignBytes = System.Text.Encoding.UTF8.GetBytes(stringToSign); + byte[] stringToSignBytes = Encoding.UTF8.GetBytes(stringToSign); byte[] signatureBytes = this.SignHmac(signingKey, stringToSignBytes); string signature = this.BytesToHex(signatureBytes); @@ -380,33 +394,84 @@ private static string GetCanonicalHost(Uri url) /// Dictionary of http headers to be signed /// Canonical Request private string GetCanonicalRequest(HttpRequestMessageBuilder requestBuilder, - SortedDictionary headersToSign) + SortedDictionary headersToSign) { var canonicalStringList = new LinkedList(); // METHOD canonicalStringList.AddLast(requestBuilder.Method.ToString()); - var resource = requestBuilder.RequestUri.PathAndQuery; - string[] path = resource.Split(new char[] { '?' }, 2); - if (!path[0].StartsWith("/")) + string queryParams = ""; + if (requestBuilder.QueryParameters != null) + { + queryParams = string.Join("&", requestBuilder.QueryParameters.Select(kvp => $"{kvp.Key}={Uri.EscapeDataString(kvp.Value)}")); + } + + var isFormData = false; + var c = requestBuilder.Request.Content; + + if (string.IsNullOrEmpty(queryParams) && c != null && + c.Headers != null && c.Headers.ContentType != null && + c.Headers.ContentType.ToString() == "application/x-www-form-urlencoded") { - path[0] = $"/{path[0]}"; + // Convert stream content to byte[] + var cntntByteData = new byte[] { }; + string contentKind = requestBuilder.Request.Content.Headers.ContentType.ToString(); + switch (contentKind) + { + case "application/x-www-form-urlencoded": + { + isFormData = true; + cntntByteData = requestBuilder.Request.Content.ReadAsByteArrayAsync().Result; + } + break; + } + + // Convert byte[] to regular string + StringBuilder cntntByteDataStr = new StringBuilder(); + // UTF conversion - String from bytes + queryParams = Encoding.UTF8.GetString(cntntByteData, 0, cntntByteData.Length); } - canonicalStringList.AddLast(path[0]); - Dictionary queryParams = - requestBuilder.QueryParameters.ToDictionary(o => o.Key, o => Uri.EscapeDataString(o.Value)); - var sb1 = new StringBuilder(); - var queryKeys = new List(queryParams.Keys); - queryKeys.Sort(StringComparer.Ordinal); - foreach (var p in queryKeys) + + if (!string.IsNullOrEmpty(queryParams)) { - if (sb1.Length > 0) - sb1.Append("&"); - sb1.AppendFormat("{0}={1}", p, queryParams[p]); + Dictionary queryParamsDict = new Dictionary() { }; + if (queryParams.EndsWith('=') && queryParams.Split('=').Length == 2) + { + queryParamsDict.Add(queryParams.Trim('='), ""); + } + else if (queryParams.Split('=').Length == 2) + { + queryParamsDict.Add(queryParams.Split('=')[0], queryParams.Split('=')[1]); + } + else if (queryParams.Split('=').Length > 2) + { + queryParamsDict = queryParams.Split(new[] { '&' }) + .Select(part => part.Split('=')) + .ToDictionary(split => split[0], split => split[1]); + } + + var sb1 = new StringBuilder(); + var queryKeys = new List(queryParamsDict.Keys); + queryKeys.Sort(StringComparer.Ordinal); + foreach (var p in queryKeys) + { + if (sb1.Length > 0) + sb1.Append("&"); + sb1.AppendFormat("{0}={1}", p, queryParamsDict[p]); + } + queryParams = sb1.ToString(); + } + if (!string.IsNullOrEmpty(queryParams) && + !isFormData && + requestBuilder.RequestUri.Query != "?location=") + { + requestBuilder.RequestUri = new Uri(requestBuilder.RequestUri + "?" + queryParams); } - var query = sb1.ToString(); - canonicalStringList.AddLast(query); + canonicalStringList.AddLast(requestBuilder.RequestUri.AbsolutePath); + canonicalStringList.AddLast(queryParams); + + // Headers to sign foreach (string header in headersToSign.Keys) { canonicalStringList.AddLast(header + ":" + s3utils.TrimAll(headersToSign[header])); @@ -421,7 +486,6 @@ private static string GetCanonicalHost(Uri url) { canonicalStringList.AddLast(sha256EmptyFileHash); } - return string.Join("\n", canonicalStringList); } @@ -493,8 +557,9 @@ private void SetSessionTokenHeader(HttpRequestMessageBuilder requestBuilder, str /// /// Set 'x-amz-content-sha256' http header. /// + /// boolean; if true role credentials, otherwise IAM user /// Instantiated requestBuilder object - private void SetContentSha256(HttpRequestMessageBuilder requestBuilder) + private void SetContentSha256(HttpRequestMessageBuilder requestBuilder, bool isSts = false) { if (this.isAnonymous) return; @@ -505,7 +570,7 @@ private void SetContentSha256(HttpRequestMessageBuilder requestBuilder) { isMultiDeleteRequest = requestBuilder.QueryParameters.Any(p => p.Key.Equals("delete", StringComparison.OrdinalIgnoreCase)); } - if (isSecure || isMultiDeleteRequest) + if (isSecure && !isSts || isMultiDeleteRequest) { requestBuilder.AddOrUpdateHeaderParameter("x-amz-content-sha256", "UNSIGNED-PAYLOAD"); return;