Skip to main content

User Management

This page describes how Artemis accounts come into existence, how the activated flag governs whether they may authenticate, and how to configure self-registration.

Account types

Every account is either internal or external, recorded in the is_internal column:

Internal accountExternal account
PasswordHashed and stored by ArtemisNot stored; the identity provider verifies it
Authenticated byArtemisInternalAuthenticationProviderLdapAuthenticationProvider, SAML2Service, or OIDCService
Password resetThrough ArtemisThrough the institution's own account management
Typical useAdmins, service accounts, guests, instances without an IdPStudents and staff of the operating institution

An account can be switched between the two through the admin user form. Turning an external account internal requires a password, which Artemis generates when the form leaves it blank.

Where accounts come from

Several paths create accounts. The important column is the last one — see The activated flag for why.

Provisioning pathAccount typeCreated activated?
Self-registration, POST /registerInternalNo — awaits activation by the user
First login through LDAPExternalYes
First login through SAML2ExternalYes
First login through OIDCExternalYes
Course member import, exam registration, admin user import (resolved against LDAP)ExternalYes
LTI launchInternalNo — activated when the launch hands over the generated password
Admin Create User formAs selectedYes
internal-admin bootstrap on startupInternalYes
Iris bot accountInternalYes

Only the first row leaves the account unactivated, and only that row is a user activating their own account. Every other path provisions the account on someone's behalf, so it must be usable immediately.

note

Course and exam import and first login can both create the same person's account, whichever happens first. Both must therefore produce an account in the same state — an imported student who has never logged in has to be able to use their repositories straight away.

An LTI launch creates the account unactivated and activates it when it hands the account holder the Artemis password generated for them, so they can also sign in outside the LMS.

Importing users

Instructors add course members and exam participants from a list of identifiers, and administrators can import users the same way. Each row may carry any combination of login, email and registration number.

Resolution happens in two stages. The Artemis database is searched first and completely; only if nothing matches is the configured LDAP consulted, and an account found there is created in Artemis. Within each stage the identifiers are tried in the order login, then email, then registration number, stopping at the first match. Blank identifiers are skipped, and a row with all three blank is reported as not found.

OutcomeWhat happens
Found in the Artemis databaseThe existing account is used as it is
Found only in the directoryAn external, activated account is created and used
Found nowhereThe row is returned to the caller as not found and no account is created

Accounts created this way are activated, exactly like accounts created on first login. This matters: an imported student who has not signed in yet must be able to clone and push straight away.

An existing account is otherwise left untouched by an import. The one exception is the case where the searched identifier matched nothing locally but the directory resolved it to a login that does already exist — for instance an import by registration number for someone whose account predates that number being recorded. Only then are the account's name, email and registration number synced from the directory.

tip

If an import reports users as not found, check that artemis.user-management.ldap.enabled is true and that the identifiers match what the directory holds. A registration number that does not exist in the directory cannot be resolved, since Artemis has nothing else to look the person up by.

The activated flag

activated decides whether an account may authenticate at all. It is enforced by:

  • every authentication provider: ArtemisInternalAuthenticationProvider, LdapAuthenticationProvider, SAML2Service, OIDCService, and the passkey provider
  • git over HTTPS (LocalVCServletService) and git over SSH (GitPublickeyAuthenticatorService)

An account is created unactivated only when its own owner is expected to activate it, which means the account must be internal. The activation key is redeemable only through GET /activate, so an externally managed account — which authenticates against the external directory — is never sent an activation mail and cannot redeem a key. Creating one unactivated therefore yields an account that nothing can ever activate. Historically such an account could still pass web login on an LDAP-only instance, because the LDAP provider was the one provider that did not consult the flag while git and every other provider refused it. That gap is now closed, so an account in this state cannot authenticate at all.

note

GET /activate and the mail carrying the key are both gated behind artemis.user-management.registration.enabled, so on an instance with self-registration disabled even an internal account has no way to redeem a key. Account creation is deliberately not narrowed to match, and checks only whether the account is internal: the LTI launch is the other path that creates an internal account through this factory. The launch discards the key immediately, because it never sends the mail that carries one, and records whether it still owes that account the password dialog in UserLti.initialized rather than in activated — see LTI account provisioning below.

Consequently there are exactly three reasons for activated = false:

ReasonSet byActivation key
Awaiting activationUserCreationService.createUser for an internal account, UserService.registerUser (always internal)Present
Deliberately deactivatedUserCreationService.deactivateUser, admin edit form — any account typeAbsent
Anonymized by an older versionUserService.anonymizeUser, together with is_deleted; no longer written, see Deleting User AccountsAbsent

The activation key therefore distinguishes an account still waiting to be activated from one an administrator switched off on purpose. That distinction matters when repairing data: activating rows that carry a key never re-enables an account somebody deactivated deliberately.

Activation and cleanup

A self-registered account is mailed a link containing its activation key. Redeeming it through GET /activate sets activated = true and clears the key. Every other write that activates an account clears it too — the administrative activate action, and the LTI launch both when it creates the account and when it hands over the generated password — so an activated account never carries one.

An account that is not activated within registration.cleanup-time-minutes (default 60) is deleted. The removal is scheduled when the account is registered and cancelled when it is activated, so the timer applies only to self-registration — no other provisioning path schedules it.

Deactivation versus deletion

  • Deactivation keeps the account and its data but refuses authentication. It also revokes the account's credentials — personal and participation VCS access tokens, SSH keys, and passkeys — because the git paths accept a token or key on its own, so deactivation would otherwise not take effect there until those credentials expired. Both routes that deactivate an account — the deactivate endpoint and the admin edit form — write a DEACTIVATE_USER entry to the audit log naming the administrator who did it and the account affected; activation is recorded as ACTIVATE_USER the same way. These are kept far longer than login records. Any activation or password-reset key still outstanding is discarded as well, so a link mailed before the deactivation cannot be used to get back in.
  • Nothing lets an account holder reverse a deactivation, through an authentication provider or otherwise. Only an administrator can activate an account again.
  • Permanent deletion removes the account row together with the data that belongs to it, after an administrator has reviewed an impact preview and confirmed it, or after the retention cleanup has selected the account and every business-domain reference to it is gone. It cannot be undone. See Deleting User Accounts for the dialog, the accounts Artemis refuses to delete, and the automatic paths.

LTI account provisioning

An LTI launch for a user Artemis does not know creates an internal account and shows that user a dialog with a generated password, which is what they authenticate with afterwards. Two things follow from that:

  • The account is created with an activation key like any internal account, and the launch discards it immediately. No activation mail is sent for such an account, so a key left in place would be a link nobody ever received that still activates the account.
  • Whether the password dialog is still owed is recorded by the lti module itself, in user_lti.initialized, and never inferred from activated.

That second point matters because activated would otherwise answer two unrelated questions with the same value. To the launch, activated = false used to mean "this account still has to be initialised"; to an administrator it means "this account is disabled". An account that an administrator had deactivated was therefore indistinguishable from one that had never been initialised, and PUT api/account/users/initialize activated it again and handed out a working password. Reaching that endpoint needs only an authenticated session, and one issued before the deactivation keeps working.

With a separate marker, initialisation happens at most once per account: the marker is claimed in a single conditional statement, so a second — or concurrent — call finds nothing to claim, and a deactivated account was initialised earlier and so carries the marker already. It can also happen zero times, which is the point: an account deactivated before it ever completed a launch is refused rather than handed a password.

note

The launch itself does not check account state. With artemis.lti.trust-external-lti-systems enabled, a deactivated account relaunching from the LMS is still issued a session, even though initialisation now refuses it. Enforcing account state on the launch is tracked separately.

Enabling self-registration

Self-registration is disabled unless artemis.user-management.registration.enabled is explicitly true; a missing section counts as disabled. Registered accounts are always internal.

artemis:
user-management:
registration:
enabled: true
allowed-email-pattern: '[a-zA-Z0-9_\-\.\+]+@(tum\.de|in\.tum\.de|mytum\.de)'
allowed-email-pattern-readable: '@tum.de, @in.tum.de, @mytum.de'
cleanup-time-minutes: 60 # non-activated accounts are deleted after this many minutes
spring:
mail:
host: <host>
port: 25
username: <username>
password: <password>
protocol: smtp
tls: true
properties.mail.smtp:
auth: true
starttls:
enable: true
ssl:
trust: <host>
jhipster:
mail:
base-url: https://artemis.tum.de
from: artemis@xcit.tum.de
management:
health:
mail:
enabled: true

