Skip to content

Commit b972a9c

Browse files
Added Managed Identity E2E tests to run on Azure ARC (Custom Agent) and Azure VM (Hosted Agent) (#5257)
* initial * yaml split * comments * pr comments --------- Co-authored-by: Gladwin Johnson <[email protected]>
1 parent 68ae955 commit b972a9c

File tree

9 files changed

+318
-2
lines changed

9 files changed

+318
-2
lines changed

LibsAndSamples.sln

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
Microsoft Visual Studio Solution File, Format Version 12.00
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
23
# Visual Studio Version 17
34
VisualStudioVersion = 17.3.32708.82
45
MinimumVisualStudioVersion = 10.0.40219.1
@@ -182,6 +183,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "tests\devapps\WAM\N
182183
EndProject
183184
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedIdentityTokenRevocation", "tests\devapps\Managed Identity apps\ManagedIdentityTokenRevocation\ManagedIdentityTokenRevocation.csproj", "{DA9C3258-DEF6-7794-9762-20CF7B826839}"
184185
EndProject
186+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Test.E2E.MSI", "tests\Microsoft.Identity.Test.E2e\Microsoft.Identity.Test.E2E.MSI.csproj", "{97995B86-AA0F-3AF9-DA40-85A6263E4391}"
185187
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacMauiAppWithBroker", "tests\devapps\MacMauiAppWithBroker\MacMauiAppWithBroker.csproj", "{AEF6BB00-931F-4638-955D-24D735625C34}"
186188
EndProject
187189
Global
@@ -1780,6 +1782,48 @@ Global
17801782
{DA9C3258-DEF6-7794-9762-20CF7B826839}.Release|x64.Build.0 = Release|Any CPU
17811783
{DA9C3258-DEF6-7794-9762-20CF7B826839}.Release|x86.ActiveCfg = Release|Any CPU
17821784
{DA9C3258-DEF6-7794-9762-20CF7B826839}.Release|x86.Build.0 = Release|Any CPU
1785+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU
1786+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU
1787+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU
1788+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|ARM.Build.0 = Debug|Any CPU
1789+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|ARM64.ActiveCfg = Debug|Any CPU
1790+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|ARM64.Build.0 = Debug|Any CPU
1791+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|iPhone.ActiveCfg = Debug|Any CPU
1792+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|iPhone.Build.0 = Debug|Any CPU
1793+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|iPhoneSimulator.ActiveCfg = Debug|Any CPU
1794+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|iPhoneSimulator.Build.0 = Debug|Any CPU
1795+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|x64.ActiveCfg = Debug|Any CPU
1796+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|x64.Build.0 = Debug|Any CPU
1797+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|x86.ActiveCfg = Debug|Any CPU
1798+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug + MobileApps|x86.Build.0 = Debug|Any CPU
1799+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1800+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|Any CPU.Build.0 = Debug|Any CPU
1801+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|ARM.ActiveCfg = Debug|Any CPU
1802+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|ARM.Build.0 = Debug|Any CPU
1803+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|ARM64.ActiveCfg = Debug|Any CPU
1804+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|ARM64.Build.0 = Debug|Any CPU
1805+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|iPhone.ActiveCfg = Debug|Any CPU
1806+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|iPhone.Build.0 = Debug|Any CPU
1807+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
1808+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
1809+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|x64.ActiveCfg = Debug|Any CPU
1810+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|x64.Build.0 = Debug|Any CPU
1811+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|x86.ActiveCfg = Debug|Any CPU
1812+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Debug|x86.Build.0 = Debug|Any CPU
1813+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|Any CPU.ActiveCfg = Release|Any CPU
1814+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|Any CPU.Build.0 = Release|Any CPU
1815+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|ARM.ActiveCfg = Release|Any CPU
1816+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|ARM.Build.0 = Release|Any CPU
1817+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|ARM64.ActiveCfg = Release|Any CPU
1818+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|ARM64.Build.0 = Release|Any CPU
1819+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|iPhone.ActiveCfg = Release|Any CPU
1820+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|iPhone.Build.0 = Release|Any CPU
1821+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
1822+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
1823+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|x64.ActiveCfg = Release|Any CPU
1824+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|x64.Build.0 = Release|Any CPU
1825+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|x86.ActiveCfg = Release|Any CPU
1826+
{97995B86-AA0F-3AF9-DA40-85A6263E4391}.Release|x86.Build.0 = Release|Any CPU
17831827
{AEF6BB00-931F-4638-955D-24D735625C34}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU
17841828
{AEF6BB00-931F-4638-955D-24D735625C34}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU
17851829
{AEF6BB00-931F-4638-955D-24D735625C34}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU
@@ -1875,6 +1919,7 @@ Global
18751919
{87679336-95BE-47E4-B42B-8F6860A0B215} = {1A37FD75-94E9-4D6F-953A-0DABBD7B49E9}
18761920
{43BCA8C7-E9F4-4067-9F54-C2127B82B5E8} = {5FAAD966-36B8-4C19-A5FA-5410DD53063D}
18771921
{DA9C3258-DEF6-7794-9762-20CF7B826839} = {BCAEE9AE-8D3E-4C77-A2E4-134E1552D5F8}
1922+
{97995B86-AA0F-3AF9-DA40-85A6263E4391} = {9B0B5396-4D95-4C15-82ED-DC22B5A3123F}
18781923
{AEF6BB00-931F-4638-955D-24D735625C34} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB}
18791924
EndGlobalSection
18801925
GlobalSection(ExtensibilityGlobals) = postSolution

build/template-build-and-run-all-tests.yaml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,28 @@ jobs: #Build and stage projects
9393
Codeql.SkipTaskAutoInjection: true
9494

9595
steps:
96-
- template: template-test-on-linux.yaml
96+
- template: template-test-on-linux.yaml
97+
98+
- job: 'RunManagedIdentityE2ETestsOnAzureArc'
99+
displayName: 'Managed Identity E2E Tests – Azure ARC Agent'
100+
dependsOn: ['BuildAndStageProjects']
101+
pool:
102+
name: 'MSALMSIAZUREARC'
103+
104+
steps:
105+
- template: template-run-mi-e2e-azurearc.yaml
106+
parameters:
107+
BuildConfiguration: 'Release'
108+
TargetFramework: 'net8.0'
109+
110+
- job: 'RunManagedIdentityE2ETestsOnImds'
111+
displayName: 'Managed Identity E2E Tests – VM / IMDS'
112+
dependsOn: ['BuildAndStageProjects']
113+
pool:
114+
name: 'ID4SMSIHostedAgent'
115+
116+
steps:
117+
- template: template-run-mi-e2e-imds.yaml
118+
parameters:
119+
BuildConfiguration: 'Release'
120+
TargetFramework: 'net8.0'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
parameters:
2+
BuildConfiguration: 'Release'
3+
TargetFramework: 'net8.0'
4+
5+
steps:
6+
# Restore
7+
- task: DotNetCoreCLI@2
8+
displayName: Restore E2E project
9+
inputs:
10+
command: restore
11+
projects: tests/Microsoft.Identity.Test.E2E/Microsoft.Identity.Test.E2E.MSI.csproj
12+
restoreArguments: >
13+
/p:RestoreTargetFrameworks=net8.0
14+
/p:INCLUDE_MOBILE_AND_LEGACY_TFM=
15+
16+
# Build
17+
- task: DotNetCoreCLI@2
18+
displayName: Build E2E project
19+
inputs:
20+
command: build
21+
projects: tests/Microsoft.Identity.Test.E2E/Microsoft.Identity.Test.E2E.MSI.csproj
22+
arguments: >
23+
--configuration $(BuildConfiguration)
24+
--framework net8.0
25+
/p:INCLUDE_MOBILE_AND_LEGACY_TFM=
26+
27+
- task: VSTest@2
28+
displayName: 'Run Managed Identity E2E Tests (.NET)'
29+
inputs:
30+
testSelector: testAssemblies
31+
testAssemblyVer2: '**/Microsoft.Identity.Test.E2E/bin/${{ parameters.BuildConfiguration }}/**/Microsoft.Identity.Test.E2E.MSI.dll'
32+
searchFolder: '$(System.DefaultWorkingDirectory)'
33+
runTestsInIsolation: true
34+
rerunFailedTests: true
35+
rerunMaxAttempts: '3'
36+
runInParallel: false
37+
codeCoverageEnabled: false
38+
failOnMinTestsNotRun: true
39+
minimumExpectedTests: '1'
40+
testFiltercriteria: 'TestCategory=MI_E2E_AzureArc'

