Skip to content

Commit

Permalink
feat: add oidc credentials provider
Browse files Browse the repository at this point in the history
  • Loading branch information
飞澋 authored and PanPanZou committed Sep 4, 2024
1 parent fabe3a9 commit fc4a6ba
Show file tree
Hide file tree
Showing 13 changed files with 464 additions and 20 deletions.
1 change: 1 addition & 0 deletions aliyun-net-sdk-core.Tests/OIDCToken.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OIDCToken
98 changes: 98 additions & 0 deletions aliyun-net-sdk-core.Tests/Units/Auth/OIDCCredentialsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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 Aliyun.Acs.Core.Auth;
using Aliyun.Acs.Core.Exceptions;
using Xunit;

namespace Aliyun.Acs.Core.Tests.Units.Auth
{
public class OIDCCredentialsProviderTest
{
[Fact]
public void TestConstructor()
{
var filePath = TestHelper.GetOIDCTokenFilePath();
var provider = new OIDCCredentialsProvider("roleArn", "providerArn", filePath, "sessionname", null);
Assert.NotNull(provider);
Assert.Equal("roleArn", provider.RoleArn);
provider.RoleArn = "new-roleArn";
Assert.Equal("new-roleArn", provider.RoleArn);

Assert.Equal("providerArn", provider.OIDCProviderArn);
provider.OIDCProviderArn = "new-providerArn";
Assert.Equal("new-providerArn", provider.OIDCProviderArn);

Assert.EndsWith("OIDCToken.txt", provider.OIDCTokenFilePath);
provider.OIDCTokenFilePath = "/tmp/oidctoken";
Assert.Equal("/tmp/oidctoken", provider.OIDCTokenFilePath);

Assert.Equal("sessionname", provider.RoleSessionName);
provider.RoleSessionName = "new-sessionname";
Assert.Equal("new-sessionname", provider.RoleSessionName);

Assert.Null(provider.Policy);
provider.Policy = "{}";
Assert.Equal("{}", provider.Policy);
}

[Fact]
public void TestEmptyRoleArn()
{
var ex = Assert.Throws<ArgumentNullException>(() => { new OIDCCredentialsProvider(null, null, null, null, null); });
Assert.StartsWith("roleArn does not exist and env ALIBABA_CLOUD_ROLE_ARN is null.", ex.Message);

ex = Assert.Throws<ArgumentNullException>(() => { new OIDCCredentialsProvider("roleArn", null, null, null, null); });
Assert.StartsWith("OIDCProviderArn does not exist and env ALIBABA_CLOUD_OIDC_PROVIDER_ARN is null.", ex.Message);

ex = Assert.Throws<ArgumentNullException>(() => { new OIDCCredentialsProvider("roleArn", "providerArn", null, null, null); });
Assert.StartsWith("OIDCTokenFilePath does not exist and env ALIBABA_CLOUD_OIDC_TOKEN_FILE is null.", ex.Message);

var provider = new OIDCCredentialsProvider("roleArn", "providerArn", "", "sessionname", null);
var notExistEx = Assert.Throws<ClientException>(() => { provider.GetCredentials(); });
Assert.Equal("OIDCTokenFilePath does not exist.", notExistEx.Message);
}

[Fact]
public void TestRealGetCredentials()
{
var filePath = TestHelper.GetOIDCTokenFilePath();
var provider = new OIDCCredentialsProvider("roleArn", "providerArn", filePath, null, "us-west-1");
Assert.NotNull(provider);
var ex = Assert.Throws<ClientException>(() => { provider.GetCredentials(); });
Assert.Contains("Parameter OIDCProviderArn is not valid", ex.Message);
}

[Fact]
public void TestParseCredentials()
{
var ex = Assert.Throws<ClientException>(() => { OIDCCredentialsProvider.ParseCredentials("{}", 3600L); });
Assert.Equal("AssumeRoleWithOIDC failed: {}", ex.Message);
ex = Assert.Throws<ClientException>(() => { OIDCCredentialsProvider.ParseCredentials("", 3600L); });
Assert.Equal("Invalid JSON", ex.Message);
var credentials =OIDCCredentialsProvider.ParseCredentials("{\"Credentials\": {\"AccessKeyId\": \"sts_ak_id\",\"AccessKeySecret\": \"sts_ak_secret\", \"SecurityToken\": \"securitytoken\",\"Expiration\": \"2021-10-20T04:27:09Z\"}}", 3600L);
Assert.Equal("sts_ak_id", credentials.GetAccessKeyId());
Assert.Equal("sts_ak_secret", credentials.GetAccessKeySecret());
Assert.Equal("securitytoken", credentials.GetSessionToken());
Assert.False(credentials.WillSoonExpire());
}
}
}
12 changes: 12 additions & 0 deletions aliyun-net-sdk-core.Tests/Units/Http/HttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
* under the License.
*/

using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;

