jayblanc opened a new pull request, #849:
URL: https://github.com/apache/unomi/pull/849

   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. 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.
   
   ## What this changes
   
   Two new settings let a deployment declare where `file` endpoints may 
resolve, 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.
   
   `EndpointValidator` (router-api, so both router-core and router-rest can use 
it) decides whether an
   endpoint may be used:
   
   - 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`), since 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 — 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 **oneshot** import route builds its own endpoint from 
`import.oneshot.uploadDir` and is
     untouched.
   
   ## Reporting the refusal
   
   `saveConfiguration` 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
   
   Unit tests, in the modules that hold the behaviour:
   
   - `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.
   
   Integration tests, for what only a running Unomi can show — that the 
settings reach the REST layer and
   the route builders, which read them through different paths:
   
   - `ProfileImportExportContainmentIT` (new) — refusal at save time answered 
400 with a reason in the
     body and nothing stored; a configuration stored while bypassing the REST 
layer is marked
     `INVALID_ENDPOINT`, consumes no file and writes none, and recovers on its 
own.
   - `ProfileExportIT`, `ProfileImportSurfersIT`, `ProfileImportActorsIT`, 
`ProfileImportRankingIT` now
     name the permitted directories. `ProfileImportBasicIT` is untouched on 
purpose: its passing unchanged
     is what shows the oneshot upload stayed out of this.
   
   ---
   
    - [x] Make sure there is a [JIRA 
issue](https://issues.apache.org/jira/browse/UNOMI-973) filed for the change — 
UNOMI-973
    - [x] Format the pull request title like `[UNOMI-XXX] - Title of the pull 
request`
    - [x] Provide integration tests for your changes
    - [x] Write a pull request description that is detailed enough to 
understand what the pull request does, how, and why
    - [ ] Run `mvn clean install -P integration-tests` to make sure basic 
checks pass
   
   On that last box, to be accurate rather than reassuring: the full 
integration-test suite has **not**
   been run locally. What was run is every unit test of `router-api`, 
`router-core` and `router-rest`, and
   the six integration-test classes this change touches or adds — 
`ProfileImportExportContainmentIT`,
   `ProfileExportIT`, `ProfileImportSurfersIT`, `ProfileImportActorsIT`, 
`ProfileImportRankingIT` and
   `ProfileImportBasicIT`, all passing. The rest of the suite is left to CI.
   
    - [ ] I hereby declare this contribution to be licenced under the [Apache 
License Version 2.0, January 2004](http://www.apache.org/licenses/LICENSE-2.0)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to