build/template-run-mi-e2e-imds.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
parameters:
2+
BuildConfiguration: 'Release'
3+
TargetFramework: 'net8.0'
4+
5+
steps:
6+
7+
# Restore
8+
- task: DotNetCoreCLI@2
9+
displayName: Restore E2E project
10+
inputs:
11+
command: restore
12+
projects: tests/Microsoft.Identity.Test.E2E/Microsoft.Identity.Test.E2E.MSI.csproj
13+
restoreArguments: >
14+
/p:RestoreTargetFrameworks=net8.0
15+
/p:INCLUDE_MOBILE_AND_LEGACY_TFM=
16+
17+
# Build
18+
- task: DotNetCoreCLI@2
19+
displayName: Build E2E project
20+
inputs:
21+
command: build
22+
projects: tests/Microsoft.Identity.Test.E2E/Microsoft.Identity.Test.E2E.MSI.csproj
23+
arguments: >
24+
--configuration $(BuildConfiguration)
25+
--framework net8.0
26+
/p:INCLUDE_MOBILE_AND_LEGACY_TFM=
27+
28+
# Test
29+
- task: VSTest@2
30+
displayName: Run Managed Identity E2E Tests (.NET)
31+
inputs:
32+
testSelector: testAssemblies
33+
testAssemblyVer2: '**/Microsoft.Identity.Test.E2E/bin/${{ parameters.BuildConfiguration }}/**/Microsoft.Identity.Test.E2E.MSI.dll'
34+
searchFolder: '$(System.DefaultWorkingDirectory)'
35+
runTestsInIsolation: true
36+
rerunFailedTests: true
37+
rerunMaxAttempts: '3'
38+
runInParallel: false
39+
failOnMinTestsNotRun: true
40+
minimumExpectedTests: '1'
41+
testFiltercriteria: 'TestCategory=MI_E2E_Imds'

src/client/Microsoft.Identity.Client/Properties/InternalsVisibleTo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
[assembly: InternalsVisibleTo("Microsoft.Identity.Test.Integration.NetCore" + KeyTokens.MSAL)]
1414
[assembly: InternalsVisibleTo("Microsoft.Identity.Test.Integration.NetFx" + KeyTokens.MSAL)]
1515
[assembly: InternalsVisibleTo("Microsoft.Identity.Test.Performance" + KeyTokens.MSAL)]
16+
[assembly: InternalsVisibleTo("Microsoft.Identity.Test.E2E.MSI" + KeyTokens.MSAL)]
1617

1718
[assembly: InternalsVisibleTo("CommonCache.Test.Common" + KeyTokens.MSAL)]
1819
[assembly: InternalsVisibleTo("CommonCache.Test.Unit" + KeyTokens.MSAL)]

tests/Microsoft.Identity.Test.Common/Core/Helpers/RunOnPlatformAttribute.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System;
45
using System.Runtime.InteropServices;
56
using Microsoft.VisualStudio.TestTools.UnitTesting;
67

