Skip to content
Open
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
2 changes: 1 addition & 1 deletion deployment/helm/ditto/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ description: |
A digital twin is a virtual, cloud based, representation of his real world counterpart
(real world “Things”, e.g. devices like sensors, smart heating, connected cars, smart grids, EV charging stations etc).
type: application
version: 3.8.16 # chart version is effectively set by release-job
version: 3.8.17 # chart version is effectively set by release-job
appVersion: 3.8.12
keywords:
- iot-chart
Expand Down
32 changes: 32 additions & 0 deletions deployment/helm/ditto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,38 @@ Alternatively, a YAML file that specifies the values for the parameters can be p
Please consult the [values.yaml](https://github.com/eclipse-ditto/ditto/blob/master/deployment/helm/ditto/values.yaml)
for all available configuration options of the Ditto Helm chart.

### Namespace root policies

Namespace root policies allow operators to define namespace-wide access grants that are transparently
merged into every policy in that namespace at enforcer-build time. The canonical default configuration
and full documentation is in
[`ditto-namespace-policies.conf`](../../../internal/utils/config/src/main/resources/ditto-namespace-policies.conf).

For Helm deployments, configure via the service-scoped values (same style as `entityCreation`):
- `policies.config.namespacePolicies`
- `things.config.namespacePolicies`

To keep enforcement consistent across services, configure the same mapping in both sections.

Example:
```yaml
policies:
config:
namespacePolicies:
org.example.devices:
- org.example:tenant-root
org.example.sensors:
- org.example:tenant-root

things:
config:
namespacePolicies:
org.example.devices:
- org.example:tenant-root
org.example.sensors:
- org.example:tenant-root
```

### Scaling options

Please note the defaults the chart comes with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ ditto {
]
}
policies {
namespace-policies {
{{- range $namespace, $policyIds := .Values.policies.config.namespacePolicies }}
"{{$namespace}}" = [
{{- range $index, $policyId := $policyIds }}
"{{$policyId}}"
{{- end }}
]
{{- end }}
}
policy {
namespace-activity-check = [
{{- range $index, $nsActivityCheck := .Values.policies.config.persistence.namespaceActivityCheckOverrides }}
Expand Down
11 changes: 11 additions & 0 deletions deployment/helm/ditto/service-config/things-extension.conf.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ ditto {
{{- end }}
]
}
policies {
namespace-policies {
{{- range $namespace, $policyIds := .Values.things.config.namespacePolicies }}
"{{$namespace}}" = [
{{- range $index, $policyId := $policyIds }}
"{{$policyId}}"
{{- end }}
]
{{- end }}
}
}
things {
thing {
namespace-activity-check = [
Expand Down
14 changes: 14 additions & 0 deletions deployment/helm/ditto/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,13 @@ policies:
revokes: []
# - namespaces: []
# authSubjects: []
# namespacePolicies configures namespace root policies used during policy enforcer resolution.
# Map namespace -> list of policy IDs to be implicitly imported for that namespace.
namespacePolicies: {}
# org.example.devices:
# - org.example:tenant-root
# org.example.sensors:
# - org.example:tenant-root
# policiesEnforcer contains configuration for Ditto "Policy Enforcers", e.g. regarding caching
policiesEnforcer:
# askWithRetry contains configuration regarding "ask with retry" for policy enforcement in policies service
Expand Down Expand Up @@ -1292,6 +1299,13 @@ things:
# - namespaces: []
# authSubjects: []
# thingDefinitions: []
# namespacePolicies configures namespace root policies used during policy enforcer resolution.
# Map namespace -> list of policy IDs to be implicitly imported for that namespace.
namespacePolicies: {}
# org.example.devices:
# - org.example:tenant-root
# org.example.sensors:
# - org.example:tenant-root
# policiesEnforcer contains configuration for Ditto "Policy Enforcers", e.g. regarding caching
policiesEnforcer:
# askWithRetry contains configuration regarding "ask with retry" for policy enforcement in things service
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# namespace root policies
# Maps namespace patterns to lists of policy IDs whose implicit entries are transparently merged
# into every matching policy's enforcer at build time.
#
# Pattern syntax (key side):
# - exact string e.g. "org.example.devices" matches only that namespace
# - prefix wildcard e.g. "org.example.*" matches any namespace starting with "org.example."
# - catch-all "*" matches every namespace
#
# Rules:
# - only entries with importable = "implicit" are merged; "explicit" and "never" are skipped.
# - local policy entries always win on label conflicts (namespace root cannot override local entries)
# - matching patterns are applied in this precedence: exact > more specific prefix wildcard > less specific prefix wildcard > "*"
# - unsupported wildcard syntax is rejected at config load time
# - if a configured root policy is missing or deleted, its entries are skipped and an ERROR is logged
# - changing a root policy triggers cache invalidation for all cached policies in matching namespaces
# - this config is read by all services that perform policy enforcement (policies, things)
#
# For Helm deployments, configure this via the policies.config.namespacePolicies and
# things.config.namespacePolicies values, which are rendered into the respective
# service extension config files.
ditto.policies.namespace-policies {
// "org.example.devices" = ["org.example:tenant-root"] # exact namespace
// "org.example.devices.*" = ["org.example:devices-root"] # more specific prefix wildcard
// "org.example.*" = ["org.example:tenant-root"] # broader prefix wildcard
// "*" = ["root:catch-all-policy"] # every namespace
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ include "ditto-cluster-downing.conf"
include "ditto-mongo.conf"
include "ditto-enforcement.conf"
include "ditto-entity-creation.conf"
include "ditto-namespace-policies.conf"
include "ditto-things-aggregator.conf"

# extension point
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.dispatch.MessageDispatcher;
import org.eclipse.ditto.internal.utils.cache.entry.Entry;
import org.eclipse.ditto.policies.enforcement.config.DefaultNamespacePoliciesConfig;
import org.eclipse.ditto.policies.enforcement.config.NamespacePoliciesConfig;
import org.eclipse.ditto.policies.model.PolicyId;

import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
Expand All @@ -30,9 +32,14 @@ protected AbstractPolicyEnforcerProvider() {

protected static AsyncCacheLoader<PolicyId, Entry<PolicyEnforcer>> policyEnforcerCacheLoader(
final ActorSystem actorSystem) {
return policyEnforcerCacheLoader(actorSystem,
DefaultNamespacePoliciesConfig.of(actorSystem.settings().config()));
}

protected static AsyncCacheLoader<PolicyId, Entry<PolicyEnforcer>> policyEnforcerCacheLoader(
final ActorSystem actorSystem, final NamespacePoliciesConfig namespacePoliciesConfig) {
final PolicyCacheLoader policyCacheLoader = PolicyCacheLoader.getSingletonInstance(actorSystem);
return new PolicyEnforcerCacheLoader(policyCacheLoader, actorSystem);
return new PolicyEnforcerCacheLoader(policyCacheLoader, actorSystem, namespacePoliciesConfig);
}

protected static MessageDispatcher enforcementCacheDispatcher(final ActorSystem actorSystem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import org.eclipse.ditto.internal.utils.cache.config.CacheConfig;
import org.eclipse.ditto.internal.utils.cache.config.DefaultCacheConfig;
import org.eclipse.ditto.internal.utils.cache.entry.Entry;
import org.eclipse.ditto.policies.enforcement.config.DefaultNamespacePoliciesConfig;
import org.eclipse.ditto.policies.enforcement.config.NamespacePoliciesConfig;
import org.eclipse.ditto.internal.utils.cluster.DistPubSubAccess;
import org.eclipse.ditto.internal.utils.namespaces.BlockedNamespaces;
import org.eclipse.ditto.internal.utils.pekko.logging.DittoDiagnosticLoggingAdapter;
Expand All @@ -56,17 +58,21 @@ final class CachingPolicyEnforcerProvider extends AbstractPolicyEnforcerProvider
private final ActorRef cachingPolicyEnforcerProviderActor;

CachingPolicyEnforcerProvider(final ActorSystem actorSystem) {
this(actorSystem, policyEnforcerCacheLoader(actorSystem), enforcementCacheDispatcher(actorSystem),
this(actorSystem,
DefaultNamespacePoliciesConfig.of(actorSystem.settings().config()),
enforcementCacheDispatcher(actorSystem),
DefaultCacheConfig.of(actorSystem.settings().config(),
PolicyEnforcerProvider.ENFORCER_CACHE_CONFIG_KEY));
}

private CachingPolicyEnforcerProvider(final ActorSystem actorSystem,
final AsyncCacheLoader<PolicyId, Entry<PolicyEnforcer>> policyEnforcerCacheLoader,
final NamespacePoliciesConfig namespacePoliciesConfig,
final MessageDispatcher cacheDispatcher,
final CacheConfig cacheConfig) {

this(actorSystem, new PolicyEnforcerCache(policyEnforcerCacheLoader, cacheDispatcher, cacheConfig),
this(actorSystem,
new PolicyEnforcerCache(policyEnforcerCacheLoader(actorSystem, namespacePoliciesConfig),
cacheDispatcher, cacheConfig, namespacePoliciesConfig),
BlockedNamespaces.of(actorSystem),
DistributedPubSub.get(actorSystem).mediator(),
cacheDispatcher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,35 @@
*/
package org.eclipse.ditto.policies.enforcement;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.internal.utils.cache.entry.Entry;
import org.eclipse.ditto.policies.enforcement.config.NamespacePoliciesConfig;
import org.eclipse.ditto.policies.model.ImportableType;
import org.eclipse.ditto.policies.model.Policy;
import org.eclipse.ditto.policies.model.PolicyEntry;
import org.eclipse.ditto.policies.model.PolicyId;
import org.eclipse.ditto.policies.model.enforcers.Enforcer;
import org.eclipse.ditto.policies.model.enforcers.PolicyEnforcers;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Policy together with its enforcer.
*/
@Immutable
public final class PolicyEnforcer {

private static final Logger LOG = LoggerFactory.getLogger(PolicyEnforcer.class);

@Nullable private final Policy policy;
private final Enforcer enforcer;

Expand All @@ -54,6 +64,82 @@ public static CompletionStage<PolicyEnforcer> withResolvedImports(final Policy p
});
}

/**
* Creates a policy enforcer from a policy, resolving both its explicit imports and any namespace root policies
* configured for the policy's namespace. Namespace root policies are resolved with their own imports and their
* implicit entries are merged last with local entries taking precedence on label conflicts.
* Matching namespace roots are applied in config precedence order: exact match first, then more
* specific prefix wildcards, then broader prefix wildcards, and finally {@code *}.
* <p>
* This is the preferred factory method to use in the cache loader. Namespace root policy resolution bypasses
* the normal READ permission pre-enforcer check, since namespace policies are operator-configured and injected
* transparently — the user never explicitly declares them in the policy's {@code imports} field.
* </p>
*
* @param policy the policy to build an enforcer for.
* @param policyResolver resolves imported policies by ID.
* @param namespacePoliciesConfig the static namespace policies configuration.
* @return a completion stage with the fully resolved PolicyEnforcer.
* @since 3.9.0
*/
public static CompletionStage<PolicyEnforcer> withResolvedImportsAndNamespacePolicies(
final Policy policy,
final Function<PolicyId, CompletionStage<Optional<Policy>>> policyResolver,
final NamespacePoliciesConfig namespacePoliciesConfig) {

return policy.withResolvedImports(policyResolver)
.thenCompose(resolvedPolicy -> mergeNamespacePolicies(resolvedPolicy, policyResolver,
namespacePoliciesConfig))
.thenApply(finalPolicy -> {
final var enforcer = PolicyEnforcers.defaultEvaluator(finalPolicy);
return new PolicyEnforcer(finalPolicy, enforcer);
});
}

/**
* Merges importable entries from configured namespace root policies into {@code resolvedPolicy}.
* Local/imported entries always win: if a label already exists in {@code resolvedPolicy}, the corresponding
* namespace root entry is skipped. Missing or deleted namespace root policies are logged as errors and silently
* skipped — the policy continues to function without them.
*/
private static CompletionStage<Policy> mergeNamespacePolicies(
final Policy resolvedPolicy,
final Function<PolicyId, CompletionStage<Optional<Policy>>> policyResolver,
final NamespacePoliciesConfig namespacePoliciesConfig) {

final String namespace = resolvedPolicy.getNamespace().orElse("");
final List<PolicyId> rootPolicies = namespacePoliciesConfig.getRootPoliciesForNamespace(namespace);

if (rootPolicies.isEmpty()) {
return CompletableFuture.completedFuture(resolvedPolicy);
}

CompletionStage<Policy> resultStage = CompletableFuture.completedFuture(resolvedPolicy);
for (final PolicyId rootPolicyId : rootPolicies) {
resultStage = resultStage.thenCompose(currentPolicy ->
policyResolver.apply(rootPolicyId).thenCompose(rootPolicyOpt -> {
if (rootPolicyOpt.isEmpty()) {
LOG.error("Namespace root policy <{}> for namespace <{}> does not exist or was deleted" +
" - skipping its entries.", rootPolicyId, namespace);
return CompletableFuture.completedFuture(currentPolicy);
}
return rootPolicyOpt.get().withResolvedImports(policyResolver)
.thenApply(rootPolicy -> mergeImplicitEntries(rootPolicy, currentPolicy));
}));
}
return resultStage;
}

private static Policy mergeImplicitEntries(final Policy rootPolicy, final Policy currentPolicy) {
Policy result = currentPolicy;
for (final PolicyEntry entry : rootPolicy) {
if (ImportableType.IMPLICIT.equals(entry.getImportableType()) && !result.contains(entry.getLabel())) {
result = result.setEntry(entry);
}
}
return result;
}

/**
* Create a policy together with its enforcer.
*
Expand Down
Loading
Loading