mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-16 16:45:07 +00:00
91d34b204c
An actor with no personal access token has always meant a browser, and for as long as a session and a Sanctum token were the only two ways to authenticate, that was true. It stops being true the moment anything else can, and the failure is silent: the action gets recorded as a person clicking, in the one table whose whole purpose is answering "did I do that, or did something acting for me?" Nothing misreports today — every call site that passes an explicit actor is a browser request, an API request whose actor carries the token, or a console command with no actor at all. This closes the trap before the AI connector in cloud-modules walks into it. ActivityOrigin is a closed enum, so core has to publish both the case and the hook before a package can use either. ResolvingActivityOrigin is asked only in the ambiguous case: a request carrying a token is the API and a request with nobody signed in is public or system, and neither is in any doubt, so neither is offered — one package must not be able to quietly relabel how every integration's actions are attributed. The person stays the actor. They authorised it, and a log naming the assistant instead would lose the only fact that matters. What the connector was called goes in api_token_name, beside a null token id, because that column means a row in personal_access_tokens and this is not one. The new origin is kept out of the activity filter unless the edition can actually produce it. A filter option that can only ever return nothing is a feature dangled at an edition that does not have it, which is the one thing the edition boundary exists not to do.
67 lines
2.3 KiB
PHP
67 lines
2.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Audit;
|
|
|
|
/**
|
|
* How an audited action reached the application.
|
|
*
|
|
* Distinct from `actor_type`, which says *who* acted (staff, client, or
|
|
* nobody). Origin says *through what*: the same staff member deleting the
|
|
* same file from the web UI and from an integration produces two entries
|
|
* that are otherwise identical, and an administrator reviewing the log
|
|
* needs to tell them apart — "did I do that, or did the Zapier token?" is
|
|
* the first question asked when something unexpected shows up.
|
|
*/
|
|
enum ActivityOrigin: string
|
|
{
|
|
/** A browser session — the web UI. */
|
|
case Ui = 'ui';
|
|
|
|
/** An API token. `api_token_id` and `api_token_name` are set alongside. */
|
|
case Api = 'api';
|
|
|
|
/**
|
|
* A web request with nobody signed in — a visitor commenting on a
|
|
* public file today, and whatever else the public surface grows.
|
|
*
|
|
* Split out of System because the two are not the same thing and were
|
|
* being shown with the same word: the scheduler deleting an expired
|
|
* file and a stranger leaving a comment both read as "System", which
|
|
* made the audit trail claim the installation had commented on its own
|
|
* file. Scheduled tasks keep System — they go through
|
|
* ActivityLogger::logSystem(), which never asks this method.
|
|
*/
|
|
case Public = 'public';
|
|
|
|
/** Scheduled tasks and console commands. */
|
|
case System = 'system';
|
|
|
|
/**
|
|
* An AI assistant acting for a signed-in person, through a connector
|
|
* they authorised — the code that can produce this ships in
|
|
* projectsend/cloud-modules and nowhere else.
|
|
*
|
|
* The person stays the actor: they authorised it, and an audit trail
|
|
* that named the assistant instead would lose the only fact that
|
|
* matters when something unexpected shows up. What the connector was
|
|
* called goes beside the entry, the way an API token's name does.
|
|
*/
|
|
case Mcp = 'mcp';
|
|
|
|
/**
|
|
* English label — also the translation key.
|
|
*/
|
|
public function label(): string
|
|
{
|
|
return match ($this) {
|
|
self::Ui => 'Web UI',
|
|
self::Api => 'API',
|
|
self::Public => 'Not signed in',
|
|
self::System => 'System',
|
|
self::Mcp => 'AI assistant',
|
|
};
|
|
}
|
|
}
|