Comprehensive knowledge base for Water Framework security, authorization, permission systems, security annotations, interceptors, and access-control patterns. Use when designing, implementing, reviewing, or debugging any security- or permission-related feature in Water modules.
You are an expert Water Framework security architect with deep knowledge of the authorization system, permission model, security annotations, interceptors, and access control patterns. You use this knowledge to guide secure design decisions, review security implementations, and help build properly secured Water Framework modules.
Package reference loaded — the complete FQCN table, standard import blocks, and critical code-generation traps from
shared/package-reference.mdare already available in this skill's context.
| Class / Annotation | Package |
|---|---|
| PermissionManager | it.water.core.api.permission |
| SecurityContext | it.water.core.api.permission |
| @AllowPermissions | it.water.core.permission.annotations |
| @AllowRoles | it.water.core.permission.annotations |
| @AllowGenericPermissions | it.water.core.permission.annotations |
| @AllowPermissionsOnReturn | it.water.core.permission.annotations |
| @AccessControl | it.water.core.permission.annotations |
| @DefaultRoleAccess | it.water.core.permission.annotations |
| CrudActions | it.water.core.permission.action |
| UnauthorizedException | it.water.core.permission.exceptions |
| OwnedResource | it.water.core.api.entity.owned |
| SharedEntity | it.water.core.api.entity.shared |
Core/Core-permission/src/main/java/it/water/core/permission/annotations/AllowPermissions.java
Core/Core-permission/src/main/java/it/water/core/permission/annotations/AllowRoles.java
Core/Core-permission/src/main/java/it/water/core/permission/annotations/AccessControl.java
Core/Core-api/src/main/java/it/water/core/api/permission/PermissionManager.java
Core/Core-api/src/main/java/it/water/core/api/permission/SecurityContext.java
(source root: Water Framework source repository root)
@AllowPermissions has four attributes — all are optional but commonly misused:
@AllowPermissions(
actions = {CrudActions.SAVE}, // action names (strings)
checkById = false, // if true, verifies ownership by ID
idParamIndex = 0, // index of the ID param when checkById=true
systemApiRef = "" // FQCN of SystemApi for the check
)
CrudActions values: "save", "update", "remove", "find", "findAll" (NOT "find-all").
Water Framework implements a layered, annotation-driven security model built on four core modules that separate interfaces from implementations, annotations from interceptors, and permission logic from persistence.
HTTP Request
|
v
REST Controller (@LoggedIn on JAX-RS endpoint)
|
v
JWT Validation -> SecurityContext creation (UserPrincipal + RolePrincipals)
|
v
Runtime.fillSecurityContext(ctx) -> Thread-bound context
|
v
Api Method invocation (annotated with @AllowPermissions / @AllowRoles / etc.)
|
v
Interceptor Framework detects annotation -> invokes matching BeforeMethodInterceptor
|
v
Interceptor calls PermissionUtil / PermissionManager
|
+-- checkPermission() -> bitwise AND on actionIds
+-- checkUserOwnsResource() -> OwnedResource / SharedEntity
+-- userHasRoles() -> role name matching
|
v
ALLOW (method executes) --or-- DENY (UnauthorizedException -> HTTP 403)
|
v
AfterMethodInterceptor (e.g., @AllowPermissionsOnReturn) validates return value
|
v
Response
| Module | Role | Key Packages |
|--------|------|-------------|
| Core-api | Interfaces & contracts | it.water.core.api.permission, it.water.core.api.security |
| Core-permission | Annotations + Action classes + Exceptions | it.water.core.permission.annotations, it.water.core.permission.action, it.water.core.permission.exceptions |
| Core-security | Interceptor implementations + SecurityContext models + PermissionUtil impl | it.water.core.security.annotations.implementation, it.water.core.security.model, it.water.core.security.util |
| Permission (module) | WaterPermission entity, PermissionManagerDefault, REST API, persistence | it.water.permission.model, it.water.permission.manager, it.water.permission.service, it.water.permission.api |
A THIRD access dimension alongside permission + ownership: tenant scoping. Full design in the multitenancy-knowledge skill. Opt-in per entity via the markers TenantResource (column companyId, null=global) / MultiTenantResource (M:N via TenantMembershipResolver).
Golden rule (lenient / backward compatible): the tenant filter/check applies only when SecurityContext.getActiveCompanyId() != null. Null (MT off, non-scoped admin, legacy token) → no tenant restriction → identical to single-tenant. There is intentionally NO isAdmin() special-casing in the tenant dimension — admin scoping derives purely from whether a company is active (the owner filter still bypasses for admin; the tenant filter does not).
Enforcement points (all in the Api layer / permission manager, never SystemApi):
BaseEntityServiceImpl.createConditionForTenantResource — ANDs the tenant condition on find/findAll/countAll (independent of and complementary to the owner/shared condition; an entity can be both and must satisfy both). save() auto-assigns companyId; update()/doUpdate restore it (no tenant-hijack).PermissionManagerDefault.checkUserOwnsResource → ANDs checkEntityBelongsToActiveTenant on by-id access, AFTER the if (user.isAdmin()) return true short-circuit (so genuine admins — who are non-scoped — are unaffected).Admin model: non-scoped by default (cross-tenant provisioning); enters a tenant only via user-level impersonation (permission UserActions.IMPERSONATE on WaterUser; admin by construction, a normal user only if granted). Impersonation token carries impersonatedBy=<caller> (audit).
Core-api (foundation - interfaces only)
|
+--- Core-permission (annotations, action classes, exceptions)
| depends on: Core-api
|
+--- Core-security (interceptors, context models, PermissionUtil impl)
| depends on: Core-api, Core-permission
|
+--- Permission-model (WaterPermission JPA entity)
| depends on: Core-api, Core-permission, JpaRepository
|
+--- Permission-api (PermissionApi, PermissionSystemApi, PermissionRepository, PermissionRestApi)
| depends on: Core-api, Permission-model
|
+--- Permission-service (service implementations)
| depends on: Permission-api, Core-security
|
+--- Permission-manager (PermissionManagerDefault)
| depends on: Core-api, Core-permission, Permission-api
|
+--- Permission-service-spring (Spring REST controller)
depends on: Permission-api
All annotations are in package it.water.core.permission.annotations.
Base path: Core/Core-permission/src/main/java/it/water/core/permission/annotations/
File: Core/Core-permission/src/main/java/it/water/core/permission/annotations/AccessControl.java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@IndexAnnotated
public @interface AccessControl {
String[] availableActions() default {};
DefaultRoleAccess[] rolesPermissions() default {};
}
Purpose: Applied to entity classes to define which actions are available and what default role permissions to create at startup.
Attributes:
| Attribute | Type | Description |
|-----------|------|-------------|
| availableActions | String[] | Action names in order. Position determines actionId: actionId = 2^position |
| rolesPermissions | DefaultRoleAccess[] | Default role-to-action mappings created at application startup |
Critical Rule: The ORDER of availableActions matters because each action's numeric ID is calculated as Math.pow(2, position). Changing the order after deployment breaks existing permission records.
Example (from WaterPermission entity):
@AccessControl(
availableActions = {
CrudActions.SAVE, // position 0 -> actionId = 1
CrudActions.UPDATE, // position 1 -> actionId = 2
CrudActions.FIND, // position 2 -> actionId = 4
CrudActions.FIND_ALL, // position 3 -> actionId = 8
CrudActions.REMOVE, // position 4 -> actionId = 16
PermissionsActions.GIVE_PERMISSIONS, // position 5 -> actionId = 32
PermissionsActions.LIST_ACTIONS // position 6 -> actionId = 64
},
rolesPermissions = {
@DefaultRoleAccess(roleName = "permissionManager",
actions = {CrudActions.SAVE, CrudActions.UPDATE, CrudActions.FIND,
CrudActions.FIND_ALL, CrudActions.REMOVE,
PermissionsActions.GIVE_PERMISSIONS, PermissionsActions.LIST_ACTIONS}),
@DefaultRoleAccess(roleName = "permissionViewer",
actions = {CrudActions.FIND, CrudActions.FIND_ALL, PermissionsActions.LIST_ACTIONS}),
@DefaultRoleAccess(roleName = "permissionEditor",
actions = {CrudActions.SAVE, CrudActions.UPDATE, CrudActions.FIND,
CrudActions.FIND_ALL, PermissionsActions.LIST_ACTIONS})
}
)
public class WaterPermission extends AbstractJpaEntity implements Permission, ProtectedEntity { ... }
Note: @IndexAnnotated triggers compile-time discovery by Atteo ClassIndex, so the framework automatically finds all @AccessControl-annotated classes at startup.
File: Core/Core-permission/src/main/java/it/water/core/permission/annotations/DefaultRoleAccess.java
@Target({ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DefaultRoleAccess {
String roleName() default "";
String[] actions() default {};
}
Purpose: Associates a role name with a set of action names. At startup, DefaultActionsManager creates the role (if not existing) and adds the specified permissions.
File: Core/Core-permission/src/main/java/it/water/core/permission/annotations/AllowPermissions.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowPermissions {
String[] actions() default {};
boolean checkById() default false;
int idParamIndex() default 0;
String systemApiRef() default "";
}
Purpose: Entity-specific permission check. Verifies the user has permission to perform the specified action(s) on a specific entity instance.
Attributes:
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| actions | String[] | {} | Action names to check (e.g., {CrudActions.SAVE}) |
| checkById | boolean | false | If true, finds entity by ID from method parameter |
| idParamIndex | int | 0 | Index of the method parameter containing the entity ID (used when checkById=true) |
| systemApiRef | String | "" | Fully qualified class name of a BaseEntitySystemApi to use for entity lookup (for non-entity APIs) |
Usage patterns:
// Pattern 1: Entity passed as parameter (default)
@AllowPermissions(actions = {CrudActions.SAVE})
public MyEntity save(MyEntity entity) { ... }
// Pattern 2: Entity found by ID
@AllowPermissions(actions = {CrudActions.FIND}, checkById = true, idParamIndex = 0)
public MyEntity find(long id) { ... }
// Pattern 3: Entity found by ID with custom SystemApi
@AllowPermissions(actions = {CrudActions.REMOVE}, checkById = true, idParamIndex = 0,
systemApiRef = "it.water.mymodule.api.MyEntitySystemApi")
public void removeFromExternalApi(long entityId) { ... }
Rules:
*ServiceImpl), NOT on interface methods (*Api). The interceptor resolves annotations on the concrete class at runtime.*SystemServiceImpl — SystemApi bypasses security by design.checkById=true, the parameter at idParamIndex MUST be of type long.File: Core/Core-permission/src/main/java/it/water/core/permission/annotations/AllowGenericPermissions.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowGenericPermissions {
String[] actions() default {};
String resourceName() default "";
String resourceParamName() default "";
}
Purpose: Resource-level (generic) permission check WITHOUT entity ID. Checks if the user has permission on the resource class, not a specific entity instance.
Attributes:
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| actions | String[] | {} | Action names to check |
| resourceName | String | "" | Explicit resource name (fully qualified class name) |
| resourceParamName | String | "" | Name of method parameter containing resource name |
Resource name resolution order:
resourceName is set → use it directlyresourceParamName is set → extract from method parameterBaseEntityApi → auto-infer from entityApi.getEntityType().getName()BaseEntityApi → throws WaterRuntimeExceptionUsage patterns:
// Pattern 1: On entity API (auto-infers resource name)
@AllowGenericPermissions(actions = {CrudActions.FIND_ALL})
public PaginableResult<MyEntity> findAll() { ... }
// Pattern 2: Explicit resource name (non-entity service)
@AllowGenericPermissions(actions = {"export"}, resourceName = "it.water.mymodule.model.MyEntity")
public byte[] exportData() { ... }
Rules:
*ServiceImpl), NOT on interface methods (*Api). The interceptor resolves annotations on the concrete class at runtime.BaseEntityApi services, MUST provide either resourceName or resourceParamName.File: Core/Core-permission/src/main/java/it/water/core/permission/annotations/AllowRoles.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowRoles {
String[] rolesNames() default {};
}
Purpose: Role-based access control. Verifies the current user has at least ONE of the specified roles.
Usage:
@AllowRoles(rolesNames = {"permissionManager", "systemAdmin"})
public void dangerousOperation() { ... }
Rules:
*ServiceImpl), NOT on interface methods (*Api). The interceptor framework resolves annotations at runtime on the concrete class — annotations on interfaces are invisible to the interceptor.WaterRuntimeException if empty.@AllowPermissions or @AllowGenericPermissions.File: Core/Core-permission/src/main/java/it/water/core/permission/annotations/AllowLoggedUser.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowLoggedUser {
}
Purpose: Marker annotation. Simply checks that the user is authenticated (has a valid SecurityContext with a non-zero loggedEntityId).
Usage:
@AllowLoggedUser
public Map<String, Map<String, Map<String, Boolean>>> entityPermissionMap(
Map<String, List<Long>> entityPks) { ... }
Rules:
checkAnnotationIsOnWaterApiClass call).UnauthorizedException("No security context found, please login") if:
currentRuntime is null, ORcurrentRuntime.getSecurityContext() is null, ORcurrentRuntime.getSecurityContext().getLoggedEntityId() == 0File: Core/Core-permission/src/main/java/it/water/core/permission/annotations/AllowPermissionsOnReturn.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowPermissionsOnReturn {
String[] actions() default {};
String systemApiRef() default "";
}
Purpose: Post-execution interceptor. Validates that the user has permission on the entity RETURNED by the method.
Usage:
@AllowPermissionsOnReturn(actions = {CrudActions.FIND})
public MyEntity findByCustomQuery(Query filter) { ... }
Rules:
BaseApi class methods only.null → passes through (no check).BaseEntity → throws WaterRuntimeException.BaseEntityApi subclasses.All interceptors are in package it.water.core.security.annotations.implementation.
Base path: Core/Core-security/src/main/java/it/water/core/security/annotations/implementation/
File: Core/Core-security/src/main/java/it/water/core/security/annotations/implementation/AbstractPermissionInterceptor.java
Provides common infrastructure for all security interceptors.
Injected dependencies:
Runtime waterRuntime — access to SecurityContextPermissionUtil waterPermissionUtil — simplified permission checkingComponentRegistry componentRegistry — component lookupActionsManager actionsManager — registered action lookupKey utility methods:
| Method | Description |
|--------|-------------|
| getAction(String className, String actionName) | Resolves a registered Action by resource class name and action name. Throws WaterRuntimeException if multiple actions match. |
| findObjectTypeInParams(Class<T> type, Object[] args) | Finds first method parameter matching the given type. Used to extract BaseEntity from method args. |
| findMethodParamIndexByName(String paramName, Object[] params) | Finds method parameter index by name. |
| checkEntityPermission(SecurityContext ctx, BaseEntity entity, String[] actions) | Iterates action names, resolves each to an Action, checks permission via PermissionUtil. Returns true if ANY action is permitted. |
| checkAnnotationIsOnWaterApiClass(Service s, Annotation a) | Validates that the annotation is on a BaseApi class. Throws WaterRuntimeException with clear message if not. |
File: Core/Core-security/src/main/java/it/water/core/security/annotations/implementation/AllowPermissionInterceptor.java
Type: BeforeMethodInterceptor<AllowPermissions>
Registration: @FrameworkComponent(services = {BeforeMethodInterceptor.class})
Logic flow:
checkAnnotationIsOnWaterApiClass)BaseEntityApi or has systemApiRef)checkById=true → find entity by ID from args using BaseEntityApi.find() or SystemApicheckById=false → find BaseEntity in method paramscheckEntityPermission(ctx, entity, actions)throw new UnauthorizedException()File: Core/Core-security/src/main/java/it/water/core/security/annotations/implementation/AllowRolesInterceptor.java
Type: BeforeMethodInterceptor<AllowRoles>
Logic flow:
rolesNames array is not null/empty (throws WaterRuntimeException)SecurityContext from RuntimepermissionUtil.userHasRoles(ctx.getLoggedUsername(), roles)throw new UnauthorizedException()File: Core/Core-security/src/main/java/it/water/core/security/annotations/implementation/AllowGenericPermissionInterceptor.java
Type: BeforeMethodInterceptor<AllowGenericPermissions>
Logic flow:
BaseEntityApi → try annotation.resourceName(), then annotation.resourceParamName(), then entityApi.getEntityType().getName()BaseEntityApi → MUST have resourceName or resourceParamNameSecurityContext from RuntimepermissionUtil.checkPermission(resourceName, action)throw new UnauthorizedException()File: Core/Core-security/src/main/java/it/water/core/security/annotations/implementation/AllowLoggedUserInterceptor.java
Type: BeforeMethodInterceptor<AllowLoggedUser>
Logic flow:
currentRuntime == null || getSecurityContext() == null || getLoggedEntityId() == 0throw new UnauthorizedException("No security context found, please login")File: Core/Core-security/src/main/java/it/water/core/security/annotations/implementation/AllowPermissionOnReturnInterceptor.java
Type: AfterMethodInterceptor<AllowPermissionsOnReturn>
Logic flow:
returnResult == null → return (pass through)BaseEntityApi:
BaseEntity → checkEntityPermission(ctx, entity, actions)Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer