// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using Amazon.S3;
using Amazon.S3.Model;
using AmazonGameLiftPlugin.Core.BucketManagement.Models;
using AmazonGameLiftPlugin.Core.Shared;
using AmazonGameLiftPlugin.Core.Shared.Logging;
using AmazonGameLiftPlugin.Core.Shared.S3Bucket;
using Serilog;
namespace AmazonGameLiftPlugin.Core.BucketManagement
{
public class BucketStore : IBucketStore
{
///
/// Regex pattern matches with rule defined in the page https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
/// Described here https://stackoverflow.com/questions/50480924/regex-for-s3-bucket-name/50484916
///
private static readonly string s_s3BucketNamePattern = @"(?=^.{3,63}$)(?!^(\d+\.)+\d+$)(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$)";
private readonly IAmazonS3Wrapper _amazonS3Wrapper;
public BucketStore(IAmazonS3Wrapper amazonS3Wrapper)
{
_amazonS3Wrapper = amazonS3Wrapper;
}
public CreateBucketResponse CreateBucket(CreateBucketRequest request)
{
ValidationResult validationResult = Validate(request);
if (!validationResult.IsValid)
{
return Response.Fail(new CreateBucketResponse
{
ErrorCode = validationResult.ErrorCode
});
}
try
{
// Create bootstrap bucket
PutBucketResponse putBucketResponse = _amazonS3Wrapper.PutBucket(new PutBucketRequest
{
BucketName = request.BucketName,
BucketRegionName = request.Region,
});
if (putBucketResponse.HttpStatusCode != HttpStatusCode.OK)
{
return Response.Fail(new CreateBucketResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {putBucketResponse.HttpStatusCode}"
});
}
// TODO (#17): Allow users to toggle audit-logging, versioning and encryption on bootstrap bucket
// Enable bootstrap bucket versioning
PutBucketVersioningResponse putBucketVersioningRequest = _amazonS3Wrapper.PutBucketVersioning(new PutBucketVersioningRequest
{
BucketName = request.BucketName,
VersioningConfig = new S3BucketVersioningConfig
{
Status = VersionStatus.Enabled
}
});
if (putBucketVersioningRequest.HttpStatusCode != HttpStatusCode.OK)
{
return Response.Fail(new CreateBucketResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {putBucketVersioningRequest.HttpStatusCode}"
});
}
// Enable bootstrap bucket server-side encryption
PutBucketEncryptionResponse putBucketEncryptionRequest = _amazonS3Wrapper.PutBucketEncryption(new PutBucketEncryptionRequest
{
BucketName = request.BucketName,
ServerSideEncryptionConfiguration = new ServerSideEncryptionConfiguration
{
ServerSideEncryptionRules = new List
{
new ServerSideEncryptionRule
{
ServerSideEncryptionByDefault = new ServerSideEncryptionByDefault
{
ServerSideEncryptionAlgorithm = ServerSideEncryptionMethod.AES256
}
}
}
}
});
if (putBucketEncryptionRequest.HttpStatusCode != HttpStatusCode.OK)
{
return Response.Fail(new CreateBucketResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {putBucketEncryptionRequest.HttpStatusCode}"
});
}
// Create logging bucket for the bootstrap bucket
string loggingBucketName = request.BucketName + "-log";
PutBucketResponse putLoggingBucketResponse = _amazonS3Wrapper.PutBucket(new PutBucketRequest
{
BucketName = loggingBucketName,
BucketRegionName = request.Region,
});
if (putLoggingBucketResponse.HttpStatusCode != HttpStatusCode.OK)
{
return Response.Fail(new CreateBucketResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {putLoggingBucketResponse.HttpStatusCode}"
});
}
// Getting Existing ACL of the logging bucket
GetACLResponse getACLResponse = _amazonS3Wrapper.GetACL(new GetACLRequest
{
BucketName = loggingBucketName
});
if (getACLResponse.HttpStatusCode != HttpStatusCode.OK)
{
return Response.Fail(new CreateBucketResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {getACLResponse.HttpStatusCode}"
});
}
S3AccessControlList s3AccessControlList = getACLResponse.AccessControlList;
s3AccessControlList.AddGrant(new S3Grantee { URI = "http://acs.amazonaws.com/groups/s3/LogDelivery" }, S3Permission.WRITE);
s3AccessControlList.AddGrant(new S3Grantee { URI = "http://acs.amazonaws.com/groups/s3/LogDelivery" }, S3Permission.READ_ACP);
// Grant logging access to the logging bucket
PutACLResponse putACLResponse = _amazonS3Wrapper.PutACL(new PutACLRequest
{
BucketName = loggingBucketName,
AccessControlList = s3AccessControlList
});
if (putACLResponse.HttpStatusCode != HttpStatusCode.OK)
{
return Response.Fail(new CreateBucketResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {putACLResponse.HttpStatusCode}"
});
}
// Enable access logging on the bootstrap bucket using the newly created logging bucket
PutBucketLoggingResponse putBucketLoggingRequest = _amazonS3Wrapper.PutBucketLogging(new PutBucketLoggingRequest
{
BucketName = request.BucketName,
LoggingConfig = new S3BucketLoggingConfig
{
TargetBucketName = loggingBucketName,
TargetPrefix = "GameLiftBootstrap",
}
});
if (putBucketLoggingRequest.HttpStatusCode != HttpStatusCode.OK)
{
return Response.Fail(new CreateBucketResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {putBucketLoggingRequest.HttpStatusCode}"
});
}
return Response.Ok(new CreateBucketResponse());
}
catch (Exception ex)
{
Logger.LogError(ex, ex.Message);
return HandleAwsException(ex, () => new CreateBucketResponse());
}
}
private ValidationResult Validate(CreateBucketRequest request)
{
if (!AwsRegionMapper.IsValidRegion(request.Region))
{
return ValidationResult.Invalid(ErrorCode.InvalidRegion);
}
bool bucketAlreadyExists = _amazonS3Wrapper.DoesBucketExist(request.BucketName);
if (bucketAlreadyExists)
{
return ValidationResult.Invalid(ErrorCode.BucketNameAlreadyExists);
}
if (!Regex.Match(request.BucketName, s_s3BucketNamePattern).Success)
{
return ValidationResult.Invalid(ErrorCode.BucketNameIsWrong);
}
return ValidationResult.Valid();
}
public Models.PutLifecycleConfigurationResponse PutLifecycleConfiguration(Models.PutLifecycleConfigurationRequest request)
{
if (!Enum.IsDefined(typeof(BucketPolicy), request.BucketPolicy) || request.BucketPolicy == BucketPolicy.None)
{
return Response.Fail(new Models.PutLifecycleConfigurationResponse()
{
ErrorCode = ErrorCode.InvalidBucketPolicy
});
}
GetLifecycleConfigurationResponse lifecycleResponse =
_amazonS3Wrapper.GetLifecycleConfiguration(request.BucketName);
LifecycleConfiguration lifecycleConfiguration = lifecycleResponse.Configuration;
if (lifecycleResponse.HttpStatusCode == HttpStatusCode.NotFound)
{
lifecycleConfiguration = new LifecycleConfiguration
{
Rules = new List()
};
}
lifecycleConfiguration.Rules.Add(new LifecycleRule
{
Id = $"GameLiftBootstrapBucketRule_{Guid.NewGuid()}",
Filter = new LifecycleFilter(),
Expiration = new LifecycleRuleExpiration
{
Days = (int)request.BucketPolicy
},
Status = new LifecycleRuleStatus("Enabled")
});
try
{
Amazon.S3.Model.PutLifecycleConfigurationResponse putLifecycleConfigurationResponse =
_amazonS3Wrapper.PutLifecycleConfiguration(new Amazon.S3.Model.PutLifecycleConfigurationRequest
{
BucketName = request.BucketName,
Configuration = lifecycleConfiguration
});
if (putLifecycleConfigurationResponse.HttpStatusCode == HttpStatusCode.OK)
{
return Response.Ok(new Models.PutLifecycleConfigurationResponse());
}
else
{
return Response.Fail(new Models.PutLifecycleConfigurationResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {putLifecycleConfigurationResponse.HttpStatusCode}"
});
}
}
catch (Exception ex)
{
Logger.LogError(ex, ex.Message);
return HandleAwsException(ex, () => new Models.PutLifecycleConfigurationResponse());
}
}
public GetBucketsResponse GetBuckets(GetBucketsRequest request)
{
try
{
ListBucketsResponse response = _amazonS3Wrapper.ListBuckets();
if (response.HttpStatusCode == HttpStatusCode.OK)
{
var buckets = new List();
if (!string.IsNullOrEmpty(request.Region))
{
foreach (string bucketName in response.Buckets.Select(bucket => bucket.BucketName))
{
GetBucketLocationResponse locationResponse = _amazonS3Wrapper.GetBucketLocation(new GetBucketLocationRequest
{
BucketName = bucketName
});
if (locationResponse.HttpStatusCode == HttpStatusCode.OK && ToBucketRegion(locationResponse) == request.Region)
{
buckets.Add(bucketName);
}
}
}
else
{
buckets = response.Buckets.Select(bucket => bucket.BucketName).ToList();
}
return Response.Ok(new GetBucketsResponse()
{
Buckets = buckets
});
}
else
{
return Response.Fail(new GetBucketsResponse()
{
ErrorCode = ErrorCode.AwsError,
ErrorMessage = $"HTTP Status Code {response.HttpStatusCode}"
});
}
}
catch (Exception ex)
{
Logger.LogError(ex, ex.Message);
return HandleAwsException(ex, () => new GetBucketsResponse());
}
}
private string ToBucketRegion(GetBucketLocationResponse getBucketLocationResponse)
{
string locationValue = getBucketLocationResponse.Location.Value;
// See: https://docs.aws.amazon.com/sdkfornet/v3/apidocs/items/S3/TS3Region.html
switch (locationValue)
{
case "":
return "us-east-1";
case "EU":
return "eu-west-1";
default:
return locationValue;
}
}
public GetAvailableRegionsResponse GetAvailableRegions(GetAvailableRegionsRequest request)
{
return Response.Ok(new GetAvailableRegionsResponse
{
Regions = AwsRegionMapper.AvailableRegions()
});
}
public GetBucketPoliciesResponse GetBucketPolicies(GetBucketPoliciesRequest request)
{
return Response.Ok(new GetBucketPoliciesResponse
{
Policies = (IEnumerable)Enum.GetValues(typeof(BucketPolicy))
});
}
private T HandleAwsException(Exception ex, Func responseObject) where T : Response
{
T response = responseObject();
if (ex is AmazonS3Exception exception)
{
response.ErrorCode = ErrorCode.AwsError;
response.ErrorMessage = exception.Message;
}
else if (ex is WebException
|| ex is ArgumentNullException)
{
response.ErrorCode = ErrorCode.AwsError;
response.ErrorMessage = ex.Message;
}
else
{
throw ex;
}
return Response.Fail(response);
}
}
}