feat: expose winter placements to n8n over an api endpoint
This commit is contained in:
1 parent
4ff963d1e2
commit
0bc77b7463
17 files changed
+977
-18
No files matched your search
@@ -8,38 +8,46 @@ use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
|
||||
/**
|
||||
* Validates the shared key a sibling website posts with. There is no person to log in and no
|
||||
* account to look up: the key is the identity, and the user it resolves to is the in-memory
|
||||
* "api" user of the api_user_provider - nothing about it may reach the user table, where a row
|
||||
* would be a login to this application's UI.
|
||||
* Validates the shared key a machine caller sends. There is no person to log in and no account to
|
||||
* look up: the key is the identity, and the user it resolves to is one of the in-memory users of
|
||||
* the api_user_provider - nothing about it may reach the user table, where a row would be a login
|
||||
* to this application's UI.
|
||||
*
|
||||
* One key per caller, each resolving to its own user and roles: the websites posting application
|
||||
* forms must not read placements, and n8n reading placements must not post forms.
|
||||
*/
|
||||
class ApiKeyTokenHandler implements AccessTokenHandlerInterface
|
||||
{
|
||||
public const USER_IDENTIFIER = 'api';
|
||||
|
||||
/**
|
||||
* @param array<string, string> $apiKeys user identifier => key
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $apiKey,
|
||||
private readonly array $apiKeys,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUserBadgeFrom(string $accessToken): UserBadge
|
||||
{
|
||||
// An unconfigured key locks the endpoint instead of opening it: hash_equals('', '')
|
||||
// is true, so without this check a deployment that forgot the secret would accept
|
||||
// an empty key from anyone.
|
||||
if ('' === $this->apiKey) {
|
||||
// An unconfigured key locks its caller out instead of opening the door: hash_equals('', '')
|
||||
// is true, so without this filter a deployment that forgot a secret would accept an empty
|
||||
// key from anyone.
|
||||
$configuredKeys = array_filter($this->apiKeys, fn (string $key) => '' !== $key);
|
||||
|
||||
if ([] === $configuredKeys) {
|
||||
$this->logger->error('API key authentication attempted while no key is configured');
|
||||
|
||||
throw new BadCredentialsException('No API key configured.');
|
||||
}
|
||||
|
||||
if (false === hash_equals($this->apiKey, $accessToken)) {
|
||||
$this->logger->warning('API key authentication failed');
|
||||
|
||||
throw new BadCredentialsException('Invalid API key.');
|
||||
foreach ($configuredKeys as $identifier => $key) {
|
||||
if (true === hash_equals($key, $accessToken)) {
|
||||
return new UserBadge((string) $identifier);
|
||||
}
|
||||
}
|
||||
|
||||
return new UserBadge(self::USER_IDENTIFIER);
|
||||
$this->logger->warning('API key authentication failed');
|
||||
|
||||
throw new BadCredentialsException('Invalid API key.');
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,10 @@ class ApplicationVoter extends Voter
|
||||
return match ($attribute) {
|
||||
static::STATUS, static::DELETE => true,
|
||||
static::VIEW => Application::STATUS_REJECTED !== $status,
|
||||
// A called-off placement frees its place, so it does not count here: re-staffing
|
||||
// must not take raising availableDispositions, which would add a place.
|
||||
static::DISPOSE => Application::STATUS_REJECTED !== $status
|
||||
&& $assignment->getAvailableDispositions() > $assignment->getDispositions()->count(),
|
||||
&& $assignment->getAvailableDispositions() > $assignment->getActiveDispositions()->count(),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user