mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 17:15:08 +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.
96 lines
4.0 KiB
PHP
96 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Platform\Capabilities;
|
|
|
|
/**
|
|
* Every capability that differs between editions, in one place.
|
|
*
|
|
* The registry is bidirectional: each capability declares the editions it
|
|
* belongs to. Community-only capabilities exist because managed
|
|
* installations handle those concerns outside the application; cloud-only
|
|
* capabilities exist because some features cannot ship inside a self-hosted
|
|
* package (commercial trust providers, vendor credentials, legal posture).
|
|
*
|
|
* Adding a capability means adding a case here and its editions below —
|
|
* enforcement points (gates, Inertia shared props, API middleware) derive
|
|
* from this enum and must not need changes.
|
|
*/
|
|
enum Capability: string
|
|
{
|
|
// Community-only — cut where the installation is managed for you.
|
|
case UsersManage = 'users.manage';
|
|
case StorageConfigure = 'storage.configure';
|
|
case EmailTransportConfigure = 'email.transport.configure';
|
|
case SystemUpdates = 'system.updates';
|
|
|
|
// Community-only — scheduled-task run history and failed-queue-job
|
|
// visibility. Cut on managed installations, where infrastructure
|
|
// monitoring happens outside this application; a transient failure
|
|
// that self-heals on the next
|
|
// run showing up in a customer-facing UI would do more harm than good.
|
|
case SchedulerMonitoring = 'scheduler.monitoring';
|
|
|
|
// Community-only — code lives in the separate projectsend/community-modules
|
|
// package (github.com/projectsend/community-modules, public and GPLv2);
|
|
// arbitrary HTML/CSS/JS injection is too risky to offer on the hosted
|
|
// platform. Separate for operational reasons, not secret ones — unlike
|
|
// cloud-modules below.
|
|
case CustomAssets = 'custom_assets.manage';
|
|
|
|
// Cloud-only — code lives in the private projectsend/cloud-modules
|
|
// package (github.com/projectsend/cloud-modules), never in this repo.
|
|
case Branding = 'branding.customize';
|
|
|
|
// Cloud-only — the storage backend is ours, supplied by the
|
|
// environment when the instance is provisioned and not the customer's
|
|
// to see or change. The counterpart of StorageConfigure above rather
|
|
// than a contradiction of it: one edition configures its own bucket,
|
|
// the other is given one. Behaviour lives in the private
|
|
// projectsend/cloud-modules package; without it this capability is
|
|
// simply inert and files stay on local disk.
|
|
case StorageManaged = 'storage.managed';
|
|
|
|
// Cloud-only — managed installations supply CAPTCHA keys centrally, so
|
|
// protection is on before anybody finds the settings screen. The
|
|
// feature itself is in both editions and behind no capability: this
|
|
// covers only the option of using *our* credentials, which cannot ship
|
|
// inside a self-hosted package.
|
|
case CaptchaManagedKeys = 'captcha.managed_keys';
|
|
|
|
// Cloud-only — letting an AI assistant act on this installation on
|
|
// somebody's behalf. Code lives in the private
|
|
// projectsend/cloud-modules package; without it this capability is
|
|
// inert, which is the point: the edition boundary here is which
|
|
// package is installed, not a flag an installation can set. Present
|
|
// in this enum even so, because a package cannot extend a closed one
|
|
// — core has to publish the key before anything can gate on it.
|
|
case AiConnector = 'ai.connector';
|
|
|
|
/**
|
|
* @return list<Edition>
|
|
*/
|
|
public function editions(): array
|
|
{
|
|
return match ($this) {
|
|
self::UsersManage,
|
|
self::StorageConfigure,
|
|
self::EmailTransportConfigure,
|
|
self::SystemUpdates,
|
|
self::SchedulerMonitoring,
|
|
self::CustomAssets => [Edition::Community],
|
|
|
|
self::Branding,
|
|
self::StorageManaged,
|
|
self::CaptchaManagedKeys,
|
|
self::AiConnector => [Edition::Cloud],
|
|
};
|
|
}
|
|
|
|
public function availableIn(Edition $edition): bool
|
|
{
|
|
return in_array($edition, $this->editions(), true);
|
|
}
|
|
}
|