|
1 | 1 | const assert = require('assert'); |
2 | 2 | const tv4 = require('tv4'); |
| 3 | +const { parseString } = require('xml2js'); |
3 | 4 |
|
4 | 5 | const withV4 = require('../support/withV4'); |
5 | 6 | const BucketUtility = require('../../lib/utility/bucket-util'); |
6 | 7 | const bucketSchema = require('../../schema/bucket'); |
7 | 8 | const bucketSchemaV2 = require('../../schema/bucketV2'); |
8 | | -const { generateToken, decryptToken } = |
9 | | - require('../../../../../lib/api/apiUtils/object/continueToken'); |
| 9 | +const { generateToken, decryptToken } = require('../../../../../lib/api/apiUtils/object/continueToken'); |
| 10 | +const AWS = require('aws-sdk'); |
| 11 | +const { IAM } = AWS; |
| 12 | +const getConfig = require('../support/config'); |
| 13 | +const { config } = require('../../../../../lib/Config'); |
| 14 | + |
| 15 | +const isVaultScality = config.backends.auth !== 'mem'; |
| 16 | +const internalPortBypassBP = config.internalPort; |
| 17 | +const vaultHost = config.vaultd?.host || 'localhost'; |
10 | 18 |
|
11 | 19 | const tests = [ |
12 | 20 | { |
@@ -490,5 +498,176 @@ describe('GET Bucket - AWS.S3.listObjects', () => { |
490 | 498 | decryptToken(data.NextContinuationToken), k); |
491 | 499 | }); |
492 | 500 | }); |
| 501 | + |
| 502 | + const describeBypass = isVaultScality && internalPortBypassBP ? describe : describe.skip; |
| 503 | + describeBypass('x-amz-optional-attributes header', () => { |
| 504 | + let policyWithoutPermission; |
| 505 | + let userWithoutPermission; |
| 506 | + let s3ClientWithoutPermission; |
| 507 | + |
| 508 | + const iamConfig = getConfig('default', { region: 'us-east-1' }); |
| 509 | + iamConfig.endpoint = `http://${vaultHost}:8600`; |
| 510 | + const iamClient = new IAM(iamConfig); |
| 511 | + |
| 512 | + before(async () => { |
| 513 | + const policyRes = await iamClient |
| 514 | + .createPolicy({ |
| 515 | + PolicyName: 'bp-bypass-policy', |
| 516 | + PolicyDocument: JSON.stringify({ |
| 517 | + Version: '2012-10-17', |
| 518 | + Statement: [{ |
| 519 | + Sid: 'AllowS3ListBucket', |
| 520 | + Effect: 'Allow', |
| 521 | + Action: [ |
| 522 | + 's3:ListBucket', |
| 523 | + ], |
| 524 | + Resource: ['*'], |
| 525 | + }], |
| 526 | + }), |
| 527 | + }) |
| 528 | + .promise(); |
| 529 | + policyWithoutPermission = policyRes.Policy; |
| 530 | + const userRes = await iamClient.createUser({ UserName: 'user-without-permission' }).promise(); |
| 531 | + userWithoutPermission = userRes.User; |
| 532 | + await iamClient |
| 533 | + .attachUserPolicy({ |
| 534 | + UserName: userWithoutPermission.UserName, |
| 535 | + PolicyArn: policyWithoutPermission.Arn, |
| 536 | + }) |
| 537 | + .promise(); |
| 538 | + |
| 539 | + const accessKeyRes = await iamClient.createAccessKey({ |
| 540 | + UserName: userWithoutPermission.UserName, |
| 541 | + }).promise(); |
| 542 | + const accessKey = accessKeyRes.AccessKey; |
| 543 | + const s3Config = getConfig('default', { |
| 544 | + credentials: new AWS.Credentials(accessKey.AccessKeyId, accessKey.SecretAccessKey), |
| 545 | + }); |
| 546 | + s3ClientWithoutPermission = new AWS.S3(s3Config); |
| 547 | + }); |
| 548 | + |
| 549 | + after(async () => { |
| 550 | + await iamClient |
| 551 | + .detachUserPolicy({ |
| 552 | + UserName: userWithoutPermission.UserName, |
| 553 | + PolicyArn: policyWithoutPermission.Arn, |
| 554 | + }) |
| 555 | + .promise(); |
| 556 | + await iamClient.deletePolicy({ PolicyArn: policyWithoutPermission.Arn }).promise(); |
| 557 | + await iamClient.deleteUser({ UserName: userWithoutPermission.UserName }).promise(); |
| 558 | + }); |
| 559 | + |
| 560 | + // eslint-disable-next-line max-len |
| 561 | + const listObjectsV2WithOptionalAttributes = async (s3, bucket, headerValue) => await new Promise((resolve, reject) => { |
| 562 | + let rawXml = ''; |
| 563 | + const req = s3.listObjectsV2({ Bucket: bucket }); |
| 564 | + |
| 565 | + req.on('build', () => { |
| 566 | + req.httpRequest.headers['x-amz-optional-object-attributes'] = headerValue; |
| 567 | + }); |
| 568 | + req.on('httpData', chunk => { rawXml += chunk; }); |
| 569 | + req.on('error', err => reject(err)); |
| 570 | + req.on('success', response => { |
| 571 | + parseString(rawXml, (err, parsedXml) => { |
| 572 | + if (err) { |
| 573 | + return reject(err); |
| 574 | + } |
| 575 | + |
| 576 | + const contents = response.data.Contents; |
| 577 | + const parsedContents = parsedXml.ListBucketResult.Contents; |
| 578 | + |
| 579 | + if (!contents || !parsedContents) { |
| 580 | + return resolve(response.data); |
| 581 | + } |
| 582 | + |
| 583 | + if (parsedContents[0]?.['x-amz-meta-department']) { |
| 584 | + contents[0]['x-amz-meta-department'] = parsedContents[0]['x-amz-meta-department'][0]; |
| 585 | + } |
| 586 | + |
| 587 | + if (parsedContents[0]?.['x-amz-meta-hr']) { |
| 588 | + contents[0]['x-amz-meta-hr'] = parsedContents[0]['x-amz-meta-hr'][0]; |
| 589 | + } |
| 590 | + |
| 591 | + return resolve(response.data); |
| 592 | + }); |
| 593 | + }); |
| 594 | + |
| 595 | + req.send(); |
| 596 | + }); |
| 597 | + |
| 598 | + it('should return an XML if the header is set', async () => { |
| 599 | + const s3 = bucketUtil.s3; |
| 600 | + const Bucket = bucketName; |
| 601 | + |
| 602 | + await s3.putObject({ |
| 603 | + Bucket, |
| 604 | + Key: 'super-power-object', |
| 605 | + Metadata: { |
| 606 | + Department: 'sales', |
| 607 | + HR: 'true', |
| 608 | + }, |
| 609 | + }).promise(); |
| 610 | + const result = await listObjectsV2WithOptionalAttributes( |
| 611 | + s3, |
| 612 | + Bucket, |
| 613 | + 'x-amz-meta-*,RestoreStatus,x-amz-meta-department', |
| 614 | + ); |
| 615 | + |
| 616 | + assert.strictEqual(result.Contents.length, 1); |
| 617 | + assert.strictEqual(result.Contents[0].Key, 'super-power-object'); |
| 618 | + assert.strictEqual(result.Contents[0]['x-amz-meta-department'], 'sales'); |
| 619 | + assert.strictEqual(result.Contents[0]['x-amz-meta-hr'], 'true'); |
| 620 | + }); |
| 621 | + |
| 622 | + it('should reject the request if the user does not have the permission', async () => { |
| 623 | + const s3 = bucketUtil.s3; |
| 624 | + const Bucket = bucketName; |
| 625 | + |
| 626 | + await s3.putObject({ |
| 627 | + Bucket, |
| 628 | + Key: 'super-power-object', |
| 629 | + Metadata: { |
| 630 | + Department: 'sales', |
| 631 | + HR: 'true', |
| 632 | + }, |
| 633 | + }).promise(); |
| 634 | + |
| 635 | + try { |
| 636 | + await listObjectsV2WithOptionalAttributes( |
| 637 | + s3ClientWithoutPermission, |
| 638 | + Bucket, |
| 639 | + 'x-amz-meta-*,RestoreStatus,x-amz-meta-department', |
| 640 | + ); |
| 641 | + throw new Error('Request should have been rejected'); |
| 642 | + } catch (err) { |
| 643 | + assert.strictEqual(err.statusCode, 403); |
| 644 | + assert.strictEqual(err.code, 'AccessDenied'); |
| 645 | + } |
| 646 | + }); |
| 647 | + |
| 648 | + it('should always (ignore permission) return an XML when the header is RestoreStatus', async () => { |
| 649 | + const s3 = bucketUtil.s3; |
| 650 | + const Bucket = bucketName; |
| 651 | + |
| 652 | + await s3.putObject({ |
| 653 | + Bucket, |
| 654 | + Key: 'super-power-object', |
| 655 | + Metadata: { |
| 656 | + Department: 'sales', |
| 657 | + HR: 'true', |
| 658 | + }, |
| 659 | + }).promise(); |
| 660 | + const result = await listObjectsV2WithOptionalAttributes( |
| 661 | + s3ClientWithoutPermission, |
| 662 | + Bucket, |
| 663 | + 'RestoreStatus', |
| 664 | + ); |
| 665 | + |
| 666 | + assert.strictEqual(result.Contents.length, 1); |
| 667 | + assert.strictEqual(result.Contents[0].Key, 'super-power-object'); |
| 668 | + assert.strictEqual(result.Contents[0]['x-amz-meta-department'], undefined); |
| 669 | + assert.strictEqual(result.Contents[0]['x-amz-meta-hr'], undefined); |
| 670 | + }); |
| 671 | + }); |
493 | 672 | }); |
494 | 673 | }); |
0 commit comments