Skip to main content
ElastiCORE 3.0 is a release candidate: 3.0.261022-rc1 on Maven Central. Projects on the 2.2 line: see the 2.2 documentation.
Version: 3.0

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
LevelWho
publicanyone, signed in or not
usera signed-in user
ownerthe row's owner (the @owner field equals the signed-in id) and the administrator
adminthe administrator - role ADMIN, changed with config: adminRole: in env.yml
nonenobody (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.

SituationAnswer
not signed in, and the level is user, owner or admin401 - title Sign in first, detail Sign in to <operation> <Entity> (e.g. Sign in to create PurchaseOrder)
signed in, not allowed403
owner: reading, updating, deleting or a command on someone else's row404 - its existence is not revealed
owner: a searchthe owner field's equality is added - only your own rows
createthe owner field is set to the signed-in id, whatever the request says (an administrator's value is kept)
updatethe owner field and @access(admin) fields keep the row's current values
owner: findAll, delete or update by conditionadministrator 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 PasswordHasher bean replaces it). It never appears in answers, searches or DTO copies, and is optional in TypeScript.
  • With @login and @password, the core service and the use case get authenticate(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)
RequestAnswer
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/signout204, the session ends
GET /api/shop/customer/me200 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=owner with an @owner that is the key, or update=admin / none. (Without @access the update level is public, 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 delete alice and sign up again as alice.
  • @expose, one key, @login and @password.
  • One @signin entity 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, PasswordHasherreplaceable 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.