false === self::isPending($role), )); } /** * The markers on an account: administrative roles the CRM claims, awaiting approval. * * @param string[] $roles * * @return string[] */ public static function pendingOnly(array $roles): array { return array_values(array_filter($roles, static fn (string $role): bool => self::isPending($role))); } /** * The roles behind those markers, labelled — what an approver acts on. A marker for an * EMPLOYEE_ONLY role on an account that is not staff is left out: it predates the rule and * goes on that account's next login, and until then it must not be approvable. * * @param string[] $roles * * @return array role => label */ public static function nominatedFrom(array $roles): array { $labels = self::labels(); $nominated = []; $isEmployee = \in_array(self::EMPLOYEE, $roles, true); foreach (self::pendingOnly($roles) as $marker) { $role = self::realRole($marker); if (false === $isEmployee && \in_array($role, self::EMPLOYEE_ONLY, true)) { continue; } if (\in_array($role, self::ADMINISTRATIVE, true)) { $nominated[$role] = $labels[$role]; } } return $nominated; } /** * The whole policy, applied on every login. * * 0. drop the claims for EMPLOYEE_ONLY roles unless ROLE_EMPLOYEE is claimed as well — they * then count as not made, so the steps below revoke and never mark them; * 1. revoke everything the CRM no longer claims — granted roles and markers alike, which is * what makes BusPro the source of truth; * 2. grant the unconditional roles it claims; * 3. mark every administrative role it claims that is not granted already. This runs after * the revocation, so a role just revoked is not immediately marked again, and an approved * role is never marked a second time; * 4. fall back to ROLE_CUSTOMER when nothing effective is left. * * Nothing here can raise a privilege: step 3 only ever produces markers. * * @param string[] $storedRoles * @param string[] $claimedRoles what the CRM reports, plus the roles derived from the account * itself (ROLE_EMPLOYEE); anything outside self::ALL is ignored * * @return string[] */ public static function sync(array $storedRoles, array $claimedRoles): array { $claimed = array_values(array_intersect(self::ALL, array_unique($claimedRoles))); if (false === \in_array(self::EMPLOYEE, $claimed, true)) { $claimed = array_values(array_diff($claimed, self::EMPLOYEE_ONLY)); } $roles = array_values(array_filter( self::assignedOnly($storedRoles), static fn (string $role): bool => \in_array(self::realRole($role), $claimed, true), )); foreach (array_intersect($claimed, self::UNCONDITIONAL) as $role) { $roles[] = $role; } foreach (array_intersect($claimed, self::ADMINISTRATIVE) as $role) { if (false === \in_array($role, $roles, true)) { $roles[] = self::pending($role); } } return self::withCustomerFallback(array_values(array_unique($roles))); } /** * Turns a marker into the role it stands for. Refuses anything the account is not nominated * for (see nominatedFrom()), so neither a hand-crafted request nor a claim revoked while the * confirmation dialog was open can grant a role the CRM never reported. * * @param string[] $storedRoles * * @return string[] * * @throws \InvalidArgumentException when $role is not pending on this account */ public static function approve(array $storedRoles, string $role): array { $roles = self::assignedOnly($storedRoles); if (false === \array_key_exists($role, self::nominatedFrom($roles))) { throw new \InvalidArgumentException(sprintf('The role "%s" is not pending approval.', $role)); } $roles = array_diff($roles, [self::pending($role)]); $roles[] = $role; return self::withCustomerFallback(array_values(array_unique($roles))); } /** * Everybody without an effective role is a customer, and nobody with one is. The fallback is * a fallback, not a baseline — ROLE_CUSTOMER and ROLE_TEAMER are mutually exclusive on * purpose. * * @param string[] $roles * * @return string[] */ private static function withCustomerFallback(array $roles): array { if ([] === array_diff(self::effectiveOnly($roles), [self::CUSTOMER])) { return array_values(array_unique([...$roles, self::CUSTOMER])); } return array_values(array_diff($roles, [self::CUSTOMER])); } private static function isPending(string $role): bool { return str_ends_with($role, self::PENDING_SUFFIX); } private static function realRole(string $role): string { return self::isPending($role) ? substr($role, 0, -\strlen(self::PENDING_SUFFIX)) : $role; } /** * @return array role => label */ public static function labels(): array { $labels = [ self::ADMIN => 'Administration', self::TEAM_ADMIN => 'Team Administration', self::MANAGER => 'Reisemanager:in', self::TEAMER => 'Teamer:in', self::CUSTOMER => 'Kund:in', self::HOUSE_MANAGER => 'Hausleitung', self::GROUPS_ADMIN => 'GRO Admin', self::GROUPS_MANAGER => 'GRO-Expert:in', self::CUSTOMER_EXPERT => 'KO-Expert:in', self::EMPLOYEE => 'Mitarbeiter:in', ]; foreach (self::ADMINISTRATIVE as $role) { $labels[self::pending($role)] = $labels[$role].' (nicht freigeschaltet)'; } return $labels; } }