Skip to content

Commit

Permalink
feat: Allow to disable listing files and folders
Browse files Browse the repository at this point in the history
Related to #370
  • Loading branch information
oxyno-zeta committed Jul 14, 2023
1 parent b763409 commit 27e565f
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 8 deletions.
3 changes: 3 additions & 0 deletions conf/config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ targets:
# redirectToSignedUrl: true
# # Signed URL expiration time
# signedUrlExpiration: 15m
# # Disable listing
# # Note: This will return an empty list or you should change the folder list template (in general or in this target)
# disableListing: false
# # Webhooks
# webhooks: []
# # Action for PUT requests on target
Expand Down
3 changes: 3 additions & 0 deletions docs/configuration/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,9 @@ targets:
# redirectToSignedUrl: true
# # Signed URL expiration time
# signedUrlExpiration: 15m
# # Disable listing
# # Note: This will return an empty list or you should change the folder list template (in general or in this target)
# disableListing: false
# # Webhooks
# webhooks: []
# # Action for PUT requests on target
Expand Down
17 changes: 9 additions & 8 deletions docs/configuration/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,15 @@ See more information [here](../feature-guide/key-rewrite.md).

## GetActionConfigConfiguration

| Key | Type | Required | Default | Description |
| ---------------------------------------- | ----------------------------------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| redirectWithTrailingSlashForNotFoundFile | Boolean | No | `false` | This option allow to do a redirect with a trailing slash when a GET request on a file (not a folder) encountered a 404 not found. |
| indexDocument | String | No | `""` | The index document name. If this document is found, get it instead of list folder. Example: `index.html` |
| streamedFileHeaders | Map[String]String | No | `nil`  |  Headers containing templates that will be added to streamed files in this target. Key corresponds to header and value to the template. If templated value is empty, the header won't be added to answer. More information [here](../feature-guide/templates.md#stream-file-case). |
| redirectToSignedUrl | Boolean | No | `false`  | Instead of streaming the file through S3-Proxy application, it will redirect to a S3 signed URL to perform the actual download. |
| signedUrlExpiration | String | No | `15m` | This will allow to set an expiration time on generated signed URL. |
| webhooks | [[WebhookConfiguration](#webhookconfiguration)] | No | `nil` | Webhooks configuration list to call when a GET request is performed |
| Key | Type | Required | Default | Description |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| redirectWithTrailingSlashForNotFoundFile | Boolean | No | `false` | This option allow to do a redirect with a trailing slash when a GET request on a file (not a folder) encountered a 404 not found. |
| indexDocument | String | No | `""` | The index document name. If this document is found, get it instead of list folder. Example: `index.html` |
| streamedFileHeaders | Map[String]String | No | `nil`  |  Headers containing templates that will be added to streamed files in this target. Key corresponds to header and value to the template. If templated value is empty, the header won't be added to answer. More information [here](../feature-guide/templates.md#stream-file-case). |
| redirectToSignedUrl | Boolean | No | `false`  | Instead of streaming the file through S3-Proxy application, it will redirect to a S3 signed URL to perform the actual download. |
| signedUrlExpiration | String | No | `15m` | This will allow to set an expiration time on generated signed URL. |
| disableListing | That will disable the listing action. That will display an empty list or you should change the folder list template (general or per target). | No | `false` |
| webhooks | [[WebhookConfiguration](#webhookconfiguration)] | No | `nil` | Webhooks configuration list to call when a GET request is performed |

## PutActionConfiguration

Expand Down
14 changes: 14 additions & 0 deletions pkg/s3-proxy/bucket/requestContext.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,20 @@ func (rctx *requestContext) manageGetFolder(ctx context.Context, key string, inp
}
}

// Check if list folders is disabled
if rctx.targetCfg.Actions != nil && rctx.targetCfg.Actions.GET != nil &&
rctx.targetCfg.Actions.GET.Config != nil &&
rctx.targetCfg.Actions.GET.Config.DisableListing {
// Answer directly
resHan.FoldersFilesList(
rctx.LoadFileContent,
make([]*responsehandler.Entry, 0),
)

// Stop
return
}

// Directory listing case
s3Entries, info, err := rctx.s3ClientManager.
GetClientForTarget(rctx.targetCfg.Name).
Expand Down
33 changes: 33 additions & 0 deletions pkg/s3-proxy/bucket/requestContext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2144,6 +2144,39 @@ func Test_requestContext_Get(t *testing.T) {
times: 1,
},
},
{
name: "list bucket should not be done when disable listing is enabled",
fields: fields{
targetCfg: &config.TargetConfig{
Name: "target",
Bucket: &config.BucketConfig{
Name: "bucket1",
Prefix: "/",
},
KeyRewriteList: []*config.TargetKeyRewriteConfig{{
SourceRegex: regexp.MustCompile(`^/folder/index\.html$`),
Target: "/fake/fake.html",
TargetType: config.RegexTargetKeyRewriteTargetType,
}},
Actions: &config.ActionsConfig{GET: &config.GetActionConfig{
Config: &config.GetActionConfigConfig{
DisableListing: true,
},
}},
},
mountPath: "/mount",
},
args: args{
input: &GetInput{RequestPath: "/folder/"},
},
s3ClientListFilesAndDirectoriesMockResult: s3ClientListFilesAndDirectoriesMockResult{
times: 0,
},
responseHandlerFoldersFilesListMockResult: responseHandlerFoldersFilesListMockResult{
times: 1,
input2: []*responsehandler.Entry{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions pkg/s3-proxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ type GetActionConfigConfig struct {
SignedURLExpiration time.Duration
RedirectWithTrailingSlashForNotFoundFile bool `mapstructure:"redirectWithTrailingSlashForNotFoundFile"`
RedirectToSignedURL bool `mapstructure:"redirectToSignedUrl"`
DisableListing bool `mapstructure:"disableListing"`
}

// WebhookConfig Webhook configuration.
Expand Down
177 changes: 177 additions & 0 deletions pkg/s3-proxy/server/server_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3504,6 +3504,183 @@ func TestPublicRouter(t *testing.T) {
<body>
<h1>Not Found /mount/folder1/</h1>
</body>
</html>`,
expectedHeaders: map[string]string{
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
"Content-Type": "text/html; charset=utf-8",
},
},
{
name: "GET a folder list with disable listing enabled",
args: args{
cfg: &config.Config{
Server: svrCfg,
ListTargets: &config.ListTargetsConfig{},
Tracing: tracingConfig,
Templates: testsDefaultGeneralTemplateConfig,
Targets: map[string]*config.TargetConfig{
"target1": {
Name: "target1",
Bucket: &config.BucketConfig{
Name: bucket,
Region: region,
S3Endpoint: s3server.URL,
Credentials: &config.BucketCredentialConfig{
AccessKey: &config.CredentialConfig{Value: accessKey},
SecretKey: &config.CredentialConfig{Value: secretAccessKey},
},
DisableSSL: true,
},
Mount: &config.MountConfig{
Path: []string{"/mount/"},
},
Actions: &config.ActionsConfig{
GET: &config.GetActionConfig{
Enabled: true,
Config: &config.GetActionConfigConfig{DisableListing: true},
},
},
},
},
},
},
inputMethod: "GET",
inputURL: "http://localhost/mount/folder1/",
expectedCode: 200,
expectedBody: `<!DOCTYPE html>
<html>
<body>
<h1>Index of /mount/folder1/</h1>
<table style="width:100%">
<thead>
<tr>
<th style="border-right:1px solid black;text-align:start">Entry</th>
<th style="border-right:1px solid black;text-align:start">Size</th>
<th style="border-right:1px solid black;text-align:start">Last modified</th>
</tr>
</thead>
<tbody style="border-top:1px solid black">
<tr>
<td style="border-right:1px solid black;padding: 0 5px"><a href="..">..</a></td>
<td style="border-right:1px solid black;padding: 0 5px"> - </td>
<td style="padding: 0 5px"> - </td>
</tr>
</tbody>
</table>
</body>
</html>`,
expectedHeaders: map[string]string{
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
"Content-Type": "text/html; charset=utf-8",
},
},
{
name: "GET a folder list with disable listing enabled, another status code and another content (general templates)",
args: args{
cfg: &config.Config{
Server: svrCfg,
ListTargets: &config.ListTargetsConfig{},
Tracing: tracingConfig,
Templates: &config.TemplateConfig{
Helpers: testsDefaultHelpersTemplateConfig,
FolderList: testsDefaultNotFoundErrorTemplateConfig,
TargetList: testsDefaultTargetListTemplateConfig,
BadRequestError: testsDefaultBadRequestErrorTemplateConfig,
NotFoundError: testsDefaultNotFoundErrorTemplateConfig,
InternalServerError: testsDefaultInternalServerErrorTemplateConfig,
UnauthorizedError: testsDefaultUnauthorizedErrorTemplateConfig,
ForbiddenError: testsDefaultForbiddenErrorTemplateConfig,
},
Targets: map[string]*config.TargetConfig{
"target1": {
Name: "target1",
Bucket: &config.BucketConfig{
Name: bucket,
Region: region,
S3Endpoint: s3server.URL,
Credentials: &config.BucketCredentialConfig{
AccessKey: &config.CredentialConfig{Value: accessKey},
SecretKey: &config.CredentialConfig{Value: secretAccessKey},
},
DisableSSL: true,
},
Mount: &config.MountConfig{
Path: []string{"/mount/"},
},
Actions: &config.ActionsConfig{
GET: &config.GetActionConfig{
Enabled: true,
Config: &config.GetActionConfigConfig{DisableListing: true},
},
},
},
},
},
},
inputMethod: "GET",
inputURL: "http://localhost/mount/folder1/",
expectedCode: 404,
expectedBody: `<!DOCTYPE html>
<html>
<body>
<h1>Not Found /mount/folder1/</h1>
</body>
</html>`,
expectedHeaders: map[string]string{
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
"Content-Type": "text/html; charset=utf-8",
},
},
{
name: "GET a folder list with disable listing enabled, another status code and another content (target override)",
args: args{
cfg: &config.Config{
Server: svrCfg,
ListTargets: &config.ListTargetsConfig{},
Tracing: tracingConfig,
Templates: testsDefaultGeneralTemplateConfig,
Targets: map[string]*config.TargetConfig{
"target1": {
Name: "target1",
Bucket: &config.BucketConfig{
Name: bucket,
Region: region,
S3Endpoint: s3server.URL,
Credentials: &config.BucketCredentialConfig{
AccessKey: &config.CredentialConfig{Value: accessKey},
SecretKey: &config.CredentialConfig{Value: secretAccessKey},
},
DisableSSL: true,
},
Mount: &config.MountConfig{
Path: []string{"/mount/"},
},
Actions: &config.ActionsConfig{
GET: &config.GetActionConfig{
Enabled: true,
},
},
Templates: &config.TargetTemplateConfig{
FolderList: &config.TargetTemplateConfigItem{
Path: "../../../templates/not-found-error.tpl",
Headers: map[string]string{
"Content-Type": "{{ template \"main.headers.contentType\" . }}",
},
Status: "404",
},
},
},
},
},
},
inputMethod: "GET",
inputURL: "http://localhost/mount/folder1/",
expectedCode: 404,
expectedBody: `<!DOCTYPE html>
<html>
<body>
<h1>Not Found /mount/folder1/</h1>
</body>
</html>`,
expectedHeaders: map[string]string{
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
Expand Down

0 comments on commit 27e565f

Please sign in to comment.