Skip to content

Commit 4884b98

Browse files
committed
1. Added required validations in lifecycle rules for filter, expiration and AboutIncompleteMultipartUpload
2. Added tests for the above validations Signed-off-by: Aayush Chouhan <[email protected]>
1 parent a3b2dbe commit 4884b98

File tree

4 files changed

+154
-22
lines changed

4 files changed

+154
-22
lines changed

src/endpoint/s3/ops/s3_put_bucket_lifecycle.js

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,42 @@ const S3Error = require('../s3_errors').S3Error;
99

1010
const true_regex = /true/i;
1111

12+
// validates lifecycle rule
13+
function validate_lifecycle_rule(rule) {
14+
15+
if (rule.ID?.length === 1 && rule.ID[0].length > s3_const.MAX_RULE_ID_LENGTH) {
16+
dbg.error('Rule should not have ID length exceed allowed limit of ', s3_const.MAX_RULE_ID_LENGTH, ' characters', rule);
17+
throw new S3Error({ ...S3Error.InvalidArgument, message: `ID length should not exceed allowed limit of ${s3_const.MAX_RULE_ID_LENGTH}` });
18+
}
19+
20+
if (!rule.Status || rule.Status.length !== 1 ||
21+
(rule.Status[0] !== s3_const.LIFECYCLE_STATUS.STAT_ENABLED && rule.Status[0] !== s3_const.LIFECYCLE_STATUS.STAT_DISABLED)) {
22+
dbg.error(`Rule should have a status value of "${s3_const.LIFECYCLE_STATUS.STAT_ENABLED}" or "${s3_const.LIFECYCLE_STATUS.STAT_DISABLED}".`, rule);
23+
throw new S3Error(S3Error.MalformedXML);
24+
}
25+
26+
if (rule.Filter?.[0] && Object.keys(rule.Filter[0]).length > 1 && !rule.Filter[0]?.And) {
27+
dbg.error('Rule should combine multiple filters using "And"', rule);
28+
throw new S3Error(S3Error.MalformedXML);
29+
}
30+
31+
if (rule.Expiration?.[0] && Object.keys(rule.Expiration[0]).length > 1) {
32+
dbg.error('Rule should specify only one expiration field: Days, Date, or ExpiredObjectDeleteMarker', rule);
33+
throw new S3Error(S3Error.MalformedXML);
34+
}
35+
36+
if (rule.AbortIncompleteMultipartUpload?.length === 1 && rule.Filter?.length === 1) {
37+
if (rule.Filter[0]?.Tag) {
38+
dbg.error('Rule should not include AbortIncompleteMultipartUpload with Tags', rule);
39+
throw new S3Error({ ...S3Error.InvalidArgument, message: 'AbortIncompleteMultipartUpload cannot be specified with Tags' });
40+
}
41+
if (rule.Filter[0]?.ObjectSizeGreaterThan || rule.Filter[0]?.ObjectSizeLessThan) {
42+
dbg.error('Rule should not include AbortIncompleteMultipartUpload with Object Size', rule);
43+
throw new S3Error({ ...S3Error.InvalidArgument, message: 'AbortIncompleteMultipartUpload cannot be specified with Object Size' });
44+
}
45+
}
46+
}
47+
1248
// parse lifecycle rule filter
1349
function parse_filter(filter) {
1450
const current_rule_filter = {};
@@ -65,7 +101,12 @@ function parse_expiration(expiration) {
65101
throw new S3Error(S3Error.InvalidArgument);
66102
}
67103
} else if (expiration.Date?.length === 1) {
68-
output_expiration.date = (new Date(expiration.Date[0])).getTime();
104+
const date = new Date(expiration.Date[0]);
105+
if (isNaN(date.getTime()) || date.getTime() !== Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())) {
106+
dbg.error('Date value must conform to the ISO 8601 format and at midnight UTC (00:00:00). Provided:', expiration.Date[0]);
107+
throw new S3Error({ ...S3Error.InvalidArgument, message: "'Date' must be at midnight GMT" });
108+
}
109+
output_expiration.date = date.getTime();
69110
} else if (expiration.ExpiredObjectDeleteMarker?.length === 1) {
70111
output_expiration.expired_object_delete_marker = true_regex.test(expiration.ExpiredObjectDeleteMarker[0]);
71112
}
@@ -89,13 +130,11 @@ async function put_bucket_lifecycle(req) {
89130
filter: {},
90131
};
91132

