The Waldur Site Agent provides robust support for managing offering users with asynchronous username generation and state management. This system enables non-blocking user processing and supports complex username generation scenarios through a pluggable backend architecture.
Offering users represent the relationship between Waldur users and marketplace offerings. The agent handles username generation, state transitions, and integration with backend systems to ensure users can access provisioned resources.
The async user creation follows a state-based workflow that prevents blocking operations:
stateDiagram-v2
[*] --> REQUESTED : User requests access
REQUESTED --> CREATING : begin_creating
CREATING --> OK : Username set (auto-transition)
CREATING --> PENDING_ACCOUNT_LINKING : Linking required
CREATING --> PENDING_ADDITIONAL_VALIDATION : Validation needed
CREATING --> ERROR_CREATING : BackendError
ERROR_CREATING --> CREATING : begin_creating (retry)
ERROR_CREATING --> PENDING_ACCOUNT_LINKING : Linking required
ERROR_CREATING --> PENDING_ADDITIONAL_VALIDATION : Validation needed
PENDING_ACCOUNT_LINKING --> OK : set_validation_complete
PENDING_ADDITIONAL_VALIDATION --> OK : set_validation_complete
PENDING_ACCOUNT_LINKING --> PENDING_ADDITIONAL_VALIDATION : Cross-transition
PENDING_ADDITIONAL_VALIDATION --> PENDING_ACCOUNT_LINKING : Cross-transition
OK --> [*] : User ready for resource access
sync_offering_users()Entry point function that processes all offering users across configured offerings.
Usage:
uv run waldur_sync_offering_users -c config.yaml
Behavior:
update_offering_users()update_offering_users()Core processing function that handles username generation and state transitions.
Process:
_process_requested_users()_process_pending_users()New Architecture: The function has been refactored into focused sub-functions:
_can_generate_usernames(): Policy validation_group_users_by_state(): Efficient user categorization_process_requested_users(): Handle new username requests_process_pending_users(): Process retry scenarios_update_user_username(): Individual user processing_handle_account_linking_error(): Account linking error management_handle_validation_error(): Validation error management_set_error_creating(): Marks user as ERROR_CREATING after backend failuresThe agent uses a pluggable backend architecture for username generation, allowing custom implementations for different identity providers and naming conventions.
The system now includes early validation to skip processing when no valid username management backend is available:
UnknownUsernameManagementBackend is detectedfrom waldur_site_agent.backend.backends import AbstractUsernameManagementBackend
class CustomUsernameBackend(AbstractUsernameManagementBackend):
def generate_username(self, offering_user: OfferingUser) -> str:
"""Generate new username based on offering user details."""
# Custom logic here
return generated_username
def get_username(self, offering_user: OfferingUser) -> Optional[str]:
"""Retrieve existing username from local identity provider."""
# Custom lookup logic here
return existing_username
def get_or_create_username(self, offering_user: OfferingUser) -> Optional[str]:
"""Get existing username or create new one if not found."""
username = self.get_username(offering_user)
if not username:
username = self.generate_username(offering_user)
return username
A backend does not have to mint usernames. If the identity is Waldur’s — the
offering uses a username_generation_policy other than service_provider, and
the backend’s job is to write Waldur’s values into an external system — set
is_username_authoritative = False:
class MirroringBackend(AbstractUsernameManagementBackend):
is_username_authoritative = False
Core then skips username generation for that offering entirely, and in particular
never PATCHes a username back over the authoritative value. Backends that leave
the default (True) are unaffected.
Such a backend does its work in sync_user_profiles(offering_users) instead of
generate_username. That hook is called with the full offering-user list on
every membership cycle, including accounts already in OK — which the
username-generation path never sees, since it only handles accounts still in
REQUESTED or a pending state. It is therefore the right place for a reconcile
loop: create what is missing, update what has drifted, leave the rest alone.
The POSIX identity Waldur holds for an account is available on the offering user
as uidnumber, primarygroup, login_shell and home_directory. These are
always requested by the membership processor, and are not gated by the offering’s
OfferingUserAttributeConfig — they are account attributes, not personal data.
waldur-site-agent-ldap is the reference implementation; see its
account_source: waldur mode.
Register your backend via entry points in pyproject.toml:
[project.entry-points."waldur_site_agent.username_management_backends"]
custom_backend = "my_package.backend:CustomUsernameBackend"
username_management_backend is not properly configuredConfigure username management per offering in your agent configuration:
offerings:
- name: "SLURM Cluster"
waldur_api_url: "https://waldur.example.com/api/"
waldur_api_token: "your-token"
waldur_offering_uuid: "offering-uuid"
backend_type: "slurm"
username_management_backend: "custom_backend" # References entry point name
backend_settings:
# ... other settings
username_generation_policy = SERVICE_PROVIDER in WaldurThe async user creation system is seamlessly integrated with the agent’s order processing workflows:
Username generation is automatically triggered during:
The OfferingBaseProcessor class provides _update_offering_users() method that:
Example usage in order processing:
# Optimized processing with conditional refresh
offering_users = user_context["offering_users"]
# Only refresh if username generation actually occurred
if self._update_offering_users(offering_users):
# Refresh local user_context cache
user_context_new = self._fetch_user_context_for_resource(waldur_resource.uuid.hex)
user_context.update(user_context_new)
# Use only users with valid usernames
valid_usernames = {
user.username for user in user_context["offering_users"]
if user.state == OfferingUserState.OK and user.username
}
Performance Improvements:
The system defines specific exceptions for different error scenarios:
OfferingUserAccountLinkingRequiredError: Raised when manual account linking is requiredOfferingUserAdditionalValidationRequiredError: Raised when additional validation steps are neededBackendError: Generic backend failure; triggers ERROR_CREATING state transitionValueError, HTTPError): Logged but do not trigger any state
transition — the user silently stays in their current state. Plugin developers should wrap backend
failures as BackendError to ensure the error state is reflected in Waldur.Both linking/validation exceptions support an optional comment_url parameter to provide links to
forms, documentation, or other resources needed for error resolution.
When exceptions occur during username generation:
State transition handling by current user state:
BackendError occurs, the user transitions to ERROR_CREATING.OfferingUserAccountLinkingRequiredError or
OfferingUserAdditionalValidationRequiredError, the user transitions to PENDING_ACCOUNT_LINKING
or PENDING_ADDITIONAL_VALIDATION respectively. If a BackendError occurs, the user transitions
to ERROR_CREATING so that admins can see the failure. On the next cycle, ERROR_CREATING users
are moved back to CREATING via begin_creating and retried.OfferingUserAccountLinkingRequiredError,
the user stays in the current state (no redundant API call). If the backend raises
OfferingUserAdditionalValidationRequiredError, the user cross-transitions to
PENDING_ADDITIONAL_VALIDATION.OfferingUserAdditionalValidationRequiredError, the user stays in the current state.
If the backend raises OfferingUserAccountLinkingRequiredError, the user cross-transitions
to PENDING_ACCOUNT_LINKING.set_validation_complete
is called (which clears service provider comments) before setting the username.When the agent runs in event_process mode, offering user username synchronization is primarily driven
by real-time STOMP events. However, transient STOMP disconnections or message loss can cause missed
updates. To address this, the main event loop includes a periodic reconciliation timer.
WALDUR_SITE_AGENT_RECONCILIATION_PERIOD_MINUTESstomp_enabled: true and a membership_sync_backendsync_offering_user_usernames() which compares usernames between source and
target offerings and patches any mismatches# Environment variable (default: 60 minutes)
WALDUR_SITE_AGENT_RECONCILIATION_PERIOD_MINUTES=60
During membership synchronization, the processor can forward user profile attributes to backends that need them (e.g., the Waldur federation backend sends attributes to the Identity Bridge API when resolving remote users).
Which attributes are forwarded is driven by the offering’s
OfferingUserAttributeConfig. Providers configure which user fields are
exposed via the Waldur admin UI (e.g., expose_email, expose_organization,
expose_gender). The agent:
OfferingUser and passes them to the backend via user_attributes.All 20+ attributes from OfferingUserAttributeConfig are supported:
username, full_name (includes first_name, last_name), email,
phone_number, organization, job_title, affiliations, gender,
personal_title, place_of_birth, country_of_residence, nationality,
nationalities, organization_country, organization_type,
organization_registry_code, eduperson_assurance, civil_number,
birth_date, identity_source, active_isds.
When the attribute config API is unavailable, the agent defaults to
exposing username, full_name, and email.
get_or_create_username() can be called multiple times safelywaldur_sync_offering_users regularly via cron or systemd timerusername_management_backend configuration to avoid
UnknownUsernameManagementBackend fallback# Check system health
uv run waldur_site_diagnostics -c config.yaml
# Manual user sync
uv run waldur_sync_offering_users -c config.yaml
# Check offering user states via API
curl -H "Authorization: Token YOUR_TOKEN" \
"https://waldur.example.com/api/marketplace-offering-users/?offering_uuid=OFFERING_UUID"