diff --git a/generator/.DevConfigs/c2e4913e-b19a-4bca-b372-322bffb0662b.json b/generator/.DevConfigs/c2e4913e-b19a-4bca-b372-322bffb0662b.json new file mode 100644 index 000000000000..084297338565 --- /dev/null +++ b/generator/.DevConfigs/c2e4913e-b19a-4bca-b372-322bffb0662b.json @@ -0,0 +1,11 @@ +{ + "services": [ + { + "serviceName": "S3", + "type": "patch", + "changeLogMessages": [ + "Move S3PostUploadSignedPolicy out of _bcl folder" + ] + } + ] +} diff --git a/sdk/src/Services/S3/Custom/Util/_bcl/S3PostUploadSignedPolicy.cs b/sdk/src/Services/S3/Custom/Util/S3PostUploadSignedPolicy.cs similarity index 54% rename from sdk/src/Services/S3/Custom/Util/_bcl/S3PostUploadSignedPolicy.cs rename to sdk/src/Services/S3/Custom/Util/S3PostUploadSignedPolicy.cs index 0a40a93c755c..456711bc0fa4 100644 --- a/sdk/src/Services/S3/Custom/Util/_bcl/S3PostUploadSignedPolicy.cs +++ b/sdk/src/Services/S3/Custom/Util/S3PostUploadSignedPolicy.cs @@ -19,17 +19,18 @@ * */ -using System; -using System.IO; -using System.Text; -using System.Xml.Serialization; - using Amazon.Runtime; using Amazon.Util; -using System.Globalization; +using Amazon.Util.Internal; +using System; using System.Collections.Generic; -using System.Text.Json.Nodes; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; using System.Text.Json; +using System.Xml.Serialization; +using System.Diagnostics.CodeAnalysis; namespace Amazon.S3.Util { @@ -47,14 +48,59 @@ namespace Amazon.S3.Util public class S3PostUploadSignedPolicy { private static JsonDocumentOptions options = new JsonDocumentOptions { AllowTrailingCommas = true }; - /// - /// Given a policy and AWS credentials, produce a S3PostUploadSignedPolicy. - /// - /// JSON string representing the policy to sign - /// Credentials to sign the policy with - /// Service region endpoint. - /// A signed policy object for use with an S3PostUploadRequest. - public static S3PostUploadSignedPolicy GetSignedPolicy(string policy, AWSCredentials credentials, RegionEndpoint region) + + private static string + KEY_POLICY = "policy", + KEY_SIGNATURE = "signature", + KEY_ACCESSKEY = "access_key"; + + /// + /// The policy document which governs what uploads can be done. + /// + public string Policy { get; set; } + + /// + /// The signature for the policy. + /// + public string Signature { get; set; } + + /// + /// The AWS Access Key Id for the credential pair that produced the signature. + /// + public string AccessKeyId { get; set; } + + /// + /// The security token from session or instance credentials. + /// + public string SecurityToken { get; set; } + + /// + /// The signing algorithm used. Required as a field in the post Amazon + /// S3 can re-calculate the signature. + /// + public string Algorithm { get; set; } + + /// + /// The date value in ISO8601 format. It is the same date used in + /// creating the signing key. + /// + public string Date { get; set; } + + /// + /// In addition to the access key ID, this provides scope information + /// used in calculating the signing key for signature calculation. + /// + public string Credential { get; set; } + + + /// + /// Given a policy and AWS credentials, produce a S3PostUploadSignedPolicy. + /// + /// JSON string representing the policy to sign + /// Credentials to sign the policy with + /// Service region endpoint. + /// A signed policy object for use with an S3PostUploadRequest. + public static S3PostUploadSignedPolicy GetSignedPolicy(string policy, AWSCredentials credentials, RegionEndpoint region) { var signedAt = AWSSDKUtils.CorrectedUtcNow; @@ -71,7 +117,7 @@ public static S3PostUploadSignedPolicy GetSignedPolicy(string policy, AWSCredent }; if (iCreds.UseToken) { extraConditions[S3Constants.PostFormDataSecurityToken] = iCreds.Token; } - var policyBytes = addConditionsToPolicy(policy, extraConditions); + var policyBytes = AddConditionsToPolicy(policy, extraConditions); var base64Policy = Convert.ToBase64String(policyBytes); @@ -91,75 +137,126 @@ public static S3PostUploadSignedPolicy GetSignedPolicy(string policy, AWSCredent }; } - private static byte[] addConditionsToPolicy(string policy, Dictionary newConditions) + /// + /// Adds or updates conditions in an S3 POST policy document while preserving other elements. + /// This method creates a new policy document that includes all original policy properties, + /// updates any existing conditions that match the new conditions, and adds any new conditions. + /// + /// Original policy document as JSON string + /// Dictionary of new conditions to add or update + /// UTF-8 bytes of the modified policy document + private static byte[] AddConditionsToPolicy(string policy, Dictionary newConditions) { - var json = JsonNode.Parse(policy, null, options) as JsonObject; - var jsonConditions = json["conditions"] as JsonArray; - - if (jsonConditions != null) + using (JsonDocument policyDoc = JsonDocument.Parse(policy, options)) { - foreach (var newCond in newConditions) + using (var ms = new MemoryStream()) { - bool found = false; - for (int i = 0; i < jsonConditions.Count; i++) + using (var writer = new Utf8JsonWriter(ms)) { - if (jsonConditions[i] is JsonObject obj && obj.ContainsKey(newCond.Key)) + // Step 1: Start writing the new policy document object + writer.WriteStartObject(); + + // Step 2: Copy all properties except "conditions" from the original policy + // (We'll handle the conditions array separately) + foreach (var property in policyDoc.RootElement.EnumerateObject()) { - obj[newCond.Key] = newCond.Value; - found = true; + if (property.Name != "conditions") + { + property.WriteTo(writer); + } } - } - if (!found) - { - var newCondition = new JsonObject + // Step 3: Start writing the new "conditions" array + writer.WritePropertyName("conditions"); + writer.WriteStartArray(); + + // Step 4: Check if the original policy has a conditions array + bool foundConditions = policyDoc.RootElement.TryGetProperty("conditions", out JsonElement conditionsElement); + + if (foundConditions && conditionsElement.ValueKind == JsonValueKind.Array) { - [newCond.Key] = newCond.Value - }; - jsonConditions.Add(newCondition); - } - } - } - return Encoding.UTF8.GetBytes(json.ToJsonString().Trim()); - } - - /// - /// The policy document which governs what uploads can be done. - /// - public string Policy { get; set; } + // Step 5: Process each existing condition in the original policy + foreach (JsonElement condition in conditionsElement.EnumerateArray()) + { + bool shouldCopyCondition = true; // Default: copy the original condition as-is + string matchedKey = null; // Track if we found a key to update - /// - /// The signature for the policy. - /// - public string Signature { get; set; } + // Step 5a: If this condition is an object, check if it contains any keys + // that match our new conditions (meaning we need to update this condition) + if (condition.ValueKind == JsonValueKind.Object) + { + // Optimized: Directly iterate over dictionary entries to avoid ToList allocation + foreach (var newCond in newConditions) + { + if (condition.TryGetProperty(newCond.Key, out _)) + { + // This condition contains a key we want to update + matchedKey = newCond.Key; + shouldCopyCondition = false; // Don't copy as-is, we'll update it + break; + } + } + } - /// - /// The AWS Access Key Id for the credential pair that produced the signature. - /// - public string AccessKeyId { get; set; } + // Step 5b: Handle the condition based on whether we need to update it + if (shouldCopyCondition) + { + // Case 1: This condition doesn't need updating, copy it as-is + condition.WriteTo(writer); + } + else if (matchedKey != null) + { + // Case 2: This condition needs updating because it contains a key + // that matches one of our new conditions + + // Start a new object for the updated condition + writer.WriteStartObject(); + + // Process each property in the original condition + foreach (var property in condition.EnumerateObject()) + { + if (property.Name == matchedKey) + { + // This is the property we want to update with the new value + writer.WriteString(matchedKey, newConditions[matchedKey]); - /// - /// The security token from session or instance credentials. - /// - public string SecurityToken { get; set; } - - /// - /// The signing algorithm used. Required as a field in the post Amazon - /// S3 can re-calculate the signature. - /// - public string Algorithm { get; set; } + // Remove this key from newConditions since we've now handled it + // (It won't need to be added as a new condition at the end) + newConditions.Remove(matchedKey); + } + else + { + // This is a property we want to preserve (not update) + // Without this else clause, properties other than the one being updated would be lost + property.WriteTo(writer); + } + } + + // End the updated condition object + writer.WriteEndObject(); + } + } + } - /// - /// The date value in ISO8601 format. It is the same date used in - /// creating the signing key. - /// - public string Date { get; set; } + // Step 6: Add any remaining new conditions that weren't updates to existing ones + foreach (var newCond in newConditions) + { + writer.WriteStartObject(); + writer.WriteString(newCond.Key, newCond.Value); + writer.WriteEndObject(); + } - /// - /// In addition to the access key ID, this provides scope information - /// used in calculating the signing key for signature calculation. - /// - public string Credential { get; set; } + // Step 7: Close the conditions array and the overall policy object + writer.WriteEndArray(); + writer.WriteEndObject(); + writer.Flush(); + + // Step 8: Return the complete modified policy document as bytes + return ms.ToArray(); + } + } + } + } /// /// Get the policy document as a human readable string. @@ -170,31 +267,34 @@ public string GetReadablePolicy() return Encoding.UTF8.GetString(Convert.FromBase64String(this.Policy)); } - private static string - KEY_POLICY = "policy", - KEY_SIGNATURE = "signature", - KEY_ACCESSKEY = "access_key"; - + /// /// JSON representation of this object /// /// JSON string public string ToJson() { - var json = new JsonObject + var obj = new Dictionary { [KEY_POLICY] = this.Policy, [KEY_SIGNATURE] = this.Signature, [KEY_ACCESSKEY] = this.AccessKeyId }; - return json.ToJsonString(); + return JsonSerializerHelper.Serialize>( + obj, + DictionaryStringStringJsonSerializerContexts.Default + ); } /// /// XML Representation of this object /// /// XML String +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("ToXml is not supported for Native AOT.")] + +#endif public string ToXml() { StringBuilder xml = new StringBuilder(1024); @@ -206,6 +306,40 @@ public string ToXml() return xml.ToString(); } + /// + /// Create an instance of this class from an XML string. + /// + /// XML string generated by ToXml() + /// Instance of S3PostUploadSignedPolicy +#if NET8_0_OR_GREATER + [RequiresUnreferencedCode("GetSignedPolicyFromXml is not supported for Native AOT.")] + +#endif + public static S3PostUploadSignedPolicy GetSignedPolicyFromXml(string policyXml) + { + var reader = new StringReader(policyXml); + XmlSerializer serializer = new XmlSerializer(typeof(S3PostUploadSignedPolicy)); + + S3PostUploadSignedPolicy policy; + try + { + policy = serializer.Deserialize(reader) as S3PostUploadSignedPolicy; + } + catch (Exception e) + { + throw new ArgumentException("Could not parse XML", e); + } + + if (String.IsNullOrEmpty(policy.AccessKeyId)) + throw new ArgumentException("XML Document requries 'AccessKeyId' field"); + if (String.IsNullOrEmpty(policy.Policy)) + throw new ArgumentException("XML Document requries 'Policy' field"); + if (String.IsNullOrEmpty(policy.Signature)) + throw new ArgumentException("XML Document requries 'Signature' field"); + + return policy; + } + /// /// Create an instance of this class from a JSON string. /// @@ -240,35 +374,5 @@ public static S3PostUploadSignedPolicy GetSignedPolicyFromJson(string policyJson throw new ArgumentException("Invalid JSON document", e); } } - - /// - /// Create an instance of this class from an XML string. - /// - /// XML string generated by ToXml() - /// Instance of S3PostUploadSignedPolicy - public static S3PostUploadSignedPolicy GetSignedPolicyFromXml(string policyXml) - { - var reader = new StringReader(policyXml); - XmlSerializer serializer = new XmlSerializer(typeof(S3PostUploadSignedPolicy)); - - S3PostUploadSignedPolicy policy; - try - { - policy = serializer.Deserialize(reader) as S3PostUploadSignedPolicy; - } - catch (Exception e) - { - throw new ArgumentException("Could not parse XML", e); - } - - if (String.IsNullOrEmpty(policy.AccessKeyId)) - throw new ArgumentException("XML Document requries 'AccessKeyId' field"); - if (String.IsNullOrEmpty(policy.Policy)) - throw new ArgumentException("XML Document requries 'Policy' field"); - if (String.IsNullOrEmpty(policy.Signature)) - throw new ArgumentException("XML Document requries 'Signature' field"); - - return policy; - } } }