133+
// validate rule
134+
validate_lifecycle_rule(rule);
135+
92136
if (rule.ID?.length === 1) {
93-
if (rule.ID[0].length > s3_const.MAX_RULE_ID_LENGTH) {
94-
dbg.error('Rule should not have ID length exceed allowed limit of ', s3_const.MAX_RULE_ID_LENGTH, ' characters', rule);
95-
throw new S3Error({ ...S3Error.InvalidArgument, message: `ID length should not exceed allowed limit of ${s3_const.MAX_RULE_ID_LENGTH}` });
96-
} else {
97-
current_rule.id = rule.ID[0];
98-
}
137+
current_rule.id = rule.ID[0];
99138
} else {
100139
// Generate a random ID if missing
101140
current_rule.id = crypto.randomUUID();
@@ -108,11 +147,6 @@ async function put_bucket_lifecycle(req) {
108147
}
109148
id_set.add(current_rule.id);
110149

111-
if (!rule.Status || rule.Status.length !== 1 ||
112-
(rule.Status[0] !== s3_const.LIFECYCLE_STATUS.STAT_ENABLED && rule.Status[0] !== s3_const.LIFECYCLE_STATUS.STAT_DISABLED)) {
113-
dbg.error(`Rule should have a status value of "${s3_const.LIFECYCLE_STATUS.STAT_ENABLED}" or "${s3_const.LIFECYCLE_STATUS.STAT_DISABLED}".`, rule);
114-
throw new S3Error(S3Error.MalformedXML);
115-
}
116150
current_rule.status = rule.Status[0];
117151

118152
if (rule.Prefix) {

src/test/lifecycle/common.js

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,10 @@ function size_gt_lt_lifecycle_configuration(Bucket, gt, lt) {
162162
Date: midnight,
163163
},
164164
Filter: {
165-
ObjectSizeLessThan: lt,
166-
ObjectSizeGreaterThan: gt
165+
And: {
166+
ObjectSizeLessThan: lt,
167+
ObjectSizeGreaterThan: gt,
168+
},
167169
},
168170
Status: 'Enabled',
169171
}, ],
@@ -368,7 +370,7 @@ function duplicate_id_lifecycle_configuration(Bucket, Key) {
368370
Bucket,
369371
LifecycleConfiguration: {
370372
Rules: [{
371-
ID1,
373+
ID: ID1,
372374
Expiration: {
373375
Days: 17,
374376
},
@@ -378,7 +380,7 @@ function duplicate_id_lifecycle_configuration(Bucket, Key) {
378380
Status: 'Enabled',
379381
},
380382
{
381-
ID2,
383+
ID: ID2,
382384
Expiration: {
383385
Days: 18,
384386
},
@@ -622,7 +624,7 @@ exports.test_rule_id_length = async function(Bucket, Key, s3) {
622624
await s3.putBucketLifecycleConfiguration(putLifecycleParams);
623625
assert.fail(`Expected error for ID length exceeding maximum allowed characters ${s3_const.MAX_RULE_ID_LENGTH}, but request was successful`);
624626
} catch (error) {
625-
assert(error.code === 'InvalidArgument', `Expected InvalidArgument: id length exceeding ${s3_const.MAX_RULE_ID_LENGTH} characters`);
627+
assert(error.Code === 'InvalidArgument', `Expected InvalidArgument: id length exceeding ${s3_const.MAX_RULE_ID_LENGTH} characters`);
626628
}
627629
};
628630

@@ -633,7 +635,8 @@ exports.test_rule_duplicate_id = async function(Bucket, Key, s3) {
633635
await s3.putBucketLifecycleConfiguration(putLifecycleParams);
634636
assert.fail('Expected error for duplicate rule ID, but request was successful');
635637
} catch (error) {
636-
assert(error.code === 'InvalidArgument', 'Expected InvalidArgument: duplicate ID found in the rules');
638+
console.log("Received error: ", error);
639+
assert(error.Code === 'InvalidArgument', 'Expected InvalidArgument: duplicate ID found in the rules');
637640
}
638641
};
639642

@@ -647,6 +650,80 @@ exports.test_rule_status_value = async function(Bucket, Key, s3) {
647650
await s3.putBucketLifecycleConfiguration(putLifecycleParams);
648651
assert.fail('Expected MalformedXML error due to wrong status value, but received a different response');
649652
} catch (error) {
650-
assert(error.code === 'MalformedXML', `Expected MalformedXML error: due to invalid status value`);
653+
assert(error.Code === 'MalformedXML', `Expected MalformedXML error: due to invalid status value`);
654+
}
655+
};
656+
657+
exports.test_invalid_filter_format = async function(Bucket, Key, s3) {
658+
const putLifecycleParams = tags_lifecycle_configuration(Bucket, Key);
659+
660+
// append prefix for invalid filter: "And" condition is missing, but multiple filters are present
661+
putLifecycleParams.LifecycleConfiguration.Rules[0].Filter.Prefix = 'test-prefix';
662+
663+
try {
664+
await s3.putBucketLifecycleConfiguration(putLifecycleParams);
665+
assert.fail('Expected MalformedXML error due to missing "And" condition for multiple filters');
666+
} catch (error) {
667+
assert(error.Code === 'MalformedXML', 'Expected MalformedXML error: due to missing "And" condition');
668+
}
669+
};
670+
671+
exports.test_invalid_expiration_date_format = async function(Bucket, Key, s3) {
672+
const putLifecycleParams = date_lifecycle_configuration(Bucket, Key);
673+
674+
// set expiration with a Date that is not at midnight UTC (incorrect time specified)
675+
putLifecycleParams.LifecycleConfiguration.Rules[0].Expiration.Date = new Date('2025-01-01T15:30:00Z');
676+
677+
try {
678+
await s3.putBucketLifecycleConfiguration(putLifecycleParams);
679+
assert.fail('Expected error due to incorrect date format (not at midnight UTC), but request was successful');
680+
} catch (error) {
681+
assert(error.Code === 'InvalidArgument', 'Expected InvalidArgument error: date must be at midnight UTC');
682+
}
683+
};
684+
685+
exports.test_expiration_multiple_fields = async function(Bucket, Key, s3) {
686+
const putLifecycleParams = days_lifecycle_configuration(Bucket, Key);
687+
688+
// append ExpiredObjectDeleteMarker for invalid expiration with multiple fields
689+
putLifecycleParams.LifecycleConfiguration.Rules[0].Expiration.ExpiredObjectDeleteMarker = false;
690+
691+
try {
692+
await s3.putBucketLifecycleConfiguration(putLifecycleParams);
693+
assert.fail('Expected MalformedXML error due to multiple expiration fields');
694+
} catch (error) {
695+
assert(error.Code === 'MalformedXML', 'Expected MalformedXML error: due to multiple expiration fields');
696+
}
697+
};
698+
699+
exports.test_abortincompletemultipartupload_with_tags = async function(Bucket, Key, s3) {
700+
const putLifecycleParams = tags_lifecycle_configuration(Bucket);
701+
702+
// invalid combination of AbortIncompleteMultipartUpload with tags
703+
putLifecycleParams.LifecycleConfiguration.Rules[0].AbortIncompleteMultipartUpload = {
704+
DaysAfterInitiation: 5
705+
};
706+
707+
try {
708+
await s3.putBucketLifecycleConfiguration(putLifecycleParams);
709+
assert.fail('Expected InvalidArgument error due to AbortIncompleteMultipartUpload specified with tags');
710+
} catch (error) {
711+
assert(error.Code === 'InvalidArgument', 'Expected InvalidArgument: AbortIncompleteMultipartUpload cannot be specified with tags');
712+
}
713+
};
714+
715+
exports.test_abortincompletemultipartupload_with_sizes = async function(Bucket, Key, s3) {
716+
const putLifecycleParams = filter_size_lifecycle_configuration(Bucket);
717+
718+
// invalid combination of AbortIncompleteMultipartUpload with object size filters
719+
putLifecycleParams.LifecycleConfiguration.Rules[0].AbortIncompleteMultipartUpload = {
720+
DaysAfterInitiation: 5
721+
};
722+
723+
try {
724+
await s3.putBucketLifecycleConfiguration(putLifecycleParams);
725+
assert.fail('Expected InvalidArgument error due to AbortIncompleteMultipartUpload specified with object size');
726+
} catch (error) {
727+
assert(error.Code === 'InvalidArgument', 'Expected InvalidArgument: AbortIncompleteMultipartUpload cannot be specified with object size');
651728
}
652729
};

src/test/system_tests/test_lifecycle.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ async function main() {
5454
await commonTests.test_rule_id(Bucket, Key, s3);
5555
await commonTests.test_filter_size(Bucket, s3);
5656
await commonTests.test_and_prefix_size(Bucket, Key, s3);
57-
await commonTests.test_rule_id_length(Bucket, Key, s3);
58-
await commonTests.test_rule_duplicate_id(Bucket, Key, s3);
59-
await commonTests.test_rule_status_value(Bucket, Key, s3);
6057

6158
const getObjectParams = {
6259
Bucket,

src/test/unit_tests/test_lifecycle.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,30 @@ mocha.describe('lifecycle', () => {
104104
mocha.it('test multipath', async () => {
105105
await commonTests.test_multipart(Bucket, Key, s3);
106106
});
107+
mocha.it('test rule ID length', async () => {
108+
await commonTests.test_rule_id_length(Bucket, Key, s3);
109+
});
110+
mocha.it('test rule duplicate ID', async () => {
111+
await commonTests.test_rule_duplicate_id(Bucket, Key, s3);
112+
});
113+
mocha.it('test rule status value', async () => {
114+
await commonTests.test_rule_status_value(Bucket, Key, s3);
115+
});
116+
mocha.it('test invalid filter format', async () => {
117+
await commonTests.test_invalid_filter_format(Bucket, Key, s3);
118+
});
119+
mocha.it('test invalid expiration date format', async () => {
120+
await commonTests.test_invalid_expiration_date_format(Bucket, Key, s3);
121+
});
122+
mocha.it('test expiration with multiple fields', async () => {
123+
await commonTests.test_expiration_multiple_fields(Bucket, Key, s3);
124+
});
125+
mocha.it('test AbortIncompleteMultipartUpload with tags', async () => {
126+
await commonTests.test_abortincompletemultipartupload_with_tags(Bucket, Key, s3);
127+
});
128+
mocha.it('test AbortIncompleteMultipartUpload with object sizes', async () => {
129+
await commonTests.test_abortincompletemultipartupload_with_sizes(Bucket, Key, s3);
130+
});
107131
});
108132

109133
mocha.describe('bucket-lifecycle-bg-worker', function() {

0 commit comments

Comments
 (0)