@scality/data-browser-library 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { fireEvent, render, screen, within } from "@testing-library/react";
3
3
  import { MemoryRouter } from "react-router";
4
- import { useGetBucketAcl, useGetBucketCors, useGetBucketLocation, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketTagging, useGetBucketVersioning, useISVBucketStatus } from "../../hooks/index.js";
4
+ import { useGetBucketAcl, useGetBucketCors, useGetBucketLocation, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, useISVBucketStatus, usePutPublicAccessBlock, useSetBucketAcl } from "../../hooks/index.js";
5
5
  import { useFeatures } from "../../hooks/useFeatures.js";
6
- import { applyBucketMocks, createTestWrapper } from "../../test/testUtils.js";
6
+ import { applyBucketMocks, createMockMutationResult, createTestWrapper } from "../../test/testUtils.js";
7
7
  import { EnhancedS3Error, ErrorCategory } from "../../utils/errorHandling.js";
8
8
  import { BucketOverview, useBucketOverviewContext } from "../buckets/BucketOverview.js";
9
9
  import * as __rspack_external__contexts_DataBrowserUICustomizationContext_js_f267b01c from "../../contexts/DataBrowserUICustomizationContext.js";
@@ -16,6 +16,9 @@ const mockUseGetBucketCors = jest.mocked(useGetBucketCors);
16
16
  const mockUseGetBucketObjectLockConfiguration = jest.mocked(useGetBucketObjectLockConfiguration);
17
17
  const mockUseGetBucketPolicy = jest.mocked(useGetBucketPolicy);
18
18
  const mockUseGetBucketTagging = jest.mocked(useGetBucketTagging);
19
+ const mockUseGetPublicAccessBlock = jest.mocked(useGetPublicAccessBlock);
20
+ const mockUsePutPublicAccessBlock = jest.mocked(usePutPublicAccessBlock);
21
+ const mockUseSetBucketAcl = jest.mocked(useSetBucketAcl);
19
22
  const mockUseISVBucketStatus = jest.mocked(useISVBucketStatus);
20
23
  const mockUseFeatures = jest.mocked(useFeatures);
