Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,12 @@ private ThingifierApiDataScopeSelection anonymousDataScopeSelection(
if (policy.allowsAnonymousDefaultScope()) {
return ThingifierApiDataScopeSelection.defaultDataScope();
}
return definition.anonymousDataScopeSelection(
scopedSessionContext(definition, "", verb, path, context, route, queryParams));
final ThingifierApiScopedSessionContext scopedSessionContext =
scopedSessionContext(definition, "", verb, path, context, route, queryParams);
if (policy.allowsAnonymousConfiguredWriteScope()) {
return definition.anonymousWriteDataScopeSelection(scopedSessionContext);
}
return definition.anonymousReadDataScopeSelection(scopedSessionContext);
}

private ApiResponse scopedSessionRejected(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
* Chooses the data scope for a missing scoped-session credential.
*
* <p>This callback is trusted server-side configuration, not a request-controlled database mapper.
* Thingifier calls it only after route matching has determined that anonymous read access is
* allowed, and before validators, authorizers, hooks, handlers, and response rendering run.
* Thingifier calls it only after route matching has determined that anonymous access is allowed for
* the current operation, and before validators, authorizers, hooks, handlers, and response
* rendering run.
*/
@FunctionalInterface
public interface ThingifierApiAnonymousDataScopeResolver {

/**
* Selects the data scope to use for an anonymous read request.
* Selects the data scope to use for an anonymous request.
*
* @param context immutable route and request context for the missing credential request
* @return trusted data-scope selection, or null to signal a configuration error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public final class ThingifierApiScopedSessionDefinition {
private ThingifierApiScopedSessionAuthenticator authenticator;
private boolean anonymousScopeForReads;
private boolean anonymousDefaultScopeForReads;
private ThingifierApiAnonymousDataScopeResolver anonymousDataScopeResolver;
private ThingifierApiAnonymousDataScopeResolver anonymousReadDataScopeResolver;
private boolean anonymousScopeForWrites;
private ThingifierApiAnonymousDataScopeResolver anonymousWriteDataScopeResolver;
private boolean authenticatedScopeForWrites;
private int missingCredentialStatusCode;
private String missingCredentialMessage;
Expand Down Expand Up @@ -104,7 +106,7 @@ public ThingifierApiScopedSessionDefinition authenticateWith(
public ThingifierApiScopedSessionDefinition allowAnonymousDefaultScopeForReads() {
this.anonymousScopeForReads = true;
this.anonymousDefaultScopeForReads = true;
this.anonymousDataScopeResolver =
this.anonymousReadDataScopeResolver =
context -> ThingifierApiDataScopeSelection.defaultDataScope();
return this;
}
Expand Down Expand Up @@ -156,17 +158,73 @@ public ThingifierApiScopedSessionDefinition allowAnonymousReadsUsingDataScope(
}
this.anonymousScopeForReads = true;
this.anonymousDefaultScopeForReads = false;
this.anonymousDataScopeResolver = resolver;
this.anonymousReadDataScopeResolver = resolver;
return this;
}

/**
* Allows write-style generated routes to use a named data scope when no credential is supplied.
*
* <p>This is intentionally opt-in for applications that offer public, demo, or single-user
* mutable state. If a credential is supplied, Thingifier still validates it and invalid
* credentials reject rather than falling back to the anonymous write scope.
*
* @param dataScopeName anonymous write data scope
* @return this definition for fluent configuration
*/
public ThingifierApiScopedSessionDefinition allowAnonymousWritesUsingDataScope(
final String dataScopeName) {
return allowAnonymousWritesUsingDataScope(
dataScopeName, DataScopeCreationPolicy.USE_EXISTING_ONLY);
}

/**
* Allows write-style generated routes to use a named data scope when no credential is supplied.
*
* @param dataScopeName anonymous write data scope
* @param creationPolicy policy used when the anonymous scope does not exist
* @return this definition for fluent configuration
*/
public ThingifierApiScopedSessionDefinition allowAnonymousWritesUsingDataScope(
final String dataScopeName, final DataScopeCreationPolicy creationPolicy) {
final ThingifierApiDataScopeSelection selection =
ThingifierApiDataScopeSelection.useDataScope(dataScopeName, creationPolicy);
return allowAnonymousWritesUsingDataScope(context -> selection);
}

/**
* Allows write-style generated routes to resolve the anonymous data scope dynamically.
*
* <p>The resolver runs only when the scoped-session credential is missing and anonymous write
* access is allowed for the route. Configuring anonymous writes clears the authenticated-write
* requirement so fluent write policy calls have predictable last-call-wins behaviour.
*
* @param resolver trusted anonymous data-scope resolver
* @return this definition for fluent configuration
*/
public ThingifierApiScopedSessionDefinition allowAnonymousWritesUsingDataScope(
final ThingifierApiAnonymousDataScopeResolver resolver) {
if (resolver == null) {
throw new IllegalArgumentException("anonymous write data-scope resolver is required");
}
this.anonymousScopeForWrites = true;
this.anonymousWriteDataScopeResolver = resolver;
this.authenticatedScopeForWrites = false;
return this;
}

/**
* Requires write-style generated routes to have a valid scoped-session credential.
*
* <p>Configuring required writes clears any anonymous write scope so fluent write policy calls
* have predictable last-call-wins behaviour.
*
* @return this definition for fluent configuration
*/
public ThingifierApiScopedSessionDefinition requireAuthenticatedScopeForWrites() {
this.authenticatedScopeForWrites = true;
this.anonymousScopeForWrites = false;
this.anonymousWriteDataScopeResolver = null;
return this;
}

Expand Down Expand Up @@ -247,7 +305,19 @@ public boolean allowsAnonymousDefaultScopeForReads() {
* @return resolver when anonymous reads are configured
*/
public Optional<ThingifierApiAnonymousDataScopeResolver> anonymousDataScopeResolver() {
return Optional.ofNullable(anonymousDataScopeResolver);
return anonymousReadDataScopeResolver();
}

/**
* Returns the trusted resolver used for missing-credential anonymous reads.
*
* <p>This named read accessor keeps the read and write anonymous scope policies explicit while
* {@link #anonymousDataScopeResolver()} remains as the original read-scope alias.
*
* @return resolver when anonymous reads are configured
*/
public Optional<ThingifierApiAnonymousDataScopeResolver> anonymousReadDataScopeResolver() {
return Optional.ofNullable(anonymousReadDataScopeResolver);
}

/**
Expand All @@ -258,10 +328,51 @@ public Optional<ThingifierApiAnonymousDataScopeResolver> anonymousDataScopeResol
*/
public ThingifierApiDataScopeSelection anonymousDataScopeSelection(
final ThingifierApiScopedSessionContext context) {
if (anonymousDataScopeResolver == null) {
return anonymousReadDataScopeSelection(context);
}

/**
* Selects the anonymous data scope for a missing-credential read request.
*
* @param context route and request context
* @return selected data scope, or null if the resolver is absent or returns null
*/
public ThingifierApiDataScopeSelection anonymousReadDataScopeSelection(
final ThingifierApiScopedSessionContext context) {
if (anonymousReadDataScopeResolver == null) {
return null;
}
return anonymousReadDataScopeResolver.selectDataScope(context);
}

/**
* @return true when write-style routes may fall back to an anonymous scope
*/
public boolean allowsAnonymousScopeForWrites() {
return anonymousScopeForWrites;
}

/**
* Returns the trusted resolver used for missing-credential anonymous writes.
*
* @return resolver when anonymous writes are configured
*/
public Optional<ThingifierApiAnonymousDataScopeResolver> anonymousWriteDataScopeResolver() {
return Optional.ofNullable(anonymousWriteDataScopeResolver);
}

/**
* Selects the anonymous data scope for a missing-credential write request.
*
* @param context route and request context
* @return selected data scope, or null if the resolver is absent or returns null
*/
public ThingifierApiDataScopeSelection anonymousWriteDataScopeSelection(
final ThingifierApiScopedSessionContext context) {
if (anonymousWriteDataScopeResolver == null) {
return null;
}
return anonymousDataScopeResolver.selectDataScope(context);
return anonymousWriteDataScopeResolver.selectDataScope(context);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ public enum Mode {
ALLOW_ANONYMOUS_DEFAULT_SCOPE,

/**
* Missing credentials use the anonymous data-scope resolver configured on the definition.
* Missing credentials use the anonymous read data-scope resolver configured on the
* definition.
*/
ALLOW_ANONYMOUS_CONFIGURED_SCOPE,

/**
* Missing credentials use the anonymous write data-scope resolver configured on the
* definition.
*/
ALLOW_ANONYMOUS_CONFIGURED_WRITE_SCOPE,

/** Missing or invalid credentials reject before validators and handlers run. */
REQUIRE_AUTHENTICATED_SCOPE
}
Expand Down Expand Up @@ -107,11 +114,20 @@ public boolean allowsAnonymousConfiguredScope() {
return mode == Mode.ALLOW_ANONYMOUS_CONFIGURED_SCOPE;
}

/**
* @return true when missing credentials should use the definition's anonymous write resolver
*/
public boolean allowsAnonymousConfiguredWriteScope() {
return mode == Mode.ALLOW_ANONYMOUS_CONFIGURED_WRITE_SCOPE;
}

/**
* @return true when missing credentials should continue anonymously
*/
public boolean allowsAnonymousScope() {
return allowsAnonymousDefaultScope() || allowsAnonymousConfiguredScope();
return allowsAnonymousDefaultScope()
|| allowsAnonymousConfiguredScope()
|| allowsAnonymousConfiguredWriteScope();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,20 @@ private Optional<ThingifierApiScopedSessionPolicy> contractDefaultScopedSessionP
definition, Mode.ALLOW_ANONYMOUS_CONFIGURED_SCOPE));
}
if (isWriteVerb(verb)) {
final Optional<ThingifierApiScopedSessionPolicy> anonymousWritePolicy =
scopedSessions.values().stream()
.filter(
ThingifierApiScopedSessionDefinition
::allowsAnonymousScopeForWrites)
.findFirst()
.map(
definition ->
ThingifierApiScopedSessionPolicy.configured(
definition,
Mode.ALLOW_ANONYMOUS_CONFIGURED_WRITE_SCOPE));
if (anonymousWritePolicy.isPresent()) {
return anonymousWritePolicy;
}
return scopedSessions.values().stream()
.filter(
ThingifierApiScopedSessionDefinition
Expand Down
Loading
Loading