Permissions
The entity system's authorization layer is built on three pieces:
- The
AuthorizeDSL (invyuh_entity_annotations) — declarative expressions likeAuthorize.permission('lms.area.view'),Authorize.allOf([...]),Authorize.self(). - An
AuthorizationProvider— synchronously holds the current actor's permission / role / user-group state and evaluatesAuthorizeexpressions. - UI
AuthorizeGuardwidgets and anAuthorizationGateshell for app-wide loading / refresh state.
This replaces the older PermissionProvider / EntityPermissionService / PermissionGuard API.
AuthorizationProvider
abstract class AuthorizationProvider — synchronous authorization provider for the entity system. The provider is the single entry point gates use to evaluate an Authorize expression. Concrete implementations populate cached state on login and refresh on demand; evaluation is pure and synchronous from there.
Cached state (abstract)
| Property | Type | Description |
|---|---|---|
permissions | Set<String> | Permission strings (e.g. 'lms.batch.view') |
roles | Set<String> | Role codes (e.g. 'qa_lead') |
userGroups | Set<String> | User-group IDs |
currentUserId | String? | Authenticated user id, or null when anonymous |
Synchronous primitives (abstract)
| Method | Returns | Description |
|---|---|---|
hasPermission(String perm) | bool | True when the actor holds perm |
hasRole(String code) | bool | True when the actor holds the role |
isMemberOf(String groupId) | bool | True when the actor is a group member |
Readiness
| Member | Type | Description |
|---|---|---|
isReady | bool | True after the first refresh attempt resolves (success or failure). Stays true. |
isRefreshing | bool | True while a refresh is in flight (concurrent calls dedupe through one Completer). |
changes | Stream<void> | Broadcasts on every refresh start/end and state transition. |
whenReady | Future<void> | Resolves when isReady becomes true. Idempotent. |
hasFreshData | bool | True when the last refresh fetched from server (vs storage / cache fallback). |
lastFreshAt | DateTime? | Timestamp of the last successful server fetch. |
Evaluation
bool can(Authorize expr, {Object? subject, EntityOwnership? ownership});Evaluates an Authorize expression against the current cached state. subject and ownership are required when the expression contains Authorize.self(). On denial, fires the onDeny callback.
Refresh
Future<void> refresh();
StreamSubscription<void> bindRefreshStream(Stream<void> events);refresh() is the single async surface — call on login, logout, or any event that may change the actor's authorization state. bindRefreshStream subscribes to a Stream<void> and triggers a refresh on every emission. EntitySystemPlugin automatically binds it to vyuh.auth.userChanges so login/logout transitions refresh the cache.
Denial logging
void Function(Authorize expression, AuthorizationContext context)? onDeny;Set once at app startup to receive a callback every time can returns false. Fired synchronously after evaluation; implementations should dispatch async logging via unawaited(...).
OpenAuthorizationProvider
final class OpenAuthorizationProvider extends AuthorizationProvider — built-in provider that allows everything. Used by default when none is supplied to EntitySystemPlugin.
final class OpenAuthorizationProvider extends AuthorizationProvider {
// permissions / roles / userGroups: returns {'*'}
// hasPermission / hasRole / isMemberOf: always true
// can(...): always true
// isReady / hasFreshData: true
// refresh(): no-op
}AuthorizeGuard
final class AuthorizeGuard extends StatelessWidget (in vyuh_entity_system_ui) — renders child when an Authorize expression passes, otherwise falls back to a surface-appropriate visual. Replaces the old PermissionGuard.
Constructor
const AuthorizeGuard({
super.key,
required this.authorize,
this.evaluator,
required this.child,
this.fallback,
this.defaults = const AuthorizeDefaults(),
this.subject,
});Surface factories
Each factory resolves a different default fallback slot from AuthorizeDefaults.
| Factory | Surface | Default fallback |
|---|---|---|
AuthorizeGuard.route | Route content | placeholder ("no access" page) |
AuthorizeGuard.menu | Menu items | hide |
AuthorizeGuard.tab | Tabs | hide |
AuthorizeGuard.section | Detail sections / panels | placeholder |
AuthorizeGuard.action | Action buttons | disable |
AuthorizeGuard.fieldRead | Field read | redact (••••) |
AuthorizeGuard.fieldWrite | Field write | disable (read-only) |
AuthorizeGuard(...) (unnamed) | Generic widget | hide |
Properties
| Property | Type | Description |
|---|---|---|
authorize | Authorize | Expression to evaluate |
evaluator | AuthorizeEvaluator? | Custom evaluator. Defaults to vyuh.entity?.authorizationProvider?.can |
child | Widget | Rendered when authorization passes |
fallback | AuthorizeFallback? | Override the surface default |
defaults | AuthorizeDefaults | App-level fallback defaults |
subject | Object? | Subject passed to the evaluator (used by Authorize.self() / ownership checks) |
typedef AuthorizeEvaluator = bool Function(
Authorize authorize, {
Object? subject,
});When evaluator is null, the guard delegates to vyuh.entity?.authorizationProvider?.can — the common case for production apps that register EntitySystemPlugin. Falls through to "allow" when no provider is registered.
Example
AuthorizeGuard.action(
authorize: Authorize.permission('equipment.edit'),
child: EditButton(onPressed: _openEditor),
)
AuthorizeGuard.route(
authorize: Authorize.allOf([
Authorize.permission('lms.batch.approve'),
Authorize.not(Authorize.self()),
]),
subject: batch,
child: BatchApprovalPage(batch: batch),
)AuthorizeFallback
sealed class AuthorizeFallback (in vyuh_entity_annotations) — what to show when an AuthorizeGuard denies.
| Variant | Visual |
|---|---|
FallbackHide() | SizedBox.shrink() |
FallbackDisable({title, message}) | CDX DisabledOverlay over the child |
FallbackPlaceholder({title, message}) | CDX LockedSurface placeholder |
FallbackRedact({mask}) | CDX RedactedValue (defaults to ••••) |
FallbackWidget(Type fallbackWidget) | Custom widget resolved from AuthorizeFallbackRegistry |
The top-level helper Widget renderAuthorizeFallback(fallback, authorize, child) exposes the same rendering pipeline so menu builders and route gates can render the vocabulary consistently.
AuthorizeFallbackRegistry
class AuthorizeFallbackRegistry (in vyuh_entity_system_ui) — maps a custom AuthorizeFallbackWidget subclass to a builder. Register once at app startup before any guard renders.
abstract class AuthorizeFallbackWidget extends StatelessWidget {
final Authorize authorize;
const AuthorizeFallbackWidget({super.key, required this.authorize});
}
class AuthorizeFallbackRegistry {
static void register<T extends AuthorizeFallbackWidget>(
Type type,
T Function(Authorize authorize) builder,
);
static AuthorizeFallbackWidget lookup(Type type, Authorize authorize);
}Throws StateError if lookup is called for an unregistered type — fail fast at the call site rather than render a silent placeholder.
AuthorizationGate
class AuthorizationGate extends StatelessWidget — top-level shell gate that ties the routed UI to the lifecycle of the AuthorizationProvider. Place it in your MaterialApp.router builder.
const AuthorizationGate({
super.key,
required this.child,
required this.loaderBuilder,
this.showRefreshingIndicator = true,
});| Property | Type | Description |
|---|---|---|
child | Widget | The routed child |
loaderBuilder | WidgetBuilder | App-specific loading view shown while the provider hydrates |
showRefreshingIndicator | bool | Whether to overlay a thin progress bar during background refreshes |
Behaviour:
- Anonymous user (login screen) → renders
childunmodified. - Authenticated, provider not yet ready → renders
loaderBuilder. - Ready → renders
child; ifisRefreshing, overlaysAuthorizationRefreshingIndicator.
MaterialApp.router(
builder: (context, child) => AuthorizationGate(
loaderBuilder: (_) => const MyAppBrandedLoader(),
child: child!,
),
)AuthorizationRefreshingIndicator
A 2px (configurable via height) progress strip used by AuthorizationGate while the provider is mid-refresh. Public so apps can use it standalone (e.g., on a settings screen with a manual "Refresh permissions" button).
EntityRoutePermissions
The configuration surface for declaring permissions on entity routes. Documented under EntityConfiguration.
When a route has permissions set, the standard route builder wraps the route body in AuthorizeGuard.route(authorize: Authorize.anyPermission(perms)) so users who fail the check see the route's "no access" placeholder.
EntityPermissionCache
class EntityPermissionCache — static cache for entity permission results. Used internally by EntityLayouts to cache detail-tab permission filtering and by other places that need a typed permission cache scoped per entity identifier.
Static methods
| Method | Description |
|---|---|
getDetailTabs(String entityIdentifier) | Cached tab identifiers for an entity |
setDetailTabs(String entityIdentifier, List<String>) | Cache tab identifiers |
hasDetailTabs(String entityIdentifier) | Whether tab cache exists |
getActions(String entityIdentifier) | Cached action identifiers |
setActions(String entityIdentifier, List<String>) | Cache action identifiers |
hasActions(String entityIdentifier) | Whether action cache exists |
getRoutePermission(String entityIdentifier, String routeName) | Cached route permission result |
setRoutePermission(String entityIdentifier, String routeName, bool allowed) | Cache a route permission |
hasRoutePermissions(String entityIdentifier) | Whether route cache exists |
clearEntityType(String entityIdentifier) | Clear cache for one entity type |
clearAll() | Clear all cached permissions |
getStats() | Cache statistics for debugging |
Clearing on logout
EntityConfiguration.clearAllPermissionCaches();This delegates to EntityPermissionCache.clearAll().
See Also
- EntityConfiguration — Where route permissions are declared
- Actions — Action authorization gating via
Authorize - Services —
EntitySystemPlugin.authorizationProvider - Glossary — Term definitions