Skip to content

Commit 9c8f94f

Browse files
Amazon s3 media options update (#11871)
Co-authored-by: ngv <[email protected]> Co-authored-by: Zoltán Lehóczky <[email protected]>
1 parent 328f027 commit 9c8f94f

File tree

10 files changed

+102
-135
lines changed

10 files changed

+102
-135
lines changed

src/OrchardCore.Build/Dependencies.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<PackageManagement Include="AngleSharp" Version="0.17.1" />
1212
<PackageManagement Include="AWSSDK.S3" Version="3.7.9.10" />
1313
<PackageManagement Include="AWSSDK.SecurityToken" Version="3.7.1.159" />
14+
<PackageManagement Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.2" />
1415
<PackageManagement Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
1516
<PackageManagement Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.2.1" />
1617
<PackageManagement Include="Azure.Identity" Version="1.6.0" />

src/OrchardCore.Cms.Web/appsettings.json

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,16 @@
4444
//}
4545
// See https://docs.orchardcore.net/en/latest/docs/reference/modules/Media.AmazonS3/#configuration to configure media storage in Amazon S3 Storage.
4646
//"OrchardCore_Media_AmazonS3": {
47-
// "BucketName": "somebucketname",
48-
// Credentials section needed only if Orchard will be hosted not in the AWS Cloud
49-
// since in AWS you don't need to store this info in the appsettings file, you just
50-
// need to set BucketName and BasePath.
51-
// "Credentials": {
52-
// "SecretKey": "",
53-
// "AccessKeyId": "",
54-
// "RegionEndpoint": "eu-central-1"
55-
// },
56-
// "BasePath": "/media",
57-
// "ProfileName": "",
58-
// "CreateBucket" : true
47+
// "Region": "eu-central-1",
48+
// "Profile": "default",
49+
// "ProfilesLocation": "",
50+
// "Credentials": {
51+
// "SecretKey": "",
52+
// "AccessKey": ""
53+
// },
54+
// "BasePath": "/media",
55+
// "CreateBucket": true,
56+
// "BucketName": ""
5957
//},
6058
// See https://docs.orchardcore.net/en/latest/docs/reference/modules/Media.Azure/#configuration to configure media storage in Azure Blob Storage.
6159
//"OrchardCore_Media_Azure":

src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public AwsStorageOptionsConfiguration(
2929

3030
public void Configure(AwsStorageOptions options)
3131
{
32-
options.BindConfiguration(_shellConfiguration);
32+
options.BindConfiguration(_shellConfiguration, _logger);
3333

3434
var templateOptions = new TemplateOptions();
3535
var templateContext = new TemplateContext(templateOptions);

src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsExtension.cs

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
using System.Collections.Generic;
33
using System.ComponentModel.DataAnnotations;
44
using Amazon;
5+
using Amazon.Extensions.NETCore.Setup;
6+
using Amazon.Runtime;
57
using Amazon.Runtime.CredentialManagement;
68
using Microsoft.Extensions.Configuration;
9+
using Microsoft.Extensions.Logging;
710
using OrchardCore.Environment.Shell.Configuration;
811
using OrchardCore.FileStorage.AmazonS3;
912

@@ -18,30 +21,20 @@ public static IEnumerable<ValidationResult> Validate(this AwsStorageOptions opti
1821
yield return new ValidationResult(Constants.ValidationMessages.BucketNameIsEmpty);
1922
}
2023

21-
if (options.Credentials != null)
24+
if (options.AwsOptions is not null)
2225
{
23-
if (String.IsNullOrWhiteSpace(options.Credentials.SecretKey))
24-
{
25-
yield return new ValidationResult(Constants.ValidationMessages.SecretKeyIsEmpty);
26-
}
27-
28-
if (String.IsNullOrWhiteSpace(options.Credentials.AccessKeyId))
29-
{
30-
yield return new ValidationResult(Constants.ValidationMessages.AccessKeyIdIsEmpty);
31-
}
32-
33-
if (String.IsNullOrWhiteSpace(options.Credentials.RegionEndpoint))
26+
if (options.AwsOptions.Region is null)
3427
{
3528
yield return new ValidationResult(Constants.ValidationMessages.RegionEndpointIsEmpty);
3629
}
3730
}
3831
}
3932

40-
public static AwsStorageOptions BindConfiguration(this AwsStorageOptions options, IShellConfiguration shellConfiguration)
33+
public static AwsStorageOptions BindConfiguration(this AwsStorageOptions options, IShellConfiguration shellConfiguration, ILogger logger)
4134
{
4235
var section = shellConfiguration.GetSection("OrchardCore_Media_AmazonS3");
4336

44-
if (section == null)
37+
if (!section.Exists())
4538
{
4639
return options;
4740
}
@@ -50,41 +43,33 @@ public static AwsStorageOptions BindConfiguration(this AwsStorageOptions options
5043
options.BasePath = section.GetValue(nameof(options.BasePath), String.Empty);
5144
options.CreateBucket = section.GetValue(nameof(options.CreateBucket), false);
5245

53-
var credentials = section.GetSection("Credentials");
54-
if (credentials.Exists())
46+
try
5547
{
56-
options.Credentials = new AwsStorageCredentials
57-
{
58-
RegionEndpoint =
59-
credentials.GetValue(nameof(options.Credentials.RegionEndpoint), RegionEndpoint.USEast1.SystemName),
60-
SecretKey = credentials.GetValue(nameof(options.Credentials.SecretKey), String.Empty),
61-
AccessKeyId = credentials.GetValue(nameof(options.Credentials.AccessKeyId), String.Empty),
62-
};
48+
// Binding AWS Options
49+
options.AwsOptions = shellConfiguration.GetAWSOptions("OrchardCore_Media_AmazonS3");
6350

64-
}
65-
else
66-
{
67-
// Attempt to load Credentials from Profile.
68-
var profileName = section.GetValue("ProfileName", String.Empty);
69-
if (!String.IsNullOrEmpty(profileName))
51+
// In case Credentials sections was specified, trying to add BasicAWSCredential to AWSOptions
52+
// since by design GetAWSOptions skips Credential section while parsing config.
53+
var credentials = section.GetSection("Credentials");
54+
if (credentials.Exists())
7055
{
71-
var chain = new CredentialProfileStoreChain();
72-
if (chain.TryGetProfile(profileName, out var basicProfile))
56+
var secretKey = credentials.GetValue(Constants.AwsCredentialParamNames.SecretKey, String.Empty);
57+
var accessKey = credentials.GetValue(Constants.AwsCredentialParamNames.AccessKey, String.Empty);
58+
59+
if (!String.IsNullOrWhiteSpace(accessKey) ||
60+
!String.IsNullOrWhiteSpace(secretKey))
7361
{
74-
var awsCredentials = basicProfile.GetAWSCredentials(chain)?.GetCredentials();
75-
if (awsCredentials != null)
76-
{
77-
options.Credentials = new AwsStorageCredentials
78-
{
79-
RegionEndpoint = basicProfile.Region.SystemName ?? RegionEndpoint.USEast1.SystemName,
80-
SecretKey = awsCredentials.SecretKey,
81-
AccessKeyId = awsCredentials.AccessKey
82-
};
83-
}
62+
var awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
63+
options.AwsOptions.Credentials = awsCredentials;
8464
}
8565
}
86-
}
8766

88-
return options;
67+
return options;
68+
}
69+
catch (ConfigurationException ex)
70+
{
71+
logger.LogCritical(ex, ex.Message);
72+
throw;
73+
}
8974
}
9075
}

src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Constants.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ internal static class ValidationMessages
66
{
77
public const string BucketNameIsEmpty = "BucketName is required attribute for S3 Media";
88

9-
public const string SecretKeyIsEmpty =
10-
"SecretKey is required attribute for S3 Media, make sure it exists in Credentials section or ProfileName you specified";
11-
12-
public const string AccessKeyIdIsEmpty =
13-
"AccessKeyId is required attribute for S3 Media, make sure it exists in Credentials section or ProfileName you specified";
9+
public const string RegionEndpointIsEmpty = "Region is required attribute for S3 Media";
10+
}
1411

15-
public const string RegionEndpointIsEmpty =
16-
"Region is required attribute for S3 Media, make sure it exists in Credentials section or ProfileName you specified";
12+
internal static class AwsCredentialParamNames
13+
{
14+
public const string SecretKey = "SecretKey";
15+
public const string AccessKey = "AccessKey";
1716
}
1817
}

