Skip to content

Commit c6d9e31

Browse files
committed
Add support for clients without credentials
Supports public buckets
1 parent ea8ffe5 commit c6d9e31

File tree

19 files changed

+157
-23
lines changed

19 files changed

+157
-23
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Genbox.ProviderTests.Misc;
2+
using Genbox.SimpleS3.Core.Abstracts;
3+
using Genbox.SimpleS3.Core.Abstracts.Enums;
4+
using Genbox.SimpleS3.Core.Enums;
5+
using Genbox.SimpleS3.Core.Extensions;
6+
using Genbox.SimpleS3.Core.Network.Responses.Buckets;
7+
using Genbox.SimpleS3.Core.Network.Responses.Objects;
8+
using Genbox.SimpleS3.Extensions.AmazonS3.Extensions;
9+
using Genbox.SimpleS3.Extensions.BackBlazeB2.Extensions;
10+
using Genbox.SimpleS3.Extensions.GoogleCloudStorage.Extensions;
11+
using Genbox.SimpleS3.Extensions.HttpClientFactory.Extensions;
12+
using Genbox.SimpleS3.Extensions.ProfileManager.Extensions;
13+
using Genbox.SimpleS3.Extensions.Wasabi.Extensions;
14+
using Genbox.SimpleS3.Utility.Shared;
15+
using Microsoft.Extensions.DependencyInjection;
16+
17+
namespace Genbox.ProviderTests.Buckets;
18+
19+
public class MiscTests : TestBase
20+
{
21+
[Theory]
22+
[MultipleProviders(S3Provider.AmazonS3)]
23+
public async Task CanAccessPublicBucket(S3Provider provider, string _1, ISimpleClient authedClient)
24+
{
25+
//This tests if we can access a public bucket without credentials
26+
//We don't use the client provided. Instead, we build a new one.
27+
28+
ServiceCollection collection = new ServiceCollection();
29+
collection.PostConfigure<SimpleS3Config>(config => config.Credentials = null);
30+
31+
ICoreBuilder builder = SimpleS3CoreServices.AddSimpleS3Core(collection);
32+
builder.UseProfileManager()
33+
.BindConfigToProfile("TestSetup-" + provider);
34+
35+
builder.UseHttpClientFactory();
36+
37+
if (provider == S3Provider.AmazonS3)
38+
builder.UseAmazonS3();
39+
else if (provider == S3Provider.BackBlazeB2)
40+
builder.UseBackBlazeB2();
41+
else if (provider == S3Provider.GoogleCloudStorage)
42+
builder.UseGoogleCloudStorage();
43+
else if (provider == S3Provider.Wasabi)
44+
builder.UseWasabi();
45+
46+
await using ServiceProvider services = collection.BuildServiceProvider();
47+
ISimpleClient client = services.GetRequiredService<ISimpleClient>();
48+
49+
//Use the authenticated client to create the bucket, but use the non-authenticated client to put/get object
50+
await CreateTempBucketAsync(provider, authedClient, async tempBucket =>
51+
{
52+
string policy = $$"""
53+
{
54+
"Version": "2012-10-17",
55+
"Id": "Policy1740044169364",
56+
"Statement": [
57+
{
58+
"Sid": "Stmt1740044164489",
59+
"Effect": "Allow",
60+
"Principal": "*",
61+
"Action": "s3:*",
62+
"Resource": "arn:aws:s3:::{{tempBucket}}/*"
63+
}
64+
]
65+
}
66+
""";
67+
68+
PutPublicAccessBlockResponse resp1 = await authedClient.PutPublicAccessBlockAsync(tempBucket, request =>
69+
{
70+
request.BlockPublicAcls = false;
71+
request.BlockPublicPolicy = false;
72+
request.IgnorePublicAcls = false;
73+
request.RestrictPublicBuckets = false;
74+
});
75+
Assert.True(resp1.IsSuccess);
76+
77+
PutBucketPolicyResponse resp2 = await authedClient.PutBucketPolicyAsync(tempBucket, policy);
78+
Assert.True(resp2.IsSuccess);
79+
80+
PutObjectResponse resp3 = await client.PutObjectStringAsync(tempBucket, "test", "test");
81+
Assert.True(resp3.IsSuccess);
82+
83+
GetObjectResponse resp4 = await client.GetObjectAsync(tempBucket, "test");
84+
Assert.True(resp4.IsSuccess);
85+
});
86+
}
87+
}

