Identity and access
Who gets in, and what they can do once they are. Cascade ships all of this to everyone. Nothing here is a paid tier and there is no license check behind it.
Every endpoint below is in the OpenAPI document at /api/v1/openapi.json, and
every one has a test that calls it.
Custom roles
Section titled “Custom roles”In the editor: Settings, Roles. Everyone reads the roles; an owner or admin makes, changes and deletes custom ones, and gives them to people in a project from the same screen.
A project has four built in roles: admin, editor, operator and viewer. They live in code and they never change. When none of them is the right size, make a custom role: a name and a list of project permissions.
A line lead who can start the line’s workflows and read their runs, and can’t edit anything, is the usual reason:
curl -X POST $CASCADE/api/v1/roles -H "Authorization: Bearer $KEY" \ -d '{"name":"line-lead","description":"runs the line","permissions":["project.read","workflow.read","workflow.execute","execution.read"]}'Then put someone in a project as that role, the same call as for a built in one:
curl -X PUT $CASCADE/api/v1/projects/$PROJECT/members/$USER -d '{"role":"line-lead"}'What to know:
GET /api/v1/permissionslists every permission a role can carry, andGET /api/v1/roleslists the four built in roles and the custom ones with what each can do. Anyone signed in can read both, because a project admin needs them to pick a role.- Making, changing and deleting a custom role is an instance owner or admin
thing, the
role.managepermission. A project admin can hand a role to people in their project and can’t write a new one, so nobody grants themselves more than they were given. - A role carries project permissions only. It can’t carry
user.manage,audit.reador any other instance permission, and asking for one is a 400. - A name is 2 to 40 lowercase letters, digits and dashes, starting with a letter. It can’t be changed afterward, because members and service accounts hold a role by its name. The four built in names are taken.
- Changing a role’s permissions reaches everyone who holds it on their next request. Nothing is cached, so there is no window where a removed permission still works.
- A service account key can hold a custom role as its project role, and an API key’s scopes still cut it down.
- Deleting a role somebody holds is refused with 409, naming how many members and service account keys hold it. Move them first.
- Role changes are in the audit log as
role.created,role.updated(with the permissions before and after) androle.deleted.
LDAP and Active Directory
Section titled “LDAP and Active Directory”People sign in with the name and password they already have at the plant:
OpenLDAP, 389 Directory Server, FreeIPA, or Active Directory. It sits next to
the local accounts and next to OIDC, and it is off until you set
HOTLOOP_CASCADE_LDAP_URL.
How a sign-in goes:
- A service account (or an anonymous search, if your directory allows it) finds the person with the user filter.
- It looks up the groups they are in.
- The directory itself checks the password, by binding as the person. Cascade
never stores that password and never sees a hash of it. The account made here
has no password of its own, so the directory password can’t be used on
/loginas a local one. - The account is made on first sign-in (unless you turn that off), and their role follows their groups at every sign-in after.
The sign-in page shows a second form when it is on, and the same thing is one
call, POST /auth/ldap/login with {"username": "...", "password": "..."}. A
wrong name and a wrong password get the same answer. An empty password never
reaches the directory, because most directories answer an empty-password bind as
success.
Settings
Section titled “Settings”All of them are environment variables. With the Helm chart, put them in env,
and the bind password in extraEnv from a secret.
| Variable | What it does |
|---|---|
HOTLOOP_CASCADE_LDAP_URL |
ldap://host:389 or ldaps://host:636. Turns it on. |
HOTLOOP_CASCADE_LDAP_FLAVOR |
ldap (default) or ad. It only picks the defaults below. |
HOTLOOP_CASCADE_LDAP_START_TLS |
true upgrades an ldap:// connection before anything is sent. |
HOTLOOP_CASCADE_LDAP_CA_FILE |
PEM file of the authorities to trust for the directory’s certificate. Empty means the system’s. |
HOTLOOP_CASCADE_LDAP_TLS_SERVER_NAME |
The name the certificate has to be for, when it isn’t the host in the URL. |
HOTLOOP_CASCADE_LDAP_BIND_DN |
The service account that looks people up. Empty is an anonymous search. |
HOTLOOP_CASCADE_LDAP_BIND_PASSWORD or _FILE |
Its password, or a file holding it. Not both. A DN with no password is refused, since that would be an anonymous bind. |
HOTLOOP_CASCADE_LDAP_USER_BASE |
Required. The DN people are searched for under. |
HOTLOOP_CASCADE_LDAP_USER_FILTER |
Finds one person. {username} is what they typed, escaped. |
HOTLOOP_CASCADE_LDAP_GROUP_BASE |
The DN groups are searched for under. Empty means no group lookup. |
HOTLOOP_CASCADE_LDAP_GROUP_FILTER |
Finds the groups a person is in. {dn} is their DN, {username} what they typed. |
HOTLOOP_CASCADE_LDAP_ID_ATTR, _EMAIL_ATTR, _NAME_ATTR |
The attributes for the stable id, the email and the display name. |
HOTLOOP_CASCADE_LDAP_ROLES |
group=role pairs, comma separated: supervisors=admin, operators=member. A group is named by its cn or by its full DN. The most powerful role among a person’s groups wins. |
HOTLOOP_CASCADE_LDAP_DEFAULT_ROLE |
member, admin or none. With a role mapping the default is none, so someone in no mapped group is turned away. |
HOTLOOP_CASCADE_LDAP_JIT |
false makes accounts by hand only. Default true. |
HOTLOOP_CASCADE_LDAP_NAME |
What the sign-in button calls it. |
The defaults for ldap: user filter (&(objectClass=inetOrgPerson)(uid={username})),
group filter (&(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)(objectClass=group))(member={dn})),
id entryUUID, email mail, name cn.
The defaults for ad: a user is found by sAMAccountName or by user principal
name, so alice and alice@plant.example both work, and a disabled account is
left out of the search. Groups use the nested-membership rule
(member:1.2.840.113556.1.4.1941:={dn}), so someone in a group that is inside a
mapped group gets the role too. The id is objectGUID, written as a GUID, email
mail, name displayName.
Things to know
Section titled “Things to know”- Active Directory wants encryption. It refuses a simple bind on a plain
connection. Use
ldaps://, orldap://withSTART_TLS=true. Cascade checks the certificate against the CA file (or the system roots) and against the server name, and there is no setting that skips the check. A directory that signs its own certificate needs its CA inCA_FILE. - The account is tied to the directory’s id, not the name. Rename someone in
the directory, or move them to another OU, and they are the same account
here. Someone with no
mailcan’t be given an account (the sign-in says so). - The directory is the authority for the address. An account that already exists here with the same email is linked to the directory entry the first time that person signs in, with no extra verification, the way a verified OIDC address is. Don’t point this at a directory you don’t run.
- The first account an instance gets is its owner, however it is made. Make the owner with setup first.
- A second factor set up here still counts. A directory password is one factor. Someone with an authenticator app or a passkey on their account is asked for it after, and instance-wide MFA enforcement applies as it does to everyone.
- Wrong passwords lock the account here too. Five in a row on an account
that is already tied to the directory locks it for a minute, doubling, the same
as a local one, before the directory’s own lockout has to. A person the
directory doesn’t know, a wrong password and a locked account are all in the
audit log as
auth.login_failedorauth.login_refused, and a person the directory let through that Cascade turned away (no group, no email, JIT off) isauth.sso_refused. - A directory that is down is a server error, not a wrong password. The sign-in says the directory didn’t answer, and the detail is in the log.
- Every call to the directory is limited to fifteen seconds, and only one person may match the user filter: two matches is a refusal naming the problem, never the first one.
Single sign-on through any SAML 2.0 identity provider. Cascade is the service provider. It sits next to local accounts, OIDC and LDAP, and it is off until you name the provider’s metadata.
The steps:
- Cascade publishes its own metadata at
/auth/saml/metadata. Give that to the provider, or enter the two values it holds: the entity id, and the assertion consumer service,<public url>/auth/saml/acs, HTTP-POST binding. - Set
HOTLOOP_CASCADE_SAML_IDP_METADATA_URLto the provider’s metadata address (something likehttps://sso.plant.example/saml/metadata), orHOTLOOP_CASCADE_SAML_IDP_METADATA_FILEto the same document saved to disk. - The sign-in page shows a “Sign in with” button. It goes to
/auth/saml/start.
What it checks on the way back: the response’s signature against the provider’s certificate in its metadata, the audience, the time window, that it answers a request this browser started, and that the sign-in was started in this browser. A response nobody asked for (a sign-in started at the provider) is refused. Each sign-in’s state works once, so a captured response can’t be played again.
| Variable | What it does |
|---|---|
HOTLOOP_CASCADE_SAML_IDP_METADATA_URL or _FILE |
The provider’s metadata. One of them turns SAML on; not both. |
HOTLOOP_CASCADE_SAML_ENTITY_ID |
How this instance names itself to the provider. Default: its metadata URL. |
HOTLOOP_CASCADE_SAML_KEY_FILE and _CERT_FILE |
This instance’s key pair, PEM, RSA or EC. Both or neither. Default: a pair made on first start as saml-sp.key and saml-sp.crt in the data directory, and kept. |
HOTLOOP_CASCADE_SAML_SIGN_REQUESTS |
true signs every request Cascade sends. The provider has to be told the certificate. Default false. |
HOTLOOP_CASCADE_SAML_NAMEID_FORMAT |
What Cascade asks the provider to identify people by: persistent (default), email or unspecified. |
HOTLOOP_CASCADE_SAML_SUBJECT_ATTR |
An attribute that identifies the person instead of the NameID. |
HOTLOOP_CASCADE_SAML_EMAIL_ATTR, _NAME_ATTR, _GROUPS_ATTR |
Which attributes hold the email, the display name and the groups. |
HOTLOOP_CASCADE_SAML_ROLES, _DEFAULT_ROLE, _JIT, _NAME |
The same as for LDAP: group to role pairs, the role for anyone in none, whether an account is made on first sign-in, and the button’s name. |
Without the attribute settings Cascade looks for the common names. The email is
email, mail, emailAddress, the LDAP object id urn:oid:0.9.2342.19200300.100.1.3
or the claim URI .../claims/emailaddress, and falls back to the NameID when
that is an address. The name is displayName, name, or the first and last
name. Groups are groups, group, memberOf, the .../claims/Group URI and
the Microsoft groups claim.
Things to know:
- Ask for a persistent id. An account is tied to the NameID, and a transient
one is different at every sign-in. Cascade asks for a persistent one and
refuses a transient one with a message saying so, rather than making a new
account at every sign-in. If your provider can only send a transient id, name
an attribute that is stable in
SUBJECT_ATTR. - The provider is the authority for the address. As with LDAP, an existing account with the same email is linked on the first SAML sign-in.
- Behind a proxy, set
HOTLOOP_CASCADE_PUBLIC_URLto the address people use. The provider posts to the assertion consumer URL built from it. - Use HTTPS. The provider answers with a POST from another site, so the cookie that ties a sign-in to its browser has to be sent cross-site, which browsers allow only on a secure connection.
- More than one pod needs the same key pair on all of them: put it in a
secret and name it with
KEY_FILEandCERT_FILE, or each pod makes its own. - A second factor set up here still counts after a SAML sign-in, and SAML never makes the provider’s own multi-factor count for it.
- Single logout isn’t supported. Signing out ends the session here.
In the editor: Settings, Sign-in and SCIM shows whether SCIM is on, the base URL to give the provider, and the tokens, with a button to make one (shown once) and to revoke one. LDAP, SAML and OpenID Connect are set in the instance’s environment when it starts, so the same screen shows which are on and where a provider points (the SAML metadata and ACS URLs), and doesn’t pretend to change them.
SCIM is how an identity provider keeps accounts in step with its directory: it
creates a person when they join, changes them when they move, switches them off
when they leave, and keeps groups. Cascade is the SCIM server (RFC 7643 and
7644, version 2.0), and the provider is the client. It is built to what the RFCs
say and to what the common identity providers send, and it is tried in every release against
scim2-tester, a SCIM client from another team that runs every check it has.
It’s off until you set HOTLOOP_CASCADE_SCIM=true.
Set it up:
- Turn it on with
HOTLOOP_CASCADE_SCIM=trueand restart. - Make a token for the provider, as an instance owner or admin:
POST /api/v1/scim-tokenswith{"name": "plant directory"}. Thesecretin the answer (it startshlcs_) is shown once, so put it in the provider straight away.GET /api/v1/scim-tokenslists them with when each was last used, andDELETE /api/v1/scim-tokens/{token}revokes one. A person has to be signed in to make a token: an API key can’t, so a leaked key can’t mint more. - Give the provider the base URL,
<public url>/scim/v2, and the token as a bearer token.
Every call needs the token, and every answer is application/scim+json. Calls
are in the audit log with the token’s name as the actor, so user.created,
user.disabled and the rest say which provider did it, along with the SCIM
events scim.user_deleted, scim.group_created, scim.group_updated and
scim.group_deleted.
What it serves
Section titled “What it serves”/ServiceProviderConfig, /ResourceTypes and /Schemas say what is supported,
and they say only what is true: filtering, PATCH, no bulk, no sorting, no ETags,
no password changes.
| Resource | Calls |
|---|---|
/Users |
GET (list, with filter, startIndex, count, attributes, excludedAttributes), POST, GET /{id}, PUT /{id}, PATCH /{id}, DELETE /{id} |
/Groups |
the same |
/.search, /Users/.search, /Groups/.search |
POST with the query in the body |
- A user is
userName,externalId,name(formatted,givenName,familyName),displayName,emailsandactive. The account’s email is the primary one, else the work one, else the first, elseuserNamewhen that is an address. Anything without an address is a 400. What the provider sends is returned as it sent it: a name it didn’t send isn’t made up.groupscomes back read-only. - A group is
displayName,externalIdandmembers, each member the id of a user. A member that isn’t a user is a 400. - Filters are the full syntax:
eq ne co sw ew gt ge lt le pr,and,or,not, parentheses andattr[filter]. Matching ignores case except foridandexternalId. A bad filter is a 400 withscimType: invalidFilter. - PATCH takes
add,replaceandremovewith or without a path, with value paths likeemails[type eq "work"].valueandmembers[value eq "id"], in any capitalization, and a boolean astrueor as the string"False"that some providers send. It changes exactly what a PUT could change. - Lists are in the order accounts were made, 100 to a page unless
countsays, 200 at most.
What it won’t do
Section titled “What it won’t do”- It never shows or changes the owner. The instance owner isn’t in the list, can’t be fetched, patched or deleted, and can’t be put in a group. An email that belongs to the owner is a conflict.
- It never deletes an account.
DELETEswitches the person off, which ends their sessions, takes them out of their groups and hides them from SCIM. Everything they made and every audit row stays. If the provider creates them again with the same address, the same account comes back, switched on.active: falsedoes the switching off without hiding them. - A SCIM account has no password. It signs in through OIDC, SAML or LDAP, which link to it by the address.
- It doesn’t touch projects. SCIM works with accounts and instance roles. Who is in which project is still decided here.
Roles from groups
Section titled “Roles from groups”By default a new account is a member and SCIM never changes anyone’s role. To make groups decide it, map group names to roles:
HOTLOOP_CASCADE_SCIM_ROLES="Plant Admins=admin, Plant Members=member"HOTLOOP_CASCADE_SCIM_DEFAULT_ROLE=memberA person’s role then follows their groups every time a membership changes: the
most powerful mapped role wins, and someone in none gets the default. Only
admin and member can be mapped, because a provider never makes an owner.
Use one source of roles. If OIDC or SAML also maps groups to roles, whichever
last changed a person’s role wins, and the two will fight.
The provider needs the same group names, and has to push groups with their members, which every provider that does SCIM groups can.