|
1 | 1 | const assert = require('assert');
|
2 | 2 | const sinon = require('sinon');
|
| 3 | +const async = require('async'); |
3 | 4 | const { promisify } = require('util');
|
4 | 5 | const metadataUtils = require('../../../lib/metadata/metadataUtils');
|
5 | 6 | const storeObject = require('../../../lib/api/apiUtils/object/storeObject');
|
6 | 7 | const metadata = require('../../../lib/metadata/wrapper');
|
7 |
| -const { DummyRequestLogger } = require('../helpers'); |
| 8 | +const { routeBackbeat, backbeatRoutes } = require('../../../lib/routes/routeBackbeat'); |
| 9 | +const { DummyRequestLogger, versioningTestUtils, makeAuthInfo } = require('../helpers'); |
8 | 10 | const dataWrapper = require('../../../lib/data/wrapper');
|
9 | 11 | const DummyRequest = require('../DummyRequest');
|
10 |
| -const { auth } = require('arsenal'); |
| 12 | +const { auth, errors } = require('arsenal'); |
| 13 | +const { default: AuthInfo } = require('arsenal/build/lib/auth/AuthInfo'); |
11 | 14 | const { config } = require('../../../lib/Config');
|
12 | 15 | const quotaUtils = require('../../../lib/api/apiUtils/quotas/quotaUtils');
|
| 16 | +const { bucketPut } = require('../../../lib/api/bucketPut'); |
| 17 | +const objectPut = require('../../../lib/api/objectPut'); |
| 18 | +const bucketPutVersioning = require('../../../lib/api/bucketPutVersioning'); |
13 | 19 |
|
14 | 20 | const log = new DummyRequestLogger();
|
15 | 21 |
|
@@ -67,7 +73,7 @@ describe('routeBackbeat', () => {
|
67 | 73 |
|
68 | 74 | // Clear require cache for routeBackbeat to make sure fresh module with stubbed dependencies
|
69 | 75 | delete require.cache[require.resolve('../../../lib/routes/routeBackbeat')];
|
70 |
| - routeBackbeat = require('../../../lib/routes/routeBackbeat'); |
| 76 | + routeBackbeat = require('../../../lib/routes/routeBackbeat').routeBackbeat; |
71 | 77 | });
|
72 | 78 |
|
73 | 79 | afterEach(() => {
|
@@ -694,3 +700,312 @@ describe('routeBackbeat', () => {
|
694 | 700 | });
|
695 | 701 | });
|
696 | 702 | });
|
| 703 | + |
| 704 | +describe('routeBackbeat authorization', () => { |
| 705 | + const bucketName = 'bucketname'; |
| 706 | + const canonicalID = 'accessKey1'; |
| 707 | + const authInfo = makeAuthInfo(canonicalID); |
| 708 | + const namespace = 'default'; |
| 709 | + const objectName = 'objectName'; |
| 710 | + const postBody = Buffer.from('I am a body', 'utf8'); |
| 711 | + |
| 712 | + const testBucket = { |
| 713 | + bucketName, |
| 714 | + namespace, |
| 715 | + headers: { |
| 716 | + 'host': `${bucketName}.s3.amazonaws.com`, |
| 717 | + }, |
| 718 | + url: `/${bucketName}`, |
| 719 | + actionImplicitDenies: false, |
| 720 | + }; |
| 721 | + |
| 722 | + const testObject = new DummyRequest({ |
| 723 | + bucketName, |
| 724 | + namespace, |
| 725 | + objectKey: objectName, |
| 726 | + headers: { |
| 727 | + 'x-amz-meta-test': 'some metadata', |
| 728 | + 'content-length': '12', |
| 729 | + }, |
| 730 | + parsedContentLength: 12, |
| 731 | + url: `/${bucketName}/${objectName}`, |
| 732 | + }, postBody); |
| 733 | + |
| 734 | + let request; |
| 735 | + let response; |
| 736 | + |
| 737 | + beforeEach(() => { |
| 738 | + sinon.stub(backbeatRoutes, 'PUT').returns({ |
| 739 | + data: sinon.stub(), |
| 740 | + metadata: sinon.stub(), |
| 741 | + multiplebackenddata: { |
| 742 | + putobject: sinon.stub(), |
| 743 | + putpart: sinon.stub(), |
| 744 | + }, |
| 745 | + }); |
| 746 | + |
| 747 | + sinon.stub(backbeatRoutes, 'POST').returns({ |
| 748 | + multiplebackenddata: { |
| 749 | + initiatempu: sinon.stub(), |
| 750 | + completempu: sinon.stub(), |
| 751 | + puttagging: sinon.stub(), |
| 752 | + }, |
| 753 | + batchdelete: sinon.stub(), |
| 754 | + index: { |
| 755 | + add: sinon.stub(), |
| 756 | + delete: sinon.stub(), |
| 757 | + }, |
| 758 | + }); |
| 759 | + |
| 760 | + sinon.stub(backbeatRoutes, 'DELETE').returns({ |
| 761 | + expiration: sinon.stub(), |
| 762 | + multiplebackenddata: { |
| 763 | + deleteobject: sinon.stub(), |
| 764 | + deleteobjecttagging: sinon.stub(), |
| 765 | + abortmpu: sinon.stub(), |
| 766 | + }, |
| 767 | + }); |
| 768 | + |
| 769 | + sinon.stub(backbeatRoutes, 'GET').returns({ |
| 770 | + metadata: sinon.stub(), |
| 771 | + multiplebackendmetadata: sinon.stub(), |
| 772 | + lifecycle: sinon.stub(), |
| 773 | + index: sinon.stub(), |
| 774 | + }); |
| 775 | + |
| 776 | + request = new DummyRequest( |
| 777 | + { |
| 778 | + method: 'GET', |
| 779 | + headers: { 'content-length': '123' }, |
| 780 | + url: '/_/backbeat/multiplebackendmetadata/bucketName/objectKey?operation=putobject', |
| 781 | + }, |
| 782 | + 'body' |
| 783 | + ); |
| 784 | + response = { |
| 785 | + setHeader: sinon.stub(), |
| 786 | + writeHead: sinon.stub(), |
| 787 | + end: sinon.stub().callsFake((body, format, cb) => cb()), |
| 788 | + }; |
| 789 | + }); |
| 790 | + |
| 791 | + afterEach(() => { |
| 792 | + sinon.restore(); |
| 793 | + }); |
| 794 | + |
| 795 | + it('should reject if the request is invalid', done => { |
| 796 | + request.url = '/_/backbeat//bucketName/objectKey?operation=putobject'; |
| 797 | + // Cover the case invalidRequest === true |
| 798 | + routeBackbeat('127.0.0.1', request, response, log, err => { |
| 799 | + assert(err.is.MethodNotAllowed); |
| 800 | + done(); |
| 801 | + }); |
| 802 | + }); |
| 803 | + |
| 804 | + it('should reject if the route is invalid', done => { |
| 805 | + request.url = '/_/backbeat/wrong/bucketName/objectKey?operation=putobject'; |
| 806 | + // Cover the case invalidRoute === true |
| 807 | + routeBackbeat('127.0.0.1', request, response, log, err => { |
| 808 | + assert(err.is.MethodNotAllowed); |
| 809 | + done(); |
| 810 | + }); |
| 811 | + }); |
| 812 | + |
| 813 | + [ |
| 814 | + { |
| 815 | + method: 'PUT', |
| 816 | + resourceType: 'metadata', |
| 817 | + target: `${bucketName}/${objectName}`, |
| 818 | + operation: null, |
| 819 | + versionId: false, |
| 820 | + expect: errors.MalformedPOSTRequest, |
| 821 | + }, |
| 822 | + { |
| 823 | + method: 'GET', |
| 824 | + resourceType: 'metadata', |
| 825 | + target: `${bucketName}/${objectName}`, |
| 826 | + operation: null, |
| 827 | + versionId: true, |
| 828 | + }, |
| 829 | + { |
| 830 | + method: 'PUT', |
| 831 | + resourceType: 'data', |
| 832 | + target: `${bucketName}/${objectName}`, |
| 833 | + operation: null, |
| 834 | + versionId: false, |
| 835 | + expect: errors.BadRequest, |
| 836 | + }, |
| 837 | + { |
| 838 | + method: 'PUT', |
| 839 | + resourceType: 'multiplebackenddata', |
| 840 | + target: `${bucketName}/${objectName}`, |
| 841 | + operation: 'putobject', |
| 842 | + versionId: false, |
| 843 | + expect: errors.BadRequest, |
| 844 | + }, |
| 845 | + { |
| 846 | + method: 'PUT', |
| 847 | + resourceType: 'multiplebackenddata', |
| 848 | + target: `${bucketName}/${objectName}`, |
| 849 | + operation: 'putpart', |
| 850 | + versionId: false, |
| 851 | + expect: errors.BadRequest, |
| 852 | + }, |
| 853 | + { |
| 854 | + method: 'DELETE', |
| 855 | + resourceType: 'multiplebackenddata', |
| 856 | + target: `${bucketName}/${objectName}`, |
| 857 | + operation: 'deleteobject', |
| 858 | + versionId: false, |
| 859 | + expect: errors.BadRequest, |
| 860 | + }, |
| 861 | + { |
| 862 | + method: 'DELETE', |
| 863 | + resourceType: 'multiplebackenddata', |
| 864 | + target: `${bucketName}/${objectName}`, |
| 865 | + operation: 'abortmpu', |
| 866 | + versionId: false, |
| 867 | + expect: errors.BadRequest, |
| 868 | + }, |
| 869 | + { |
| 870 | + method: 'DELETE', |
| 871 | + resourceType: 'multiplebackenddata', |
| 872 | + target: `${bucketName}/${objectName}`, |
| 873 | + operation: 'deleteobjecttagging', |
| 874 | + versionId: false, |
| 875 | + expect: errors.BadRequest, |
| 876 | + }, |
| 877 | + { |
| 878 | + method: 'POST', |
| 879 | + resourceType: 'multiplebackenddata', |
| 880 | + target: `${bucketName}/${objectName}`, |
| 881 | + operation: 'initiatempu', |
| 882 | + versionId: false, |
| 883 | + expect: errors.BadRequest, |
| 884 | + }, |
| 885 | + { |
| 886 | + method: 'POST', |
| 887 | + resourceType: 'multiplebackenddata', |
| 888 | + target: `${bucketName}/${objectName}`, |
| 889 | + operation: 'completempu', |
| 890 | + versionId: false, |
| 891 | + expect: errors.BadRequest, |
| 892 | + }, |
| 893 | + { |
| 894 | + method: 'POST', |
| 895 | + resourceType: 'multiplebackenddata', |
| 896 | + target: `${bucketName}/${objectName}`, |
| 897 | + operation: 'puttagging', |
| 898 | + versionId: false, |
| 899 | + expect: errors.BadRequest, |
| 900 | + }, |
| 901 | + { |
| 902 | + method: 'GET', |
| 903 | + resourceType: 'multiplebackendmetadata', |
| 904 | + target: `${bucketName}/${objectName}`, |
| 905 | + operation: null, |
| 906 | + versionId: false, |
| 907 | + expect: errors.BadRequest, |
| 908 | + }, |
| 909 | + { |
| 910 | + method: 'POST', |
| 911 | + resourceType: 'batchdelete', |
| 912 | + target: null, |
| 913 | + operation: null, |
| 914 | + versionId: false, |
| 915 | + expect: errors.MalformedPOSTRequest, |
| 916 | + }, |
| 917 | + { |
| 918 | + method: 'GET', |
| 919 | + resourceType: 'lifecycle', |
| 920 | + target: `${bucketName}?list-type=wrong`, |
| 921 | + operation: null, |
| 922 | + versionId: false, |
| 923 | + expect: errors.BadRequest, |
| 924 | + }, |
| 925 | + { |
| 926 | + method: 'POST', |
| 927 | + resourceType: 'index', |
| 928 | + target: bucketName, |
| 929 | + operation: 'add', |
| 930 | + versionId: false, |
| 931 | + expect: errors.BadRequest, |
| 932 | + }, |
| 933 | + { |
| 934 | + method: 'POST', |
| 935 | + resourceType: 'index', |
| 936 | + target: bucketName, |
| 937 | + operation: 'delete', |
| 938 | + versionId: false, |
| 939 | + expect: errors.BadRequest, |
| 940 | + }, |
| 941 | + { |
| 942 | + method: 'GET', |
| 943 | + resourceType: 'index', |
| 944 | + target: null, |
| 945 | + operation: 'delete', |
| 946 | + versionId: false, |
| 947 | + expect: errors.NotImplemented, |
| 948 | + } |
| 949 | + ].forEach(testCase => { |
| 950 | + it(`should call method ${testCase.method} ${testCase.resourceType}`, done => { |
| 951 | + let hasQuery = false; |
| 952 | + let versionIdParsed = null; |
| 953 | + request.method = testCase.method; |
| 954 | + request.url = `/_/backbeat/${testCase.resourceType}/${testCase.target}`; |
| 955 | + if (testCase.operation) { |
| 956 | + request.url += `?operation=${testCase.operation}`; |
| 957 | + hasQuery = true; |
| 958 | + } |
| 959 | + |
| 960 | + // Mock auth server to ignore auth in this test |
| 961 | + sinon.stub(auth.server, 'doAuth').callsFake((req, log, cb) => |
| 962 | + cb(null, new AuthInfo({ |
| 963 | + canonicalID: 'abcdef/lifecycle', |
| 964 | + accountDisplayName: 'Lifecycle Service Account', |
| 965 | + }), undefined, undefined, { |
| 966 | + accountQuota: 1000, |
| 967 | + }) |
| 968 | + ); |
| 969 | + |
| 970 | + const enableVersioningRequest = |
| 971 | + versioningTestUtils.createBucketPutVersioningReq(bucketName, 'Enabled'); |
| 972 | + |
| 973 | + return async.series([ |
| 974 | + next => bucketPut(authInfo, testBucket, log, next), |
| 975 | + next => bucketPutVersioning(authInfo, enableVersioningRequest, log, next), |
| 976 | + next => objectPut(authInfo, testObject, undefined, log, (err, res) => { |
| 977 | + versionIdParsed = res['x-amz-version-id']; |
| 978 | + if (testCase.versionId) { |
| 979 | + request.url += `${(hasQuery ? '&' : '?')}&versionId=${versionIdParsed}`; |
| 980 | + } |
| 981 | + next(err); |
| 982 | + }), |
| 983 | + next => routeBackbeat('127.0.0.1', request, response, log, next), |
| 984 | + ], err => { |
| 985 | + if (testCase.expect) { |
| 986 | + assert.strictEqual(err.code, testCase.expect.code); |
| 987 | + return done(); |
| 988 | + } |
| 989 | + assert.ifError(err); |
| 990 | + assert.strictEqual(Array.isArray(request.finalizerHooks), true); |
| 991 | + assert.strictEqual(request.apiMethods[0], 'objectReplicate'); |
| 992 | + assert.strictEqual(request.apiMethods.length, 1); |
| 993 | + assert.strictEqual(request.accountQuotas, 1000); |
| 994 | + return done(); |
| 995 | + }); |
| 996 | + }); |
| 997 | + }); |
| 998 | + |
| 999 | + // Although the authz result is by default an implicit deny, the |
| 1000 | + // ACL should prevent any further processing for non-service or |
| 1001 | + // non-account identities. |
| 1002 | + it('should return access denied if doAuth returns an error', done => { |
| 1003 | + request.method = 'PUT'; |
| 1004 | + request.url = `/_/backbeat/metadata/${bucketName}/${objectName}?operation=putobject`; |
| 1005 | + |
| 1006 | + routeBackbeat('127.0.0.1', request, response, log, err => { |
| 1007 | + assert(err.is.AccessDenied); |
| 1008 | + done(); |
| 1009 | + }); |
| 1010 | + }); |
| 1011 | +}); |
0 commit comments