Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,26 @@ limitations under the License.
## Reporting a Vulnerability

`apache/unomi` follows the [Apache Software Foundation security process](https://www.apache.org/security/). Please report suspected
vulnerabilities privately to `security@apache.org` (the ASF security team routes Unomi reports to the project's private list, `private@unomi.apache.org`); do not open public
GitHub issues or pull requests for security reports.
vulnerabilities privately to `security@apache.org` (the ASF Security Team routes Unomi reports to the project's private PMC list,
`private@unomi.apache.org`); do not open public GitHub issues or pull requests for security reports.

## How the PMC handles reports

Unomi follows the default ASF process in
[ASF Project Security for Committers — Handling a possible vulnerability](https://apache.org/security/committers.html#vulnerability-handling).
Unomi does not currently maintain a dedicated `security@unomi.apache.org` list, so further private mail about an undisclosed issue
should be copied to `security@apache.org` as that guide requires.

Summary of the steps the PMC applies:

1. **Work in private** — no public Jira/GitHub issues; commit messages must not call out the security nature of the fix until announcement.
2. **Acknowledge** — email the reporter (cc `security@apache.org` / `private@unomi.apache.org`).
3. **Investigate** — triage against [THREAT_MODEL.md](./THREAT_MODEL.md); **accept** or **reject** each distinct finding (a multi-issue report may be split).
4. **If rejected** — write to the reporter explaining why (cc security lists). Rejection reasons include out-of-model / by-design findings and issues that affect **only unreleased** development code with no released-line impact (still fix before the next GA when appropriate).
5. **If accepted** — tell the reporter we are working on a fix; request CVE ID(s) via [cveprocess.apache.org](https://cveprocess.apache.org) or `security@apache.org` (ASF Security can advise on splitting/merging CVEs).
6. **Resolve** — agree the fix privately; document on the ASF CVE portal; share fix + draft announcement with the reporter; commit without security references; ship a release that includes the fix.
7. **Announce** — with or after the release announcement (reporter, project lists, `security@apache.org`, `oss-security@lists.openwall.com`).
8. **Complete** — update [unomi.apache.org/security/](https://unomi.apache.org/security/) and CVE references.

## Threat Model

Expand Down
152 changes: 88 additions & 64 deletions THREAT_MODEL.md

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ required Unomi tarball.

## Launching docker-compose using Maven project

Unomi requires a search engine (ElasticSearch or OpenSearch) so it is recommended to run Unomi and the search engine using docker-compose:
Unomi requires a search engine (ElasticSearch or OpenSearch) so it is recommended to run Unomi and the search engine using docker-compose.

Set admin and health passwords first (required; no known defaults are shipped):

```
export UNOMI_ROOT_PASSWORD='choose-a-strong-password'
export UNOMI_HEALTHCHECK_PASSWORD='choose-a-strong-health-password'
mvn docker:start
```

Expand Down Expand Up @@ -72,6 +76,8 @@ For Unomi (with ElasticSearch):
```bash
docker pull apache/unomi:3.1.0-SNAPSHOT
docker run -d --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
-e UNOMI_ROOT_PASSWORD='choose-a-strong-password' \
-e UNOMI_HEALTHCHECK_PASSWORD='choose-a-strong-health-password' \
-e UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200 \
apache/unomi:3.1.0-SNAPSHOT
```
Expand All @@ -81,6 +87,8 @@ For Unomi (with OpenSearch):
```bash
docker pull apache/unomi:3.1.0-SNAPSHOT
docker run -d --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
-e UNOMI_ROOT_PASSWORD='choose-a-strong-password' \
-e UNOMI_HEALTHCHECK_PASSWORD='choose-a-strong-health-password' \
-e UNOMI_DISTRIBUTION=unomi-distribution-opensearch \
-e UNOMI_OPENSEARCH_ADDRESSES=opensearch:9200 \
-e UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \
Expand All @@ -93,6 +101,8 @@ For ElasticSearch:

```bash
docker run -d --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
-e UNOMI_ROOT_PASSWORD='choose-a-strong-password' \
-e UNOMI_HEALTHCHECK_PASSWORD='choose-a-strong-health-password' \
-e UNOMI_ELASTICSEARCH_ADDRESSES=host.docker.internal:9200 \
apache/unomi:3.1.0-SNAPSHOT
```
Expand All @@ -101,6 +111,8 @@ For OpenSearch:

```bash
docker run -d --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
-e UNOMI_ROOT_PASSWORD='choose-a-strong-password' \
-e UNOMI_HEALTHCHECK_PASSWORD='choose-a-strong-health-password' \
-e UNOMI_DISTRIBUTION=unomi-distribution-opensearch \
-e UNOMI_OPENSEARCH_ADDRESSES=host.docker.internal:9200 \
-e UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \
Expand All @@ -112,6 +124,8 @@ Note: Linux doesn't support the host.docker.internal DNS lookup method yet, it s
## Environment Variables

### Common Variables
- `UNOMI_ROOT_PASSWORD`: Required admin (`karaf`) password — no known default
- `UNOMI_HEALTHCHECK_PASSWORD`: Required health-check (`health`) password — no known default
- `UNOMI_AUTO_START`: Boolean to specify if unomi auto start with karaf (defaults to `true`)
- `UNOMI_DISTRIBUTION`: Specifies the Unomi Distribution Feature to use (`unomi-distribution-elasticsearch` or `unomi-distribution-opensearch`, defaults to `unomi-distribution-elasticsearch`)

Expand All @@ -133,7 +147,7 @@ Multi-tenancy requires a tenant before client endpoints such as `/cxs/context.js

```bash
curl -X POST http://localhost:8181/cxs/tenants \
--user karaf:karaf \
--user "karaf:${UNOMI_ROOT_PASSWORD}" \
-H "Content-Type: application/json" \
-d '{"requestedId":"default","properties":{"name":"Default Tenant"}}'
```
Expand Down
2 changes: 2 additions & 0 deletions docker/src/main/docker/docker-compose-build-es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ services:
- UNOMI_AUTO_START=true
- UNOMI_DISTRIBUTION=unomi-distribution-elasticsearch
- UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200
- UNOMI_ROOT_PASSWORD=${UNOMI_ROOT_PASSWORD:?Set UNOMI_ROOT_PASSWORD}
- UNOMI_HEALTHCHECK_PASSWORD=${UNOMI_HEALTHCHECK_PASSWORD:?Set UNOMI_HEALTHCHECK_PASSWORD}
# Debug settings
- KARAF_DEBUG=${DEBUG:-false}
- KARAF_DEBUG_PORT=${DEBUG_PORT:-5005}
Expand Down
2 changes: 2 additions & 0 deletions docker/src/main/docker/docker-compose-build-os.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ services:
- UNOMI_OPENSEARCH_ADDRESSES=opensearch-node1:9200
- UNOMI_OPENSEARCH_USERNAME=admin
- UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
- UNOMI_ROOT_PASSWORD=${UNOMI_ROOT_PASSWORD:?Set UNOMI_ROOT_PASSWORD}
- UNOMI_HEALTHCHECK_PASSWORD=${UNOMI_HEALTHCHECK_PASSWORD:?Set UNOMI_HEALTHCHECK_PASSWORD}
# Debug settings
- KARAF_DEBUG=${DEBUG:-false}
- KARAF_DEBUG_PORT=${DEBUG_PORT:-5005}
Expand Down
4 changes: 4 additions & 0 deletions docker/src/main/docker/docker-compose-cluster.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ services:
environment:
- UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200
- UNOMI_CLUSTER_NODEID=unomi-3-node-1
- UNOMI_ROOT_PASSWORD=${UNOMI_ROOT_PASSWORD:?Set UNOMI_ROOT_PASSWORD}
- UNOMI_HEALTHCHECK_PASSWORD=${UNOMI_HEALTHCHECK_PASSWORD:?Set UNOMI_HEALTHCHECK_PASSWORD}
ports:
- 8181:8181
- 9443:9443
Expand All @@ -58,6 +60,8 @@ services:
environment:
- UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200
- UNOMI_CLUSTER_NODEID=unomi-3-node-2
- UNOMI_ROOT_PASSWORD=${UNOMI_ROOT_PASSWORD:?Set UNOMI_ROOT_PASSWORD}
- UNOMI_HEALTHCHECK_PASSWORD=${UNOMI_HEALTHCHECK_PASSWORD:?Set UNOMI_HEALTHCHECK_PASSWORD}
ports:
- 8182:8181
- 9444:9443
Expand Down
3 changes: 3 additions & 0 deletions docker/src/main/docker/docker-compose-es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ services:
- UNOMI_AUTO_START=true
- UNOMI_DISTRIBUTION=unomi-distribution-elasticsearch
- UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200
# Required admin password (no known default is shipped). Override via .env / shell.
- UNOMI_ROOT_PASSWORD=${UNOMI_ROOT_PASSWORD:?Set UNOMI_ROOT_PASSWORD}
- UNOMI_HEALTHCHECK_PASSWORD=${UNOMI_HEALTHCHECK_PASSWORD:?Set UNOMI_HEALTHCHECK_PASSWORD}
# Debug settings
- KARAF_DEBUG=${DEBUG:-false}
- KARAF_DEBUG_PORT=${DEBUG_PORT:-5005}
Expand Down
3 changes: 3 additions & 0 deletions docker/src/main/docker/docker-compose-os.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ services:
- UNOMI_OPENSEARCH_ADDRESSES=opensearch-node1:9200
- UNOMI_OPENSEARCH_USERNAME=admin
- UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
# Required admin password (no known default is shipped). Override via .env / shell.
- UNOMI_ROOT_PASSWORD=${UNOMI_ROOT_PASSWORD:?Set UNOMI_ROOT_PASSWORD}
- UNOMI_HEALTHCHECK_PASSWORD=${UNOMI_HEALTHCHECK_PASSWORD:?Set UNOMI_HEALTHCHECK_PASSWORD}
# Debug settings
- KARAF_DEBUG=${DEBUG:-false}
- KARAF_DEBUG_PORT=${DEBUG_PORT:-5005}
Expand Down
30 changes: 30 additions & 0 deletions docker/src/main/docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,36 @@ export KARAF_OPTS="-Dunomi.autoStart=${UNOMI_AUTO_START} -Dunomi.distribution=${

echo "KARAF_OPTS: $KARAF_OPTS"

if [ -z "${UNOMI_ROOT_PASSWORD}" ] && [ "${UNOMI_SKIP_ROOT_PASSWORD_CHECK}" != "true" ]; then
cat >&2 <<'EOF'
ERROR: UNOMI_ROOT_PASSWORD is not set.

Apache Unomi does not ship a known default admin password.
Pass it when starting the container, for example:

docker run -e UNOMI_ROOT_PASSWORD='choose-a-strong-password' \
-e UNOMI_HEALTHCHECK_PASSWORD='choose-a-strong-health-password' ...

Or with docker compose, export both UNOMI_ROOT_PASSWORD and UNOMI_HEALTHCHECK_PASSWORD first.
EOF
exit 1
fi

if [ -z "${UNOMI_HEALTHCHECK_PASSWORD}" ] && [ "${UNOMI_SKIP_HEALTHCHECK_PASSWORD_CHECK}" != "true" ]; then
cat >&2 <<'EOF'
ERROR: UNOMI_HEALTHCHECK_PASSWORD is not set.

Apache Unomi does not ship a known default health-check password.
Pass it when starting the container, for example:

docker run -e UNOMI_ROOT_PASSWORD='choose-a-strong-password' \
-e UNOMI_HEALTHCHECK_PASSWORD='choose-a-strong-health-password' ...

Or with docker compose, export both UNOMI_ROOT_PASSWORD and UNOMI_HEALTHCHECK_PASSWORD first.
EOF
exit 1
fi

# Function to check cluster health for a specific node
check_node_health() {
local node_url="$1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<details>${project.description}</details>
<feature>wrap</feature>
<feature>unomi-services</feature>
<!-- REST endpoints use @RequiresRole from org.apache.unomi.rest.security -->
<feature>unomi-rest-api</feature>
<configfile finalname="/etc/org.apache.unomi.groovy.actions.cfg">mvn:org.apache.unomi/unomi-groovy-actions-services/${project.version}/cfg/groovyactionscfg</configfile>
<bundle start="false">mvn:org.apache.unomi/unomi-groovy-actions-services/${project.version}</bundle>
<bundle start="false">mvn:org.apache.unomi/unomi-groovy-actions-rest/${project.version}</bundle>
Expand Down
10 changes: 10 additions & 0 deletions extensions/groovy-actions/rest/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
<artifactId>unomi-groovy-actions-services</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.unomi</groupId>
<artifactId>unomi-rest</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.osgi</groupId>
Expand Down Expand Up @@ -106,6 +111,11 @@
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
import org.apache.unomi.api.security.UnomiRoles;
import org.apache.unomi.groovy.actions.services.GroovyActionsService;
import org.apache.unomi.rest.security.RequiresRole;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
Expand All @@ -35,6 +37,7 @@
@Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
@CrossOriginResourceSharing(allowAllOrigins = true, allowCredentials = true)
@Path("/groovyActions")
@RequiresRole(UnomiRoles.ADMINISTRATOR)
@Component(service = GroovyActionsEndPoint.class, property = "osgi.jaxrs.resource=true")
public class GroovyActionsEndPoint {

Expand All @@ -55,6 +58,7 @@ public void setGroovyActionsService(GroovyActionsService groovyActionsService) {
* Uploads a Groovy action script and registers a matching action type.
* <p>
* The multipart field {@code file} must be a {@code .groovy} file; the action id is derived from the filename.
* Restricted to system administrators (JAAS); tenant administrators cannot upload scripts.
*
* @param file the Groovy script upload
* @return an empty success response
Expand All @@ -79,6 +83,7 @@ public Response save(@Multipart(value = "file") Attachment file) {

/**
* Deletes the Groovy action and its action type entry.
* Restricted to system administrators (JAAS).
*
* @param actionId the action identifier
* @api.status 204 empty Action deleted.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.unomi.groovy.actions.rest;

import org.apache.unomi.api.security.UnomiRoles;
import org.apache.unomi.rest.security.RequiresRole;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
* Regression: Groovy upload/delete must stay restricted to system administrators.
*/
class GroovyActionsEndPointRoleTest {

@Test
void endpointRequiresSystemAdministratorRole() {
RequiresRole requiresRole = GroovyActionsEndPoint.class.getAnnotation(RequiresRole.class);
assertNotNull(requiresRole, "GroovyActionsEndPoint must declare @RequiresRole");
assertArrayEquals(new String[]{UnomiRoles.ADMINISTRATOR}, requiresRole.value());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<details>Apache Karaf feature for the Apache Unomi Context Server extension</details>
<feature>wrap</feature>
<feature>unomi-services</feature>
<!-- REST endpoints use @RequiresRole from org.apache.unomi.rest.security -->
<feature>unomi-rest-api</feature>
<bundle start="false">mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.jsch/0.1.55_1</bundle>
<bundle start="false">mvn:commons-net/commons-net/${commons-net.version}</bundle>
<bundle start="false">wrap:mvn:org.apache.kafka/kafka-clients/${kafka.client.version}</bundle>
Expand Down
10 changes: 10 additions & 0 deletions extensions/router/router-rest/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
<artifactId>unomi-router-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.unomi</groupId>
<artifactId>unomi-rest</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.osgi</groupId>
Expand Down Expand Up @@ -103,6 +108,11 @@
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package org.apache.unomi.router.rest;

import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
import org.apache.unomi.api.security.UnomiRoles;
import org.apache.unomi.api.services.ProfileService;
import org.apache.unomi.rest.security.RequiresRole;
import org.apache.unomi.router.api.ExportConfiguration;
import org.apache.unomi.router.api.services.ImportExportConfigurationService;
import org.apache.unomi.router.api.services.ProfileExportService;
Expand Down Expand Up @@ -47,6 +49,7 @@
allowCredentials = true
)
@Path("/exportConfiguration")
@RequiresRole(UnomiRoles.ADMINISTRATOR)
@Component(service=ExportConfigurationServiceEndPoint.class,property = "osgi.jaxrs.resource=true")
public class ExportConfigurationServiceEndPoint extends AbstractConfigurationServiceEndpoint<ExportConfiguration> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
import org.apache.unomi.api.security.UnomiRoles;
import org.apache.unomi.api.services.ConfigSharingService;
import org.apache.unomi.rest.security.RequiresRole;
import org.apache.unomi.router.api.ImportConfiguration;
import org.apache.unomi.router.api.RouterConstants;
import org.apache.unomi.router.api.services.ImportExportConfigurationService;
Expand Down Expand Up @@ -54,6 +56,7 @@
allowCredentials = true
)
@Path("/importConfiguration")
@RequiresRole(UnomiRoles.ADMINISTRATOR)
@Component(service=ImportConfigurationServiceEndPoint.class,property = "osgi.jaxrs.resource=true")
public class ImportConfigurationServiceEndPoint extends AbstractConfigurationServiceEndpoint<ImportConfiguration> {

Expand Down
Loading
Loading