NAME
Concierge - Extensible service layer that manages the relationship between an application and its users.
VERSION
v0.13.0
SYNOPSIS
use Concierge;
# Open an existing desk (created by Concierge::Desk::Setup)
my $desk = Concierge->open_desk('./desk');
my $concierge = $desk->{concierge};
# Register a user
$concierge->add_user({
user_id => 'alice',
moniker => 'Alice',
email => 'alice@example.com',
password => 'secret123',
});
# Log in -- returns a Concierge::Desk::User object
my $login = $concierge->login_user({
user_id => 'alice',
password => 'secret123',
});
my $user = $login->{user};
# User object provides direct access
say $user->moniker; # "Alice"
say $user->session_id; # random hex string
say $user->is_logged_in; # 1
# Restore user from a cookie on next request
my $restore = $concierge->restore_user($user->user_key);
my $same_user = $restore->{user};
# Log out -- via Concierge, or directly on the user object
$concierge->logout_user($user->session_id);
$user->logout;
CONCEPTS
Concierge is built around four ideas: it is extensible, it behaves as a service layer, it orchestrates rather than reimplements, and it exists to simplify an application's access to operational resources. The sections below define each term as used throughout this documentation; "Architecture" and "EXTENSIBILITY" further down build on them.
Extensible
Concierge is extensible in two independent ways:
Each identity-core component (Auth, Sessions, Users) is itself extensible as to backend and storage configuration -- see each component's own POD, and "Component Substitution" for replacing a component outright.
Components beyond the identity core may be added to a desk. Concierge can treat an added component purely as a pass-through:
my $res = $concierge->{'OthComp'}->OthCompMthd(); my $res = $concierge->OthComp->OthCompMthd(); my $OthCompObj = $concierge->OthComp; # or my $OthCompObj = $concierge->{'OthComp'}; my $res = $OthCompObj->OthCompMthd(); # $concierge no longer involved
Service Layer
Concierge is a service layer: application code depends on it to manage the configured components without crashing or interfering with the application's control flow. It is designed so that a partially-working concierge is never returned, and failures are never silent.
The setup methods (
build_desk()orbuild_quick_desk()) must validate their entire configuration or fail with messages. If setup succeeds, the Concierge config for the app is saved.The runtime method,
open_desk()must fully instantiate the desk's saved configuration or fail with messages. If open_desk() succeeds, it will return a fully capable concierge to the app.Nearly every setup validation failure -- a missing or unknown backend, a missing required setting, a component's
setup()failing -- returns a structured{ success => 0, message => '...' }response rather than raising an exception. The one exception is a failure to create a storage directory on disk, which is treated as an unrecoverable environment problem and raises an exception instead.Most open_desk() instantiation problems (an outdated config format, a corrupt config file, an auth backend that fails to initialize) are likewise returned as structured failures. Two cases are stricter and raise an exception instead: the desk directory itself does not exist, or a non-optional added component fails to instantiate. An added component marked
optionaldoes not raise in this case -- see Concierge::Desk::UnavailableComponent.Failure of
open_desk()itself is the sole post-setup exception.With the protections in setup methods and
open_desk(), any failure is always clearly reported, and a concierge desk location or object is only ever handed back to the application if it is fully functional.Once a desk is open, the concierge suite's API methods are never fatal to the application: every API method returns a structured response indicating success or failure (see "Return Values"), so the application always retains control flow.
Orchestration
Concierge's job is to coordinate access to services and resources an application needs, not to reimplement them. Depending on the component, this takes one of two forms:
For the identity core, Concierge directly provides the capability:
login_user()authenticates via Auth, retrieves a record from Users, and creates a session via Sessions in one coordinated call.For an added component, Concierge's involvement can end at handoff: the component satisfies the minimal contract in Concierge::Desk::Component so it can be loaded and reached through the concierge, but Concierge does not inherit from it or in any way intervene in its operations - that is for the application as it uses the component.
Operational Resources
An application's "operational resources" are the services and data stores that support its main purpose without being that purpose -- authentication, sessions, and user records are the built-in examples, but the term applies equally to anything an application uses to support its main functionality.
Concierge is deliberately agnostic about what an operational resource actually is: other than the identity core components, an added component needs only to satisfy the contract in Concierge::Desk::Component to be set up, deployed, and provided to the application the same way.
DESCRIPTION
Concierge manages the relationship between an application and its users: identity, sessions, access, and the information an application keeps about who is using it. It does this through an extensible service layer -- see "CONCEPTS" for the terms used throughout this documentation.
Applications primarily interact with Concierge and the Concierge::Desk::User objects it returns; most operations never require touching a component module directly. When a needed operation isn't exposed as a Concierge-level method, any component -- identity-core or added -- remains reachable through its own accessor (see "Component Accessors" and "EXTENSIBILITY").
What the Suite Provides
Concierge handles orchestration -- coordinating components and returning consistent structured results. As "Orchestration" above describes, this takes one of two co-equal forms: direct, built-in coordination for the identity core, or handoff to whatever additional components a desk is configured with.
The identity core -- Auth, Sessions, and Users -- ships with every installation and is described in detail below. An added component is just as much a capability of the suite once it's attached to a desk; see "Additional Components" for how those are configured, loaded, and reached.
Authentication (Concierge::Auth): Argon2id password hashing and verification; no plaintext credentials are ever written to disk. Also provides random token, UUID, word-passphrase, and hex-ID generators. The component is substitutable: any replacement implementing the same method contract (authenticate, enroll, change_credentials, etc. -- see Concierge::Auth::Base) can replace it for LDAP, OAuth, or any other scheme.
Sessions (Concierge::Sessions): Full session lifecycle -- creation, retrieval, expiry, and cleanup -- with SQLite, file, or in-memory storage. Sessions carry arbitrary key/value data. A single-session-per-user policy is enforced: creating a new session automatically removes any prior session for that user. Expired sessions are cleaned up each time a desk is opened.
User Records (Concierge::Users): User data store with a configurable field schema. Standard fields (moniker, email, phone, access_level, user_status, term_ends, and others) are built in and can be selectively overridden. Applications add their own fields via app_fields at setup time. Provides built-in SQLite, YAML, and CSV/TSV backends, with filtering and listing operations.
For the full API of any component, see its own documentation.
Desks
A desk is a storage directory containing the configuration for the application's concierge. Normally the data and any config files for the identity core components are kept there, and any additional components may be configured for that as well. At the same time, paths may be provided to other locations to accommodate considerations such as security and interfacing with existing systems.
Use Concierge::Desk::Setup to create a desk, then open_desk() to load it at runtime.
User Participation Levels
Concierge provides three graduated levels of user participation, each returning a Concierge::Desk::User object:
- Visitor --
admit_visitor() -
Assigned a unique identifier only. No session, no stored data. Suitable for anonymous tracking (e.g., cookies).
- Guest --
checkin_guest() -
Assigned an identifier and a session. Can store temporary data (e.g., a shopping cart). No authentication or persistent user record.
- Logged-in user --
login_user() -
Authenticated with credentials. Has a session, persistent user data, and full access to the User object's data methods.
A guest can be converted to a logged-in user with login_guest(), transferring any session data accumulated during the guest session.
User Keys
Each active user (guest or logged-in) is tracked by a user_key -- a random token stored in the concierge's user_keys mapping alongside the user's user_id and session_id. This mapping is persisted to user_keys.json in the desk directory and synchronized against active sessions when the desk is opened.
Return Values
All methods return a hashref with at least success (0 or 1) and message:
# Success
{ success => 1, message => '...', ... }
# Failure
{ success => 0, message => 'error description' }
Success responses include additional fields relevant to the operation:
User lifecycle methods (
login_user(),restore_user(),checkin_guest(),admit_visitor(),login_guest()) returnuser, a Concierge::Desk::User object. Guest and visitor results also setis_guestoris_visitorto 1.open_desk()returnsconcierge, the ready-to-use Concierge object.User management methods return
user_id.remove_user()also returnsdeleted_from(arrayref of component names) and, if any deletion failed,warnings(arrayref).verify_user()returnsverified(0 or 1),exists_in_auth, andexists_in_users.list_users()returnsuser_ids(arrayref) andcount. Withinclude_data => 1, also returnsusers(hashref keyed by user_id).
See the individual method descriptions below for the complete field list.
Methods never die during normal operation. The one exception is open_desk(), which uses Carp::croak to locate the problem -- either the desk directory does not exist, or a non-optional added component (see "Additional Components") fails to load.
Architecture
Concierge ships with three identity core components:
- Concierge::Auth -- credential storage and verification
- Concierge::Sessions -- session lifecycle and persistence
- Concierge::Users -- user records with configurable field schemas
These three are tightly orchestrated: a single login_user() call authenticates via Auth, retrieves a record from Users, and creates a session through Sessions. This coordination is what the identity core exists to provide -- applications interact with the Concierge API and the Concierge::Desk::User objects it returns, not with the components directly. It is one of the two orchestration forms described in "CONCEPTS", not the whole of what Concierge does: the same Concierge-level API also hands off to any added component, reached through its own accessor, with the application composing its own capabilities from what it gets back.
The identity core is designed to be sufficient on its own, but the component pattern it follows -- backend abstraction, setup-time configuration, and Concierge-level orchestration -- is intentionally replicable. Each identity core component can also be substituted with a conforming replacement, and additional components (Organizations, Assets, etc.) can be added by following the same conventions. See "EXTENSIBILITY" for details, and "CONCEPTS" for the ideas behind it.
METHODS
Desk Management
open_desk
my $result = Concierge->open_desk($desk_location);
my $concierge = $result->{concierge};
Opens an existing desk directory created by Concierge::Desk::Setup. Reads the configuration file, instantiates all component modules, loads the user_keys mapping, and runs session cleanup.
Croaks if $desk_location is not an existing directory, or if a non-optional added component (see "Additional Components") fails to load.
Returns { success => 1, concierge => $obj } on success.
new_concierge
my $concierge = Concierge->new_concierge();
Low-level constructor: returns a bare, uninitialized Concierge object with no components attached. Used internally by open_desk(). Application code should use open_desk() (or Concierge::Desk::Setup::build_desk() for first-time setup) rather than calling this directly.
save_user_keys
my $result = $concierge->save_user_keys();
Writes the in-memory user_keys mapping (user_key => session/user_id lookup data, used by restore_user()) to persistent storage as JSON. Called automatically after any operation that changes the mapping (login, logout, guest promotion, user removal, etc.); applications do not normally need to call it directly.
Returns { success => 1 } on success, or { success => 0, message => '...' } if the file cannot be written.
Component Accessors
auth, sessions, users
my $auth_obj = $concierge->auth;
my $sessions_obj = $concierge->sessions;
my $users_obj = $concierge->users;
Read-only accessors returning the live, already-instantiated component object ( Concierge::Auth, Concierge::Sessions, or Concierge::Users, or a substitute -- see "EXTENSIBILITY" ) for direct use when an operation isn't exposed as a Concierge-level convenience method. This is the same "bare accessor" direct component access described in "Component Substitution".
User Lifecycle
admit_visitor
my $result = $concierge->admit_visitor();
my $user = $result->{user}; # Concierge::Desk::User (visitor)
Creates a visitor with a generated identifier. No session is created and no data is stored.
checkin_guest
my $result = $concierge->checkin_guest(\%session_opts);
my $user = $result->{user}; # Concierge::Desk::User (guest)
Creates a guest with a generated identifier and a session. The optional %session_opts hashref may include timeout (in seconds; defaults to 1800).
login_user
my $result = $concierge->login_user(\%credentials, \%session_opts);
my $user = $result->{user}; # Concierge::Desk::User (logged-in)
Authenticates user_id and password from %credentials, retrieves the user's data record, creates a session, and returns a fully-equipped User object. If the user already has an active session, the previous session is replaced.
restore_user
my $result = $concierge->restore_user($user_key);
my $user = $result->{user}; # Concierge::Desk::User (guest or logged-in)
Reconstructs a User object from a user_key (typically stored in a cookie or URL token). Looks up the key in the concierge mapping, validates the session, and determines whether the user is a guest or logged-in user.
Logged-in users are restored with their full user data snapshot and backend closures. Guests are restored with their session only.
If the session has expired, the stale mapping entry is cleaned up and the method returns failure. The application can then redirect to login or create a new guest as appropriate.
Returns { success => 1, user => $user } on success. Guest restores also include is_guest => 1.
login_guest
my $result = $concierge->login_guest(\%credentials, $guest_user_key);
my $user = $result->{user}; # Concierge::Desk::User (logged-in)
Converts a guest to a logged-in user. If %credentials' user_id does not already belong to a known user, registers a new account first (same requirements as add_user()); if it does belong to a known user (e.g. a returning customer who browsed as a guest before logging in to pay), registration is skipped and the existing account is used instead. Authenticates with %credentials, transfers any data from the guest's session to the new session, then deletes the guest session and removes the guest's user_key mapping.
Returns failure if user_id exists in only one of the Auth/Users components (an inconsistent state) rather than attempting either path.
logout_user
my $result = $concierge->logout_user($session_id);
Deletes the session and removes the user_key mapping entry.
Also available directly on the returned user object as $user->logout -- see Concierge::Desk::User. Unlike this method, the user-object form needs no $session_id argument (it's bound at construction) and works for both guest and logged-in users, since logout only needs a session, not an Auth-backed identity.
Admin Operations
add_user
my $result = $concierge->add_user(\%user_input);
Registers a new user. %user_input must include user_id, moniker, and password. Any additional fields (email, phone, application- defined fields, etc.) are stored in the Users component. The password is stored separately in the Auth component and never reaches the user data store.
If password validation fails, the Users record is rolled back.
remove_user
my $result = $concierge->remove_user($user_id);
Removes the user from all components: Users, Auth, Sessions, and the user_keys mapping. Attempts all deletions; the response includes deleted_from (arrayref) and warnings (arrayref, if any component deletion failed).
verify_user
my $result = $concierge->verify_user($user_id);
Checks whether $user_id exists in both Auth and Users components. Returns verified => 1 only if present in both. Includes exists_in_auth and exists_in_users flags, and a warning if the user exists in one component but not the other.
list_users
# IDs only
my $result = $concierge->list_users($filter, \%options);
my @ids = @{ $result->{user_ids} };
# With full data
my $result = $concierge->list_users('', { include_data => 1 });
my %users = %{ $result->{users} };
Returns user IDs from the Users component. $filter is a string passed through to Concierge::Users. With include_data => 1, fetches each user's full record into a users hash keyed by user_id. With fields => [...], returns only the specified fields per user.
get_user_data
my $result = $concierge->get_user_data($user_id, @fields);
my $data = $result->{user};
Retrieves user data from the Users component. If @fields is provided, returns only those fields; otherwise returns all fields.
update_user_data
my $result = $concierge->update_user_data($user_id, \%updates);
Updates the user's record in the Users component. The user_id and password fields are filtered out and cannot be changed through this method.
Password Operations
Initial password registration is handled by add_user(), which sets the password atomically with user creation. The methods here operate on passwords for existing users.
verify_password
my $result = $concierge->verify_password($user_id, $password);
Checks whether $password is correct for $user_id. Returns success => 1 if the password matches.
Also available directly on a logged-in user object as $user->verify_password($password) -- see Concierge::Desk::User. The user-object form needs no $user_id argument (it's bound at construction) and collapses the hashref result to a plain 1/0/ undef scalar; undef means the method isn't applicable to that user (guests and visitors have no Auth-backed identity to check against).
reset_password
my $result = $concierge->reset_password($user_id, $new_password);
Sets a new password for an existing user. The application is responsible for verifying the user's identity before calling this method.
Also available directly on a logged-in user object as $user->reset_password($new_password) -- see Concierge::Desk::User. Same argument-binding and scalar-collapsing behavior as verify_password above, except success collapses to 1 and any non-success (including inapplicability to guests/visitors) collapses to undef.
PARAMETER FILTERS
Concierge uses Params::Filter to enforce data segregation at method boundaries:
$auth_data_filter-- extracts onlyuser_idandpassword$user_data_filter-- extracts everything exceptpassword$session_data_filter-- extractsuser_idplus non-credential fields$user_update_filter-- excludesuser_idandpasswordfrom updates
These ensure that credentials never leak into user data stores and that identity fields cannot be changed via update operations.
EXTENSIBILITY
See "CONCEPTS" for the ideas behind extensibility, service-layer guarantees, and orchestration that this section implements.
Component Substitution
Each identity core component can be replaced with a drop-in alternative as long as the replacement implements the methods Concierge calls on it.
Auth -- Concierge calls:
$auth->authenticate($user_id, $credential)
$auth->is_id_known($user_id)
$auth->enroll($user_id, $credential, \%opts?)
$auth->change_credentials($user_id, $new_credential)
$auth->revoke($user_id)
A substitute backend must implement this contract -- see Concierge::Auth::Base, which also provides working default Concierge::Auth::Generators methods (used for visitor/guest identifiers, independent of authentication) to any backend that inherits from it; a substitute only needs to override these if it wants different generation logic.
Sessions -- Concierge calls:
Concierge::Sessions->new(%args)-- constructor; acceptsstorage_dirandbackend_class$sessions->new_session(%args)-- returns{ success => 1, session => $obj }$sessions->get_session($session_id)-- returns{ success => 1, session => $obj }$sessions->delete_session($session_id)-- returns{ success => 1|0, ... }$sessions->cleanup_sessions()-- returns{ success => 1, deleted_count => N, active => [...] }
Users -- Concierge calls:
Concierge::Users->new($config_file)-- constructor$users->register_user(\%data)-- returns{ success => 1|0, message => '...' }$users->get_user($user_id)-- returns{ success => 1, user => \%data }$users->update_user($user_id, \%updates)-- returns{ success => 1|0, ... }$users->delete_user($user_id)-- returns{ success => 1|0, ... }$users->list_users($filter)-- returns{ success => 1, user_ids => [...] }
Additional Components
Beyond the identity core, a desk can declare additional components -- Organizations, Assets, Catalog, etc. -- via a components block passed to Concierge::Desk::Setup::build_desk():
my $result = Concierge::Desk::Setup::build_desk({
base_dir => './desk',
auth => { backend => 'pwd' },
sessions => { backend => 'database' },
users => { backend => 'database' },
components => {
organizations => {
class => 'Concierge::Organizations',
optional => 0,
dir => 'organizations', # optional; resolved like
# other components' dirs
# ...any other setup() args for this component...
},
},
});
Each entry is resolved once, at build time: build_desk() creates the component's storage directory, requires class, calls my $comp = $class->new(), then calls $comp->setup(\%entry_config). The hashref setup() returns is persisted verbatim into concierge.conf as that component's payload -- this is never recomputed at open_desk()/runtime. A setup() failure always fails the entire build, regardless of the optional flag.
At open_desk() time, Concierge reads each component's persisted payload back out of concierge.conf, requires its class, and calls $class->new($payload):
If
new()succeeds, the component is attached at$concierge->{$name}, and a same-named accessor is installed automatically (e.g.,$concierge->organizations) if one doesn't already exist.If
new()dies and the component is notoptional, the exception propagates uncaught out ofopen_desk()-- a desk must never open half-instantiated.If
new()dies and the component isoptional, a Concierge::Desk::UnavailableComponent stand-in is attached instead. Any method called on it returns{ success => 0, message => "Component '$name' unavailable: ..." }.
Access the component through the concierge object once the desk is open:
my $c = Concierge->open_desk($desk_dir)->{concierge};
my $r = $c->organizations->add_record('acme', \%data);
unless ($r->{success}) {
# handles both a genuine failure and an UnavailableComponent
# substitution identically
}
A component only needs to satisfy the duck-typed contract documented in Concierge::Desk::Component -- there is no isa check anywhere in this mechanism. A component whose job is per-record CRUD is responsible for its own CRUD methods; Concierge does not standardize or scaffold them.
Sessions, Auth, and Users are not wired through this generic mechanism -- they remain hardcoded core affordances, loaded unconditionally by open_desk() as described above. The components block is only for components genuinely additional to that identity core.
Deferred component initialization (defer)
defer => 1 postpones a component's real, potentially expensive new($payload) call until the component's first actual use, instead of paying that cost for every process that opens the desk regardless of whether the component is ever called. This is an opt-in escape valve, not a new default -- reach for it only when a specific component's own startup (a network database, an external API handshake) has demonstrated it's a problem in practice.
components => {
reports => {
class => 'Concierge::Reports',
defer => 1, # defer new() until first real use
optional => 1, # REQUIRED to be true whenever defer is true
},
},
defer forces optional: build_desk() rejects defer => 1, optional => 0. This preserves Concierge's guarantee that, once a desk is open, its API methods are never fatal to the application -- a required-and-deferred component's eventual failure would happen mid-process, potentially while other users are already being served, which croaking would violate.
Three checks apply to a defer component, in addition to the ordinary build-time setup() check every component gets:
Build-time probe (tier 1b). Immediately after
setup()succeeds,build_desk()runs the same cheap reachability check tier 2 will later run (the component's ownprobe()class method if it has one, otherwise a default require-only check -- see "PROBING" in Concierge::Desk::Component). This check is unconditionally build-fatal, regardless ofoptional-- deliberately, to catch a broken probe or an unrequireable class at a human-attended build, rather than only discovering it later, unattended, as a silentUnavailableComponentsubstitution.Open-time revalidation (tier 2).
open_desk()runs the same probe-or-default check again, on every process start. A passing probe installs a Concierge::Desk::DeferredComponent stand-in in place of the real component. A failing probe resolves immediately to Concierge::Desk::UnavailableComponent, exactly like any other optional component failure -- sincedeferalways impliesoptional, this is never open-fatal.First real use (tier 3). The
DeferredComponentstand-in defers the actualnew($payload)call until the first method is actually called on it, then caches the result (or, on failure, anUnavailableComponentstand-in) and delegates every subsequent call to it. A tier 3 failure is permanent for the life of the process -- Concierge never retries a failed deferred build, matching the existingUnavailableComponentprecedent.
Concierge never enforces a timeout around a component's probe() or new(), at any tier. If a component's own startup can hang or block, its author is expected to self-enforce a reasonable $max_wait_for_load inside its own probe()/new(). This matters especially for defer: Concierge runs single-process with cooperative concurrency, so a blocking sleep-and-retry loop inside a component's probe() or new() stalls the entire application for every current user, not just the caller that triggered it -- and a tier 3 (first-use) stall is worse than a tier 1b/2 (build/open-time) one, since it can happen mid-process, while other users' requests are in flight.
Future Components
Concierge::Organizations is the first component built on this mechanism -- a simple JSON-file-backed records store for multi-tenancy (users belonging to organizations), installed and namespaced independently of this distribution, exactly like Auth/Sessions/Users. The following illustrate other kinds of components the Concierge:: namespace is suited for. These are not roadmap commitments -- they are examples of what the component pattern enables:
Concierge::Assets-- files, images, or other owned resourcesConcierge::Guides-- role and permission recordsConcierge::Catalog-- product or content recordsConcierge::Calendar-- event and booking records
The backend_class Pattern
Concierge::Auth, Concierge::Sessions, and Concierge::Users each support multiple interchangeable storage/backend implementations of their own (e.g. Auth's password vs. future LDAP/OAuth backends, Sessions' database vs. file backends, Users' database/file/YAML backends). All three follow the same convention: the component itself accepts only backend_class, an already-resolved, fully-qualified class name, and never guesses a class from a short friendly name -- that resolution happens one layer up, in Concierge::Desk::Setup's internal per-component catalogs (%AUTH_BACKENDS, %SESSIONS_BACKENDS, %USERS_BACKENDS), which map a desk config's friendly backend name ('pwd', 'database', 'yaml', etc.) to its class and any settings that backend requires.
A new component with multiple interchangeable backend implementations of its own should adopt this same backend_class convention: accept only a fully-qualified class name at the component layer, and leave friendly-name-to-class resolution to whatever builds the desk config around it.
Contributing
If you build a component that might be useful to others, contributions to the Concierge:: namespace on CPAN are welcome. The conventions to follow are: satisfy the duck-typed contract in Concierge::Desk::Component, use the { success = 1|0, message => '...' }> return convention, accept a desk config block via setup(), and include comprehensive tests and POD. Open an issue or pull request at https://github.com/bwva/Concierge to discuss before publishing.
SEE ALSO
Concierge::Desk::Setup -- desk creation and configuration
Concierge::Desk::User -- user objects returned by lifecycle methods
Concierge::Desk::Component -- contract documentation for additional components
Concierge::Desk::UnavailableComponent -- stand-in for a failed optional component
Concierge::Auth, Concierge::Sessions, Concierge::Users -- identity core component modules
Concierge::Organizations -- example additional component (multi-tenancy records store)
AUTHOR
Bruce Van Allen <bva@cruzio.com>
LICENSE
This module is free software; you can redistribute it and/or modify it under the terms of the Artistic License 2.0.