21
24
  const mockUseDataBrowserUICustomization = (config = {})=>{
@@ -69,6 +72,20 @@ describe('BucketOverview', ()=>{
69
72
  jest.clearAllMocks();
70
73
  mockHookDefaults();
71
74
  mockUseDataBrowserUICustomization({});
75
+ mockUseGetPublicAccessBlock.mockReturnValue({
76
+ data: {
77
+ PublicAccessBlockConfiguration: {
78
+ BlockPublicAcls: false,
79
+ IgnorePublicAcls: false,
80
+ BlockPublicPolicy: false,
81
+ RestrictPublicBuckets: false
82
+ }
83
+ },
84
+ status: 'success',
85
+ error: null
86
+ });
87
+ mockUsePutPublicAccessBlock.mockReturnValue(createMockMutationResult(jest.fn()));
88
+ mockUseSetBucketAcl.mockReturnValue(createMockMutationResult(jest.fn()));
72
89
  });
73
90
  it('renders bucket overview with all sections', ()=>{
74
91
  renderBucketOverview();
@@ -100,7 +117,9 @@ describe('BucketOverview', ()=>{
100
117
  status: 'success'
101
118
  });
102
119
  renderBucketOverview();
103
- expect(screen.getByText('Inactive')).toBeInTheDocument();
120
+ const versioningLabel = screen.getByText('Versioning');
121
+ const row = versioningLabel.closest('[class]').parentElement;
122
+ expect(within(row).getByText('Inactive')).toBeInTheDocument();
104
123
  });
105
124
  it('displays bucket location', ()=>{
106
125
  mockUseGetBucketLocation.mockReturnValue({
@@ -156,7 +175,9 @@ describe('BucketOverview', ()=>{
156
175
  status: 'success'
157
176
  });
158
177
  renderBucketOverview();
159
- expect(screen.getByText('Inactive')).toBeInTheDocument();
178
+ const retentionLabel = screen.getByText('Default Retention');
179
+ const row = retentionLabel.closest('[class]').parentElement;
180
+ expect(within(row).getByText('Inactive')).toBeInTheDocument();
160
181
  });
161
182
  it('shows default retention with days in Governance mode', ()=>{
162
183
  mockUseGetBucketObjectLockConfiguration.mockReturnValue({
@@ -378,7 +399,9 @@ describe('BucketOverview', ()=>{
378
399
  status: 'success'
379
400
  });
380
401
  renderBucketOverview();
381
- expect(screen.getByText('Yes')).toBeInTheDocument();
402
+ const publicToggle = document.getElementById('publicToggle');
403
+ expect(publicToggle).toBeInTheDocument();
404
+ expect(publicToggle).toBeChecked();
382
405
  });
383
406
  it('shows non-public bucket when no public grants', ()=>{
384
407
  mockUseGetBucketAcl.mockReturnValue({
@@ -398,8 +421,68 @@ describe('BucketOverview', ()=>{
398
421
  status: 'success'
399
422
  });
400
423
  renderBucketOverview();
401
- const publicValues = screen.getAllByText('No');
402
- expect(publicValues.length).toBeGreaterThan(0);
424
+ const publicToggle = document.getElementById('publicToggle');
425
+ expect(publicToggle).toBeInTheDocument();
426
+ expect(publicToggle).not.toBeChecked();
427
+ });
428
+ it('shows disabled toggle and warning when IgnorePublicAcls blocks public access', ()=>{
429
+ mockUseGetBucketAcl.mockReturnValue({
430
+ data: {
431
+ Owner: {
432
+ DisplayName: 'owner'
433
+ },
434
+ Grants: [
435
+ {
436
+ Grantee: {
437
+ URI: 'http://acs.amazonaws.com/groups/global/AllUsers'
438
+ },
439
+ Permission: 'READ'
440
+ }
441
+ ]
442
+ },
443
+ status: 'success'
444
+ });
445
+ mockUseGetPublicAccessBlock.mockReturnValue({
446
+ data: {
447
+ PublicAccessBlockConfiguration: {
448
+ BlockPublicAcls: true,
449
+ IgnorePublicAcls: true,
450
+ BlockPublicPolicy: false,
451
+ RestrictPublicBuckets: false
452
+ }
453
+ },
454
+ status: 'success',
455
+ error: null
456
+ });
457
+ renderBucketOverview();
458
+ const publicToggle = document.getElementById('publicToggle');
459
+ expect(publicToggle).toBeInTheDocument();
460
+ expect(publicToggle).not.toBeChecked();
461
+ expect(publicToggle).toBeDisabled();
462
+ expect(screen.getByText('Public access blocked by advanced settings below.')).toBeInTheDocument();
463
+ });
464
+ it('shows PAB collapsible section when PAB is supported', ()=>{
465
+ renderBucketOverview();
466
+ expect(screen.getByText('Public Access Block (PAB)')).toBeInTheDocument();
467
+ });
468
+ it('hides PAB section when API returns 501', ()=>{
469
+ mockUseGetPublicAccessBlock.mockReturnValue({
470
+ data: void 0,
471
+ status: 'error',
472
+ error: {
473
+ statusCode: 501
474
+ }
475
+ });
476
+ renderBucketOverview();
477
+ expect(screen.queryByText('Public Access Block (PAB)')).not.toBeInTheDocument();
478
+ });
479
+ it('expands PAB section and shows 4 toggles', ()=>{
480
+ renderBucketOverview();
481
+ fireEvent.click(screen.getByText('Public Access Block (PAB)'));
482
+ expect(screen.getByText('BlockPublicAcls')).toBeInTheDocument();
483
+ expect(screen.getByText('IgnorePublicAcls')).toBeInTheDocument();
484
+ expect(screen.getByText('BlockPublicPolicy')).toBeInTheDocument();
485
+ expect(screen.getByText('RestrictPublicBuckets')).toBeInTheDocument();
403
486
  });
404
487
  it('shows loading states while data is being fetched', ()=>{
405
488
  mockUseGetBucketVersioning.mockReturnValue({
@@ -516,7 +599,9 @@ describe('BucketOverview', ()=>{
516
599
  status: 'success'
517
600
  });
518
601
  renderBucketOverview();
519
- expect(screen.getByText('Inactive')).toBeInTheDocument();
602
+ const versioningLabel = screen.getByText('Versioning');
603
+ const row = versioningLabel.closest('[class]').parentElement;
604
+ expect(within(row).getByText('Inactive')).toBeInTheDocument();
520
605
  });
521
606
  it('handles CORS error state', ()=>{
522
607
  mockUseGetBucketCors.mockReturnValue({
@@ -28,7 +28,8 @@ const createBucketNameColumn = (onNavigateToBucket)=>({
28
28
  },
29
29
  cellStyle: {
30
30
  flex: '1',
31
- width: 'unset'
31
+ width: 'unset',
32
+ minWidth: 0
32
33
  }
33
34
  });
34
35
  const createLocationColumn = (locationMap)=>({
@@ -1,15 +1,16 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { ConstrainedText, Icon, Loader, Stack, spacing } from "@scality/core-ui";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { ConstrainedText, Icon, Loader, Stack, Text, Toggle, Tooltip, spacing, useToast } from "@scality/core-ui";
3
3
  import { Box, Button } from "@scality/core-ui/dist/next";
4
- import { createContext, memo, useContext, useMemo } from "react";
4
+ import { Fragment as external_react_Fragment, createContext, memo, useCallback, useContext, useMemo } from "react";
5
5
  import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
6
- import { useGetBucketAcl, useGetBucketCors, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketVersioning, useISVBucketStatus } from "../../hooks/index.js";
6
+ import { useGetBucketAcl, useGetBucketCors, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketVersioning, useISVBucketStatus, useSetBucketAcl } from "../../hooks/index.js";
7
7
  import { isNotFoundError } from "../../utils/errorHandling.js";
8
8
  import { EditRetentionButton } from "../objects/ObjectLock/EditRetentionButton.js";
9
9
  import { useDataBrowserConfig } from "../providers/DataBrowserProvider.js";
10
10
  import { Body, Group, GroupContent, GroupName, GroupValues, Key, Row, Table, TableContainer, Value } from "../ui/Table.elements.js";
11
11
  import { BucketConfigEditButton } from "./BucketConfigEditButton.js";
12
12
  import { BucketLocation } from "./BucketLocation.js";
13
+ import { PublicAccessBlock, usePublicAccessBlockStatus } from "./PublicAccessBlock.js";
13
14
  const DEFAULT_PUBLIC_ACL_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';
14
15
  const STATUS_PENDING = 'pending';
15
16
  const STATUS_ERROR = 'error';
@@ -189,8 +190,26 @@ const createCorsField = (bucketName, corsData, corsStatus, corsError, onEditCors
189
190
  })
190
191
  };
191
192
  };
192
- const createPublicField = (bucketName, aclStatus, isPublic, publicField, renderPublic)=>{
193
- const defaultValue = aclStatus === STATUS_PENDING ? /*#__PURE__*/ jsx(Loader, {}) : isPublic ? 'Yes' : 'No';
193
+ const createPublicField = ({ bucketName, aclStatus, isPublic, pabStatus, onPublicToggle, publicField, renderPublic })=>{
194
+ let defaultValue;
195
+ if (aclStatus === STATUS_PENDING || pabStatus.isPending) defaultValue = /*#__PURE__*/ jsx(Loader, {});
196
+ else {
197
+ const isDisabled = !pabStatus.isNotImplemented && (pabStatus.isPublicIgnored || pabStatus.isSettingPublicBlocked && !isPublic);
198
+ const toggleElement = /*#__PURE__*/ jsx(Toggle, {
199
+ id: "publicToggle",
200
+ disabled: isDisabled,
201
+ toggle: isPublic,
202
+ label: isPublic ? 'Yes' : 'No',
203
+ onChange: ()=>onPublicToggle?.()
204
+ });
205
+ defaultValue = isDisabled ? /*#__PURE__*/ jsx(Tooltip, {
206
+ overlay: "Public access is blocked by Public Access Block settings",
207
+ children: /*#__PURE__*/ jsx(Box, {
208
+ display: "inline-block",
209
+ children: toggleElement
210
+ })
211
+ }) : toggleElement;
212
+ }
194
213
  return {
195
214
  id: 'public',
196
215
  label: 'Public',
@@ -434,16 +453,54 @@ const PermissionsSection = /*#__PURE__*/ memo(({ onEditPolicy, onEditCors, owner
434
453
  const { data: policyData, error: policyError, status: policyStatus } = useGetBucketPolicy({
435
454
  Bucket: bucketName
436
455
  });
437
- const isPublic = useMemo(()=>aclData?.Grants?.some((grant)=>grant.Grantee?.URI === (config.publicAclIndicator || DEFAULT_PUBLIC_ACL_URI)) ?? false, [
456
+ const pabStatus = usePublicAccessBlockStatus(bucketName);
457
+ const { mutate: setBucketAcl } = useSetBucketAcl();
458
+ const { showToast } = useToast();
459
+ const hasPublicAcl = useMemo(()=>aclData?.Grants?.some((grant)=>grant.Grantee?.URI === (config.publicAclIndicator || DEFAULT_PUBLIC_ACL_URI)) ?? false, [
438
460
  aclData?.Grants,
439
461
  config.publicAclIndicator
440
462
  ]);
463
+ const isPublic = hasPublicAcl && !pabStatus.isPublicIgnored;
464
+ const handlePublicToggle = useCallback(()=>{
465
+ setBucketAcl({
466
+ Bucket: bucketName,
467
+ ACL: isPublic ? 'private' : 'public-read'
468
+ }, {
469
+ onSuccess: ()=>{
470
+ showToast({
471
+ open: true,
472
+ message: `Bucket is now ${isPublic ? 'private' : 'public'}`,
473
+ status: 'success'
474
+ });
475
+ },
476
+ onError: (error)=>{
477
+ showToast({
478
+ open: true,
479
+ message: error instanceof Error ? error.message : 'Failed to update bucket visibility',
480
+ status: 'error'
481
+ });
482
+ }
483
+ });
484
+ }, [
485
+ bucketName,
486
+ isPublic,
487
+ setBucketAcl,
488
+ showToast
489
+ ]);
441
490
  const fields = useMemo(()=>{
442
491
  const defaultFields = {
443
492
  owner: createOwnerField(bucketName, aclData, aclStatus, ownerField, renderOwner),
444
493
  acl: createAclField(bucketName, aclData, aclStatus, aclField, renderAcl),
445
494
  cors: createCorsField(bucketName, corsData, corsStatus, corsError, onEditCors, corsField, renderCors, isISVManaged, isvApplication),
446
- public: createPublicField(bucketName, aclStatus, isPublic, publicField, renderPublic),
495
+ public: createPublicField({
496
+ bucketName,
497
+ aclStatus,
498
+ isPublic,
499
+ pabStatus,
500
+ onPublicToggle: handlePublicToggle,
501
+ publicField,
502
+ renderPublic
503
+ }),
447
504
  bucketPolicy: createBucketPolicyField(bucketName, policyData, policyStatus, policyError, onEditPolicy, bucketPolicyField, renderBucketPolicy, isISVManaged, isvApplication)
448
505
  };
449
506
  return mergeFieldsWithExtras(defaultFields, extraBucketOverviewPermissions, bucketName, [
@@ -478,14 +535,42 @@ const PermissionsSection = /*#__PURE__*/ memo(({ onEditPolicy, onEditCors, owner
478
535
  policyStatus,
479
536
  isPublic,
480
537
  isISVManaged,
481
- isvApplication
538
+ isvApplication,
539
+ pabStatus,
540
+ handlePublicToggle
482
541
  ]);
542
+ const showPab = !pabStatus.isNotImplemented && !pabStatus.isPending;
483
543
  return /*#__PURE__*/ jsx(Section, {
484
544
  title: "Permissions",
485
- children: fields.map((field)=>/*#__PURE__*/ jsx(Field, {
486
- label: field.label,
487
- actions: field.actions,
488
- children: field.value
545
+ children: fields.map((field)=>/*#__PURE__*/ jsxs(external_react_Fragment, {
546
+ children: [
547
+ /*#__PURE__*/ jsx(Field, {
548
+ label: field.label,
549
+ actions: field.actions,
550
+ children: field.value
551
+ }),
552
+ 'public' === field.id && showPab && /*#__PURE__*/ jsxs(Fragment, {
553
+ children: [
554
+ (pabStatus.isPublicIgnored || pabStatus.isSettingPublicBlocked && !isPublic) && /*#__PURE__*/ jsxs(Row, {
555
+ children: [
556
+ /*#__PURE__*/ jsx(Key, {}),
557
+ /*#__PURE__*/ jsx(Value, {
558
+ children: /*#__PURE__*/ jsx(Text, {
559
+ color: "statusWarning",
560
+ variant: "Small",
561
+ children: "Public access blocked by advanced settings below."
562
+ })
563
+ })
564
+ ]
565
+ }),
566
+ /*#__PURE__*/ jsx(PublicAccessBlock, {
567
+ bucketName: bucketName,
568
+ data: pabStatus.data,
569
+ status: pabStatus.status
570
+ })
571
+ ]
572
+ })
573
+ ]
489
574
  }, field.id))
490
575
  });
491
576
  });
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { Banner, Icon, Loader, Modal, Stack, Text, Tooltip, Wrap } from "@scality/core-ui";
2
+ import { Banner, ConstrainedText, Icon, Loader, Modal, Stack, Text, Tooltip, Wrap } from "@scality/core-ui";
3
3
  import { Input } from "@scality/core-ui/dist/components/inputv2/inputv2";
4
4
  import { Box, Button } from "@scality/core-ui/dist/next";
5
5
  import { useMemo, useState } from "react";
@@ -91,7 +91,10 @@ const EmptyBucketButton = ({ bucketName })=>{
91
91
  }),
92
92
  /*#__PURE__*/ jsx(Modal, {
93
93
  isOpen: isEmptyModalOpen,
94
- title: `Empty Bucket '${bucketName}'?`,
94
+ title: /*#__PURE__*/ jsx(ConstrainedText, {
95
+ text: `Empty Bucket '${bucketName}'?`,
96
+ lineClamp: 1
97
+ }),
95
98
  close: ()=>{
96
99
  if (!isEmptying) {
97
100
  setIsEmptyModalOpen(false);
@@ -0,0 +1,18 @@
1
+ import type { GetPublicAccessBlockOutput } from '@aws-sdk/client-s3';
2
+ interface PublicAccessBlockProps {
3
+ bucketName: string;
4
+ data: GetPublicAccessBlockOutput | undefined;
5
+ status: 'pending' | 'success' | 'error';
6
+ }
7
+ export declare const PublicAccessBlock: import("react").MemoExoticComponent<({ bucketName, data, status }: PublicAccessBlockProps) => import("react/jsx-runtime").JSX.Element | null>;
8
+ export declare function usePublicAccessBlockStatus(bucketName: string): {
9
+ data: import("@aws-sdk/client-s3").GetPublicAccessBlockCommandOutput | undefined;
10
+ status: "error" | "success" | "pending";
11
+ isNotImplemented: boolean;
12
+ isPending: boolean;
13
+ isError: boolean;
14
+ isAnyBlockActive: boolean;
15
+ isPublicIgnored: boolean;
16
+ isSettingPublicBlocked: boolean;
17
+ };
18
+ export {};
@@ -0,0 +1,113 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Icon, Text, Toggle, Tooltip, spacing } from "@scality/core-ui";
3
+ import { Accordion } from "@scality/core-ui/dist/components/accordion/Accordion.component";
4
+ import { Box } from "@scality/core-ui/dist/next";
5
+ import { memo, useCallback, useMemo } from "react";
6
+ import { useGetPublicAccessBlock, usePutPublicAccessBlock } from "../../hooks/index.js";
7
+ import { Key, Row, Value } from "../ui/Table.elements.js";
8
+ const PAB_SETTINGS = [
9
+ {
10
+ key: 'BlockPublicAcls',
11
+ label: 'BlockPublicAcls',
12
+ tooltip: 'Rejects PUT requests that include public ACL grants. Existing public ACLs are not affected.'
13
+ },
14
+ {
15
+ key: 'IgnorePublicAcls',
16
+ label: 'IgnorePublicAcls',
17
+ tooltip: 'Ignores all public ACLs on this bucket and its objects. The ACLs still exist but have no effect on access.'
18
+ },
19
+ {
20
+ key: 'BlockPublicPolicy',
21
+ label: 'BlockPublicPolicy',
22
+ tooltip: 'Rejects bucket policy changes that would grant public access. Existing public policies are not affected.'
23
+ },
24
+ {
25
+ key: 'RestrictPublicBuckets',
26
+ label: 'RestrictPublicBuckets',
27
+ tooltip: 'Restricts access to buckets with public policies to only AWS service principals and authorized users within the bucket owner\'s account. The policy still exists but public and cross-account access is denied.'
28
+ }
29
+ ];
30
+ const PublicAccessBlock = /*#__PURE__*/ memo(({ bucketName, data, status })=>{
31
+ const { mutate: putPublicAccessBlock, isPending } = usePutPublicAccessBlock();
32
+ const config = data?.PublicAccessBlockConfiguration;
33
+ const handleToggle = useCallback((key)=>{
34
+ const currentConfig = config ?? {};
35
+ putPublicAccessBlock({
36
+ Bucket: bucketName,
37
+ PublicAccessBlockConfiguration: {
38
+ ...currentConfig,
39
+ [key]: !currentConfig[key]
40
+ }
41
+ });
42
+ }, [
43
+ bucketName,
44
+ config,
45
+ putPublicAccessBlock
46
+ ]);
47
+ if ('pending' === status) return null;
48
+ return /*#__PURE__*/ jsx(Box, {
49
+ marginTop: spacing.r8,
50
+ marginBottom: spacing.r16,
51
+ children: /*#__PURE__*/ jsx(Accordion, {
52
+ id: "pab",
53
+ title: "Public Access Block (PAB)",
54
+ children: 'error' === status ? /*#__PURE__*/ jsx(Text, {
55
+ color: "statusWarning",
56
+ children: "Failed to load Public Access Block settings"
57
+ }) : PAB_SETTINGS.map(({ key, label, tooltip })=>/*#__PURE__*/ jsxs(Row, {
58
+ children: [
59
+ /*#__PURE__*/ jsx(Key, {
60
+ children: /*#__PURE__*/ jsxs(Box, {
61
+ display: "flex",
62
+ alignItems: "center",
63
+ gap: spacing.r4,
64
+ children: [
65
+ label,
66
+ /*#__PURE__*/ jsx(Tooltip, {
67
+ overlay: tooltip,
68
+ children: /*#__PURE__*/ jsx(Icon, {
69
+ name: "Info",
70
+ size: "xs"
71
+ })
72
+ })
73
+ ]
74
+ })
75
+ }),
76
+ /*#__PURE__*/ jsx(Value, {
77
+ children: /*#__PURE__*/ jsx(Toggle, {
78
+ id: `pab-${key}`,
79
+ disabled: isPending,
80
+ toggle: config?.[key] ?? false,
81
+ label: config?.[key] ? 'Active' : 'Inactive',
82
+ onChange: ()=>handleToggle(key)
83
+ })
84
+ })
85
+ ]
86
+ }, key))
87
+ })
88
+ });
89
+ });
90
+ PublicAccessBlock.displayName = 'PublicAccessBlock';
91
+ function usePublicAccessBlockStatus(bucketName) {
92
+ const { data, status, error } = useGetPublicAccessBlock({
93
+ Bucket: bucketName
94
+ });
95
+ const isNotImplemented = error?.statusCode === 501;
96
+ const config = data?.PublicAccessBlockConfiguration;
97
+ const isAnyBlockActive = useMemo(()=>!!(config?.BlockPublicAcls || config?.IgnorePublicAcls || config?.BlockPublicPolicy || config?.RestrictPublicBuckets), [
98
+ config
99
+ ]);
100
+ const isPublicIgnored = !!config?.IgnorePublicAcls;
101
+ const isSettingPublicBlocked = !!config?.BlockPublicAcls;
102
+ return {
103
+ data,
104
+ status,
105
+ isNotImplemented,
106
+ isPending: 'pending' === status,
107
+ isError: 'error' === status && !isNotImplemented,
108
+ isAnyBlockActive,
109
+ isPublicIgnored,
110
+ isSettingPublicBlocked
111
+ };
112
+ }
113
+ export { PublicAccessBlock, usePublicAccessBlockStatus };
@@ -13,6 +13,7 @@ export { BucketVersioning } from './buckets/BucketVersioning';
13
13
  export { DeleteBucketButton } from './buckets/DeleteBucketButton';
14
14
  export { EmptyBucketButton } from './buckets/EmptyBucketButton';
15
15
  export { BucketNotificationFormPage } from './buckets/notifications/BucketNotificationFormPage';
16
+ export { PublicAccessBlock, usePublicAccessBlockStatus } from './buckets/PublicAccessBlock';
16
17
  export { DataBrowserUI } from './DataBrowserUI';
17
18
  export { CreateFolderButton } from './objects/CreateFolderButton';
18
19
  export { ObjectDetails } from './objects/ObjectDetails';
@@ -13,6 +13,7 @@ import { BucketVersioning } from "./buckets/BucketVersioning.js";
13
13
  import { DeleteBucketButton } from "./buckets/DeleteBucketButton.js";
14
14
  import { EmptyBucketButton } from "./buckets/EmptyBucketButton.js";
15
15
  import { BucketNotificationFormPage } from "./buckets/notifications/BucketNotificationFormPage.js";
16
+ import { PublicAccessBlock, usePublicAccessBlockStatus } from "./buckets/PublicAccessBlock.js";
16
17
  import { DataBrowserUI } from "./DataBrowserUI.js";
17
18
  import { CreateFolderButton } from "./objects/CreateFolderButton.js";
18
19
  import { ObjectDetails } from "./objects/ObjectDetails/index.js";
@@ -25,4 +26,4 @@ import { DataBrowserProvider, useDataBrowserConfig, useDataBrowserContext, useIn
25
26
  import { QueryProvider } from "./providers/QueryProvider.js";
26
27
  import { MetadataSearch } from "./search/MetadataSearch.js";
27
28
  import { ArrayFieldActions } from "./ui/ArrayFieldActions.js";
28
- export { ArrayFieldActions, Breadcrumb, BucketAccessor, BucketCorsPage, BucketCreate, BucketLifecycleFormPage, BucketList, BucketNotificationFormPage, BucketOverview, BucketOverviewField, BucketOverviewSection, BucketPage, BucketPolicyPage, BucketReplicationFormPage, BucketVersioning, CreateFolderButton, DataBrowserBreadcrumb, DataBrowserProvider, DataBrowserUI, DefaultReplicationDestinationFields, DeleteBucketButton, EditRetentionButton, EmptyBucketButton, MetadataSearch, ObjectDetails, ObjectList, ObjectLockSettings, ObjectPage, QueryProvider, UploadButton, baseBucketCreateSchema, bucketErrorMessage, bucketNameValidationSchema, useBreadcrumbPaths, useBucketOverviewContext, useDataBrowserConfig, useDataBrowserContext, useDataBrowserNavigate, useInvalidateQueries };
29
+ export { ArrayFieldActions, Breadcrumb, BucketAccessor, BucketCorsPage, BucketCreate, BucketLifecycleFormPage, BucketList, BucketNotificationFormPage, BucketOverview, BucketOverviewField, BucketOverviewSection, BucketPage, BucketPolicyPage, BucketReplicationFormPage, BucketVersioning, CreateFolderButton, DataBrowserBreadcrumb, DataBrowserProvider, DataBrowserUI, DefaultReplicationDestinationFields, DeleteBucketButton, EditRetentionButton, EmptyBucketButton, MetadataSearch, ObjectDetails, ObjectList, ObjectLockSettings, ObjectPage, PublicAccessBlock, QueryProvider, UploadButton, baseBucketCreateSchema, bucketErrorMessage, bucketNameValidationSchema, useBreadcrumbPaths, useBucketOverviewContext, useDataBrowserConfig, useDataBrowserContext, useDataBrowserNavigate, useInvalidateQueries, usePublicAccessBlockStatus };
@@ -6,7 +6,7 @@ import { useLocation, useNavigate } from "react-router";
6
6
  import styled_components from "styled-components";
7
7
  import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
8
8
  import { useListObjectVersions, useListObjects, useSearchObjects, useSearchObjectsVersions } from "../../hooks/index.js";
9
- import { useGetPresignedDownload } from "../../hooks/presignedOperations.js";
9
+ import { useDownloadObject } from "../../hooks/useDownloadObject.js";
10
10
  import { useBatchObjectLegalHold } from "../../hooks/useBatchObjectLegalHold.js";
11
11
  import { useFeatures } from "../../hooks/useFeatures.js";
12
12
  import { useQueryParams } from "../../utils/hooks.js";
@@ -62,25 +62,6 @@ const removePrefix = (path, prefix)=>{
62
62
  return path;
63
63
  };
64
64
  const createLegalHoldKey = (key, versionId)=>`${key}:${versionId ?? 'null'}`;
65
- const downloadFile = (url, filename, onCleanup)=>{
66
- try {
67
- new URL(url);
68
- const link = document.createElement('a');
69
- link.href = url;
70
- link.download = filename;
71
- link.style.display = 'none';
72
- link.rel = 'noopener noreferrer';
73
- document.body.appendChild(link);
74
- link.click();
75
- const timeoutId = setTimeout(()=>{
76
- if (document.body.contains(link)) document.body.removeChild(link);
77
- }, 100);
78
- onCleanup?.(timeoutId);
79
- } catch (error) {
80
- console.error('Invalid download URL:', url, error);
81
- throw new Error('Failed to initiate download: Invalid URL');
82
- }
83
- };
84
65
  const createNameColumn = (onPrefixChange, onDownload)=>({
85
66
  Header: 'Name',
86
67
  accessor: 'displayName',
@@ -302,7 +283,7 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
302
283
  const navigate = useNavigate();
303
284
  const [searchValue, setSearchValue] = useState(queryParams.get(SEARCH_QUERY_PARAM) || '');
304
285
  const [selectedObjects, setSelectedObjects] = useState([]);
305
- const { mutateAsync: getPresignedDownload } = useGetPresignedDownload();
286
+ const { mutateAsync: downloadObject } = useDownloadObject();
306
287
  const downloadingRef = useRef(new Set());
307
288
  const downloadTimeoutsRef = useRef(new Map());
308
289
  const { showToast } = useToast();
@@ -337,18 +318,13 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
337
318
  try {
338
319
  const rawFilename = object.displayName || object.Key.split('/').pop() || 'download';
339
320
  const sanitized = rawFilename.replace(/["\r\n\t]/g, '');
340
- const encoded = encodeURIComponent(sanitized);
341
- const result = await getPresignedDownload({
321
+ await downloadObject({
342
322
  Bucket: bucketName,
343
323
  Key: object.Key,
344
- ResponseContentDisposition: `attachment; filename="${sanitized}"; filename*=UTF-8''${encoded}`,
345
324
  ...versionId ? {
346
325
  VersionId: versionId
347
- } : {}
348
- });
349
- if (!result?.Url) throw new Error('Failed to generate presigned URL: No URL returned');
350
- downloadFile(result.Url, sanitized, (timeoutId)=>{
351
- downloadTimeoutsRef.current.set(`${downloadKey}_link`, timeoutId);
326
+ } : {},
327
+ filename: sanitized
352
328
  });
353
329
  } catch (error) {
354
330
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@@ -361,13 +337,12 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
361
337
  const timeoutId = setTimeout(()=>{
362
338
  downloadingRef.current.delete(downloadKey);
363
339
  downloadTimeoutsRef.current.delete(downloadKey);
364
- downloadTimeoutsRef.current.delete(`${downloadKey}_link`);
365
340
  }, 1000);
366
341
  downloadTimeoutsRef.current.set(downloadKey, timeoutId);
367
342
  }
368
343
  }, [
369
344
  bucketName,
370
- getPresignedDownload,
345
+ downloadObject,
371
346
  showToast
372
347
  ]);
373
348
  const isSearchActive = isMetadataSearchEnabled && Boolean(metadataSearchQuery);
@@ -24,7 +24,7 @@ describe('usePresigningS3Client', ()=>{
24
24
  }));
25
25
  });
26
26
  it('should create an S3 client with the direct endpoint when no proxy is configured', ()=>{
27
- renderHook(()=>usePresigningS3Client(), {
27
+ const { result } = renderHook(()=>usePresigningS3Client(), {
28
28
  wrapper: createTestWrapper(testConfig, testCredentials)
29
29
  });
30
30
  expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
@@ -32,34 +32,38 @@ describe('usePresigningS3Client', ()=>{
32
32
  region: testConfig.region,
33
33
  forcePathStyle: true
34
34
  }));
35
+ expect(result.current.client).toBeDefined();
36
+ });
37
+ it('should return identity rewriteUrl when no proxy is configured', ()=>{
38
+ const { result } = renderHook(()=>usePresigningS3Client(), {
39
+ wrapper: createTestWrapper(testConfig, testCredentials)
40
+ });
41
+ const url = 'http://s3.example.com/bucket/key?X-Amz-Signature=abc';
42
+ expect(result.current.rewriteUrl(url)).toBe(url);
35
43
  });
36
- it('should create an S3 client with the proxy endpoint (not target) when proxy is enabled', ()=>{
44
+ it('should create an S3 client with the target endpoint when proxy is enabled', ()=>{
37
45
  renderHook(()=>usePresigningS3Client(), {
38
46
  wrapper: createTestWrapper(proxyConfig, testCredentials)
39
47
  });
40
48
  expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
41
- endpoint: 'https://proxy.example.com/s3',
49
+ endpoint: 'http://internal-s3.cluster.local',
42
50
  region: testConfig.region,
43
51
  forcePathStyle: true
44
52
  }));
45
53
  const constructorCall = MockedS3Client.mock.calls[0][0];
46
- expect(constructorCall?.endpoint).not.toContain('internal-s3.cluster.local');
54
+ expect(constructorCall?.endpoint).not.toContain('proxy.example.com');
47
55
  });
48
- it('should not attach proxy middleware even when proxy is enabled', ()=>{
49
- const mockUse = jest.fn();
50
- MockedS3Client.mockImplementation(()=>({
51
- config: {},
52
- middlewareStack: {
53
- use: mockUse,
54
- add: jest.fn()
55
- }
56
- }));
57
- renderHook(()=>usePresigningS3Client(), {
56
+ it('should rewrite presigned URLs from target to proxy endpoint', ()=>{
57
+ const { result } = renderHook(()=>usePresigningS3Client(), {
58
58
  wrapper: createTestWrapper(proxyConfig, testCredentials)
59
59
  });
60
- expect(mockUse).not.toHaveBeenCalled();
60
+ const targetUrl = 'http://internal-s3.cluster.local/my-bucket/my-key?X-Amz-Signature=abc123';
61
+ const rewritten = result.current.rewriteUrl(targetUrl);
62
+ expect(rewritten).toContain('https://proxy.example.com');
63
+ expect(rewritten).toContain('/s3/my-bucket/my-key');
64
+ expect(rewritten).toContain('X-Amz-Signature=abc123');
61
65
  });
62
- it('should use origin-relative proxy endpoint resolved with window.location.origin', ()=>{
66
+ it('should rewrite URLs with origin-relative proxy endpoint', ()=>{
63
67
  const originRelativeConfig = {
64
68
  ...testConfig,
65
69
  proxy: {
@@ -68,13 +72,30 @@ describe('usePresigningS3Client', ()=>{
68
72
  target: 'http://artesca-data-connector-s3api.zenko.svc.cluster.local'
69
73
  }
70
74
  };
71
- const originalOrigin = window.location.origin;
72
- renderHook(()=>usePresigningS3Client(), {
75
+ const { result } = renderHook(()=>usePresigningS3Client(), {
73
76
  wrapper: createTestWrapper(originRelativeConfig, testCredentials)
74
77
  });
75
78
  expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
76
- endpoint: `${originalOrigin}/zenko/s3`
79
+ endpoint: 'http://artesca-data-connector-s3api.zenko.svc.cluster.local'
77
80
  }));
81
+ const targetUrl = 'http://artesca-data-connector-s3api.zenko.svc.cluster.local/bucket/key?X-Amz-Signature=xyz';
82
+ const rewritten = result.current.rewriteUrl(targetUrl);
83
+ expect(rewritten).toContain('/zenko/s3/bucket/key');
84
+ expect(rewritten).toContain('X-Amz-Signature=xyz');
85
+ });
86
+ it('should not attach proxy middleware even when proxy is enabled', ()=>{
87
+ const mockUse = jest.fn();
88
+ MockedS3Client.mockImplementation(()=>({
89
+ config: {},
90
+ middlewareStack: {
91
+ use: mockUse,
92
+ add: jest.fn()
93
+ }
94
+ }));
95
+ renderHook(()=>usePresigningS3Client(), {
96
+ wrapper: createTestWrapper(proxyConfig, testCredentials)
97
+ });
98
+ expect(mockUse).not.toHaveBeenCalled();
78
99
  });
79
100
  it('should use direct endpoint when proxy is disabled', ()=>{
80
101
  const disabledProxyConfig = {
@@ -91,14 +112,14 @@ describe('usePresigningS3Client', ()=>{
91
112
  endpoint: testConfig.endpoint
92
113
  }));
93
114
  });
94
- it('should return the same client instance on re-render when s3ConfigIdentifier is unchanged', ()=>{
115
+ it('should return the same result on re-render when s3ConfigIdentifier is unchanged', ()=>{
95
116
  const { result, rerender } = renderHook(()=>usePresigningS3Client(), {
96
117
  wrapper: createTestWrapper(testConfig, testCredentials)
97
118
  });
98
- const firstClient = result.current;
119
+ const first = result.current;
99
120
  rerender();
100
- const secondClient = result.current;
101
- expect(firstClient).toBe(secondClient);
121
+ const second = result.current;
122
+ expect(first).toBe(second);
102
123
  expect(MockedS3Client).toHaveBeenCalledTimes(1);
103
124
  });
104
125
  });
@@ -4,7 +4,7 @@
4
4
  * This file contains hooks for managing bucket configuration settings
5
5
  * like ACL, policies, versioning, CORS, lifecycle, etc.
6
6
  */
7
- import { type DeleteBucketCorsCommandInput, type DeleteBucketCorsCommandOutput, type DeleteBucketLifecycleCommandInput, type DeleteBucketLifecycleCommandOutput, type DeleteBucketPolicyCommandInput, type DeleteBucketPolicyCommandOutput, type DeleteBucketReplicationCommandInput, type DeleteBucketReplicationCommandOutput, type DeleteBucketTaggingCommandInput, type DeleteBucketTaggingCommandOutput, type GetBucketAclCommandInput, type GetBucketAclCommandOutput, type GetBucketCorsCommandInput, type GetBucketCorsCommandOutput, type GetBucketEncryptionCommandInput, type GetBucketEncryptionCommandOutput, type GetBucketLifecycleConfigurationCommandInput, type GetBucketLifecycleConfigurationCommandOutput, type GetBucketNotificationConfigurationCommandInput, type GetBucketNotificationConfigurationCommandOutput, type GetBucketPolicyCommandInput, type GetBucketPolicyCommandOutput, type GetBucketReplicationCommandInput, type GetBucketReplicationCommandOutput, type GetBucketTaggingCommandInput, type GetBucketTaggingCommandOutput, type GetBucketVersioningCommandInput, type GetBucketVersioningCommandOutput, type GetObjectLockConfigurationCommandInput, type GetObjectLockConfigurationCommandOutput, type GetPublicAccessBlockCommandInput, type GetPublicAccessBlockCommandOutput, type PutBucketAclCommandInput, type PutBucketAclCommandOutput, type PutBucketCorsCommandInput, type PutBucketCorsCommandOutput, type PutBucketEncryptionCommandInput, type PutBucketEncryptionCommandOutput, type PutBucketLifecycleConfigurationCommandInput, type PutBucketLifecycleConfigurationCommandOutput, type PutBucketNotificationConfigurationCommandInput, type PutBucketNotificationConfigurationCommandOutput, type PutBucketPolicyCommandInput, type PutBucketPolicyCommandOutput, type PutBucketReplicationCommandInput, type PutBucketReplicationCommandOutput, type PutBucketTaggingCommandInput, type PutBucketTaggingCommandOutput, type PutBucketVersioningCommandInput, type PutBucketVersioningCommandOutput, type PutObjectLockConfigurationCommandInput, type PutObjectLockConfigurationCommandOutput } from '@aws-sdk/client-s3';
7
+ import { type DeleteBucketCorsCommandInput, type DeleteBucketCorsCommandOutput, type DeleteBucketLifecycleCommandInput, type DeleteBucketLifecycleCommandOutput, type DeleteBucketPolicyCommandInput, type DeleteBucketPolicyCommandOutput, type DeleteBucketReplicationCommandInput, type DeleteBucketReplicationCommandOutput, type DeleteBucketTaggingCommandInput, type DeleteBucketTaggingCommandOutput, type DeletePublicAccessBlockCommandInput, type DeletePublicAccessBlockCommandOutput, type GetBucketAclCommandInput, type GetBucketAclCommandOutput, type GetBucketCorsCommandInput, type GetBucketCorsCommandOutput, type GetBucketEncryptionCommandInput, type GetBucketEncryptionCommandOutput, type GetBucketLifecycleConfigurationCommandInput, type GetBucketLifecycleConfigurationCommandOutput, type GetBucketNotificationConfigurationCommandInput, type GetBucketNotificationConfigurationCommandOutput, type GetBucketPolicyCommandInput, type GetBucketPolicyCommandOutput, type GetBucketReplicationCommandInput, type GetBucketReplicationCommandOutput, type GetBucketTaggingCommandInput, type GetBucketTaggingCommandOutput, type GetBucketVersioningCommandInput, type GetBucketVersioningCommandOutput, type GetObjectLockConfigurationCommandInput, type GetObjectLockConfigurationCommandOutput, type GetPublicAccessBlockCommandInput, type GetPublicAccessBlockCommandOutput, type PutBucketAclCommandInput, type PutBucketAclCommandOutput, type PutBucketCorsCommandInput, type PutBucketCorsCommandOutput, type PutBucketEncryptionCommandInput, type PutBucketEncryptionCommandOutput, type PutBucketLifecycleConfigurationCommandInput, type PutBucketLifecycleConfigurationCommandOutput, type PutBucketNotificationConfigurationCommandInput, type PutBucketNotificationConfigurationCommandOutput, type PutBucketPolicyCommandInput, type PutBucketPolicyCommandOutput, type PutBucketReplicationCommandInput, type PutBucketReplicationCommandOutput, type PutBucketTaggingCommandInput, type PutBucketTaggingCommandOutput, type PutBucketVersioningCommandInput, type PutBucketVersioningCommandOutput, type PutObjectLockConfigurationCommandInput, type PutObjectLockConfigurationCommandOutput, type PutPublicAccessBlockCommandInput, type PutPublicAccessBlockCommandOutput } from '@aws-sdk/client-s3';
8
8
  /**
9
9
  * Hook for retrieving S3 bucket access control list (ACL) settings
10
10
  *
@@ -173,3 +173,5 @@ export declare const useDeleteBucketReplication: (options?: Omit<import("@tansta
173
173
  * This configuration determines whether public access is blocked at the bucket level.
174
174
  */
175
175
  export declare const useGetPublicAccessBlock: (params?: GetPublicAccessBlockCommandInput | undefined, options?: Omit<import("@tanstack/react-query").UseQueryOptions<GetPublicAccessBlockCommandOutput, import("..").EnhancedS3Error, GetPublicAccessBlockCommandOutput, readonly unknown[]>, "queryKey" | "queryFn"> | undefined) => import("@tanstack/react-query").UseQueryResult<GetPublicAccessBlockCommandOutput, import("..").EnhancedS3Error>;
176
+ export declare const usePutPublicAccessBlock: (options?: Omit<import("@tanstack/react-query").UseMutationOptions<PutPublicAccessBlockCommandOutput, import("..").EnhancedS3Error, PutPublicAccessBlockCommandInput, unknown>, "mutationFn"> | undefined) => import("@tanstack/react-query").UseMutationResult<PutPublicAccessBlockCommandOutput, import("..").EnhancedS3Error, PutPublicAccessBlockCommandInput>;
177
+ export declare const useDeletePublicAccessBlock: (options?: Omit<import("@tanstack/react-query").UseMutationOptions<DeletePublicAccessBlockCommandOutput, import("..").EnhancedS3Error, DeletePublicAccessBlockCommandInput, unknown>, "mutationFn"> | undefined) => import("@tanstack/react-query").UseMutationResult<DeletePublicAccessBlockCommandOutput, import("..").EnhancedS3Error, DeletePublicAccessBlockCommandInput>;
@@ -1,4 +1,4 @@
1
- import { DeleteBucketCorsCommand, DeleteBucketLifecycleCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, GetBucketAclCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, PutBucketAclCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketLifecycleConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutObjectLockConfigurationCommand } from "@aws-sdk/client-s3";
1
+ import { DeleteBucketCorsCommand, DeleteBucketLifecycleCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeletePublicAccessBlockCommand, GetBucketAclCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, PutBucketAclCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketLifecycleConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand } from "@aws-sdk/client-s3";
2
2
  import { useCreateS3MutationHook } from "./factories/useCreateS3MutationHook.js";
3
3
  import { useCreateS3QueryHook } from "./factories/useCreateS3QueryHook.js";
4
4
  const useGetBucketAcl = useCreateS3QueryHook(GetBucketAclCommand, 'GetBucketAcl');
@@ -65,4 +65,11 @@ const useDeleteBucketReplication = useCreateS3MutationHook(DeleteBucketReplicati
65
65
  'GetBucketReplication'
66
66
  ]);
67
67
  const useGetPublicAccessBlock = useCreateS3QueryHook(GetPublicAccessBlockCommand, 'GetPublicAccessBlock');
68
- export { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning };
68
+ const usePutPublicAccessBlock = useCreateS3MutationHook(PutPublicAccessBlockCommand, 'PutPublicAccessBlock', [
69
+ 'GetPublicAccessBlock',
70
+ 'ListBuckets'
71
+ ]);
72
+ const useDeletePublicAccessBlock = useCreateS3MutationHook(DeletePublicAccessBlockCommand, 'DeletePublicAccessBlock', [
73
+ 'GetPublicAccessBlock'
74
+ ]);
75
+ export { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeletePublicAccessBlock, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, usePutPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning };
@@ -14,5 +14,5 @@
14
14
  */
15
15
  export { useCreateS3InfiniteQueryHook } from './useCreateS3InfiniteQueryHook';
16
16
  export { useCreateS3LoginHook } from './useCreateS3LoginHook';
17
- export { useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook, } from './useCreateS3MutationHook';
17
+ export { type UrlRewriter, useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook, } from './useCreateS3MutationHook';
18
18
  export { useCreateS3QueryHook } from './useCreateS3QueryHook';
@@ -3,4 +3,5 @@ import { type UseMutationOptions, type UseMutationResult } from '@tanstack/react
3
3
  import { type EnhancedS3Error } from '../../utils/errorHandling';
4
4
  export declare function useCreateS3MutationHook<TInput extends object, TOutput>(Command: new (input: TInput) => any, operationName: string, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput>, "mutationFn">) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
5
5
  export declare function useCreateS3FunctionMutationHook<TInput, TOutput>(operation: (s3Client: S3Client, input: TInput) => Promise<TOutput>, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput, unknown>, "mutationFn"> | undefined) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
6
- export declare function useCreatePresigningMutationHook<TInput, TOutput>(operation: (s3Client: S3Client, input: TInput) => Promise<TOutput>, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput, unknown>, "mutationFn"> | undefined) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
6
+ export type UrlRewriter = (url: string) => string;
7
+ export declare function useCreatePresigningMutationHook<TInput, TOutput>(operation: (s3Client: S3Client, input: TInput, rewriteUrl: UrlRewriter) => Promise<TOutput>, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput>, "mutationFn">) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
@@ -57,6 +57,24 @@ function useCreateS3FunctionMutationHook(operation, invalidationKeys) {
57
57
  return createFunctionMutationHook(operation, useS3Client, invalidationKeys);
58
58
  }
59
59
  function useCreatePresigningMutationHook(operation, invalidationKeys) {
60
- return createFunctionMutationHook(operation, usePresigningS3Client, invalidationKeys);
60
+ return (options)=>{
61
+ const { s3ConfigIdentifier } = useDataBrowserContext();
62
+ const { client, rewriteUrl } = usePresigningS3Client();
63
+ const queryClient = useQueryClient();
64
+ return useMutation({
65
+ mutationFn: async (params)=>operation(client, params, rewriteUrl),
66
+ onSuccess: (_data, _variables)=>{
67
+ if (invalidationKeys) invalidationKeys.forEach((key)=>{
68
+ queryClient.invalidateQueries({
69
+ queryKey: [
70
+ s3ConfigIdentifier,
71
+ key
72
+ ]
73
+ });
74
+ });
75
+ },
76
+ ...options
77
+ });
78
+ };
61
79
  }
62
80
  export { useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook };
@@ -1,4 +1,4 @@
1
- export { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, } from './bucketConfiguration';
1
+ export { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeletePublicAccessBlock, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, usePutPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, } from './bucketConfiguration';
2
2
  export { useBuckets, useCreateBucket, useDeleteBucket, useGetBucketLocation, useValidateBucketAccess, } from './bucketOperations';
3
3
  export type { LoginConfig, LoginMutationResult } from './loginOperations';
4
4
  export { useLoginMutation } from './loginOperations';
@@ -1,4 +1,4 @@
1
- import { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning } from "./bucketConfiguration.js";
1
+ import { useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeletePublicAccessBlock, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetPublicAccessBlock, usePutPublicAccessBlock, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning } from "./bucketConfiguration.js";
2
2
  import { useBuckets, useCreateBucket, useDeleteBucket, useGetBucketLocation, useValidateBucketAccess } from "./bucketOperations.js";
3
3
  import { useLoginMutation } from "./loginOperations.js";
4
4
  import { useCopyObject, useCreateFolder, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useListMultipartUploads, useListObjectVersions, useListObjects, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, useRestoreObject, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useUploadObjects } from "./objectOperations.js";
@@ -17,4 +17,4 @@ import { useS3Client } from "./useS3Client.js";
17
17
  import { useS3ConfigSwitch } from "./useS3ConfigSwitch.js";
18
18
  import { useSupportedNotificationEvents } from "./useSupportedNotificationEvents.js";
19
19
  import { useTableRowSelection } from "./useTableRowSelection.js";
20
- export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
20
+ export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useDeletePublicAccessBlock, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, usePutPublicAccessBlock, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
@@ -3,7 +3,7 @@ import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
3
3
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
4
  import { createS3OperationError } from "../utils/errorHandling.js";
5
5
  import { useCreatePresigningMutationHook } from "./factories/index.js";
6
- const generatePresignedDownloadUrl = async (client, config)=>{
6
+ const generatePresignedDownloadUrl = async (client, config, rewriteUrl)=>{
7
7
  try {
8
8
  const { Bucket, Key, expiresIn = 3600, ...awsOptions } = config;
9
9
  const command = new GetObjectCommand({
@@ -16,7 +16,7 @@ const generatePresignedDownloadUrl = async (client, config)=>{
16
16
  });
17
17
  const expiresAt = new Date(Date.now() + 1000 * expiresIn);
18
18
  return {
19
- Url: url,
19
+ Url: rewriteUrl(url),
20
20
  ExpiresAt: expiresAt,
21
21
  Bucket: Bucket,
22
22
  Key: Key,
@@ -26,7 +26,7 @@ const generatePresignedDownloadUrl = async (client, config)=>{
26
26
  throw createS3OperationError(error, 'GeneratePresignedDownload', config.Bucket, config.Key);
27
27
  }
28
28
  };
29
- const generatePresignedUploadUrl = async (client, config)=>{
29
+ const generatePresignedUploadUrl = async (client, config, rewriteUrl)=>{
30
30
  try {
31
31
  const { Bucket, Key, expiresIn = 3600, ...awsOptions } = config;
32
32
  const command = new PutObjectCommand({
@@ -39,7 +39,7 @@ const generatePresignedUploadUrl = async (client, config)=>{
39
39
  });
40
40
  const expiresAt = new Date(Date.now() + 1000 * expiresIn);
41
41
  return {
42
- Url: url,
42
+ Url: rewriteUrl(url),
43
43
  ExpiresAt: expiresAt,
44
44
  Bucket: Bucket,
45
45
  Key: Key
@@ -48,7 +48,7 @@ const generatePresignedUploadUrl = async (client, config)=>{
48
48
  throw createS3OperationError(error, 'GeneratePresignedUpload', config.Bucket, config.Key);
49
49
  }
50
50
  };
51
- const generatePresignedPost = async (client, config)=>{
51
+ const generatePresignedPost = async (client, config, rewriteUrl)=>{
52
52
  try {
53
53
  const { Bucket, Key, expiresIn = 3600, ...awsOptions } = config;
54
54
  const presignedPost = await createPresignedPost(client, {
@@ -60,6 +60,7 @@ const generatePresignedPost = async (client, config)=>{
60
60
  const expiresAt = new Date(Date.now() + 1000 * expiresIn);
61
61
  return {
62
62
  ...presignedPost,
63
+ url: rewriteUrl(presignedPost.url),
63
64
  ExpiresAt: expiresAt
64
65
  };
65
66
  } catch (error) {
@@ -0,0 +1,18 @@
1
+ import { type GetObjectCommandInput } from '@aws-sdk/client-s3';
2
+ import { type EnhancedS3Error } from '../utils/errorHandling';
3
+ type DownloadObjectInput = GetObjectCommandInput & {
4
+ filename: string;
5
+ };
6
+ /**
7
+ * Hook for downloading S3 objects via the S3 SDK client.
8
+ *
9
+ * Uses the normal S3 client (with proxy middleware) to fetch the object,
10
+ * avoiding presigned URL signature issues through reverse proxies.
11
+ *
12
+ * Download strategy:
13
+ * 1. If File System Access API is available (Chrome/Edge): stream
14
+ * directly to disk via showSaveFilePicker, no memory buffering
15
+ * 2. Otherwise: buffer via transformToByteArray and download via Blob URL
16
+ */
17
+ export declare const useDownloadObject: () => import("@tanstack/react-query").UseMutationResult<void, EnhancedS3Error, DownloadObjectInput, unknown>;
18
+ export {};
@@ -0,0 +1,59 @@
1
+ import { GetObjectCommand } from "@aws-sdk/client-s3";
2
+ import { useMutation } from "@tanstack/react-query";
3
+ import { createS3OperationError } from "../utils/errorHandling.js";
4
+ import { useS3Client } from "./useS3Client.js";
5
+ const supportsStreamDownload = ()=>'undefined' != typeof window && 'showSaveFilePicker' in window;
6
+ async function streamDownload(stream, filename) {
7
+ const handle = await window.showSaveFilePicker({
8
+ suggestedName: filename
9
+ });
10
+ const writable = await handle.createWritable();
11
+ await stream.pipeTo(writable);
12
+ }
13
+ function downloadViaBlob(bytes, filename, contentType) {
14
+ const blob = new Blob([
15
+ bytes
16
+ ], {
17
+ type: contentType
18
+ });
19
+ const url = URL.createObjectURL(blob);
20
+ const link = document.createElement('a');
21
+ link.href = url;
22
+ link.download = filename;
23
+ link.style.display = 'none';
24
+ document.body.appendChild(link);
25
+ link.click();
26
+ setTimeout(()=>{
27
+ URL.revokeObjectURL(url);
28
+ if (document.body.contains(link)) document.body.removeChild(link);
29
+ }, 100);
30
+ }
31
+ const useDownloadObject = ()=>{
32
+ const s3Client = useS3Client();
33
+ return useMutation({
34
+ mutationFn: async ({ filename, ...params })=>{
35
+ try {
36
+ if (supportsStreamDownload()) {
37
+ const response = await s3Client.send(new GetObjectCommand(params));
38
+ if (!response.Body) throw new Error('Empty response body');
39
+ try {
40
+ const stream = response.Body.transformToWebStream();
41
+ await streamDownload(stream, filename);
42
+ return;
43
+ } catch (error) {
44
+ if (error?.name === 'AbortError') return;
45
+ console.warn('Stream download failed, falling back to blob:', error);
46
+ }
47
+ }
48
+ const response = await s3Client.send(new GetObjectCommand(params));
49
+ if (!response.Body) throw new Error('Empty response body');
50
+ const bytes = await response.Body.transformToByteArray();
51
+ const contentType = response.ContentType || 'application/octet-stream';
52
+ downloadViaBlob(bytes, filename, contentType);
53
+ } catch (error) {
54
+ throw createS3OperationError(error, 'DownloadObject', params.Bucket, params.Key);
55
+ }
56
+ }
57
+ });
58
+ };
59
+ export { useDownloadObject };
@@ -1,13 +1,20 @@
1
1
  import { S3Client } from '@aws-sdk/client-s3';
2
2
  /**
3
- * Hook to get an S3 client specifically for presigned URL generation.
3
+ * Hook for presigned URL generation that works through reverse proxies.
4
4
  *
5
- * When proxy is configured, presigned URLs must use the proxy endpoint
6
- * (browser-accessible) rather than the internal target. This client is
7
- * created WITHOUT proxy middleware so that `getSignedUrl` produces URLs
8
- * pointing to the proxy endpoint instead of the internal S3 service.
5
+ * Returns an S3 client pointed at the real S3 target (for correct SigV4
6
+ * signing) plus a `rewriteUrl` function that rewrites the generated URL
7
+ * to go through the proxy so browsers can reach it.
9
8
  *
10
- * For non-proxy configurations, this returns a standard S3 client
11
- * identical to `useS3Client`.
9
+ * Flow:
10
+ * 1. getSignedUrl() signs against the S3 target → signature covers
11
+ * target host + clean path (no proxy prefix)
12
+ * 2. rewriteUrl() replaces host/protocol/port and prepends the proxy
13
+ * path prefix → URL is browser-accessible
14
+ * 3. Browser hits the proxy (NGINX), which strips the prefix and
15
+ * forwards to S3 with the target Host → matches the signature
12
16
  */
13
- export declare const usePresigningS3Client: () => S3Client;
17
+ export declare const usePresigningS3Client: () => {
18
+ client: S3Client;
19
+ rewriteUrl: (url: string) => string;
20
+ };
@@ -1,19 +1,47 @@
1
1
  import { S3Client } from "@aws-sdk/client-s3";
2
2
  import { useMemo } from "react";
3
3
  import { useDataBrowserContext } from "../components/providers/DataBrowserProvider.js";
4
- import { resolveProxyEndpoint } from "../utils/proxyMiddleware.js";
4
+ import { parseEndpoint, resolveProxyEndpoint } from "../utils/proxyMiddleware.js";
5
5
  const usePresigningS3Client = ()=>{
6
6
  const { s3ConfigIdentifier, getS3Config } = useDataBrowserContext();
7
7
  if (!getS3Config) throw new Error('usePresigningS3Client: S3 config not available. Ensure DataBrowserProvider has getS3Config prop set.');
8
8
  return useMemo(()=>{
9
9
  const config = getS3Config();
10
- const endpoint = config.proxy?.enabled ? resolveProxyEndpoint(config.proxy.endpoint) : config.endpoint;
11
- return new S3Client({
12
- endpoint,
10
+ if (!config.proxy?.enabled) {
11
+ const client = new S3Client({
12
+ endpoint: config.endpoint,
13
+ credentials: config.credentials,
14
+ forcePathStyle: config.forcePathStyle ?? true,
15
+ region: config.region
16
+ });
17
+ return {
18
+ client,
19
+ rewriteUrl: (url)=>url
20
+ };
21
+ }
22
+ const target = parseEndpoint(config.proxy.target);
23
+ const targetEndpoint = `${target.protocol}//${target.hostname}${80 !== target.port && 443 !== target.port ? `:${target.port}` : ''}`;
24
+ const client = new S3Client({
25
+ endpoint: targetEndpoint,
13
26
  credentials: config.credentials,
14
27
  forcePathStyle: config.forcePathStyle ?? true,
15
28
  region: config.region
16
29
  });
30
+ const proxyEndpoint = resolveProxyEndpoint(config.proxy.endpoint);
31
+ const proxy = parseEndpoint(proxyEndpoint);
32
+ const proxyOrigin = `${proxy.protocol}//${proxy.hostname}${80 !== proxy.port && 443 !== proxy.port ? `:${proxy.port}` : ''}`;
33
+ const proxyPathPrefix = proxy.pathname || '';
34
+ const rewriteUrl = (url)=>{
35
+ const parsed = new URL(url);
36
+ const rewritten = new URL(proxyOrigin);
37
+ rewritten.pathname = proxyPathPrefix + parsed.pathname;
38
+ rewritten.search = parsed.search;
39
+ return rewritten.toString();
40
+ };
41
+ return {
42
+ client,
43
+ rewriteUrl
44
+ };
17
45
  }, [
18
46
  s3ConfigIdentifier
19
47
  ]);
@@ -0,0 +1,2 @@
1
+ export declare const getPublicAccessBlockHandler: import("msw").HttpHandler;
2
+ export declare const putPublicAccessBlockHandler: import("msw").HttpHandler;
@@ -0,0 +1,50 @@
1
+ import { HttpResponse, http } from "msw";
2
+ import { createS3ErrorXml, getS3BaseUrl } from "../utils.js";
3
+ const getPublicAccessBlockHandler = http.get(`${getS3BaseUrl()}/:bucketName`, async ({ request, params })=>{
4
+ const { bucketName } = params;
5
+ const url = new URL(request.url);
6
+ if (!url.searchParams.has('publicAccessBlock')) return;
7
+ if ('string' == typeof bucketName) {
8
+ if (bucketName.includes('pab-not-implemented')) return HttpResponse.xml(createS3ErrorXml('NotImplemented', 'A header you provided implies functionality that is not implemented'), {
9
+ status: 501
10
+ });
11
+ if (bucketName.includes('pab-not-found')) return HttpResponse.xml(createS3ErrorXml('NoSuchPublicAccessBlockConfiguration', 'The public access block configuration was not found', bucketName), {
12
+ status: 404
13
+ });
14
+ if (bucketName.includes('pab-all-blocked')) return HttpResponse.xml(`<?xml version="1.0" encoding="UTF-8"?>
15
+ <PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
16
+ <BlockPublicAcls>TRUE</BlockPublicAcls>
17
+ <IgnorePublicAcls>TRUE</IgnorePublicAcls>
18
+ <BlockPublicPolicy>TRUE</BlockPublicPolicy>
19
+ <RestrictPublicBuckets>TRUE</RestrictPublicBuckets>
20
+ </PublicAccessBlockConfiguration>`, {
21
+ status: 200
22
+ });
23
+ if (bucketName.includes('pab-acl-blocked')) return HttpResponse.xml(`<?xml version="1.0" encoding="UTF-8"?>
24
+ <PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
25
+ <BlockPublicAcls>TRUE</BlockPublicAcls>
26
+ <IgnorePublicAcls>TRUE</IgnorePublicAcls>
27
+ <BlockPublicPolicy>FALSE</BlockPublicPolicy>
28
+ <RestrictPublicBuckets>FALSE</RestrictPublicBuckets>
29
+ </PublicAccessBlockConfiguration>`, {
30
+ status: 200
31
+ });
32
+ }
33
+ return HttpResponse.xml(`<?xml version="1.0" encoding="UTF-8"?>
34
+ <PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
35
+ <BlockPublicAcls>FALSE</BlockPublicAcls>
36
+ <IgnorePublicAcls>FALSE</IgnorePublicAcls>
37
+ <BlockPublicPolicy>FALSE</BlockPublicPolicy>
38
+ <RestrictPublicBuckets>FALSE</RestrictPublicBuckets>
39
+ </PublicAccessBlockConfiguration>`, {
40
+ status: 200
41
+ });
42
+ });
43
+ const putPublicAccessBlockHandler = http.put(`${getS3BaseUrl()}/:bucketName`, async ({ request })=>{
44
+ const url = new URL(request.url);
45
+ if (!url.searchParams.has('publicAccessBlock')) return;
46
+ return new HttpResponse(null, {
47
+ status: 200
48
+ });
49
+ });
50
+ export { getPublicAccessBlockHandler, putPublicAccessBlockHandler };
@@ -3,6 +3,7 @@ import { deleteBucketHandler } from "./handlers/deleteBucket.js";
3
3
  import { getBucketAclHandler } from "./handlers/getBucketAcl.js";
4
4
  import { getBucketLocationHandler } from "./handlers/getBucketLocation.js";
5
5
  import { getBucketPolicyHandler } from "./handlers/getBucketPolicy.js";
6
+ import { getPublicAccessBlockHandler, putPublicAccessBlockHandler } from "./handlers/getPublicAccessBlock.js";
6
7
  import { headObjectHandler } from "./handlers/headObject.js";
7
8
  import { listBucketsHandler } from "./handlers/listBuckets.js";
8
9
  import { listObjectsHandler } from "./handlers/listObjects.js";
@@ -126,7 +127,9 @@ const s3Handlers = [
126
127
  getObjectLegalHoldHandler,
127
128
  putObjectHandler,
128
129
  headObjectHandler,
129
- putBucketAclHandler
130
+ putBucketAclHandler,
131
+ getPublicAccessBlockHandler,
132
+ putPublicAccessBlockHandler
130
133
  ];
131
134
  const handlers = s3Handlers;
132
135
  export { createBucketHandler, handlers as default, getBucketVersioningHandler, s3Handlers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scality/data-browser-library",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "A modular React component library for browsing S3 buckets and objects",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",