using Aliyun.Acs.Core.Http;
using Aliyun.Acs.Core.Transform;
Expand All @@ -30,6 +32,16 @@ namespace Aliyun.Acs.Core.Tests.Units.Http
{
public class HttpRequestTest
{
[Fact]
public void CredentialsHeadersTest()
{
var instance = new HttpRequest("https://testurl");
instance.Headers.Add("User-Agent", UserAgent.Resolve(null, null));
var pattern = @"^Alibaba Cloud \(.+?\) \S+ Core/\S+$";
var isMatch = Regex.IsMatch(instance.Headers["User-Agent"], pattern);
Assert.True(isMatch);
}

[Fact]
public void ConnectTimeoutTest()
{
Expand Down
8 changes: 8 additions & 0 deletions aliyun-net-sdk-core.Tests/Units/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public class TestHelper
private static readonly string slash = EnvironmentUtil.GetOSSlash();
private static readonly string homePath = EnvironmentUtil.GetHomePath();

private static readonly string Slash = Environment.OSVersion.Platform == PlatformID.Unix ? "/" : "\\";
private static readonly string HomePath = Environment.CurrentDirectory;

public static string GetOIDCTokenFilePath()
{
return HomePath + Slash + "OIDCToken.txt";
}

public static void RemoveEnvironmentValue()
{
Environment.SetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID", null);
Expand Down
16 changes: 16 additions & 0 deletions aliyun-net-sdk-core.Tests/Units/Utils/ParameterHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

using System;
using System.Collections.Generic;
using System.Text;

using Aliyun.Acs.Core.Http;
Expand Down Expand Up @@ -115,5 +116,20 @@ public void StringToMethodType()
Assert.True(MethodType.OPTIONS == ParameterHelper.StringToMethodType("options"));
Assert.True(null == ParameterHelper.StringToMethodType("test"));
}

[Fact]
public void GetFormDataTest()
{
var parameters = new Dictionary<string, string>
{
{ "key", "value" }
};

var output = ParameterHelper.GetFormData(parameters);
Assert.Equal("key=value", Encoding.UTF8.GetString(output));
parameters.Add("key1", "value1");
output = ParameterHelper.GetFormData(parameters);
Assert.Equal("key=value&key1=value1", Encoding.UTF8.GetString(output));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<ItemGroup>
<Compile Remove="**/obj/**" />
<None Remove="**/obj/**" />
<None Update="OIDCToken.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
Expand Down
163 changes: 163 additions & 0 deletions aliyun-net-sdk-core/Auth/Provider/OIDCCredentialsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System;
using System.Text;
using System.Collections.Generic;

using Aliyun.Acs.Core.Utils;
using Aliyun.Acs.Core.Exceptions;

using Aliyun.Acs.Core.Http;

using Newtonsoft.Json;

namespace Aliyun.Acs.Core.Auth
{
public class OIDCCredentialsProvider : AlibabaCloudCredentialsProvider
{
public string RoleArn { get; set; }
public string OIDCProviderArn { get; set; }
public string OIDCTokenFilePath { get; set; }
public string RoleSessionName { get; set; }
public string Policy { get; set; }

private readonly string stsEndpoint;

private readonly long durationSeconds;

private BasicSessionCredentials credentials;

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 does not exist and env ALIBABA_CLOUD_OIDC_PROVIDER_ARN is null.");
OIDCTokenFilePath = ParameterHelper.ValidateEnvNotNull(oidcTokenFilePath, "ALIBABA_CLOUD_OIDC_TOKEN_FILE", "oidcTokenFilePath", "OIDCTokenFilePath does not exist and env ALIBABA_CLOUD_OIDC_TOKEN_FILE is 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<string, string>
{
{ "Action", "AssumeRoleWithOIDC" },
{ "Format", "JSON" },
{ "Version", "2015-04-01" },
{ "Timestamp", ParameterHelper.FormatIso8601Date(DateTime.UtcNow) }
};

string url;
try
{
url = stsEndpoint + "?" + "Action=AssumeRoleWithOIDC&Format=JSON&Version=2015-04-01&Timestamp=" + ParameterHelper.FormatIso8601Date(DateTime.UtcNow);
}
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<string, string>
{
{ "DurationSeconds", durationSeconds.ToString() },
{ "RoleArn", RoleArn },
{ "OIDCProviderArn", OIDCProviderArn },
{ "OIDCToken", oidcToken },
{ "RoleSessionName", RoleSessionName },
{ "Policy", Policy }
};

var content = ParameterHelper.GetFormData(body);
httpRequest.SetContent(content, "UTF-8", FormatType.FORM);
httpRequest.Headers.Add("User-Agent", UserAgent.Resolve(null, null));

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 map = JsonConvert.DeserializeObject<Dictionary<string, object>>(responseBody);
var requestID = map["RequestId"];
var message = string.Format("{0}(RequestID: {1}, Code: {2})", map["Message"], requestID, map["Code"]);
throw new ClientException("AssumeRoleWithOIDC failed: " + message);
}

return httpResponse.GetHttpContentString();
}

internal static BasicSessionCredentials ParseCredentials(string body, long durationSeconds)
{
var map = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
if (map == null)
{
throw new ClientException("Invalid JSON");
}
else if (map.ContainsKey("Credentials"))
{
var credentialsJson = JsonConvert.SerializeObject(DictionaryUtil.Get(map, "Credentials"));
var credentials = JsonConvert.DeserializeObject<Dictionary<string, string>> (credentialsJson);
var accessKeyId = DictionaryUtil.Get(credentials, "AccessKeyId");
var accessKeySecret = DictionaryUtil.Get(credentials, "AccessKeySecret");
var securityToken = DictionaryUtil.Get(credentials, "SecurityToken");
return new BasicSessionCredentials(accessKeyId, accessKeySecret, securityToken, durationSeconds);
}
else
{
throw new ClientException("AssumeRoleWithOIDC failed: " + body);
}
}

public AlibabaCloudCredentials GetCredentials()
{
if (credentials == null || credentials.WillSoonExpire())
{
var body = InvokeAssumeRoleWithOIDC();
credentials = ParseCredentials(body, durationSeconds);
}
return credentials;
}
}
}
8 changes: 4 additions & 4 deletions aliyun-net-sdk-core/Http/HttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,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));

Expand All @@ -142,4 +142,4 @@ public void SetHttpsInsecure(bool ignoreCertificate = false)
IgnoreCertificate = ignoreCertificate;
}
}
}
}
Loading

0 comments on commit fc4a6ba

Please sign in to comment.