src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Startup.cs

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
using System.IO;
33
using System.Linq;
44
using System.Text;
5-
using Amazon;
6-
using Amazon.Runtime;
75
using Amazon.S3;
86
using Microsoft.AspNetCore.Hosting;
97
using Microsoft.AspNetCore.Http;
@@ -36,7 +34,7 @@ public override void ConfigureServices(IServiceCollection services)
3634
{
3735
services.AddTransient<IConfigureOptions<AwsStorageOptions>, AwsStorageOptionsConfiguration>();
3836

39-
var storeOptions = new AwsStorageOptions().BindConfiguration(_configuration);
37+
var storeOptions = new AwsStorageOptions().BindConfiguration(_configuration, _logger);
4038
var validationErrors = storeOptions.Validate().ToList();
4139
var stringBuilder = new StringBuilder();
4240

@@ -87,31 +85,8 @@ public override void ConfigureServices(IServiceCollection services)
8785
services.AddSingleton<IMediaFileStoreCache>(serviceProvider =>
8886
serviceProvider.GetRequiredService<IMediaFileStoreCacheFileProvider>());
8987

90-
services.AddSingleton<IAmazonS3>(serviceProvider =>
91-
{
92-
var options = serviceProvider.GetRequiredService<IOptions<AwsStorageOptions>>().Value;
93-
if (options.Credentials == null)
94-
{
95-
return new AmazonS3Client();
96-
}
97-
98-
var config = new AmazonS3Config
99-
{
100-
RegionEndpoint = RegionEndpoint.GetBySystemName(options.Credentials.RegionEndpoint),
101-
UseHttp = true,
102-
ForcePathStyle = true,
103-
UseArnRegion = true
104-
};
105-
106-
if (String.IsNullOrWhiteSpace(options.Credentials.AccessKeyId))
107-
{
108-
return new AmazonS3Client(new ECSTaskCredentials(), config);
109-
}
110-
111-
return new AmazonS3Client(options.Credentials.AccessKeyId,
112-
options.Credentials.SecretKey,
113-
config);
114-
});
88+
// Registering IAmazonS3 client using AWS registration factory.
89+
services.AddAWSService<IAmazonS3>(storeOptions.AwsOptions);
11590

11691
services.Replace(ServiceDescriptor.Singleton<IMediaFileStore>(serviceProvider =>
11792
{
Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace OrchardCore.FileStorage.AmazonS3;
1+
using Amazon.Extensions.NETCore.Setup;
2+
3+
namespace OrchardCore.FileStorage.AmazonS3;
24

35
/// <summary>
46
/// AWS storage options.
@@ -21,35 +23,8 @@ public class AwsStorageOptions
2123
public bool CreateBucket { get; set; }
2224

2325
/// <summary>
24-
/// Gets or sets the credentials.
25-
/// <remarks>
26-
/// Credentials section can be set directly via configuration or get loaded from the configured ProfileName.
27-
/// For development purposes, it is recommended to specify just ProfileName.
28-
/// For a production environment this section should be null, AWS SDK Services will get the default credentials
29-
/// from environment variables.
30-
/// </remarks>
26+
/// Gets or sets the AWS Options.
3127
/// </summary>
32-
public AwsStorageCredentials Credentials { get; set; }
33-
28+
public AWSOptions AwsOptions { get; set; }
3429
}
3530

36-
/// <summary>
37-
/// The AWS storage credentials.
38-
/// </summary>
39-
public class AwsStorageCredentials
40-
{
41-
/// <summary>
42-
/// AWS region name
43-
/// </summary>
44-
public string RegionEndpoint { get; set; }
45-
46-
/// <summary>
47-
/// AWS account secret key. Do not use root's user secret key!
48-
/// </summary>
49-
public string SecretKey { get; set; }
50-
51-
/// <summary>
52-
/// AWS account access key Id.
53-
/// </summary>
54-
public string AccessKeyId { get; set; }
55-
}

src/OrchardCore/OrchardCore.FileStorage.AmazonS3/OrchardCore.FileStorage.AmazonS3.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<ItemGroup>
1818
<PackageReference Include="AWSSDK.S3" />
1919
<PackageReference Include="AWSSDK.SecurityToken" />
20+
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" />
2021
</ItemGroup>
2122

2223
<ItemGroup>

src/docs/reference/modules/Media.AmazonS3/README.md

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,22 @@ The following configuration values are used by default and can be customized:
1818
{
1919
"OrchardCore": {
2020
"OrchardCore_Media_AmazonS3": {
21-
// Your AWS S3 Bucket name
22-
"BucketName": "",
21+
// If you have AWS CLI installed and configured you may just specify a profile name.
22+
"Profile": "",
23+
// In case your AWS profiles are located not in the default place.
24+
"ProfilesLocation": "",
25+
"Region": "",
2326
// This section needed only if Orchard will be hosted not in the AWS Cloud
2427
// You can obtain all that information in the IAM Management Console
2528
"Credentials": {
2629
"SecretKey": "",
27-
"AccessKeyId": "",
28-
"RegionEndpoint": ""
30+
"AccessKey": ""
2931
},
3032
// Optionally, set to a path to store media in a subdirectory inside your container.
3133
"BasePath": "/media",
32-
// If you have aws cli installed and configured you may just specify profile name
33-
"ProfileName": "",
34-
"CreateBucket": false
34+
"CreateBucket": false,
35+
// Your AWS S3 Bucket name.
36+
"BucketName": ""
3537
}
3638
}
3739
}
@@ -45,7 +47,29 @@ In case you are hosting Orchard Core inside AWS (EC2, EKS, etc.) you need to con
4547

4648
In case you are hosting Orchard Core outside of AWS, you should fill the `Credentials` section or if you have AWS CLI installed and configured on your server you may specify only configured profile name (`default` if a profile name was not chosen during AWS CLI configuration).
4749

48-
You can find region endpoints in the [Official AWS S3 Documentation](https://docs.aws.amazon.com/general/latest/gr/s3.html), see Region column. For example for the Frankfurt region you should use `eu-central-1`
50+
You can find region endpoints in the [Official AWS S3 Documentation](https://docs.aws.amazon.com/general/latest/gr/s3.html), see Region column. For example for the Frankfurt region you should use `eu-central-1`
51+
52+
## AWS Credentials and its loading order
53+
54+
`OrchardCore_Media_AmazonS3` is a subset of `AWSOptions` configuration and should be configured the same as a generic [AWSOptions](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-netcore.html).
55+
56+
### Credentials loading order
57+
58+
1. Credentials property of `AWSOptions˙.
59+
2. Shared Credentials File (Custom Location). When both the profile and profile location are specified.
60+
3. SDK Store (Windows Only). When an instance of `AWSOptions˙ is provided and only the profile is set (profile location is null or empty).
61+
4. Shared Credentials File (Default Location). When an instance of `AWSOptions˙ is provided and only the profile is set (profile location is null or empty).
62+
5. AWS Web Identity Federation Credentials. When an OIDC token file exists and is set in the environment variables.
63+
6. `CredentialsProfileStoreChain`
64+
1. SDK Store (Windows Only) encrypted using Windows Data Protection API.
65+
2. Shared Credentials File in the default location.
66+
7. Environment variables. When the Access Key ID and Secret Access Key environment variables are set.
67+
8. ECS Task Credentials or EC2 Instance Credentials. When using IAM roles with ECS tasks and ECS instances.
68+
69+
!!! note
70+
The AWS team wants to encourage using profiles instead of embedding credentials directly into `appsettings.X.json` files where they would accidentally get checked into source control.
71+
If you have an option to use profiles or environment variables - you should use it instead of direct credentials.
72+
4973

5074
## AWS S3 Bucket Configuration
5175

@@ -95,13 +119,14 @@ The `BucketName` property and the `BasePath` property are the only templatable p
95119
"OrchardCore": {
96120
"OrchardCore_Media_AmazonS3": {
97121
"BucketName": "{{ ShellSettings.Name }}-media",
122+
"Region": "",
98123
"Credentials": {
99124
"SecretKey": "",
100-
"AccessKeyId": "",
101-
"RegionEndpoint": ""
125+
"AccessKey": ""
102126
},
103127
"BasePath": "/media",
104-
"ProfileName": ""
128+
"Profile": "",
129+
"ProfilesLocation": ""
105130
}
106131
}
107132
}
@@ -114,13 +139,14 @@ The `BucketName` property and the `BasePath` property are the only templatable p
114139
"OrchardCore": {
115140
"OrchardCore_Media_AmazonS3": {
116141
"BucketName": "",
142+
"Region": "",
117143
"Credentials": {
118144
"SecretKey": "",
119-
"AccessKeyId": "",
120-
"RegionEndpoint": ""
145+
"AccessKey": ""
121146
},
122147
"BasePath": "{{ ShellSettings.Name }}/Media",
123-
"ProfileName": ""
148+
"Profile": "",
149+
"ProfilesLocation" : ""
124150
}
125151
}
126152
}

src/docs/releases/1.5.0.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Orchard Core 1.5.0
2+
3+
## Breaking Changes
4+
5+
* The `OrchardCore_Media_AmazonS3` config section was changed: `RegionEndpoint` was renamed to `Region` and extracted from `Credentials` section to the root section of `OrchardCore_Media_AmazonS3`, `AccessKeyId` was renamed to `AccessKey`, `ProfileName` was renamed to `Profile`. See [this pull request](https://github.com/OrchardCMS/OrchardCore/pull/11871) for details.
6+
7+

0 commit comments

Comments
 (0)