Src/SimpleS3.BackBlazeB2/BackBlazeB2Client.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public BackBlazeB2Client(string keyId, string accessKey, BackBlazeB2Region regio
3939
/// <param name="credentials">The credentials to use</param>
4040
/// <param name="region">The region you wish to use</param>
4141
/// <param name="networkConfig">Network configuration</param>
42-
public BackBlazeB2Client(IAccessKey credentials, BackBlazeB2Region region, NetworkConfig? networkConfig = null) : this(new BackBlazeB2Config(credentials, region), networkConfig) {}
42+
public BackBlazeB2Client(IAccessKey? credentials, BackBlazeB2Region region, NetworkConfig? networkConfig = null) : this(new BackBlazeB2Config(credentials, region), networkConfig) {}
4343

4444
/// <summary>Creates a new instance of <see cref="BackBlazeB2Client" /></summary>
4545
/// <param name="config">The configuration you want to use</param>

Src/SimpleS3.Core.Abstracts/Enums/ErrorCode.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,5 +255,12 @@ public enum ErrorCode
255255
UnresolvableGrantByEmailAddress,
256256

257257
/// <summary>The bucket POST must contain the specified field name. If it is specified, check the order of the fields.</summary>
258-
UserKeyMustBeSpecified
258+
UserKeyMustBeSpecified,
259+
260+
/// <summary>If you want to apply the Bucket owner enforced setting to disable ACLs, your bucket ACL must give full control only to the bucket owner. Your bucket ACL cannot give access to an external AWS account or any other group. For example, if your CreateBucket request sets Bucket owner enforced and specifies a bucket ACL that provides access to an external AWS account, your request fails with a 400 error and returns the InvalidBucketAclWithObjectOwnership error code. Similarly, if your PutBucketOwnershipControls request sets Bucket owner enforced on a bucket that has a bucket ACL that grants permissions to others, the request fails.</summary>
261+
InvalidBucketAclWithObjectOwnership,
262+
263+
InvalidBucketAclWithBlockPublicAccessError,
264+
265+
MalformedPolicy
259266
}

Src/SimpleS3.Core.Abstracts/SimpleS3Config.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ public SimpleS3Config(string providerName, string endpoint)
1515
Endpoint = endpoint;
1616
}
1717

18-
public SimpleS3Config(IAccessKey credentials, string endpoint, string regionCode) : this(string.Empty, endpoint) //Used internally
18+
public SimpleS3Config(IAccessKey? credentials, string endpoint, string regionCode) : this(string.Empty, endpoint) //Used internally
1919
{
2020
Credentials = credentials;
2121
RegionCode = regionCode;
2222
}
2323

2424
/// <summary>The credentials to use when communicating with S3.</summary>
25-
public IAccessKey Credentials { get; set; }
25+
public IAccessKey? Credentials { get; set; }
2626

2727
/// <summary>
2828
/// There are 3 different signing modes: 1. Unsigned - means the request will be sent without a signature at all. 2. FullSignature - Means the full payload will be hashed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Genbox.SimpleS3.Core.Abstracts;
2+
using Genbox.SimpleS3.Core.Abstracts.Enums;
3+
using Genbox.SimpleS3.Core.Common.Validation;
4+
using Genbox.SimpleS3.Core.Extensions;
5+
using Genbox.SimpleS3.Core.Network.Requests.Objects;
6+
using Genbox.SimpleS3.Extensions.HttpClientFactory.Extensions;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace Genbox.SimpleS3.Core.Tests.OfflineTests;
10+
11+
public class AuthenticationTests
12+
{
13+
[Fact]
14+
public void PreSignRequireCredentials()
15+
{
16+
ServiceCollection collection = new ServiceCollection();
17+
collection.Configure<SimpleS3Config>(config =>
18+
{
19+
config.Endpoint = "http://something.com";
20+
config.RegionCode = "us-east-1";
21+
config.NamingMode = NamingMode.PathStyle;
22+
config.Credentials = null;
23+
});
24+
25+
ICoreBuilder builder = SimpleS3CoreServices.AddSimpleS3Core(collection);
26+
builder.UseHttpClientFactory();
27+
28+
using var provider = collection.BuildServiceProvider();
29+
ISimpleClient client = provider.GetRequiredService<ISimpleClient>();
30+
31+
Assert.Throws<InvalidOperationException>(() => client.SignRequest(new GetObjectRequest("test", "name"), TimeSpan.FromHours(1)));
32+
}
33+
}

