|
1 | 1 | const assert = require('assert'); |
2 | 2 |
|
3 | | -const { isLifecycleSession, checkBucketPolicyResult, checkBucketPolicy } = |
| 3 | +const { isLifecycleSession, checkBucketPolicyResult, checkBucketPolicy, |
| 4 | + isBucketAuthorized, isObjAuthorized, evaluateBucketPolicyWithIAM } = |
4 | 5 | require('../../../../lib/api/apiUtils/authorization/permissionChecks.js'); |
5 | 6 | const { DummyRequestLogger } = require('../../helpers'); |
6 | 7 |
|
7 | 8 | const stubLog = new DummyRequestLogger(); |
8 | 9 |
|
| 10 | +const ownerCanonicalId = '79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be'; |
| 11 | +const otherCanonicalId = 'aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffgggggggghhhhhhh1'; |
| 12 | + |
9 | 13 | describe('authInfoHelper', () => { |
10 | 14 | const tests = [ |
11 | 15 | { |
@@ -708,3 +712,214 @@ describe('checkBucketPolicy Principal logic', () => { |
708 | 712 | }); |
709 | 713 | }); |
710 | 714 | }); |
| 715 | + |
| 716 | +describe('aclRequired field in isBucketAuthorized', () => { |
| 717 | + function makeBucket(owner, acl, bucketPolicy) { |
| 718 | + return { |
| 719 | + getOwner: () => owner, |
| 720 | + getAcl: () => ({ |
| 721 | + Canned: acl?.Canned || '', |
| 722 | + FULL_CONTROL: acl?.FULL_CONTROL || [], |
| 723 | + READ: acl?.READ || [], |
| 724 | + READ_ACP: acl?.READ_ACP || [], |
| 725 | + WRITE: acl?.WRITE || [], |
| 726 | + WRITE_ACP: acl?.WRITE_ACP || [], |
| 727 | + }), |
| 728 | + getBucketPolicy: () => bucketPolicy || null, |
| 729 | + }; |
| 730 | + } |
| 731 | + |
| 732 | + function makeRequest() { |
| 733 | + return { serverAccessLog: {}, socket: { remoteAddress: '127.0.0.1' }, headers: {} }; |
| 734 | + } |
| 735 | + |
| 736 | + function makeAuthInfo(canonicalID, arn, isIAMUser = false) { |
| 737 | + return { |
| 738 | + getCanonicalID: () => canonicalID, |
| 739 | + getArn: () => arn, |
| 740 | + isRequesterAnIAMUser: () => isIAMUser, |
| 741 | + }; |
| 742 | + } |
| 743 | + |
| 744 | + it('should not set aclRequired when requester is bucket owner', () => { |
| 745 | + const request = makeRequest(); |
| 746 | + const bucket = makeBucket(ownerCanonicalId); |
| 747 | + const authInfo = makeAuthInfo(ownerCanonicalId, 'arn:aws:iam::123456789012:/owner/'); |
| 748 | + isBucketAuthorized(bucket, 'bucketGet', ownerCanonicalId, authInfo, stubLog, |
| 749 | + request, { bucketGet: false }); |
| 750 | + assert.strictEqual(request.serverAccessLog.aclRequired, undefined); |
| 751 | + }); |
| 752 | + |
| 753 | + it('should set aclRequired to Yes for non-owner with no bucket policy', () => { |
| 754 | + const request = makeRequest(); |
| 755 | + const bucket = makeBucket(ownerCanonicalId, { READ: [otherCanonicalId] }); |
| 756 | + const authInfo = makeAuthInfo(otherCanonicalId, 'arn:aws:iam::999999999999:user/other'); |
| 757 | + isBucketAuthorized(bucket, 'bucketGet', otherCanonicalId, authInfo, stubLog, |
| 758 | + request, { bucketGet: false }); |
| 759 | + assert.strictEqual(request.serverAccessLog.aclRequired, 'Yes'); |
| 760 | + }); |
| 761 | + |
| 762 | + it('should not set aclRequired when bucket policy explicitly allows', () => { |
| 763 | + const request = makeRequest(); |
| 764 | + // Use an IAM user in the bucket owner's account so principal match is OK (not CROSS_ACCOUNT) |
| 765 | + // and the owner early-return doesn't fire (requester is IAM user, not account root) |
| 766 | + const iamUserCanonicalId = ownerCanonicalId; |
| 767 | + const bucket = makeBucket(ownerCanonicalId, {}, { |
| 768 | + Statement: [{ |
| 769 | + Effect: 'Allow', |
| 770 | + Principal: { AWS: 'arn:aws:iam::123456789012:user/iamuser' }, |
| 771 | + Action: ['s3:ListBucket'], |
| 772 | + Resource: ['arn:aws:s3:::test-bucket'], |
| 773 | + }], |
| 774 | + }); |
| 775 | + const authInfo = makeAuthInfo(iamUserCanonicalId, 'arn:aws:iam::123456789012:user/iamuser', true); |
| 776 | + isBucketAuthorized(bucket, 'bucketGet', iamUserCanonicalId, authInfo, stubLog, |
| 777 | + request, { bucketGet: false }); |
| 778 | + assert.strictEqual(request.serverAccessLog.aclRequired, undefined); |
| 779 | + }); |
| 780 | + |
| 781 | + it('should not set aclRequired when bucket policy explicitly denies', () => { |
| 782 | + const request = makeRequest(); |
| 783 | + const iamUserCanonicalId = ownerCanonicalId; |
| 784 | + const bucket = makeBucket(ownerCanonicalId, { READ: [iamUserCanonicalId] }, { |
| 785 | + Statement: [{ |
| 786 | + Effect: 'Deny', |
| 787 | + Principal: { AWS: 'arn:aws:iam::123456789012:user/iamuser' }, |
| 788 | + Action: ['s3:ListBucket'], |
| 789 | + Resource: ['arn:aws:s3:::test-bucket'], |
| 790 | + }], |
| 791 | + }); |
| 792 | + const authInfo = makeAuthInfo(iamUserCanonicalId, 'arn:aws:iam::123456789012:user/iamuser', true); |
| 793 | + isBucketAuthorized(bucket, 'bucketGet', iamUserCanonicalId, authInfo, stubLog, |
| 794 | + request, { bucketGet: false }); |
| 795 | + assert.strictEqual(request.serverAccessLog.aclRequired, undefined); |
| 796 | + }); |
| 797 | + |
| 798 | + it('should set aclRequired to Yes when bucket policy returns DEFAULT_DENY', () => { |
| 799 | + const request = makeRequest(); |
| 800 | + // Policy grants PutObject to a different principal — nothing matches |
| 801 | + // the bucketGet request from otherCanonicalId, so checkBucketPolicy |
| 802 | + // returns DEFAULT_DENY and falls back to ACL evaluation. |
| 803 | + const bucket = makeBucket(ownerCanonicalId, { READ: [otherCanonicalId] }, { |
| 804 | + Statement: [{ |
| 805 | + Effect: 'Allow', |
| 806 | + Principal: { AWS: 'arn:aws:iam::111111111111:root' }, |
| 807 | + Action: ['s3:PutObject'], |
| 808 | + Resource: ['arn:aws:s3:::test-bucket/*'], |
| 809 | + }], |
| 810 | + }); |
| 811 | + const authInfo = makeAuthInfo(otherCanonicalId, 'arn:aws:iam::999999999999:user/other'); |
| 812 | + isBucketAuthorized(bucket, 'bucketGet', otherCanonicalId, authInfo, stubLog, |
| 813 | + request, { bucketGet: false }); |
| 814 | + assert.strictEqual(request.serverAccessLog.aclRequired, 'Yes'); |
| 815 | + }); |
| 816 | + |
| 817 | + it('should handle missing serverAccessLog gracefully', () => { |
| 818 | + const request = {}; |
| 819 | + const bucket = makeBucket(ownerCanonicalId, { READ: [otherCanonicalId] }); |
| 820 | + const authInfo = makeAuthInfo(otherCanonicalId, 'arn:aws:iam::999999999999:user/other'); |
| 821 | + assert.doesNotThrow(() => { |
| 822 | + isBucketAuthorized(bucket, 'bucketGet', otherCanonicalId, authInfo, stubLog, |
| 823 | + request, { bucketGet: false }); |
| 824 | + }); |
| 825 | + }); |
| 826 | +}); |
| 827 | + |
| 828 | +describe('aclRequired field in isObjAuthorized', () => { |
| 829 | + function makeBucket(owner, acl, bucketPolicy) { |
| 830 | + return { |
| 831 | + getOwner: () => owner, |
| 832 | + getName: () => 'test-bucket', |
| 833 | + getAcl: () => ({ |
| 834 | + Canned: acl?.Canned || '', |
| 835 | + FULL_CONTROL: acl?.FULL_CONTROL || [], |
| 836 | + READ: acl?.READ || [], |
| 837 | + READ_ACP: acl?.READ_ACP || [], |
| 838 | + WRITE: acl?.WRITE || [], |
| 839 | + WRITE_ACP: acl?.WRITE_ACP || [], |
| 840 | + }), |
| 841 | + getBucketPolicy: () => bucketPolicy || null, |
| 842 | + }; |
| 843 | + } |
| 844 | + |
| 845 | + function makeRequest() { |
| 846 | + return { serverAccessLog: {} }; |
| 847 | + } |
| 848 | + |
| 849 | + function makeAuthInfo(canonicalID, arn) { |
| 850 | + return { |
| 851 | + getCanonicalID: () => canonicalID, |
| 852 | + getArn: () => arn, |
| 853 | + isRequesterAnIAMUser: () => false, |
| 854 | + }; |
| 855 | + } |
| 856 | + |
| 857 | + function makeObjectMD(ownerId) { |
| 858 | + return { |
| 859 | + 'owner-id': ownerId, |
| 860 | + acl: { |
| 861 | + Canned: '', |
| 862 | + FULL_CONTROL: [], |
| 863 | + READ: [], |
| 864 | + READ_ACP: [], |
| 865 | + WRITE: [], |
| 866 | + WRITE_ACP: [], |
| 867 | + }, |
| 868 | + }; |
| 869 | + } |
| 870 | + |
| 871 | + it('should not set aclRequired when requester is object owner', () => { |
| 872 | + const request = makeRequest(); |
| 873 | + const bucket = makeBucket(ownerCanonicalId); |
| 874 | + const objectMD = makeObjectMD(otherCanonicalId); |
| 875 | + const authInfo = makeAuthInfo(otherCanonicalId, 'arn:aws:iam::999999999999:/account/'); |
| 876 | + isObjAuthorized(bucket, objectMD, 'objectGet', otherCanonicalId, authInfo, stubLog, |
| 877 | + request, { objectGet: false }); |
| 878 | + assert.strictEqual(request.serverAccessLog.aclRequired, undefined); |
| 879 | + }); |
| 880 | + |
| 881 | + it('should set aclRequired to Yes for non-owner with no bucket policy', () => { |
| 882 | + const request = makeRequest(); |
| 883 | + const bucket = makeBucket(ownerCanonicalId); |
| 884 | + const objectMD = makeObjectMD(ownerCanonicalId); |
| 885 | + objectMD.acl.READ = [otherCanonicalId]; |
| 886 | + const authInfo = makeAuthInfo(otherCanonicalId, 'arn:aws:iam::999999999999:user/other'); |
| 887 | + isObjAuthorized(bucket, objectMD, 'objectGet', otherCanonicalId, authInfo, stubLog, |
| 888 | + request, { objectGet: false }); |
| 889 | + assert.strictEqual(request.serverAccessLog.aclRequired, 'Yes'); |
| 890 | + }); |
| 891 | +}); |
| 892 | + |
| 893 | +describe('aclRequired field in evaluateBucketPolicyWithIAM', () => { |
| 894 | + function makeBucket(owner, bucketPolicy) { |
| 895 | + return { |
| 896 | + getOwner: () => owner, |
| 897 | + getAcl: () => ({ |
| 898 | + Canned: '', |
| 899 | + FULL_CONTROL: [], |
| 900 | + READ: [], |
| 901 | + READ_ACP: [], |
| 902 | + WRITE: [], |
| 903 | + WRITE_ACP: [], |
| 904 | + }), |
| 905 | + getBucketPolicy: () => bucketPolicy || null, |
| 906 | + }; |
| 907 | + } |
| 908 | + |
| 909 | + function makeAuthInfo(canonicalID, arn) { |
| 910 | + return { |
| 911 | + getCanonicalID: () => canonicalID, |
| 912 | + getArn: () => arn, |
| 913 | + isRequesterAnIAMUser: () => false, |
| 914 | + }; |
| 915 | + } |
| 916 | + |
| 917 | + it('should not set aclRequired (ACLs are not actually consulted)', () => { |
| 918 | + const request = { serverAccessLog: {} }; |
| 919 | + const bucket = makeBucket(ownerCanonicalId); |
| 920 | + const authInfo = makeAuthInfo(otherCanonicalId, 'arn:aws:iam::999999999999:user/other'); |
| 921 | + evaluateBucketPolicyWithIAM(bucket, 'objectDelete', otherCanonicalId, authInfo, |
| 922 | + { objectDelete: false }, stubLog, request); |
| 923 | + assert.strictEqual(request.serverAccessLog.aclRequired, undefined); |
| 924 | + }); |
| 925 | +}); |
0 commit comments