Access and sign-in
Who may do what is written on the entity's meta: line and enforced in the regenerated core
service - not in the controller you own - so a change in the DSL reaches every entry point on the
next build. This needs the 3.0 rule engine with a bundle that has jpa-service-access: every v3
bundle (DDD, hexagonal, -Pv3, Kotlin). The 2.x generator does not process these annotations.
@access and @owner
PurchaseOrder:
meta: entity @root @expose @owner(customerId)
@access(read=owner, create=user, cancel=owner) # ship and delete not written -> admin
| Level | Who |
|---|---|
public | anyone, signed in or not |
user | a signed-in user |
owner | the row's owner (the @owner field equals the signed-in id) and the administrator |
admin | the administrator - role ADMIN, changed with config: adminRole: in env.yml |
none | nobody (the application itself still may) |
Operations: read (read and search), create, update, delete, and the command names (ship,
cancel ...). An operation not written is admin's - *=user sets another default. Without
@access nothing is checked.
| Situation | Answer |
|---|---|
not signed in, and the level is user, owner or admin | 401 - title Sign in first, detail Sign in to <operation> <Entity> (e.g. Sign in to create PurchaseOrder) |
| signed in, not allowed | 403 |
owner: reading, updating, deleting or a command on someone else's row | 404 - its existence is not revealed |
owner: a search | the owner field's equality is added - only your own rows |
create | the owner field is set to the signed-in id, whatever the request says (an administrator's value is kept) |
update | the owner field and @access(admin) fields keep the row's current values |
owner: findAll, delete or update by condition | administrator only |
no one signed in (outside a request, @Async, ...) | a visitor - fail-closed |
inside CurrentUser.runAsSystem(() -> ...) | the application itself - everything allowed |
Another entity's whole row (a nested DTO) is carried in a DTO only when that entity's read level
is public; otherwise the build leaves the field out with a warning (its id stays).
A field only the administrator writes
Customer:
meta: entity @root @editable @expose @owner(customerId) @access(read=owner, create=public, update=owner)
fields:
role: UserRole @default(UserRole.USER) @access(admin) # a member cannot make themselves an administrator
What anyone else sends is replaced - by the entity's default on create, by the row's value on update.
@password and @login
login: string(10)! @login @unique
password: string(255)! @password # long enough for the hash (PBKDF2, about 100 characters)
- The password is stored only as a hash on every write path (PBKDF2-SHA256 by default; a
PasswordHasherbean replaces it). It never appears in answers, searches or DTO copies, and is optional in TypeScript. - With
@loginand@password, the core service and the use case getauthenticate(login, password): the row, or empty when the login is missing, matches two rows, or the password is wrong. A missing login costs the same hashing time, so the answer time does not reveal accounts. - The login must be unique: without
@signin, a login that is not@unique(nor the key, nor a one-column unique@Index) is a DSL warning.
@signin - the sign-in API, generated
Customer:
meta: entity @root @editable @expose(20) @signin @owner(customerId) @access(read=owner, create=public, update=owner)
fields:
customerId: long @id @sequence
login: string(10)! @search(eq) @login
password: string(255)! @password
role: UserRole @default(UserRole.USER) @access(admin)
| Request | Answer |
|---|---|
POST /api/shop/customer/signin {"login": "...", "password": "..."} (the keys are the two fields' names) | 200 the customer (no password), a session starts; wrong: 401 Wrong login or password |
POST /api/shop/customer/signout | 204, the session ends |
GET /api/shop/customer/me | 200 the signed-in customer; 401 for a visitor |
The core service and use case get me(); the TypeScript client signIn(login, password), signOut()
and me() (null for a visitor). It is a cookie session, so the screens and the API share an origin.
The @login column becomes unique = true - a taken login answers 409.
The session holds the row's key and its role. The role is the @role field, otherwise a field
named role, and only when it is an enum or string that only the administrator writes
(@access(admin)); an enum gives its constant name (ADMIN), compared with adminRole. Otherwise a
member could send "role":"ADMIN" and sign in as an administrator, so the build warns and signs in
without a role.
@signin is generated only when it is safe - otherwise a DSL warning says why and how to fix it:
- Only the member themselves and the administrator may change the row:
update=ownerwith an@ownerthat is the key, orupdate=admin/none. (Without@accessthe update level ispublic, and anyone could change the administrator's password.) - If the key is one the request chooses (not
@genid/@sequence), deleting it is limited the same way - otherwise someone could deletealiceand sign up again asalice. @expose, one key,@loginand@password.- One
@signinentity per application - the session holds a bare key, so two would read each other's rows. Multi-module builds count per module that gets the controller.
A sign-in rule of your own (a locked account, attempts) overrides authenticate(login, password) in
the owned service - the generated signin calls it. The controller is yours: an edited controller
does not receive the endpoints, and the build says which are missing.
Signing in by hand
For a flow of your own - sign up and be signed in at once:
SessionCurrentUser.signIn(request, customer.getCustomerId(), customer.getRole().name());
SessionCurrentUser.signOut(request);
With Spring Security, register an io.elasticore.springboot3.security.CurrentUser bean instead and do
not use @signin (authenticate stays).
Runtime classes
CurrentUser - id(), signedIn(), hasRole("ADMIN") | who is asking, as the generated services see it (default SessionCurrentUser) |
CurrentUser.runAsSystem(() -> ...) | the application itself: start-up data, nightly jobs |
ACCESS.require(currentUser, "report") | the entity's @access for an operation of your own (fields of the generated service) |
AccessPolicy, PasswordHasher | replaceable beans |
NotSignedInException (401), ForbiddenException (403) | io.elasticore.springboot3.security |
Generated service tests (testCode: true) run entities with access rules as the application itself
(RunAsSystemExtension); a test of the rules themselves signs in as a user.