@@ -99,4 +100,26 @@ public override TestResult[] Execute(ITestMethod testMethod)
99100
return base.Execute(testMethod);
100101
}
101102
}
103+
104+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
105+
public sealed class RunOnAzureDevOpsAttribute : TestMethodAttribute
106+
{
107+
public override TestResult[] Execute(ITestMethod testMethod)
108+
{
109+
// TF_BUILD is true for all Azure DevOps agents
110+
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TF_BUILD")))
111+
{
112+
return new[]
113+
{
114+
new TestResult
115+
{
116+
Outcome = UnitTestOutcome.Inconclusive,
117+
TestFailureException = new AssertInconclusiveException("Skipped outside Azure DevOps")
118+
}
119+
};
120+
}
121+
122+
return base.Execute(testMethod);
123+
}
124+
}
102125
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Identity.Client;
5+
using Microsoft.Identity.Client.AppConfig;
6+
using Microsoft.Identity.Test.Common.Core.Helpers;
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
using System;
9+
using System.Threading.Tasks;
10+
11+
namespace Microsoft.Identity.Test.E2E
12+
{
13+
[TestClass]
14+
public class ManagedIdentityAzureArcTests
15+
{
16+
private const string ArmScope = "https://management.azure.com";
17+
18+
private static IManagedIdentityApplication BuildSami()
19+
{
20+
var builder = ManagedIdentityApplicationBuilder
21+
.Create(ManagedIdentityId.SystemAssigned);
22+
23+
builder.Config.AccessorOptions = null;
24+
25+
return builder.Build();
26+
}
27+
28+
[TestCategory("MI_E2E_AzureArc")]
29+
[RunOnAzureDevOps]
30+
[TestMethod]
31+
public async Task AcquireToken_ForSami_OnAzureArc_Succeeds()
32+
{
33+
var mi = BuildSami();
34+
var result = await mi.AcquireTokenForManagedIdentity(ArmScope)
35+
.ExecuteAsync()
36+
.ConfigureAwait(false);
37+
38+
Assert.IsFalse(string.IsNullOrEmpty(result.AccessToken));
39+
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
40+
41+
var second = await mi.AcquireTokenForManagedIdentity(ArmScope).ExecuteAsync().ConfigureAwait(false);
42+
43+
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
44+
Assert.AreEqual(TokenSource.Cache, second.AuthenticationResultMetadata.TokenSource);
45+
Assert.AreEqual(result.AccessToken, second.AccessToken, "Expected identical AT from cache.");
46+
}
47+
}
48+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Identity.Client;
5+
using Microsoft.Identity.Client.AppConfig;
6+
using Microsoft.Identity.Test.Common.Core.Helpers;
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
using System;
9+
using System.Threading.Tasks;
10+
11+
namespace Microsoft.Identity.Test.E2E
12+
{
13+
[TestClass]
14+
public class ManagedIdentityImdsTests
15+
{
16+
private const string ArmScope = "https://management.azure.com";
17+
18+
private static IManagedIdentityApplication BuildMi(
19+
string userAssignedId = null,
20+
string idType = null)
21+
{
22+
ManagedIdentityId miId = userAssignedId is null
23+
? ManagedIdentityId.SystemAssigned
24+
: idType.ToLowerInvariant() switch
25+
{
26+
"clientid" => ManagedIdentityId.WithUserAssignedClientId(userAssignedId),
27+
"resourceid" => ManagedIdentityId.WithUserAssignedResourceId(userAssignedId),
28+
"objectid" => ManagedIdentityId.WithUserAssignedObjectId(userAssignedId),
29+
_ => throw new ArgumentOutOfRangeException(nameof(idType))
30+
};
31+
32+
var builder = ManagedIdentityApplicationBuilder.Create(miId);
33+
builder.Config.AccessorOptions = null;
34+
return builder.Build();
35+
}
36+
37+
[RunOnAzureDevOps]
38+
[TestCategory("MI_E2E_Imds")]
39+
[DataTestMethod]
40+
[DataRow(null /*SAMI*/, null, DisplayName = "SAMI")]
41+
[DataRow("4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6", "clientid", DisplayName = "UAMI-ClientId")]
42+
[DataRow("/subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourcegroups/MSAL_MSI/providers/Microsoft.ManagedIdentity/userAssignedIdentities/LabVaultAccess_UAMI",
43+
"resourceid", DisplayName = "UAMI-ResourceId")]
44+
[DataRow("1eee55b7-168a-46be-8d19-30e830ee9611", "objectid", DisplayName = "UAMI-ObjectId")]
45+
public async Task AcquireToken_OnImds_Succeeds(string id, string idType)
46+
{
47+
var mi = BuildMi(id, idType);
48+
49+
var result = await mi.AcquireTokenForManagedIdentity(ArmScope)
50+
.ExecuteAsync()
51+
.ConfigureAwait(false);
52+
53+
Assert.IsFalse(string.IsNullOrEmpty(result.AccessToken), "AccessToken should not be empty.");
54+
55+
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource,
56+
"First call must hit MSI endpoint.");
57+
58+
var second = await mi.AcquireTokenForManagedIdentity(ArmScope)
59+
.ExecuteAsync()
60+
.ConfigureAwait(false);
61+
62+
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
63+
Assert.AreEqual(TokenSource.Cache, second.AuthenticationResultMetadata.TokenSource);
64+
Assert.AreEqual(result.AccessToken, second.AccessToken);
65+
}
66+
}
67+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0</TargetFrameworks>
5+
<IsPackable>false</IsPackable>
6+
<Configurations>Debug;Release</Configurations>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="..\..\src\client\Microsoft.Identity.Client\Microsoft.Identity.Client.csproj" />
11+
<ProjectReference Include="..\Microsoft.Identity.Test.Common\Microsoft.Identity.Test.Common.csproj" />
12+
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
14+
<PackageReference Include="MSTest.TestAdapter" />
15+
<PackageReference Include="MSTest.TestFramework" />
16+
17+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
18+
<PackageReference Include="System.Net.Http" />
19+
<PackageReference Include="System.Text.Json" />
20+
<PackageReference Include="coverlet.collector" />
21+
<PackageReference Include="NUnit" />
22+
<PackageReference Include="NUnit3TestAdapter" />
23+
<PackageReference Include="StrongNamer" />
24+
<PackageReference Include="System.Formats.Asn1" />
25+
</ItemGroup>
26+
27+
</Project>

0 commit comments

Comments
 (0)