Src/SimpleS3.Core/Internals/Authentication/SigningKeyBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Genbox.SimpleS3.Core.Abstracts;
44
using Genbox.SimpleS3.Core.Abstracts.Authentication;
55
using Genbox.SimpleS3.Core.Common.Helpers;
6+
using Genbox.SimpleS3.Core.Common.Validation;
67
using Genbox.SimpleS3.Core.Internals.Misc;
78
using Microsoft.Extensions.Logging;
89
using Microsoft.Extensions.Options;
@@ -36,6 +37,7 @@ public SigningKeyBuilder(IOptions<SimpleS3Config> options, ILogger<SigningKeyBui
3637

3738
public byte[] CreateSigningKey(DateTimeOffset dateTime)
3839
{
40+
Validator.RequireNotNull(_config.Credentials, "If we get to this point, we expect to have valid credentials");
3941
_logger.LogTrace("Creating key created on {DateTime}", dateTime);
4042

4143
//https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html

Src/SimpleS3.Core/Internals/Builders/HeaderAuthorizationBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public void BuildAuthorization(IRequest request)
2929

3030
internal string BuildInternal(DateTimeOffset date, IReadOnlyDictionary<string, string> headers, byte[] signature)
3131
{
32+
Validator.RequireNotNull(_config.Credentials, "If we get to this point, we expect to have valid credentials");
3233
logger.LogTrace("Building header based authorization");
3334

3435
string scope = scopeBuilder.CreateScope("s3", date);

Src/SimpleS3.Core/Internals/Network/ChunkedContentRequestStreamWrapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public ChunkedContentRequestStreamWrapper(IOptions<SimpleS3Config> config, IChun
3030
_config = config.Value;
3131
}
3232

33-
public bool IsSupported(IRequest request) => _config.PayloadSignatureMode == SignatureMode.StreamingSignature && request is ISupportStreaming;
33+
public bool IsSupported(IRequest request) => _config.PayloadSignatureMode == SignatureMode.StreamingSignature && request is ISupportStreaming && _config.Credentials != null;
3434

3535
public Stream Wrap(Stream input, IRequest request)
3636
{

Src/SimpleS3.Core/Internals/Network/DefaultSignedRequestHandler.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public DefaultSignedRequestHandler(IOptions<SimpleS3Config> options, IMarshalFac
4848

4949
public string SignRequest<TReq>(TReq request, TimeSpan expiresIn) where TReq : IRequest
5050
{
51+
if (_config.Credentials == null)
52+
throw new InvalidOperationException("You cannot pre-sign requests without first providing credentials");
53+
5154
request.Timestamp = DateTimeOffset.UtcNow;
5255
request.RequestId = Guid.NewGuid();
5356

Src/SimpleS3.Core/Internals/Validation/Validators/Configs/ConfigValidator.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ namespace Genbox.SimpleS3.Core.Internals.Validation.Validators.Configs;
77

88
internal sealed class ConfigValidator : ConfigValidatorBase<SimpleS3Config>
99
{
10-
public ConfigValidator(IValidator<IAccessKey> validator)
10+
public ConfigValidator(IValidator<IAccessKey?> validator)
1111
{
1212
RuleFor(x => x.Endpoint).NotEmpty().WithMessage("You must provide an endpoint.");
1313
RuleFor(x => x.RegionCode).NotEmpty().WithMessage("You must provide a region");
1414
RuleFor(x => x.PayloadSignatureMode).IsInEnum().Must(x => x != SignatureMode.Unknown).WithMessage("You must provide a valid payload signature mode");
1515
RuleFor(x => x.NamingMode).IsInEnum().Must(x => x != NamingMode.Unknown).WithMessage("You must provide a valid naming mode");
16-
RuleFor(x => x.NamingMode).IsInEnum().Must(x => x == NamingMode.PathStyle).When(x => !x.Endpoint.Contains('{')).WithMessage("You can only use NamingMode.VirtualHost when you specify an endpoint template.");
1716

18-
RuleFor(x => x.ObjectKeyValidationMode).IsInEnum().Must(x => x != ObjectKeyValidationMode.Unknown).WithMessage("You must provide a valid object key validation mode");
17+
// We have to check x.EndPoint != null due to the way validation is run
18+
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
19+
RuleFor(x => x.NamingMode).IsInEnum().Must(x => x == NamingMode.PathStyle).When(x => x.Endpoint?.Contains('{') == false).WithMessage("You can only use NamingMode.VirtualHost when you specify an endpoint template.");
1920

20-
IRuleBuilderOptions<SimpleS3Config, IAccessKey> validatorRule = RuleFor(x => x.Credentials).NotNull().WithMessage("You must provide credentials");
21-
validatorRule.SetValidator(validator);
21+
RuleFor(x => x.ObjectKeyValidationMode).IsInEnum().Must(x => x != ObjectKeyValidationMode.Unknown).WithMessage("You must provide a valid object key validation mode");
22+
RuleFor(x => x.Credentials).SetValidator(validator);
2223

2324
//See https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
2425
RuleFor(x => x.StreamingChunkSize).GreaterThanOrEqualTo(8096).WithMessage("Please specify a chunk size of more than or equal to 8096 bytes");

0 commit comments

Comments
 (0)