Users can then register on the start page with any address matching allowed-email-pattern. If no pattern is configured, any address is accepted. A working mail server is required: without it the activation mail is never delivered and the account is deleted once the cleanup time elapses.

Each of the external mechanisms is enabled by its own property. ldap.enabled and saml2.enabled have no default — Artemis refuses to start if they are absent — while oidc.enabled defaults to false.

artemis:
user-management:
ldap:
enabled: true # required; LDAP lookups for user details and LDAP authentication
url: <url>
user-dn: <user-dn>
password: <password>
base: <base>
allowed-username-pattern: '^([a-z]{2}\d{2}[a-z]{3})$'
saml2:
enabled: false # required
oidc:
enabled: false # optional, defaults to false
use-external: true # advertises external password reset to the client; does NOT enable or disable LDAP
warning

use-external does not switch LDAP authentication on or off, despite its name. It is only reported through /management/info so the client knows whether to send users to an external password reset. LDAP is controlled exclusively by artemis.user-management.ldap.enabled.

See SAML2 Login and Registration and OIDC Login and Registration for the provider-specific setup.

Login form and rate limits

The login form asks for an identifier first and then calls GET /login-options to decide whether to show a password field or send the user to the configured identity provider. The answer is derived from local account state only: an internal account gets the password form, and everything else — an external account, and an identifier this instance has not seen — is sent to the identity provider, which is also where a first-time user is provisioned.

The account endpoints are rate limited in two independent places, and both are needed: nginx keys on the real TCP peer, while the application-level limiter runs per Artemis node and covers deployments that do not terminate at this nginx configuration.

Endpointnginx zoneApplication limit
POST /authenticate, /login/webauthnloginlimit, 30 r/mAUTHENTICATION, 30 rpm
POST /saml2loginlimit, 30 r/mnone — the SAML2 callback exchanges an already established assertion
GET /login-optionsloginoptionslimit, 30 r/mLOGIN_OPTIONS, 30 rpm
/register, /activate, /account/reset-password/*accountrecoverylimit, 5 r/mACCOUNT_MANAGEMENT, 5 rpm
git credential handshake (/info/refs)gitauthlimit, 60 r/mAUTHENTICATION, 30 rpm

Application-level limits can be overridden per type under artemis.rate-limiting, for example artemis.rate-limiting.login-options-requests-per-minute. Rate limiting as a whole is off unless artemis.rate-limiting.enabled is true.

If you run your own reverse proxy instead of the shipped nginx configuration, mirror the zones above; otherwise only the application-level limits apply — and the SAML2 callback, which has none of its own, is then unbounded.

Audit log

Deliberate changes to an account are recorded in the audit log, which keeps them far longer than login records. The entries relevant to account state:

Event typeRecorded whenPrincipalData
ACTIVATE_USERAn account is activated, by an administrator or by its owner redeeming an activation keyWhoever performed it, or system if unauthenticateduser=<login>
DEACTIVATE_USERAn account is deactivated, through either the deactivate action or the admin edit formThe acting administratoruser=<login>
ADMIN_CHANGE_USER_PASSWORDAn administrator sets a user's passwordThe acting administratorWhich credential types were revoked

All three are classified as security events, so they are written to security_audit_event and appear under the Security tab of Server Administration → Audits in the admin UI, or through GET /api/admin/audits. DEACTIVATE_USER in particular is the only audit record of a wholesale credential revocation, since the revocation itself writes none. Retention is configured separately per log; see Cleanup Service.

Troubleshooting

A user can sign in to the website but git authentication fails. Their account is not activated. This combination is only possible on a version where the LDAP provider did not yet consult the flag, since it now does; on such a version web login succeeded while both git paths refused. Check activated for the account and activate it if it was never meant to be off.

A user reports they cannot sign in at all after being deactivated. That is the intended effect: deactivation refuses authentication through every provider and revokes the account's tokens, SSH keys and passkeys. Only an administrator can activate the account again, and the revoked credentials have to be created anew afterwards.

A newly registered account disappeared. An account that is not activated within registration.cleanup-time-minutes is deleted. Usually the activation mail was never delivered — verify the mail configuration and that management.health.mail.enabled reports healthy.

An imported student has no registration number. Only accounts resolved through the directory carry one. An account created on first login through SAML2 or OIDC has one only if the provider supplies it through the configured registration-number attribute.

Search documentation