From 80c32b3e6f6ed4fe4fddbde59e383e48c1ae5985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=9E=E6=BE=8B?= Date: Fri, 30 Aug 2024 09:56:13 +0800 Subject: [PATCH] feat: add oidc credentials provider --- .../Units/Reader/JsonReader.cs | 2 + .../Auth/Provider/OIDCCredentialsProvider.cs | 135 ++++++++++++++++++ aliyun-net-sdk-core/Http/HttpRequest.cs | 30 +++- aliyun-net-sdk-core/Http/HttpResponse.cs | 24 ++++ aliyun-net-sdk-core/Http/UserAgent.cs | 9 +- aliyun-net-sdk-core/Utils/AuthUtils.cs | 73 ++++++++++ aliyun-net-sdk-core/Utils/ParameterHelper.cs | 74 +++++++++- 7 files changed, 338 insertions(+), 9 deletions(-) create mode 100644 aliyun-net-sdk-core/Auth/Provider/OIDCCredentialsProvider.cs create mode 100644 aliyun-net-sdk-core/Utils/AuthUtils.cs diff --git a/aliyun-net-sdk-core.Tests/Units/Reader/JsonReader.cs b/aliyun-net-sdk-core.Tests/Units/Reader/JsonReader.cs index 3c012469f2..fa3123388a 100644 --- a/aliyun-net-sdk-core.Tests/Units/Reader/JsonReader.cs +++ b/aliyun-net-sdk-core.Tests/Units/Reader/JsonReader.cs @@ -17,6 +17,7 @@ * under the License. */ +using System; using Aliyun.Acs.Core.Reader; using Aliyun.Acs.Core.Transform; @@ -36,6 +37,7 @@ public void Read() { var json = new JsonReader(); var result = json.Read(jsonStr, itemsTitle); + Console.WriteLine("result----{0}", result); var context = new UnmarshallerContext(); context.ResponseDictionary = result; Assert.Equal("value", context.StringValue(itemsTitle + ".key")); diff --git a/aliyun-net-sdk-core/Auth/Provider/OIDCCredentialsProvider.cs b/aliyun-net-sdk-core/Auth/Provider/OIDCCredentialsProvider.cs new file mode 100644 index 0000000000..25f96ccbe5 --- /dev/null +++ b/aliyun-net-sdk-core/Auth/Provider/OIDCCredentialsProvider.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using Aliyun.Acs.Core.Utils; +using Aliyun.Acs.Core.Exceptions; +using Aliyun.Acs.Core.Reader; + +using Aliyun.Acs.Core.Http; + +namespace Aliyun.Acs.Core.Auth +{ + public class OIDCCredentialsProvider : AlibabaCloudCredentialsProvider + { + private string RoleArn { get; set; } + private string OIDCProviderArn { get; set; } + private string OIDCTokenFilePath { get; set; } + private string RoleSessionName { get; set; } + private string Policy { get; set; } + + private readonly string stsEndpoint; + + private readonly long durationSeconds; + + private BasicSessionCredentials credentials; + private IAcsClient stsClient; + + public OIDCCredentialsProvider(string roleArn, string oidcProviderArn, string oidcTokenFilePath, string roleSessionName, string regionId) + { + RoleArn = ParameterHelper.ValidateEnvNotNull(roleArn, "ALIBABA_CLOUD_ROLE_ARN", "roleArn", "roleArn does not exist and env ALIBABA_CLOUD_ROLE_ARN is null."); + OIDCProviderArn = ParameterHelper.ValidateEnvNotNull(oidcProviderArn, "ALIBABA_CLOUD_OIDC_PROVIDER_ARN", "oidcProviderArn", "OIDCProviderArn must not be null."); + OIDCTokenFilePath = ParameterHelper.ValidateEnvNotNull(oidcTokenFilePath, "ALIBABA_CLOUD_OIDC_TOKEN_FILE", "oidcTokenFilePath", "OIDCTokenFilePath must not be null."); + + if (!string.IsNullOrEmpty(roleSessionName)) + { + RoleSessionName = roleSessionName; + } + else + { + RoleSessionName = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ROLE_SESSION_NAME"); + } + + if (string.IsNullOrEmpty(RoleSessionName)) + { + RoleSessionName = "DEFAULT_ROLE_SESSION_NAME_FOR_C#_SDK_V1"; + } + + if (string.IsNullOrEmpty(regionId)) + { + stsEndpoint = "https://sts.aliyuncs.com/"; + } + else + { + stsEndpoint = string.Format("https://sts.{0}.aliyuncs.com/", regionId); + } + + durationSeconds = 3600; + } + + public string InvokeAssumeRoleWithOIDC() + { + var queries = new Dictionary + { + { "Action", "AssumeRoleWithOIDC" }, + { "Format", "JSON" }, + { "Version", "2015-04-01" }, + { "Timestamp", ParameterHelper.FormatIso8601Date(DateTime.UtcNow) } + }; + + string url; + try + { + url = stsEndpoint + "?" + ParameterHelper.GetFormData(queries); + } + catch (Exception ex) + { + throw new ClientException("AssumeRoleWithOIDC failed: " + ex.Message); + } + + var httpRequest = new HttpRequest(url) + { + Method = MethodType.POST, + ContentType = FormatType.FORM, + }; + + httpRequest.SetConnectTimeoutInMilliSeconds(1000); + httpRequest.SetReadTimeoutInMilliSeconds(3000); + + var oidcToken = AuthUtils.GetOIDCToken(OIDCTokenFilePath); + if (oidcToken == null) + { + throw new ClientException("Read OIDC token failed"); + } + + var body = new Dictionary + { + { "DurationSeconds", durationSeconds.ToString() }, + { "RoleArn", RoleArn }, + { "OIDCProviderArn", OIDCProviderArn }, + { "OIDCToken", oidcToken }, + { "RoleSessionName", RoleSessionName }, + { "Policy", Policy } + }; + + var content = ParameterHelper.GetFormDataWithoutNullOrEmpty(body); + httpRequest.SetContent(content, "UTF-8", FormatType.FORM); + + HttpResponse httpResponse; + try + { + httpResponse = HttpResponse.GetResponse(httpRequest); + } + catch (Exception ex) + { + throw new ClientException("AssumeRoleWithOIDC failed " + ex.Message); + } + + if (!httpResponse.isSuccess()) + { + var responseBody = httpResponse.GetHttpContentString(); + var jsonReader = new JsonReader(); + var requestID = jsonReader.Read(responseBody, "RequestId"); + var msg = jsonReader.Read(responseBody, "Message"); + var code = jsonReader.Read(responseBody, "Code"); + var message = string.Format("{0}(RequestID: {1}, Code: {2})", msg, requestID, code); + throw new ClientException("AssumeRoleWithOIDC failed: " + message); + } + + return httpResponse.GetHttpContentString(); + } + + public AlibabaCloudCredentials GetCredentials() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/aliyun-net-sdk-core/Http/HttpRequest.cs b/aliyun-net-sdk-core/Http/HttpRequest.cs index be16be4d34..4e903ac541 100644 --- a/aliyun-net-sdk-core/Http/HttpRequest.cs +++ b/aliyun-net-sdk-core/Http/HttpRequest.cs @@ -28,15 +28,29 @@ namespace Aliyun.Acs.Core.Http public class HttpRequest { private int timeout = 100000; + protected static readonly string UserAgent = "User-Agent"; + private static readonly string DefaultUserAgent; + + static HttpRequest() + { + DefaultUserAgent = new UserAgent().GetDefaultUserAgent(); + } public HttpRequest() { + Headers = new Dictionary + { + { UserAgent, DefaultUserAgent } + }; } public HttpRequest(string strUrl) { Url = strUrl; - Headers = new Dictionary(); + Headers = new Dictionary + { + { UserAgent, DefaultUserAgent } + }; } public HttpRequest(string strUrl, Dictionary tmpHeaders) @@ -45,6 +59,14 @@ public HttpRequest(string strUrl, Dictionary tmpHeaders) if (null != tmpHeaders) { Headers = tmpHeaders; + Headers[UserAgent] = DefaultUserAgent; + } + else + { + Headers = new Dictionary + { + { UserAgent, DefaultUserAgent } + }; } } @@ -117,9 +139,9 @@ public void SetContent(byte[] content, string encoding, FormatType? format) DictionaryUtil.Pop(Headers, "Content-Type"); DictionaryUtil.Add(Headers, "Content-MD5", strMd5); - if(this.Method.ToString() == "POST" || this.Method.ToString() == "PUT") - { - DictionaryUtil.Add(Headers, "Content-Length", contentLen); + if(this.Method.ToString() == "POST" || this.Method.ToString() == "PUT") + { + DictionaryUtil.Add(Headers, "Content-Length", contentLen); } DictionaryUtil.Add(Headers, "Content-Type", ParameterHelper.FormatTypeToString(type)); diff --git a/aliyun-net-sdk-core/Http/HttpResponse.cs b/aliyun-net-sdk-core/Http/HttpResponse.cs index d6fbb10881..d645a57400 100644 --- a/aliyun-net-sdk-core/Http/HttpResponse.cs +++ b/aliyun-net-sdk-core/Http/HttpResponse.cs @@ -55,6 +55,30 @@ public HttpResponse() ContentType = format; } + public string GetHttpContentString() + { + string stringContent = string.Empty; + if (this.Content != null) + { + try + { + if (string.IsNullOrWhiteSpace(this.Encoding)) + { + stringContent = Convert.ToBase64String(this.Content); + } + else + { + stringContent = System.Text.Encoding.GetEncoding(Encoding).GetString(this.Content); + } + } + catch + { + throw new ClientException("Can not parse response due to unsupported encoding."); + } + } + return stringContent; + } + private static void ParseHttpResponse(HttpResponse httpResponse, HttpWebResponse httpWebResponse) { httpResponse.Content = ReadContent(httpResponse, httpWebResponse); diff --git a/aliyun-net-sdk-core/Http/UserAgent.cs b/aliyun-net-sdk-core/Http/UserAgent.cs index 553e2ae1bc..2dcca9f20e 100644 --- a/aliyun-net-sdk-core/Http/UserAgent.cs +++ b/aliyun-net-sdk-core/Http/UserAgent.cs @@ -31,7 +31,7 @@ namespace Aliyun.Acs.Core.Http { public class UserAgent { - private static string DEFAULT_MESSAGE; + public static string DEFAULT_MESSAGE; private readonly List excludedList = new List(); private readonly Dictionary userAgent = new Dictionary(); @@ -46,7 +46,12 @@ public UserAgent() DEFAULT_MESSAGE = "Alibaba Cloud (" + OSVersion + ") "; DEFAULT_MESSAGE += ClientVersion; - DEFAULT_MESSAGE += " Core/" + CoreVersion; + DEFAULT_MESSAGE += " Credentials/" + CoreVersion; + } + + internal string GetDefaultUserAgent() + { + return DEFAULT_MESSAGE; } public void SetTheValue() diff --git a/aliyun-net-sdk-core/Utils/AuthUtils.cs b/aliyun-net-sdk-core/Utils/AuthUtils.cs new file mode 100644 index 0000000000..adbf2f38bb --- /dev/null +++ b/aliyun-net-sdk-core/Utils/AuthUtils.cs @@ -0,0 +1,73 @@ +/* + * 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 System; +using System.Text; +using System.IO; +using System.Security; +using System.Threading.Tasks; +using Aliyun.Acs.Core.Exceptions; + +namespace Aliyun.Acs.Core.Utils +{ + public class AuthUtils + { + private static volatile string oidcToken; + + static AuthUtils authUtils = new AuthUtils(); + + AuthUtils() + { + } + + + public static string GetOIDCToken(string OIDCTokenFilePath) + { + byte[] buffer; + if (!File.Exists(OIDCTokenFilePath)) + { + throw new ClientException("OIDCTokenFilePath " + OIDCTokenFilePath + " does not exist."); + } + try + { + using (var inStream = new FileStream(OIDCTokenFilePath, FileMode.Open, FileAccess.Read)) + { + buffer = new byte[inStream.Length]; + inStream.Read(buffer, 0, buffer.Length); + } + oidcToken = Encoding.UTF8.GetString(buffer); + } + catch (UnauthorizedAccessException) + { + throw new ClientException("OIDCTokenFilePath " + OIDCTokenFilePath + " cannot be read."); + } + catch (SecurityException) + { + throw new ClientException("Security Exception: Do not have the required permission. " + "OIDCTokenFilePath " + OIDCTokenFilePath); + } + catch (IOException e) + { + Console.WriteLine(e.StackTrace); + } + return oidcToken; + } + + + } +} diff --git a/aliyun-net-sdk-core/Utils/ParameterHelper.cs b/aliyun-net-sdk-core/Utils/ParameterHelper.cs index 12f24a3c01..2fd5dd3155 100644 --- a/aliyun-net-sdk-core/Utils/ParameterHelper.cs +++ b/aliyun-net-sdk-core/Utils/ParameterHelper.cs @@ -18,8 +18,11 @@ */ using System; +using System.Web; +using System.Text; using System.Globalization; using System.Security.Cryptography; +using System.Collections.Generic; using Aliyun.Acs.Core.Http; @@ -42,12 +45,12 @@ public static string GetRFC2616Date(DateTime datetime) datetime = DateTime.UtcNow; } - return datetime.ToUniversalTime().GetDateTimeFormats('r') [0]; + return datetime.ToUniversalTime().GetDateTimeFormats('r')[0]; } public static string Md5Sum(byte[] buff) { - using(MD5 md5 = new MD5CryptoServiceProvider()) + using (MD5 md5 = new MD5CryptoServiceProvider()) { var output = md5.ComputeHash(buff); return BitConverter.ToString(output).Replace("-", ""); @@ -56,7 +59,7 @@ public static string Md5Sum(byte[] buff) public static string Md5SumAndBase64(byte[] buff) { - using(MD5 md5 = new MD5CryptoServiceProvider()) + using (MD5 md5 = new MD5CryptoServiceProvider()) { var output = md5.ComputeHash(buff); // string md5Str = BitConverter.ToString(output).Replace("-", ""); @@ -142,5 +145,70 @@ public static string FormatTypeToString(FormatType? formatType) } } } + + public static byte[] GetFormData(Dictionary parameters) + { + var result = new StringBuilder(); + var first = true; + + foreach (var entry in parameters) + { + if (!first) + { + result.Append("&"); + } + else + { + first = false; + } + + result.Append(HttpUtility.UrlEncode(entry.Key)); + result.Append("="); + result.Append(HttpUtility.UrlEncode(entry.Value)); + } + + return Encoding.UTF8.GetBytes(result.ToString()); + } + + public static byte[] GetFormDataWithoutNullOrEmpty(Dictionary parameters) + { + var result = new StringBuilder(); + var first = true; + + foreach (var entry in parameters) + { + if (string.IsNullOrEmpty(entry.Value)) + continue; + + if (!first) + { + result.Append("&"); + } + result.AppendFormat( + "{0}={1}", + HttpUtility.UrlEncode(entry.Key), + HttpUtility.UrlEncode(entry.Value) + ); + first = false; + } + return Encoding.UTF8.GetBytes(result.ToString()); + } + + + public static string ValidateEnvNotNull(string obj, string envVariableName, string paramName, string message) + { + if (obj == null) + { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envVariableName))) + { + throw new ArgumentNullException(paramName, message); + } + else + { + return Environment.GetEnvironmentVariable(envVariableName); + } + } + return obj; + } } }