settings->get(Setting::ClientsAutoApprove) === true; } /** * Whether an address is free for a new account. * * The unique index on `email` spans soft-deleted rows — AvailableEmailRule * is built on exactly that, so a deleted account keeps its address until * erasure takes the row away. The registration form learns this from * validation. The machine paths have no form to validate: a directory or * an identity provider hands over an address and provision() inserts it, * so without asking first the insert raises a QueryException in the * middle of somebody's sign-in. */ public function addressIsFree(string $email): bool { return ! User::withTrashed()->where('email', $email)->exists(); } /** * @param bool|null $autoApprove Null asks Setting::ClientsAutoApprove, * which is the right question for the * public registration form. A caller * that has already established who * somebody is — LDAP, an identity * provider — passes its own answer * instead. * @param array $context Placeholders for the action's * log template, e.g. which * provider an account came from. * @param int $storageQuotaMb 0 means no per-account quota and * inherits the site default at * enforcement time — see * ClientStorageUsage::quotaMb(). Same * meaning as ClientAccounts::create()'s * parameter of the same name; a caller * with no quota to offer (the public * registration form, LDAP) leaves it at 0. */ public function provision( string $name, string $email, string $password, Action $action, AuthSource $source = AuthSource::Local, ?string $ldapDn = null, ?bool $autoApprove = null, array $context = [], int $storageQuotaMb = 0, ): User { $autoApprove ??= $this->autoApproves(); // Only when the account arrives already approved. A request that // still needs a decision is not yet a client this installation has // taken on, and counting one would let a stranger exhaust a paid // limit from the registration form — see SeatAllowance. The guard // for those sits on approval instead. if ($autoApprove) { $this->seats->guardClient(); } $client = User::create([ 'type' => UserType::Client, 'active' => $autoApprove, 'account_requested' => ! $autoApprove, 'role_id' => Role::query()->where('name', SystemRole::Client->value)->value('id'), 'name' => $name, 'email' => $email, 'password' => $password, 'storage_quota_mb' => $storageQuotaMb, ]); // Not mass-assignable: where an account's credentials live is a // security decision, not an attribute a form may set. if ($source !== AuthSource::Local || $ldapDn !== null) { $client->forceFill([ 'auth_source' => $source, 'ldap_dn' => $ldapDn, 'ldap_synced_at' => $ldapDn === null ? null : now(), ])->save(); } $this->activity->log($action, $client, $client, $context); $this->joinAutoGroup($client); $this->notifyAdministrators($client, pending: ! $autoApprove); $this->notifyStaffInApp($client); return $client; } /** * The group every self-provisioned client joins, if one is configured. * Direct membership, no approval — an administrator chose this in * settings. */ private function joinAutoGroup(User $client): void { $autoGroupId = (int) $this->settings->get(Setting::ClientsAutoGroup); if ($autoGroupId <= 0) { return; } $group = Group::query()->find($autoGroupId); $group?->members()->syncWithoutDetaching([$client->id]); } /** * The bell, for staff who administer clients. * * Separate from notifyAdministrators() above, and not a replacement for * it: that one emails a list of raw addresses an operator typed into a * setting, which need not correspond to any account in this * installation. This one reaches the people actually signed in, which * is the only place an account arriving unannounced was ever going to * be noticed. * * Recipients are resolved here rather than inside Notifier, which * authorizes nothing by design — see its security contract. Two rules, * and the second is the one worth stating: a client-scoped staff member * is not told. Their whole view is the clients assigned to them, and a * brand-new account is assigned to nobody, so the notification would * link them to a screen they are refused. */ private function notifyStaffInApp(User $client): void { $recipients = User::query() ->where('type', UserType::Staff) ->get() ->filter(fn (User $staff): bool => ! $staff->isClientScoped() && $this->permissions->allows($staff, Permission::ManageClients)); $this->notifier->send('client_registered', $recipients, subject: $client, data: [ 'clientName' => $client->name, 'clientEmail' => $client->email, ]); } private function notifyAdministrators(User $client, bool $pending): void { if ($this->settings->get(Setting::EmailNotificationsEnabled) !== true) { return; } $addresses = $this->settings->get(Setting::AdminNotificationEmails); foreach (is_array($addresses) ? $addresses : [] as $address) { Notification::route('mail', $address)->notify( new AdminClientRegisteredNotification($client->name, $client->email, $pending) ); } } }