[ 
https://issues.apache.org/jira/browse/UNOMI-973?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Jerome Blanchard updated UNOMI-973:
-----------------------------------
    Description: 
*Today*

A recurrent import configuration names a `source`, and a recurrent export 
configuration names a `destination`. Both are used as Apache Camel endpoint 
URIs, and the only thing that governs them is a scheme allow-list, 
`org.apache.unomi.router.config.allowedEndpoints`, whose shipped default is 
`file,ftp,sftp,ftps`.

With `file` in that list, *any absolute path on the Unomi host is accepted* — 
including Unomi's own `deploy/`, `etc/` and `data/` directories, and anything 
else the process can reach. A deployment has no way to say where profile import 
and export files are supposed to live, and an administrator has no way to know 
they have typed a path that will disrupt the installation.

Two smaller problems sit beside it:
 * A configuration whose endpoint is refused is answered {*}HTTP 200{*}. It is 
stored, no route is ever built for it, and the only trace is one `ERROR` line 
in the log. The caller cannot tell a working  configuration from one that will 
never run.
 * An endpoint with no scheme — or a blank destination — raises 
`StringIndexOutOfBoundsException` out of `configure()`, because the scheme is 
read with `substring(0, uri.indexOf(':'))` without checking the index. The 
exception aborts `addRoutes`, so *{*}one malformed configuration costs the 
deployment every other route of the same batch{*}*, silently, at startup.

*Proposal*

Let a deployment declare where `file` endpoints may resolve, and refuse the 
ones that resolve elsewhere.

Two new settings, one per direction, each accepting a comma-separated list:

```
config.import.baseDir=${org.apache.unomi.router.config.import.baseDir:-${karaf.data}/router/import/}
config.export.baseDir=${org.apache.unomi.router.config.export.baseDir:-${karaf.data}/router/export/}
```

They are kept apart on purpose: with a single shared directory, an export 
writing a `.csv` could be picked up by an import route polling the same place. 
A deployment that wants one directory can point both settings at it.

*Behaviour*
 * Containment is *recursive* — any depth under a permitted base directory is 
accepted — and does not require the directory to exist, since an export 
destination is created on first write.
 * It covers the directory the URI names *and every path-bearing option it 
carries* (`fileName`,  `tempFileName`, `move`, `moveFailed`, `preMove`, 
`doneFileName`, `include`, `antInclude`,  `antFilter`). Validating only the 
directory would let an option resolve elsewhere and quietly defeat  the setting.
 * It is decided on canonical paths: percent-encoding decoded, `RAW(...)` 
unwrapped, parent segments normalized, symbolic links followed. Paths are 
compared component by component, so a sibling directory that merely shares a 
textual prefix with a permitted one is not taken for one of its children.
 * Relative option values keep working — `move=.done` resolves under the 
endpoint's own directory, which is how the feature is normally used, and the 
router appends `moveFailed=.error` itself. `ftp`, `sftp` and `ftps` carry no 
local path and are unaffected; the scheme allow-list keeps governing them.
 * The *oneshot* import route builds its own endpoint from 
`import.oneshot.uploadDir` and is unaffected. Oneshot configurations name no 
endpoint and are not validated.

*Reporting the refusal*

`saveConfiguration` now validates the endpoint of a recurrent configuration 
{*}before storing it{*}, and answers *`400 Bad Request`* with the reason in the 
body as `text/plain`. The configuration is not stored, so the caller gets a 
synchronous, actionable answer instead of a `200` followed by silence.

The permitted directories are an operational setting while the configurations 
are user data, so the two drift apart: a configuration that was legitimate when 
it was created is refused once the deployment is reconfigured. To keep that 
visible rather than silent, a configuration whose route cannot be built is 
marked with a new status, `INVALID_ENDPOINT`, and saved. Nothing is deleted and 
no startup is blocked — correcting or removing it belongs to whoever owns it. 
Restoring the permitted directories clears the mark on its own at the next 
rebuild, so an operational change can be undone without touching any 
configuration.

`INVALID_ENDPOINT` is deliberately not one of the existing execution statuses: 
those report on a run that happened, this one says no run can. Keeping them 
apart is what makes the mark safe to clear automatically — the record of a run 
that genuinely failed is left alone.

*Robustness*
 * The scheme is matched against the allow-list as a {*}set of whole 
schemes{*}, rather than searching the raw setting for a substring.
 * An endpoint with no scheme, or a blank destination, is reported and skipped 
instead of raising out of `configure()`; the other configurations of the batch 
keep their routes.

*Compatibility*

*Breaking.* A recurrent import or export configuration using `file` and 
resolving outside the permitted base directories stops building its route, and 
is marked `INVALID_ENDPOINT`. A deployment that relies on another location must 
set `config.import.baseDir` / `config.export.baseDir` accordingly. This needs a 
release note.

*Tests*
 * `FileEndpointContainmentTest` (router-core) — route construction for both 
directions: in-bounds sources and destinations keep building routes, at any 
depth and whether or not the directory exists;  out-of-bounds ones build none, 
including through each path-bearing option, encoded parent segments,   `RAW()`, 
symbolic links, and prefix-sharing siblings.
 * `RefusedConfigurationStatusTest` (router-core) — the status is set on 
refusal, cleared on recovery,   a genuinely failed run keeps its own status, 
and the save does not schedule a route refresh (which would rebuild, refuse and 
save again without end).
 * `ConfigurationEndpointValidationTest` (router-rest) — refusal answers 400 
and stores nothing; a oneshot import that names no endpoint is still stored.
 * Existing integration tests updated to the permitted directories: 
`ProfileExportIT`, `ProfileImportSurfersIT`, `ProfileImportActorsIT`, 
`ProfileImportRankingIT`. `ProfileImportBasicIT` (oneshot) is unchanged, which 
is the point.

  was:
*Today*

A recurrent import configuration names a `source`, and a recurrent export 
configuration names a `destination`. Both are used as Apache Camel endpoint 
URIs, and the only thing that governs them is a scheme allow-list, 
`org.apache.unomi.router.config.allowedEndpoints`, whose shipped default is 
`file,ftp,sftp,ftps`.

With `file` in that list, *{*}any absolute path on the Unomi host is 
accepted{*}* — including Unomi's own `deploy/`, `etc/` and `data/` directories, 
and anything else the process can reach. A deployment has no way to say where 
profile import and export files are supposed to live, and an administrator has 
no way to know they have typed a path that will disrupt the installation.

Two smaller problems sit beside it:
 * A configuration whose endpoint is refused is answered *{*}HTTP 200{*}*. It 
is stored, no route is ever built for it, and the only trace is one `ERROR` 
line in the log. The caller cannot tell a working  configuration from one that 
will never run.
 * An endpoint with no scheme — or a blank destination — raises 
`StringIndexOutOfBoundsException` out of `configure()`, because the scheme is 
read with `substring(0, uri.indexOf(':'))` without checking the index. The 
exception aborts `addRoutes`, so **one malformed configuration costs the 
deployment every other route of the same batch**, silently, at startup.

*Proposal*

Let a deployment declare where `file` endpoints may resolve, and refuse the 
ones that resolve elsewhere.

Two new settings, one per direction, each accepting a comma-separated list:

```
config.import.baseDir=${org.apache.unomi.router.config.import.baseDir:-${karaf.data}/router/import/}
config.export.baseDir=${org.apache.unomi.router.config.export.baseDir:-${karaf.data}/router/export/}
```

They are kept apart on purpose: with a single shared directory, an export 
writing a `.csv` could be picked up by an import route polling the same place. 
A deployment that wants one directory can point both settings at it.

*Behaviour*
 * Containment is *{*}recursive{*}* — any depth under a permitted base 
directory is accepted — and does not require the directory to exist, since an 
export destination is created on first write.
 * It covers the directory the URI names *{*}and every path-bearing option it 
carries{*}* (`fileName`,  `tempFileName`, `move`, `moveFailed`, `preMove`, 
`doneFileName`, `include`, `antInclude`,  `antFilter`). Validating only the 
directory would let an option resolve elsewhere and quietly defeat  the setting.
 * It is decided on canonical paths: percent-encoding decoded, `RAW(...)` 
unwrapped, parent segments normalized, symbolic links followed. Paths are 
compared component by component, so a sibling directory that merely shares a 
textual prefix with a permitted one is not taken for one of its children.
 * Relative option values keep working — `move=.done` resolves under the 
endpoint's own directory, which is how the feature is normally used, and the 
router appends `moveFailed=.error` itself. `ftp`, `sftp` and `ftps` carry no 
local path and are unaffected; the scheme allow-list keeps governing them.
 * The *{*}oneshot{*}* import route builds its own endpoint from 
`import.oneshot.uploadDir` and is unaffected. Oneshot configurations name no 
endpoint and are not validated.

*Reporting the refusal*

`saveConfiguration` now validates the endpoint of a recurrent configuration 
*{*}before storing it{*}*, and answers *{*}`400 Bad Request`{*}* with the 
reason in the body as `text/plain`. The configuration is not stored, so the 
caller gets a synchronous, actionable answer instead of a `200` followed by 
silence.

The permitted directories are an operational setting while the configurations 
are user data, so the two drift apart: a configuration that was legitimate when 
it was created is refused once the deployment is reconfigured. To keep that 
visible rather than silent, a configuration whose route cannot be built is 
marked with a new status, `INVALID_ENDPOINT`, and saved. Nothing is deleted and 
no startup is blocked — correcting or removing it belongs to whoever owns it. 
Restoring the permitted directories clears the mark on its own at the next 
rebuild, so an operational change can be undone without touching any 
configuration.

`INVALID_ENDPOINT` is deliberately not one of the existing execution statuses: 
those report on a run that happened, this one says no run can. Keeping them 
apart is what makes the mark safe to clear automatically — the record of a run 
that genuinely failed is left alone.

*Robustness*
 * The scheme is matched against the allow-list as a *{*}set of whole 
schemes{*}*, rather than searching the raw setting for a substring.
 * An endpoint with no scheme, or a blank destination, is reported and skipped 
instead of raising out of `configure()`; the other configurations of the batch 
keep their routes.

*Compatibility*

*{*}Breaking.{*}* A recurrent import or export configuration using `file` and 
resolving outside the permitted base directories stops building its route, and 
is marked `INVALID_ENDPOINT`. A deployment that relies on another location must 
set `config.import.baseDir` / `config.export.baseDir` accordingly. This needs a 
release note.

*Tests*
 * `FileEndpointContainmentTest` (router-core) — route construction for both 
directions: in-bounds sources and destinations keep building routes, at any 
depth and whether or not the directory exists;  out-of-bounds ones build none, 
including through each path-bearing option, encoded parent segments,   `RAW()`, 
symbolic links, and prefix-sharing siblings.
 * `RefusedConfigurationStatusTest` (router-core) — the status is set on 
refusal, cleared on recovery,   a genuinely failed run keeps its own status, 
and the save does not schedule a route refresh (which would rebuild, refuse and 
save again without end).
 * `ConfigurationEndpointValidationTest` (router-rest) — refusal answers 400 
and stores nothing; a oneshot import that names no endpoint is still stored.
 * Existing integration tests updated to the permitted directories: 
`ProfileExportIT`, `ProfileImportSurfersIT`, `ProfileImportActorsIT`, 
`ProfileImportRankingIT`. `ProfileImportBasicIT` (oneshot) is unchanged, which 
is the point.


> Confine recurrent import/export file endpoints to configurable base 
> directories
> -------------------------------------------------------------------------------
>
>                 Key: UNOMI-973
>                 URL: https://issues.apache.org/jira/browse/UNOMI-973
>             Project: Apache Unomi
>          Issue Type: Improvement
>          Components: unomi(-core)
>    Affects Versions: unomi-3.0.1
>            Reporter: Jerome Blanchard
>            Priority: Major
>
> *Today*
> A recurrent import configuration names a `source`, and a recurrent export 
> configuration names a `destination`. Both are used as Apache Camel endpoint 
> URIs, and the only thing that governs them is a scheme allow-list, 
> `org.apache.unomi.router.config.allowedEndpoints`, whose shipped default is 
> `file,ftp,sftp,ftps`.
> With `file` in that list, *any absolute path on the Unomi host is accepted* — 
> including Unomi's own `deploy/`, `etc/` and `data/` directories, and anything 
> else the process can reach. A deployment has no way to say where profile 
> import and export files are supposed to live, and an administrator has no way 
> to know they have typed a path that will disrupt the installation.
> Two smaller problems sit beside it:
>  * A configuration whose endpoint is refused is answered {*}HTTP 200{*}. It 
> is stored, no route is ever built for it, and the only trace is one `ERROR` 
> line in the log. The caller cannot tell a working  configuration from one 
> that will never run.
>  * An endpoint with no scheme — or a blank destination — raises 
> `StringIndexOutOfBoundsException` out of `configure()`, because the scheme is 
> read with `substring(0, uri.indexOf(':'))` without checking the index. The 
> exception aborts `addRoutes`, so *{*}one malformed configuration costs the 
> deployment every other route of the same batch{*}*, silently, at startup.
> *Proposal*
> Let a deployment declare where `file` endpoints may resolve, and refuse the 
> ones that resolve elsewhere.
> Two new settings, one per direction, each accepting a comma-separated list:
> ```
> config.import.baseDir=${org.apache.unomi.router.config.import.baseDir:-${karaf.data}/router/import/}
> config.export.baseDir=${org.apache.unomi.router.config.export.baseDir:-${karaf.data}/router/export/}
> ```
> They are kept apart on purpose: with a single shared directory, an export 
> writing a `.csv` could be picked up by an import route polling the same 
> place. A deployment that wants one directory can point both settings at it.
> *Behaviour*
>  * Containment is *recursive* — any depth under a permitted base directory is 
> accepted — and does not require the directory to exist, since an export 
> destination is created on first write.
>  * It covers the directory the URI names *and every path-bearing option it 
> carries* (`fileName`,  `tempFileName`, `move`, `moveFailed`, `preMove`, 
> `doneFileName`, `include`, `antInclude`,  `antFilter`). Validating only the 
> directory would let an option resolve elsewhere and quietly defeat  the 
> setting.
>  * It is decided on canonical paths: percent-encoding decoded, `RAW(...)` 
> unwrapped, parent segments normalized, symbolic links followed. Paths are 
> compared component by component, so a sibling directory that merely shares a 
> textual prefix with a permitted one is not taken for one of its children.
>  * Relative option values keep working — `move=.done` resolves under the 
> endpoint's own directory, which is how the feature is normally used, and the 
> router appends `moveFailed=.error` itself. `ftp`, `sftp` and `ftps` carry no 
> local path and are unaffected; the scheme allow-list keeps governing them.
>  * The *oneshot* import route builds its own endpoint from 
> `import.oneshot.uploadDir` and is unaffected. Oneshot configurations name no 
> endpoint and are not validated.
> *Reporting the refusal*
> `saveConfiguration` now validates the endpoint of a recurrent configuration 
> {*}before storing it{*}, and answers *`400 Bad Request`* with the reason in 
> the body as `text/plain`. The configuration is not stored, so the caller gets 
> a synchronous, actionable answer instead of a `200` followed by silence.
> The permitted directories are an operational setting while the configurations 
> are user data, so the two drift apart: a configuration that was legitimate 
> when it was created is refused once the deployment is reconfigured. To keep 
> that visible rather than silent, a configuration whose route cannot be built 
> is marked with a new status, `INVALID_ENDPOINT`, and saved. Nothing is 
> deleted and no startup is blocked — correcting or removing it belongs to 
> whoever owns it. Restoring the permitted directories clears the mark on its 
> own at the next rebuild, so an operational change can be undone without 
> touching any configuration.
> `INVALID_ENDPOINT` is deliberately not one of the existing execution 
> statuses: those report on a run that happened, this one says no run can. 
> Keeping them apart is what makes the mark safe to clear automatically — the 
> record of a run that genuinely failed is left alone.
> *Robustness*
>  * The scheme is matched against the allow-list as a {*}set of whole 
> schemes{*}, rather than searching the raw setting for a substring.
>  * An endpoint with no scheme, or a blank destination, is reported and 
> skipped instead of raising out of `configure()`; the other configurations of 
> the batch keep their routes.
> *Compatibility*
> *Breaking.* A recurrent import or export configuration using `file` and 
> resolving outside the permitted base directories stops building its route, 
> and is marked `INVALID_ENDPOINT`. A deployment that relies on another 
> location must set `config.import.baseDir` / `config.export.baseDir` 
> accordingly. This needs a release note.
> *Tests*
>  * `FileEndpointContainmentTest` (router-core) — route construction for both 
> directions: in-bounds sources and destinations keep building routes, at any 
> depth and whether or not the directory exists;  out-of-bounds ones build 
> none, including through each path-bearing option, encoded parent segments,   
> `RAW()`, symbolic links, and prefix-sharing siblings.
>  * `RefusedConfigurationStatusTest` (router-core) — the status is set on 
> refusal, cleared on recovery,   a genuinely failed run keeps its own status, 
> and the save does not schedule a route refresh (which would rebuild, refuse 
> and save again without end).
>  * `ConfigurationEndpointValidationTest` (router-rest) — refusal answers 400 
> and stores nothing; a oneshot import that names no endpoint is still stored.
>  * Existing integration tests updated to the permitted directories: 
> `ProfileExportIT`, `ProfileImportSurfersIT`, `ProfileImportActorsIT`, 
> `ProfileImportRankingIT`. `ProfileImportBasicIT` (oneshot) is unchanged, 
> which is the point.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to