Identity, roles and form access
Manage system identity, respondent identity and explicit form access rules without treating authorization as a client-side feature.
Form access control developer guide
This guide describes the server contract for system-user access to ordinary, System, Functional, and Survey forms. Respondent access to a deployed survey remains a separate identity and deployment workflow.
Chinese edition: FORM_ACCESS_CONTROL_DEVELOPER_GUIDE.zh-CN.md.
Security invariants
1. The server is authoritative. A menu condition, hidden button, route guard, or client-supplied filter is never authorization.
2. Department scope is the outer boundary for department-owned definitions and department-scoped rows; form ACL is the inner permission boundary. A null form-definition department is an Administrator-published shared definition, not a shared business row.
3. Explicit `deny` wins over grants and built-in defaults. Public read from `isAnonymous` remains public by design, but never carries edit/delete/submission capabilities across department scope. `Administrator` is the recovery principal and cannot be denied.
4. Designer access, form deletion, submission administration, and business-data use are separate operations.
5. Only startup/layout, login, licensing, and security-administration infrastructure may retain hard-coded Administrator restrictions.
ACL operations
Rules are stored in `app_form_access_rules`. Subjects are a system-user ID or a role ID.
| Permission | Server meaning |
|---|---|
| `deny` | Reject every ordinary capability for the subject. |
| `read` | Use the form: render it, submit it, and execute the trusted business-data commands declared by the form. |
| `edit` | Read and change the form definition, metadata, and Designer state. It does not grant submission administration. |
| `delete` | Delete the form definition after department and domain-integrity checks. |
| `submissions` | Review, update, and delete generic JSON submissions. For a Survey form it also grants assisted entry. |
| `create` | Create a form in the global creation scope; it is not a per-form permission. |
The global creation scope uses `form_id = 00000000-0000-0000-0000-000000000000`. The compatibility seed grants `create` to FormDesigner until an Administrator deliberately changes the configuration.
Built-in defaults remain part of effective ACL evaluation:
- Administrator can read, edit, create, manage submissions, and assist. It can delete forms outside the protected infrastructure set.
- FormDesigner can edit non-infrastructure forms and can read Survey forms, but does not implicitly manage submissions or delete definitions.
- SurveyAssistant can use the Survey Responses workspace and assist with Survey forms. An explicit `submissions` grant can authorize another role or user without adding SurveyAssistant.
Department and form classification
For a department-owned form, the current user's active department subtree must contain `form_definitions.department_id` before grants are evaluated. A null `form_definitions.department_id` instead means an Administrator-published shared definition: the ownership check passes for every system-user department, but deny/grant rules, built-in defaults, protected-form rules, authentication, and `isAnonymous` semantics are unchanged. This exception applies only to form definitions; a null `department_id` in a scoped business table remains inaccessible to ordinary users.
`form_definitions.hidden` is deliberately outside the permission calculation. A hidden form is omitted from every non-Administrator Form Center page before cursor pagination, while Administrators can still discover it. Direct form routes and APIs continue to enforce the same Read/Edit/Delete/ManageSubmissions ACL plus department scope. Use `hidden` to reduce catalog clutter, `is_active` to control operational availability, and ACL/department ownership to enforce security; none substitutes for another.
The narrow infrastructure exception contains forms required to render the platform/system-user/respondent shell (including Form Center and the respondent dashboard), authenticate users/respondents, manage licensing/schema, or maintain users, roles, departments, and ACLs. Non-administrators cannot read the security-management forms or redesign/delete this protected infrastructure. Form Center remains reachable through the separate create-form capability.
Operational Survey management forms, Survey notifications, Submission Center, and Module Starter are not blanket Administrator-only forms. Their normal access is computed from ACL plus either matching department ownership or the shared-definition rule, so no operational-form sharing whitelist is required.
Effective evaluation order
`FormAccessControlService` is the single authority for individual requests and batch capability projection:
1. Preserve the Administrator recovery rule, except that protected infrastructure is still not deletable.
2. Reject non-administrators from Administrator-only security forms.
3. Validate authentication and the applicable department rule.
4. Apply explicit deny.
5. Apply explicit grants and built-in defaults.
6. Apply domain rules, such as protected infrastructure or an active Survey deployment that prevents deletion.
`CanReadAsync`, `CanEditAsync`, `CanDeleteAsync`, `CanManageSubmissionsAsync`, `CanAssistSurveyAsync`, and `EvaluateAsync` must remain semantically aligned. A list or menu must never project a capability that the target endpoint rejects for a different role-only reason.
Host and module endpoint authorization
Built-in endpoints use `FormAccessControlService`. Trusted modules should depend on the SDK abstraction instead of copying role checks:
using FormPlatform.DataAccess.Forms;
group.MapPost("/command", async (
HttpContext http,
IFormAccessAuthorizer authorization,
CancellationToken ct) =>
{
var allowed = await authorization.AuthorizeAsync(
OwningSystemFormId,
http.User,
FormAccessOperation.Read,
ct);
return allowed ? Results.Ok() : Results.Forbid();
}).RequireAuthorization();
Choose the operation by the action, not by the route prefix:
- rendering, normal submission, and schema-declared business-data commands use `Read`;
- Designer/schema changes use `Edit`;
- definition deletion uses `Delete`;
- historic generic-submission administration and Survey assisted entry use `ManageSubmissions`.
Copying a complete form definition requires both the global `create` capability and `Edit` on the source because the copy includes metadata, action code, and backend mapping—not only viewer-visible schema.
`/api/admin/...` is a naming convention, not proof that an Administrator policy is correct. Keep `RequireAuthorization("Administration")` only for genuine global infrastructure such as Data Model/schema migration, system identities and roles, ACL administration, licensing state, or global media administration.
Business-data department scope
Form authorization does not replace row authorization. API-mode controls and module endpoints must inject `IDepartmentScopeResolver`, resolve the authenticated system-user ID, and compile `scope.DepartmentIds` into a server-owned `IN` predicate. Never accept the allowed department IDs from the browser.
Platform ORM scope is automatic and cannot be redirected by a form schema. The `app_departments` Data Model is scoped on physical column `id`; every other model exposing physical column `department_id` is scoped through the logical attribute mapped to that column. A model without `department_id` is global. Do not add `departmentScopeProperty` to controls or `metadata.mapping`; old saved values are ignored.
The convention protects DataGrid, ItemRenderer, AsyncSelect, Tree, entity lists, complete-key and route-ID loads, inserts, updates, copies, and deletes. It also scopes selected joined references and validates declared references before writes. For an ordinary user, `department_id IS NULL` is inaccessible rather than shared. Inserts fill a missing/null value from the actor's current department; no-department actors fail closed, and updates cannot clear or move the value outside the active subtree.
At startup, `DepartmentIdFormFieldInitializer` inspects each Data Model form's actual data source after module migrations and form initializers. When both the physical table and trusted model expose `department_id`, it idempotently adds a missing `Department ID` text input and, for an explicit mapping, its direct attribute mapping. It validates only that owned addition, so an unrelated stale mapping does not block the repair and is not rewritten. It never replaces an existing data control or treats this visible field as authorization. A schema/model mismatch is logged and left unchanged; refresh the Data Model instead of inventing a client-only mapping.
`IFormEntityDataService` keeps its original signatures for binary compatibility and adds scoped overloads that accept trusted department IDs. The built-in implementation rejects a legacy unscoped call when the resolved Data Model has a department boundary; replacement implementations must implement the scoped overloads.
Business row copy/delete and Survey management commands are form-use operations, so they require `Read`, not `Edit`. The saved grid/filter/mapping and department predicate decide which records the authorized form user may operate. Form definition deletion continues to require `Delete`.
Survey administration
Survey administration uses two related layers:
- Survey management forms (deployments, lists, respondents, responses, push, and notifications) require the owning system form's effective `Read` capability.
- Creating or changing a deployment also requires `Read` on the selected target Survey form; a forged form ID cannot turn deployment-management access into access to another form. A shared Survey definition may be referenced by deployments in any allowed department; a department-owned Survey definition must match the deployment department.
- Assisted entry additionally requires the Survey Responses workspace and either the target Survey form's `submissions` grant or the SurveyAssistant built-in default. Deployment/list/respondent row scope is still enforced independently of a shared target definition.
Restoring a `survey_deployment` row without its referenced Survey form does not create a usable deployment. Deployment-only assisted routes resolve the target definition first; a missing definition returns `409 survey.deploymentFormMissing` with the deployment/form IDs instead of an opaque 404. This remains fail-closed: respondent collections, survey content, and assisted operations never bypass the target-form ACL because the form was lost. A recovery process must install the original stable form definition and must not silently rebind the deployment to another survey.
Survey management queries and mutations filter `survey_deployment`, `survey_list`, `survey_respondent`, notification, option, membership, and push-recipient paths by the server-resolved subtree. A posted key cannot select an out-of-scope row. Administrator requests omit this predicate.
Respondent-page notification delivery is intentionally different from management. A null `system_notification.department_id` publishes the active notification globally; a non-null value publishes it to active local respondents in that department and its active descendants. Anonymous/pre-authentication requests, external identities, and local respondents without a valid active department chain receive global notifications only. The server derives the local respondent's ancestor chain from stored data and never trusts a browser-supplied department.
The notification endpoint remains anonymous because the pre-authentication login page also consumes it. Since the application's default authentication scheme is the management cookie, an anonymous-capable endpoint must explicitly call `AuthenticateAsync(SurveyAuthentication.Scheme)` before resolving a dashboard respondent. It must not infer respondent identity from the default `HttpContext.User`; a missing or invalid respondent cookie follows the global-only path.
Respondents remain a distinct authentication scheme. Their deployment, list membership, time window, response ownership, and repeat-submission rules are not replaced by system-user ACLs.
During assisted entry, Action Code receives the authorized actor as `context.systemUser` and the target identity as `context.respondent` / `context.actingRespondent`. `context.administrator` remains a compatibility alias for older forms; its presence no longer proves that the actor has the Administrator role. The server authorization result is always authoritative.
Offline assisted entry uses the same authorization decision, but as a distinct grant mode. Issuing a package requires Survey Responses workspace Read, target Survey Read, and target assisted-entry permission. The grant binds the system operator and one assigned respondent; file upload and response synchronization require that same operator to sign in again and repeat ACL, membership, and department-scope checks. The browser cannot submit or replace either identity. Revoking ACL before synchronization therefore prevents the queued write even though the encrypted package can still be opened locally.
Functional form dispatch
A URL-mapped Functional form must authorize the form before calling its feature handler. The built-in `/api/features` route requires a valid `formId`, verifies that the referenced Functional form is actually mapped to that route, applies the platform's active-state rule for anonymous access, evaluates `Read` plus department context, applies trusted system values/options, and discards caller-supplied form/user identity fields.
The internal form submission dispatcher invokes the feature service directly after the outer `/api/forms/{id}/submissions` endpoint has authorized the form. This prevents a direct anonymous URL call from bypassing the form contract.
Client capability projection
Login and `/api/auth/me` return client-safe effective state:
{
"canCreateForms": true,
"formAccess": {
"form-guid": {
"canRead": true,
"canEdit": false,
"canDelete": false,
"canManageSubmissions": true
}
}
}
SystemLeftPane uses these values for operational menu visibility. It no longer repeats Administrator checks for Survey management, Submission Center, Module Starter, or arbitrary functional forms. The server still re-evaluates every request; the projection is only a consistent UI hint and refreshes on the next login or forced `/me` reload after ACL changes.
Compatibility and extension checklist
When adding a system or functional form:
1. Give it a stable form ID and decide whether it is genuine security/startup infrastructure, a department-owned operational definition, or an Administrator-published shared definition. Do not add shared operational forms to a code whitelist.
2. Put every HTTP operation behind `IFormAccessAuthorizer` with the correct operation.
3. Add row-level department filtering to each query, insert, update, delete, import, option, export, and bulk endpoint.
4. For ORM records, expose the trusted physical `department_id` column in the Data Model; for API records, use `IDepartmentScopeResolver` explicitly.
5. Grant creator ACL when an endpoint creates or copies a form, and preserve/resolve department ownership on the server.
6. Use `formAccess[formId].canRead` or `canCreateForms` for navigation. Do not infer authorization from role names in Vue.
7. Keep individual and batch ACL evaluation equivalent.
8. Verify that explicit deny wins; no-department users can receive only shared-form ACL capabilities while department-scoped data still fails closed; parent departments see active descendants; siblings are isolated; and forged keys cannot cross the row boundary.
Seeded operational system forms are recreated when missing on a later application start. A valid `Delete` capability therefore controls the delete request, while the initializer's deterministic seed lifecycle may restore a required first-party definition after restart. Initializers must still preserve later authorized Designer changes and use narrow, idempotent upgrades.
Code map
| Area | Main source |
|---|---|
| ACL rules and effective evaluation | `src/FormPlatform.Host/Data/FormAccessControl.cs` |
| Infrastructure exemption classification | `src/FormPlatform.Host/Data/SystemManagementGridService.cs` |
| SDK module authorization | `src/FormPlatform.Sdk/Forms/FormAuthorization.cs` |
| Automatic ORM scope | `src/FormPlatform.Sdk/DataAccess/Forms/DepartmentScopeConvention.cs`, `FormEntityDataService.cs` |
| Viewer, Designer, data, and submission routes | `src/FormPlatform.Host/Hosting/Endpoints/FormEndpoints.cs` |
| Survey management and row scope | `src/FormPlatform.Host/Hosting/Endpoints/SurveyAdministrationEndpoints.cs`, `src/FormPlatform.Host/Data/SurveyApplicationService.cs` |
| Functional URL dispatch | `src/FormPlatform.Host/Hosting/Endpoints/SystemDataEndpoints.cs`, `src/FormPlatform.Host/Data/FormSubmissionDispatcher.cs` |
| Effective client capabilities | `src/FormPlatform.Host/Hosting/Endpoints/AuthenticationEndpoints.cs` |
| Navigation migration | `src/FormPlatform.Host/Data/SystemLayoutForms.cs`, `SurveySystemForms.cs`, `ModuleStarterSystemForm.cs` |
See also Department multitenancy.