2019-05-09 06:54:39 +00:00
< ? php
2019-10-01 04:57:41 +00:00
2025-02-11 04:49:22 +00:00
use Appwrite\Auth\Key ;
2024-03-01 02:08:30 +00:00
use Appwrite\Auth\MFA\Type\TOTP ;
2026-02-23 20:35:38 +00:00
use Appwrite\Bus\Events\RequestCompleted ;
2022-05-26 11:51:08 +00:00
use Appwrite\Event\Audit ;
2024-02-20 11:40:55 +00:00
use Appwrite\Event\Build ;
2022-06-22 10:51:49 +00:00
use Appwrite\Event\Database as EventDatabase ;
2022-05-26 11:51:08 +00:00
use Appwrite\Event\Delete ;
2022-04-04 06:30:07 +00:00
use Appwrite\Event\Event ;
2022-11-15 18:13:17 +00:00
use Appwrite\Event\Func ;
2025-12-18 06:30:43 +00:00
use Appwrite\Event\Mail ;
2026-03-11 14:01:26 +00:00
use Appwrite\Event\Message\Usage as UsageMessage ;
2023-12-07 10:25:19 +00:00
use Appwrite\Event\Messaging ;
2026-03-11 14:01:26 +00:00
use Appwrite\Event\Publisher\Usage as UsagePublisher ;
2024-11-04 15:20:43 +00:00
use Appwrite\Event\Realtime ;
2024-11-04 15:05:54 +00:00
use Appwrite\Event\Webhook ;
2024-03-06 17:34:21 +00:00
use Appwrite\Extend\Exception ;
2024-03-07 23:30:23 +00:00
use Appwrite\Extend\Exception as AppwriteException ;
2026-01-07 14:57:57 +00:00
use Appwrite\Functions\EventProcessor ;
2025-02-11 04:49:22 +00:00
use Appwrite\SDK\Method ;
2026-03-11 14:01:26 +00:00
use Appwrite\Usage\Context ;
2025-11-04 06:08:35 +00:00
use Appwrite\Utopia\Database\Documents\User ;
2022-05-26 11:51:08 +00:00
use Appwrite\Utopia\Request ;
2024-03-06 17:34:21 +00:00
use Appwrite\Utopia\Response ;
2019-11-29 18:23:29 +00:00
use Utopia\Abuse\Abuse ;
2026-02-23 20:01:54 +00:00
use Utopia\Bus\Bus ;
2022-07-23 17:42:42 +00:00
use Utopia\Cache\Adapter\Filesystem ;
use Utopia\Cache\Cache ;
2024-03-06 17:34:21 +00:00
use Utopia\Config\Config ;
2022-05-26 11:51:08 +00:00
use Utopia\Database\Database ;
2022-08-31 08:52:55 +00:00
use Utopia\Database\DateTime ;
2021-10-07 15:35:17 +00:00
use Utopia\Database\Document ;
2026-02-26 06:44:35 +00:00
use Utopia\Database\Exception\Duplicate as DuplicateException ;
2024-02-16 16:26:50 +00:00
use Utopia\Database\Helpers\Role ;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Validator\Authorization ;
2026-01-07 07:04:28 +00:00
use Utopia\Database\Validator\Authorization\Input ;
2026-02-16 16:48:26 +00:00
use Utopia\Database\Validator\Roles ;
2026-02-10 05:04:24 +00:00
use Utopia\Http\Http ;
2024-04-01 11:02:47 +00:00
use Utopia\System\System ;
2025-08-01 10:22:27 +00:00
use Utopia\Telemetry\Adapter as Telemetry ;
2024-04-01 11:08:46 +00:00
use Utopia\Validator\WhiteList ;
2019-11-29 18:23:29 +00:00
2025-11-04 06:08:35 +00:00
$parseLabel = function ( string $label , array $responsePayload , array $requestParams , User $user ) {
2022-08-16 12:28:30 +00:00
preg_match_all ( '/{(.*?)}/' , $label , $matches );
foreach ( $matches [ 1 ] ? ? [] as $pos => $match ) {
$find = $matches [ 0 ][ $pos ];
$parts = explode ( '.' , $match );
if ( count ( $parts ) !== 2 ) {
2023-08-01 16:21:34 +00:00
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , " The server encountered an error while parsing the label: $label . Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose " );
2022-08-16 12:28:30 +00:00
}
$namespace = $parts [ 0 ] ? ? '' ;
$replace = $parts [ 1 ] ? ? '' ;
$params = match ( $namespace ) {
2026-03-11 14:01:26 +00:00
'user' => ( array ) $user ,
2022-08-16 12:28:30 +00:00
'request' => $requestParams ,
default => $responsePayload ,
};
if ( array_key_exists ( $replace , $params )) {
2025-10-24 20:42:10 +00:00
$replacement = $params [ $replace ];
// Convert to string if it's not already a string
2026-03-11 14:01:26 +00:00
if ( ! is_string ( $replacement )) {
2025-10-30 02:07:15 +00:00
if ( is_array ( $replacement )) {
$replacement = json_encode ( $replacement );
} elseif ( is_object ( $replacement ) && method_exists ( $replacement , '__toString' )) {
2026-03-11 14:01:26 +00:00
$replacement = ( string ) $replacement ;
2025-10-30 02:07:15 +00:00
} elseif ( is_scalar ( $replacement )) {
2026-03-11 14:01:26 +00:00
$replacement = ( string ) $replacement ;
2025-10-30 02:07:15 +00:00
} else {
throw new Exception ( Exception :: GENERAL_SERVER_ERROR , " The server encountered an error while parsing the label: $label . Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose " );
}
2025-10-24 20:42:10 +00:00
}
$label = \str_replace ( $find , $replacement , $label );
2022-08-16 12:28:30 +00:00
}
}
2026-03-11 14:01:26 +00:00
2022-08-16 12:28:30 +00:00
return $label ;
};
2019-11-29 18:23:29 +00:00
2026-02-04 05:30:22 +00:00
Http :: init ()
2024-02-16 16:26:50 +00:00
-> groups ([ 'api' ])
-> inject ( 'utopia' )
-> inject ( 'request' )
2024-12-12 10:30:26 +00:00
-> inject ( 'dbForPlatform' )
2024-11-20 04:29:57 +00:00
-> inject ( 'dbForProject' )
2025-01-12 11:28:38 +00:00
-> inject ( 'queueForAudits' )
2024-02-16 16:26:50 +00:00
-> inject ( 'project' )
-> inject ( 'user' )
-> inject ( 'session' )
-> inject ( 'servers' )
-> inject ( 'mode' )
2024-09-04 02:16:18 +00:00
-> inject ( 'team' )
2025-02-11 07:56:39 +00:00
-> inject ( 'apiKey' )
2026-01-07 07:04:28 +00:00
-> inject ( 'authorization' )
2026-03-16 02:35:03 +00:00
-> action ( function ( Http $utopia , Request $request , Database $dbForPlatform , Database $dbForProject , Audit $queueForAudits , Document $project , User $user , ? Document $session , array $servers , string $mode , Document $team , ? Key $apiKey , Authorization $authorization ) {
2024-02-16 16:26:50 +00:00
$route = $utopia -> getRoute ();
2026-04-07 09:10:18 +00:00
if ( $route === null ) {
throw new AppwriteException ( AppwriteException :: GENERAL_ROUTE_NOT_FOUND );
}
2024-10-08 07:54:40 +00:00
2025-11-04 06:08:35 +00:00
/**
* Handle user authentication and session validation .
*
* This function follows a series of steps to determine the appropriate user session
* based on cookies , headers , and JWT tokens .
*
* Process :
*
* Project & Role Validation :
* 1. Check if the project is empty . If so , throw an exception .
* 2. Get the roles configuration .
* 3. Determine the role for the user based on the user document .
* 4. Get the scopes for the role .
*
* API Key Authentication :
* 5. If there is an API key :
* - Verify no user session exists simultaneously
* - Check if key is expired
* - Set role and scopes from API key
* - Handle special app role case
* - For standard keys , update last accessed time
*
* User Activity :
* 6. If the project is not the console and user is not admin :
* - Update user ' s last activity timestamp
*
* Access Control :
* 7. Get the method from the route
* 8. Validate namespace permissions
* 9. Validate scope permissions
* 10. Check if user is blocked
*
* Security Checks :
* 11. Verify password status ( check if reset required )
* 12. Validate MFA requirements :
* - Check if MFA is enabled
* - Verify email status
* - Verify phone status
* - Verify authenticator status
* 13. Handle Multi - Factor Authentication :
* - Check remaining required factors
* - Validate factor completion
* - Throw exception if factors incomplete
*/
// Step 1: Check if project is empty
2024-02-16 17:58:51 +00:00
if ( $project -> isEmpty ()) {
throw new Exception ( Exception :: PROJECT_NOT_FOUND );
}
2025-11-04 06:08:35 +00:00
// Step 2: Get roles configuration
2024-07-23 22:47:46 +00:00
$roles = Config :: getParam ( 'roles' , []);
2025-02-11 04:49:22 +00:00
2025-11-04 06:08:35 +00:00
// Step 3: Determine role for user
// TODO get scopes from the identity instead of the user roles config. The identity will containn the scopes the user authorized for the access token.
2025-02-11 04:49:22 +00:00
$role = $user -> isEmpty ()
2024-02-16 16:26:50 +00:00
? Role :: guests () -> toString ()
: Role :: users () -> toString ();
2025-11-04 06:08:35 +00:00
// Step 4: Get scopes for the role
2024-07-23 19:49:11 +00:00
$scopes = $roles [ $role ][ 'scopes' ];
2024-02-16 16:26:50 +00:00
2025-11-04 06:08:35 +00:00
// Step 5: API Key Authentication
2026-03-11 14:01:26 +00:00
if ( ! empty ( $apiKey )) {
2025-11-04 06:08:35 +00:00
// Check if key is expired
2025-02-12 08:22:08 +00:00
if ( $apiKey -> isExpired ()) {
throw new Exception ( Exception :: PROJECT_KEY_EXPIRED );
2024-05-16 08:39:15 +00:00
}
2024-05-14 11:58:31 +00:00
2025-11-04 06:08:35 +00:00
// Set role and scopes from API key
2025-02-12 07:34:38 +00:00
$role = $apiKey -> getRole ();
2025-02-11 07:56:39 +00:00
$scopes = $apiKey -> getScopes ();
2024-02-16 16:26:50 +00:00
2025-11-04 06:08:35 +00:00
// Handle special app role case
if ( $apiKey -> getRole () === User :: ROLE_APPS ) {
2026-02-16 15:14:43 +00:00
// Disable authorization checks for project API keys
2026-02-16 16:39:12 +00:00
if (( $apiKey -> getType () === API_KEY_STANDARD || $apiKey -> getType () === API_KEY_DYNAMIC ) && $apiKey -> getProjectId () === $project -> getId ()) {
2026-02-16 15:14:43 +00:00
$authorization -> setDefaultStatus ( false );
}
2025-09-13 22:58:21 +00:00
2025-11-04 06:08:35 +00:00
$user = new User ([
2025-02-12 08:25:53 +00:00
'$id' => '' ,
'status' => true ,
2026-03-19 03:55:22 +00:00
'type' => ACTIVITY_TYPE_KEY_PROJECT ,
2025-02-12 08:25:53 +00:00
'email' => 'app.' . $project -> getId () . '@service.' . $request -> getHostname (),
'password' => '' ,
'name' => $apiKey -> getName (),
]);
2024-05-06 08:35:27 +00:00
2025-02-12 07:34:38 +00:00
$queueForAudits -> setUser ( $user );
}
2025-01-12 11:28:38 +00:00
2025-11-04 06:08:35 +00:00
// For standard keys, update last accessed time
2025-12-27 18:08:12 +00:00
if ( \in_array ( $apiKey -> getType (), [ API_KEY_STANDARD , API_KEY_ORGANIZATION , API_KEY_ACCOUNT ])) {
2025-12-29 12:24:26 +00:00
$dbKey = null ;
2026-03-11 14:01:26 +00:00
if ( ! empty ( $apiKey -> getProjectId ())) {
2025-12-27 17:44:01 +00:00
$dbKey = $project -> find (
key : 'secret' ,
find : $request -> getHeader ( 'x-appwrite-key' , '' ),
subject : 'keys'
);
2026-03-11 14:01:26 +00:00
} elseif ( ! empty ( $apiKey -> getUserId ())) {
2025-12-27 17:44:01 +00:00
$dbKey = $user -> find (
key : 'secret' ,
find : $request -> getHeader ( 'x-appwrite-key' , '' ),
subject : 'keys'
);
2026-03-11 14:01:26 +00:00
} elseif ( ! empty ( $apiKey -> getTeamId ())) {
2025-12-27 17:44:01 +00:00
$dbKey = $team -> find (
key : 'secret' ,
find : $request -> getHeader ( 'x-appwrite-key' , '' ),
subject : 'keys'
);
}
2024-05-06 09:55:59 +00:00
2025-04-15 22:50:53 +00:00
if ( ! $dbKey ) {
throw new Exception ( Exception :: USER_UNAUTHORIZED );
}
2024-05-06 09:55:59 +00:00
2025-12-27 17:44:01 +00:00
$updates = new Document ();
2025-04-25 10:18:04 +00:00
$accessedAt = $dbKey -> getAttribute ( 'accessedAt' , 0 );
2024-05-06 09:55:59 +00:00
2025-04-15 22:50:53 +00:00
if ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_KEY_ACCESS )) > $accessedAt ) {
2025-12-27 17:44:01 +00:00
$updates -> setAttribute ( 'accessedAt' , DateTime :: now ());
2025-04-15 22:50:53 +00:00
}
2024-05-06 08:35:27 +00:00
2025-04-15 22:50:53 +00:00
$sdkValidator = new WhiteList ( $servers , true );
$sdk = $request -> getHeader ( 'x-sdk-name' , 'UNKNOWN' );
2025-01-12 11:28:38 +00:00
2025-04-25 10:18:04 +00:00
if ( $sdk !== 'UNKNOWN' && $sdkValidator -> isValid ( $sdk )) {
2025-04-15 22:50:53 +00:00
$sdks = $dbKey -> getAttribute ( 'sdks' , []);
2024-05-06 09:55:59 +00:00
2026-03-11 14:01:26 +00:00
if ( ! in_array ( $sdk , $sdks )) {
2025-04-15 22:50:53 +00:00
$sdks [] = $sdk ;
2025-01-12 11:28:38 +00:00
2025-12-27 17:44:01 +00:00
$updates -> setAttribute ( 'sdks' , $sdks );
$updates -> setAttribute ( 'accessedAt' , Datetime :: now ());
2024-05-06 09:55:59 +00:00
}
2024-02-16 16:26:50 +00:00
}
2025-04-15 22:50:53 +00:00
2026-03-11 14:01:26 +00:00
if ( ! $updates -> isEmpty ()) {
2026-01-21 15:05:43 +00:00
$dbForPlatform -> getAuthorization () -> skip ( fn () => $dbForPlatform -> updateDocument ( 'keys' , $dbKey -> getId (), $updates ));
2026-01-15 15:16:09 +00:00
2026-03-11 14:01:26 +00:00
if ( ! empty ( $apiKey -> getProjectId ())) {
2026-01-21 15:05:43 +00:00
$dbForPlatform -> getAuthorization () -> skip ( fn () => $dbForPlatform -> purgeCachedDocument ( 'projects' , $project -> getId ()));
2026-03-11 14:01:26 +00:00
} elseif ( ! empty ( $apiKey -> getUserId ())) {
2026-01-21 15:05:43 +00:00
$dbForPlatform -> getAuthorization () -> skip ( fn () => $dbForPlatform -> purgeCachedDocument ( 'users' , $user -> getId ()));
2026-03-11 14:01:26 +00:00
} elseif ( ! empty ( $apiKey -> getTeamId ())) {
2026-01-21 15:05:43 +00:00
$dbForPlatform -> getAuthorization () -> skip ( fn () => $dbForPlatform -> purgeCachedDocument ( 'teams' , $team -> getId ()));
2025-04-15 22:50:53 +00:00
}
2024-02-16 16:26:50 +00:00
}
2025-04-15 22:50:53 +00:00
2026-03-19 03:55:22 +00:00
$userClone = clone $user ;
$userClone -> setAttribute ( 'type' , match ( $apiKey -> getType ()) {
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT ,
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT ,
API_KEY_ORGANIZATION => ACTIVITY_TYPE_KEY_ORGANIZATION ,
2026-03-19 11:59:35 +00:00
default => ACTIVITY_TYPE_KEY_PROJECT ,
2026-03-19 03:55:22 +00:00
});
$queueForAudits -> setUser ( $userClone );
2024-02-16 16:26:50 +00:00
}
2026-02-16 16:48:26 +00:00
// Apply permission
if ( $apiKey -> getType () === API_KEY_ORGANIZATION ) {
2026-02-16 15:14:43 +00:00
$authorization -> addRole ( Role :: team ( $team -> getId ()) -> toString ());
$authorization -> addRole ( Role :: team ( $team -> getId (), 'owner' ) -> toString ());
2026-02-16 16:48:26 +00:00
} elseif ( $apiKey -> getType () === API_KEY_ACCOUNT ) {
$authorization -> addRole ( Role :: user ( $user -> getId ()) -> toString ());
$authorization -> addRole ( Role :: users () -> toString ());
2026-02-16 15:14:43 +00:00
2026-02-16 17:08:52 +00:00
if ( $user -> getAttribute ( 'emailVerification' , false ) || $user -> getAttribute ( 'phoneVerification' , false )) {
2026-02-16 16:48:26 +00:00
$authorization -> addRole ( Role :: user ( $user -> getId (), Roles :: DIMENSION_VERIFIED ) -> toString ());
$authorization -> addRole ( Role :: users ( Roles :: DIMENSION_VERIFIED ) -> toString ());
} else {
$authorization -> addRole ( Role :: user ( $user -> getId (), Roles :: DIMENSION_UNVERIFIED ) -> toString ());
$authorization -> addRole ( Role :: users ( Roles :: DIMENSION_UNVERIFIED ) -> toString ());
}
2026-02-16 16:59:44 +00:00
foreach ( \array_filter ( $user -> getAttribute ( 'memberships' , []), fn ( $membership ) => ( $membership [ 'confirm' ] ? ? false ) === true ) as $nodeMembership ) {
2026-02-16 16:48:26 +00:00
$authorization -> addRole ( Role :: team ( $nodeMembership [ 'teamId' ]) -> toString ());
$authorization -> addRole ( Role :: member ( $nodeMembership -> getId ()) -> toString ());
2026-02-16 17:08:52 +00:00
foreach (( $nodeMembership [ 'roles' ] ? ? []) as $nodeRole ) {
2026-02-16 16:48:26 +00:00
$authorization -> addRole ( Role :: team ( $nodeMembership [ 'teamId' ], $nodeRole ) -> toString ());
}
}
foreach ( $user -> getAttribute ( 'labels' , []) as $nodeLabel ) {
$authorization -> addRole ( 'label:' . $nodeLabel );
}
2024-02-16 16:26:50 +00:00
}
2025-02-12 07:34:38 +00:00
} // Admin User Authentication
2026-03-11 14:01:26 +00:00
elseif (( $project -> getId () === 'console' && ! $team -> isEmpty () && ! $user -> isEmpty ()) || ( $project -> getId () !== 'console' && ! $user -> isEmpty () && $mode === APP_MODE_ADMIN )) {
2024-09-04 02:16:18 +00:00
$teamId = $team -> getId ();
2024-07-23 19:49:11 +00:00
$adminRoles = [];
$memberships = $user -> getAttribute ( 'memberships' , []);
foreach ( $memberships as $membership ) {
2024-07-23 22:47:46 +00:00
if ( $membership -> getAttribute ( 'confirm' , false ) === true && $membership -> getAttribute ( 'teamId' ) === $teamId ) {
2024-07-23 19:49:11 +00:00
$adminRoles = $membership -> getAttribute ( 'roles' , []);
break ;
}
}
2024-02-16 16:26:50 +00:00
2024-07-23 19:49:11 +00:00
if ( empty ( $adminRoles )) {
throw new Exception ( Exception :: USER_UNAUTHORIZED );
}
2024-02-16 16:26:50 +00:00
2026-02-19 11:18:39 +00:00
$projectId = $project -> getId ();
if ( $projectId === 'console' && str_starts_with ( $route -> getPath (), '/v1/projects/:projectId' )) {
$uri = $request -> getURI ();
$projectId = explode ( '/' , $uri )[ 3 ];
2024-07-23 22:47:46 +00:00
}
2024-10-08 07:54:40 +00:00
2026-02-19 11:18:39 +00:00
// Base scopes for admin users to allow listing teams and projects.
// Useful for those who have project-specific roles but don't have team-wide role.
$scopes = [ 'teams.read' , 'projects.read' ];
foreach ( $adminRoles as $adminRole ) {
2026-03-11 14:01:26 +00:00
$isTeamWideRole = ! str_starts_with ( $adminRole , 'project-' );
2026-02-19 11:18:39 +00:00
$isProjectSpecificRole = $projectId !== 'console' && str_starts_with ( $adminRole , 'project-' . $projectId );
if ( $isTeamWideRole || $isProjectSpecificRole ) {
$role = match ( str_starts_with ( $adminRole , 'project-' )) {
true => substr ( $adminRole , strrpos ( $adminRole , '-' ) + 1 ),
false => $adminRole ,
};
$roleScopes = $roles [ $role ][ 'scopes' ] ? ? [];
$scopes = \array_merge ( $scopes , $roleScopes );
$authorization -> addRole ( $role );
}
}
/**
* For console projects resource , we use platform DB .
* Enabling authorization restricts admin user to the projects they have access to .
*/
if ( $project -> getId () === 'console' && ( $route -> getPath () === '/v1/projects' || $route -> getPath () === '/v1/projects/:projectId' )) {
$authorization -> setDefaultStatus ( true );
} else {
// Otherwise, disable authorization checks.
$authorization -> setDefaultStatus ( false );
}
2024-07-23 19:49:11 +00:00
}
2024-07-23 19:54:52 +00:00
2024-07-23 19:49:11 +00:00
$scopes = \array_unique ( $scopes );
2024-02-16 16:26:50 +00:00
2026-03-13 07:21:02 +00:00
// Intentional: impersonators get users.read so they can discover a target user
// before impersonation starts, and keep that access while impersonating.
2026-03-13 07:06:07 +00:00
if (
! $user -> isEmpty ()
&& (
$user -> getAttribute ( 'impersonator' , false )
|| $user -> getAttribute ( 'impersonatorUserId' )
)
) {
2026-03-12 18:08:25 +00:00
$scopes [] = 'users.read' ;
$scopes = \array_unique ( $scopes );
}
2026-01-07 07:04:28 +00:00
$authorization -> addRole ( $role );
foreach ( $user -> getRoles ( $authorization ) as $authRole ) {
$authorization -> addRole ( $authRole );
2024-02-16 16:26:50 +00:00
}
2026-02-19 11:18:39 +00:00
/**
* We disable authorization checks above to ensure other endpoints ( list teams , members , etc . ) will continue working .
* But , for actions on resources ( sites , functions , etc . ) in a non - console project , we explicitly check
* whether the admin user has necessary permission on the project ( sites , functions , etc . don ' t have permissions associated to them ) .
*/
2026-03-11 14:01:26 +00:00
if ( empty ( $apiKey ) && ! $user -> isEmpty () && $project -> getId () !== 'console' && $mode === APP_MODE_ADMIN ) {
2026-02-19 11:18:39 +00:00
$input = new Input ( Database :: PERMISSION_READ , $project -> getPermissionsByType ( Database :: PERMISSION_READ ));
$initialStatus = $authorization -> getStatus ();
$authorization -> enable ();
2026-03-11 14:01:26 +00:00
if ( ! $authorization -> isValid ( $input )) {
2026-02-19 11:18:39 +00:00
throw new Exception ( Exception :: PROJECT_NOT_FOUND );
}
$authorization -> setStatus ( $initialStatus );
}
2025-11-04 06:08:35 +00:00
// Step 6: Update project and user last activity
2026-03-11 14:01:26 +00:00
if ( ! $project -> isEmpty () && $project -> getId () !== 'console' ) {
2025-04-17 14:13:09 +00:00
$accessedAt = $project -> getAttribute ( 'accessedAt' , 0 );
2024-11-20 04:29:57 +00:00
if ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_PROJECT_ACCESS )) > $accessedAt ) {
2026-03-05 10:01:37 +00:00
$authorization -> skip ( fn () => $dbForPlatform -> updateDocument ( 'projects' , $project -> getId (), new Document ([
'accessedAt' => DateTime :: now ()
])));
2024-11-20 04:29:57 +00:00
}
}
2026-03-11 14:01:26 +00:00
if ( ! empty ( $user -> getId ())) {
2026-03-13 08:18:39 +00:00
$impersonatorUserId = $user -> getAttribute ( 'impersonatorUserId' );
2025-04-17 14:13:09 +00:00
$accessedAt = $user -> getAttribute ( 'accessedAt' , 0 );
2026-03-13 08:18:39 +00:00
// Skip updating accessedAt for impersonated requests so we don't attribute activity to the target user.
2026-03-13 20:48:41 +00:00
if ( ! $impersonatorUserId && DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_USER_ACCESS )) > $accessedAt ) {
2024-11-20 04:29:57 +00:00
$user -> setAttribute ( 'accessedAt' , DateTime :: now ());
2026-03-11 14:01:26 +00:00
if ( $project -> getId () !== 'console' && $mode !== APP_MODE_ADMIN ) {
2026-03-06 09:42:07 +00:00
$dbForProject -> updateDocument ( 'users' , $user -> getId (), new Document ([
'accessedAt' => $user -> getAttribute ( 'accessedAt' )
]));
2024-11-20 04:29:57 +00:00
} else {
2026-03-06 09:42:07 +00:00
$authorization -> skip ( fn () => $dbForPlatform -> updateDocument ( 'users' , $user -> getId (), new Document ([
'accessedAt' => $user -> getAttribute ( 'accessedAt' )
])));
2024-11-20 04:29:57 +00:00
}
}
}
2025-11-04 06:08:35 +00:00
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
2024-12-16 05:59:01 +00:00
/**
2025-02-11 04:49:22 +00:00
* @ var ? Method $method
2024-12-16 05:59:01 +00:00
*/
$method = $route -> getLabel ( 'sdk' , false );
2025-03-27 08:03:28 +00:00
// Take the first method if there's more than one,
// namespace can not differ between methods on the same route
2025-02-11 04:49:22 +00:00
if ( \is_array ( $method )) {
2025-01-10 05:23:04 +00:00
$method = $method [ 0 ];
}
2026-03-11 14:01:26 +00:00
if ( ! empty ( $method )) {
2024-12-16 05:59:01 +00:00
$namespace = $method -> getNamespace ();
2025-02-11 04:49:22 +00:00
2024-02-16 16:26:50 +00:00
if (
2024-12-16 05:59:01 +00:00
array_key_exists ( $namespace , $project -> getAttribute ( 'services' , []))
2026-03-11 14:01:26 +00:00
&& ! $project -> getAttribute ( 'services' , [])[ $namespace ]
2026-03-16 06:26:07 +00:00
&& ! ( $user -> isPrivileged ( $authorization -> getRoles ()) || $user -> isApp ( $authorization -> getRoles ()))
2024-02-16 16:26:50 +00:00
) {
throw new Exception ( Exception :: GENERAL_SERVICE_DISABLED );
}
}
2025-11-04 06:08:35 +00:00
// Step 9: Validate scope permissions
2026-03-11 14:01:26 +00:00
$allowed = ( array ) $route -> getLabel ( 'scope' , 'none' );
2025-08-27 02:43:34 +00:00
if ( empty ( \array_intersect ( $allowed , $scopes ))) {
throw new Exception ( Exception :: GENERAL_UNAUTHORIZED_SCOPE , $user -> getAttribute ( 'email' , 'User' ) . ' (role: ' . \strtolower ( $roles [ $role ][ 'label' ]) . ') missing scopes (' . \json_encode ( $allowed ) . ')' );
2024-02-16 16:26:50 +00:00
}
2025-11-04 06:08:35 +00:00
// Step 10: Check if user is blocked
2026-03-11 14:01:26 +00:00
if ( $user -> getAttribute ( 'status' ) === false ) { // Account is blocked
2024-02-16 16:26:50 +00:00
throw new Exception ( Exception :: USER_BLOCKED );
}
2025-11-04 06:08:35 +00:00
// Step 11: Verify password status
2024-02-16 16:26:50 +00:00
if ( $user -> getAttribute ( 'reset' )) {
throw new Exception ( Exception :: USER_PASSWORD_RESET_REQUIRED );
}
2025-11-04 06:08:35 +00:00
// Step 12: Validate MFA requirements
2024-04-17 09:10:33 +00:00
$mfaEnabled = $user -> getAttribute ( 'mfa' , false );
$hasVerifiedEmail = $user -> getAttribute ( 'emailVerification' , false );
$hasVerifiedPhone = $user -> getAttribute ( 'phoneVerification' , false );
$hasVerifiedAuthenticator = TOTP :: getAuthenticatorFromUser ( $user ) ? -> getAttribute ( 'verified' ) ? ? false ;
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator ;
$minimumFactors = ( $mfaEnabled && $hasMoreFactors ) ? 2 : 1 ;
2025-11-04 06:08:35 +00:00
// Step 13: Handle Multi-Factor Authentication
2026-03-11 14:01:26 +00:00
if ( ! in_array ( 'mfa' , $route -> getGroups ())) {
2024-04-23 23:43:53 +00:00
if ( $session && \count ( $session -> getAttribute ( 'factors' , [])) < $minimumFactors ) {
2024-04-17 09:10:33 +00:00
throw new Exception ( Exception :: USER_MORE_FACTORS_REQUIRED );
2024-02-16 16:26:50 +00:00
}
}
});
2026-02-04 05:30:22 +00:00
Http :: init ()
2022-08-02 01:10:48 +00:00
-> groups ([ 'api' ])
2022-07-22 06:00:42 +00:00
-> inject ( 'utopia' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'user' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
2023-12-07 10:25:19 +00:00
-> inject ( 'queueForMessaging' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForAudits' )
-> inject ( 'queueForDeletes' )
-> inject ( 'queueForDatabase' )
2024-02-20 11:40:55 +00:00
-> inject ( 'queueForBuilds' )
2026-03-11 14:01:26 +00:00
-> inject ( 'usage' )
2025-12-07 20:29:45 +00:00
-> inject ( 'queueForFunctions' )
-> inject ( 'queueForMails' )
2022-07-22 06:00:42 +00:00
-> inject ( 'dbForProject' )
2024-12-20 14:44:50 +00:00
-> inject ( 'timelimit' )
2024-01-04 06:53:22 +00:00
-> inject ( 'resourceToken' )
2022-07-22 06:00:42 +00:00
-> inject ( 'mode' )
2025-02-12 11:19:51 +00:00
-> inject ( 'apiKey' )
2025-04-03 02:44:29 +00:00
-> inject ( 'plan' )
2024-11-22 04:21:03 +00:00
-> inject ( 'devKey' )
2025-08-01 10:22:27 +00:00
-> inject ( 'telemetry' )
2025-12-07 20:29:45 +00:00
-> inject ( 'platform' )
2026-01-07 07:04:28 +00:00
-> inject ( 'authorization' )
2026-03-16 02:35:03 +00:00
-> action ( function ( Http $utopia , Request $request , Response $response , Document $project , User $user , Event $queueForEvents , Messaging $queueForMessaging , Audit $queueForAudits , Delete $queueForDeletes , EventDatabase $queueForDatabase , Build $queueForBuilds , Context $usage , Func $queueForFunctions , Mail $queueForMails , Database $dbForProject , callable $timelimit , Document $resourceToken , string $mode , ? Key $apiKey , array $plan , Document $devKey , Telemetry $telemetry , array $platform , Authorization $authorization ) {
2022-07-22 06:00:42 +00:00
2026-03-16 02:15:26 +00:00
$response -> setUser ( $user );
2026-03-16 06:26:07 +00:00
$request -> setUser ( $user );
2026-03-16 02:15:26 +00:00
2023-02-19 11:04:12 +00:00
$route = $utopia -> getRoute ();
2026-04-07 09:03:43 +00:00
if ( $route === null ) {
throw new AppwriteException ( AppwriteException :: GENERAL_ROUTE_NOT_FOUND );
}
2026-03-19 15:00:42 +00:00
$path = $route -> getMatchedPath ();
$databaseType = match ( true ) {
str_contains ( $path , '/documentsdb' ) => DATABASE_TYPE_DOCUMENTSDB ,
str_contains ( $path , '/vectorsdb' ) => DATABASE_TYPE_VECTORSDB ,
default => '' ,
};
2022-07-22 06:00:42 +00:00
2024-03-04 22:12:54 +00:00
if (
array_key_exists ( 'rest' , $project -> getAttribute ( 'apis' , []))
2026-03-11 14:01:26 +00:00
&& ! $project -> getAttribute ( 'apis' , [])[ 'rest' ]
2026-03-16 06:26:07 +00:00
&& ! ( $user -> isPrivileged ( $authorization -> getRoles ()) || $user -> isApp ( $authorization -> getRoles ()))
2024-03-04 22:12:54 +00:00
) {
throw new AppwriteException ( AppwriteException :: GENERAL_API_DISABLED );
}
2022-08-19 04:20:19 +00:00
/*
2022-07-22 06:00:42 +00:00
* Abuse Check
*/
2025-09-16 16:13:38 +00:00
2022-08-11 23:53:52 +00:00
$abuseKeyLabel = $route -> getLabel ( 'abuse-key' , 'url:{url},ip:{ip}' );
$timeLimitArray = [];
2025-09-16 16:13:38 +00:00
2026-03-11 14:01:26 +00:00
$abuseKeyLabel = ( ! is_array ( $abuseKeyLabel )) ? [ $abuseKeyLabel ] : $abuseKeyLabel ;
2021-06-07 05:17:29 +00:00
2022-08-11 23:53:52 +00:00
foreach ( $abuseKeyLabel as $abuseKey ) {
2023-11-16 17:34:38 +00:00
$start = $request -> getContentRangeStart ();
$end = $request -> getContentRangeEnd ();
2024-12-20 14:44:50 +00:00
$timeLimit = $timelimit ( $abuseKey , $route -> getLabel ( 'abuse-limit' , 0 ), $route -> getLabel ( 'abuse-time' , 3600 ));
2022-08-11 23:53:52 +00:00
$timeLimit
2024-02-12 01:18:19 +00:00
-> setParam ( '{projectId}' , $project -> getId ())
2023-05-23 13:43:03 +00:00
-> setParam ( '{userId}' , $user -> getId ())
-> setParam ( '{userAgent}' , $request -> getUserAgent ( '' ))
-> setParam ( '{ip}' , $request -> getIP ())
-> setParam ( '{url}' , $request -> getHostname () . $route -> getPath ())
2023-11-16 17:34:38 +00:00
-> setParam ( '{method}' , $request -> getMethod ())
2026-03-11 14:01:26 +00:00
-> setParam ( '{chunkId}' , ( int ) ( $start / ( $end + 1 - $start )));
2022-08-11 23:53:52 +00:00
$timeLimitArray [] = $timeLimit ;
}
2021-06-07 05:17:29 +00:00
2022-07-22 06:00:42 +00:00
$closestLimit = null ;
2021-06-07 05:17:29 +00:00
2026-01-07 07:04:28 +00:00
$roles = $authorization -> getRoles ();
2026-03-16 06:26:07 +00:00
$isPrivilegedUser = $user -> isPrivileged ( $roles );
$isAppUser = $user -> isApp ( $roles );
2021-06-07 05:17:29 +00:00
2022-07-22 06:00:42 +00:00
foreach ( $timeLimitArray as $timeLimit ) {
foreach ( $request -> getParams () as $key => $value ) { // Set request params as potential abuse keys
2026-03-11 14:01:26 +00:00
if ( ! empty ( $value )) {
2022-07-22 06:00:42 +00:00
$timeLimit -> setParam ( '{param-' . $key . '}' , ( \is_array ( $value )) ? \json_encode ( $value ) : $value );
}
2021-11-09 10:21:24 +00:00
}
2021-06-07 05:17:29 +00:00
2022-07-22 06:00:42 +00:00
$abuse = new Abuse ( $timeLimit );
2022-08-13 03:21:50 +00:00
$remaining = $timeLimit -> remaining ();
2025-09-16 16:13:38 +00:00
2022-08-13 03:21:50 +00:00
$limit = $timeLimit -> limit ();
2024-12-20 14:44:50 +00:00
$time = $timeLimit -> time () + $route -> getLabel ( 'abuse-time' , 3600 );
2025-09-16 16:13:38 +00:00
2022-08-13 03:21:50 +00:00
if ( $limit && ( $remaining < $closestLimit || is_null ( $closestLimit ))) {
$closestLimit = $remaining ;
2022-07-22 06:00:42 +00:00
$response
2022-08-13 03:21:50 +00:00
-> addHeader ( 'X-RateLimit-Limit' , $limit )
-> addHeader ( 'X-RateLimit-Remaining' , $remaining )
2024-04-23 23:43:53 +00:00
-> addHeader ( 'X-RateLimit-Reset' , $time );
2021-02-28 18:36:13 +00:00
}
2021-06-07 05:17:29 +00:00
2024-04-01 11:02:47 +00:00
$enabled = System :: getEnv ( '_APP_OPTIONS_ABUSE' , 'enabled' ) !== 'disabled' ;
2025-09-16 16:13:38 +00:00
2022-07-22 06:00:42 +00:00
if (
2022-08-31 03:50:53 +00:00
$enabled // Abuse is enabled
2026-03-11 14:01:26 +00:00
&& ! $isAppUser // User is not API key
&& ! $isPrivilegedUser // User is not an admin
2024-11-22 04:21:03 +00:00
&& $devKey -> isEmpty () // request doesn't not contain development key
2022-08-31 03:50:53 +00:00
&& $abuse -> check () // Route is rate-limited
) {
2022-08-08 14:44:07 +00:00
throw new Exception ( Exception :: GENERAL_RATE_LIMIT_EXCEEDED );
2021-02-28 18:36:13 +00:00
}
2021-11-09 10:21:24 +00:00
}
2021-01-05 12:22:20 +00:00
2025-12-07 20:29:45 +00:00
/**
* TODO : ( @ loks0n )
* Avoid mutating the message across file boundaries - it ' s difficult to reason about at scale .
*/
2022-11-16 05:30:57 +00:00
/*
* Background Jobs
*/
2022-12-20 16:11:30 +00:00
$queueForEvents
2022-07-22 06:00:42 +00:00
-> setEvent ( $route -> getLabel ( 'event' , '' ))
-> setProject ( $project )
2022-08-11 13:47:53 +00:00
-> setUser ( $user );
2022-04-04 06:30:07 +00:00
2022-12-20 16:11:30 +00:00
$queueForAudits
2022-07-22 06:00:42 +00:00
-> setMode ( $mode )
-> setUserAgent ( $request -> getUserAgent ( '' ))
-> setIP ( $request -> getIP ())
2025-01-03 10:00:55 +00:00
-> setHostname ( $request -> getHostname ())
2022-09-04 08:23:24 +00:00
-> setEvent ( $route -> getLabel ( 'audits.event' , '' ))
2025-01-12 11:28:38 +00:00
-> setProject ( $project );
2025-01-21 06:37:03 +00:00
/* If a session exists, use the user associated with the session */
2026-03-11 14:01:26 +00:00
if ( ! $user -> isEmpty ()) {
2025-01-21 06:41:20 +00:00
$userClone = clone $user ;
2025-01-14 08:17:01 +00:00
// $user doesn't support `type` and can cause unintended effects.
2026-03-22 02:31:46 +00:00
if ( empty ( $user -> getAttribute ( 'type' ))) {
$userClone -> setAttribute ( 'type' , $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER );
}
2025-01-21 06:41:20 +00:00
$queueForAudits -> setUser ( $userClone );
2025-01-12 11:28:38 +00:00
}
2021-02-28 18:36:13 +00:00
2025-12-07 20:29:45 +00:00
/* Auto-set projects */
2022-12-20 16:11:30 +00:00
$queueForDeletes -> setProject ( $project );
$queueForDatabase -> setProject ( $project );
2024-02-20 12:06:35 +00:00
$queueForMessaging -> setProject ( $project );
2025-12-07 20:29:45 +00:00
$queueForFunctions -> setProject ( $project );
$queueForBuilds -> setProject ( $project );
2026-02-16 13:16:55 +00:00
$queueForMails -> setProject ( $project );
2025-12-07 20:29:45 +00:00
/* Auto-set platforms */
$queueForFunctions -> setPlatform ( $platform );
$queueForBuilds -> setPlatform ( $platform );
$queueForMails -> setPlatform ( $platform );
2022-08-09 11:57:33 +00:00
$useCache = $route -> getLabel ( 'cache' , false );
2025-08-01 10:22:27 +00:00
$storageCacheOperationsCounter = $telemetry -> createCounter ( 'storage.cache.operations.load' );
2022-08-09 11:57:33 +00:00
if ( $useCache ) {
2025-04-03 02:44:29 +00:00
$route = $utopia -> match ( $request );
$isImageTransformation = $route -> getPath () === '/v1/storage/buckets/:bucketId/files/:fileId/preview' ;
2026-03-16 06:26:07 +00:00
$isDisabled = isset ( $plan [ 'imageTransformations' ]) && $plan [ 'imageTransformations' ] === - 1 && ! $user -> isPrivileged ( $authorization -> getRoles ());
2025-04-03 02:44:29 +00:00
2025-05-16 08:56:12 +00:00
$key = $request -> cacheIdentifier ();
2026-03-11 14:01:26 +00:00
$cacheLog = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'cache' , $key ));
2022-08-15 13:55:11 +00:00
$cache = new Cache (
new Filesystem ( APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project -> getId ())
);
2025-06-30 17:03:30 +00:00
$timestamp = 60 * 60 * 24 * 180 ; // Temporarily increase the TTL to 180 day to ensure files in the cache are still fetched.
2022-08-09 11:57:33 +00:00
$data = $cache -> load ( $key , $timestamp );
2022-11-10 10:08:01 +00:00
2026-03-11 14:01:26 +00:00
if ( ! empty ( $data ) && ! $cacheLog -> isEmpty ()) {
2025-04-11 14:52:19 +00:00
$parts = explode ( '/' , $cacheLog -> getAttribute ( 'resourceType' , '' ));
2022-11-10 10:08:01 +00:00
$type = $parts [ 0 ] ? ? null ;
2026-03-11 14:01:26 +00:00
if ( $type === 'bucket' && ( ! $isImageTransformation || ! $isDisabled )) {
2022-11-10 10:08:01 +00:00
$bucketId = $parts [ 1 ] ? ? null ;
2026-01-07 07:04:28 +00:00
$bucket = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'buckets' , $bucketId ));
2022-11-10 10:08:01 +00:00
2026-03-11 14:01:26 +00:00
$isToken = ! $resourceToken -> isEmpty () && $resourceToken -> getAttribute ( 'bucketInternalId' ) === $bucket -> getSequence ();
2026-03-16 06:26:07 +00:00
$isPrivilegedUser = $user -> isPrivileged ( $authorization -> getRoles ());
2023-08-16 21:58:25 +00:00
2026-03-11 14:01:26 +00:00
if ( $bucket -> isEmpty () || ( ! $bucket -> getAttribute ( 'enabled' ) && ! $isAppUser && ! $isPrivilegedUser )) {
2022-11-10 10:08:01 +00:00
throw new Exception ( Exception :: STORAGE_BUCKET_NOT_FOUND );
}
2026-03-11 14:01:26 +00:00
if ( ! $bucket -> getAttribute ( 'transformations' , true ) && ! $isAppUser && ! $isPrivilegedUser ) {
2025-10-28 08:43:38 +00:00
throw new Exception ( Exception :: STORAGE_BUCKET_TRANSFORMATIONS_DISABLED );
}
2022-11-10 10:08:01 +00:00
$fileSecurity = $bucket -> getAttribute ( 'fileSecurity' , false );
2026-01-07 07:04:28 +00:00
$valid = $authorization -> isValid ( new Input ( Database :: PERMISSION_READ , $bucket -> getRead ()));
2026-03-11 14:01:26 +00:00
if ( ! $fileSecurity && ! $valid && ! $isToken ) {
2022-11-10 10:08:01 +00:00
throw new Exception ( Exception :: USER_UNAUTHORIZED );
}
2024-02-25 08:12:28 +00:00
$parts = explode ( '/' , $cacheLog -> getAttribute ( 'resource' ));
2022-11-10 10:08:01 +00:00
$fileId = $parts [ 1 ] ? ? null ;
2026-03-11 14:01:26 +00:00
if ( $fileSecurity && ! $valid && ! $isToken ) {
2025-05-26 05:42:11 +00:00
$file = $dbForProject -> getDocument ( 'bucket_' . $bucket -> getSequence (), $fileId );
2022-11-10 10:08:01 +00:00
} else {
2026-01-07 07:04:28 +00:00
$file = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'bucket_' . $bucket -> getSequence (), $fileId ));
2022-11-10 10:08:01 +00:00
}
2026-03-11 14:01:26 +00:00
if ( ! $resourceToken -> isEmpty () && $resourceToken -> getAttribute ( 'fileInternalId' ) !== $file -> getSequence ()) {
2024-01-04 06:53:22 +00:00
throw new Exception ( Exception :: USER_UNAUTHORIZED );
}
2022-11-10 10:08:01 +00:00
if ( $file -> isEmpty ()) {
throw new Exception ( Exception :: STORAGE_FILE_NOT_FOUND );
}
2026-03-11 14:01:26 +00:00
// Do not update transformedAt if it's a console user
2026-03-16 06:26:07 +00:00
if ( ! $user -> isPrivileged ( $authorization -> getRoles ())) {
2025-03-03 13:19:49 +00:00
$transformedAt = $file -> getAttribute ( 'transformedAt' , '' );
if ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_PROJECT_ACCESS )) > $transformedAt ) {
$file -> setAttribute ( 'transformedAt' , DateTime :: now ());
2026-03-06 09:42:07 +00:00
$authorization -> skip ( fn () => $dbForProject -> updateDocument ( 'bucket_' . $file -> getAttribute ( 'bucketInternalId' ), $file -> getId (), new Document ([
'transformedAt' => $file -> getAttribute ( 'transformedAt' )
])));
2025-03-03 13:19:49 +00:00
}
2025-01-28 14:49:55 +00:00
}
2022-11-10 10:08:01 +00:00
}
2022-08-09 13:43:37 +00:00
2026-04-09 16:05:14 +00:00
$accessedAt = $cacheLog -> getAttribute ( 'accessedAt' , '' );
if ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_CACHE_UPDATE )) > $accessedAt ) {
$authorization -> skip ( fn () => $dbForProject -> updateDocument ( 'cache' , $cacheLog -> getId (), new Document ([
'accessedAt' => DateTime :: now (),
])));
// Refresh the filesystem file's mtime so TTL-based expiry in cache->load() stays valid
$cache -> save ( $key , $data );
}
2022-08-09 13:43:37 +00:00
$response
2024-10-22 09:38:05 +00:00
-> addHeader ( 'Cache-Control' , sprintf ( 'private, max-age=%d' , $timestamp ))
2022-08-09 13:43:37 +00:00
-> addHeader ( 'X-Appwrite-Cache' , 'hit' )
2025-04-03 02:44:29 +00:00
-> setContentType ( $cacheLog -> getAttribute ( 'mimeType' ));
2025-08-01 10:22:27 +00:00
$storageCacheOperationsCounter -> add ( 1 , [ 'result' => 'hit' ]);
2026-03-11 14:01:26 +00:00
if ( ! $isImageTransformation || ! $isDisabled ) {
2025-04-03 02:44:29 +00:00
$response -> send ( $data );
}
2022-08-09 13:43:37 +00:00
} else {
2025-08-01 10:22:27 +00:00
$storageCacheOperationsCounter -> add ( 1 , [ 'result' => 'miss' ]);
2024-06-13 06:40:03 +00:00
$response
-> addHeader ( 'Cache-Control' , 'no-cache, no-store, must-revalidate' )
-> addHeader ( 'Pragma' , 'no-cache' )
2024-10-22 09:38:05 +00:00
-> addHeader ( 'Expires' , '0' )
2025-02-12 07:34:38 +00:00
-> addHeader ( 'X-Appwrite-Cache' , 'miss' );
2022-08-09 13:43:37 +00:00
}
}
2022-07-22 06:00:42 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: init ()
2024-02-27 09:08:39 +00:00
-> groups ([ 'session' ])
-> inject ( 'user' )
-> inject ( 'request' )
2026-03-18 03:40:11 +00:00
-> action ( function ( User $user , Request $request ) {
2024-02-27 09:08:39 +00:00
if ( \str_contains ( $request -> getURI (), 'oauth2' )) {
return ;
}
2026-03-11 14:01:26 +00:00
if ( ! $user -> isEmpty ()) {
2024-02-27 09:08:39 +00:00
throw new Exception ( Exception :: USER_SESSION_ALREADY_EXISTS );
}
});
2023-05-23 13:43:03 +00:00
/**
* Limit user session
*
* Delete older sessions if the number of sessions have crossed
* the session limit set for the project
*/
2026-02-04 05:30:22 +00:00
Http :: shutdown ()
2023-05-23 13:43:03 +00:00
-> groups ([ 'session' ])
-> inject ( 'utopia' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
-> inject ( 'dbForProject' )
2026-02-04 05:30:22 +00:00
-> action ( function ( Http $utopia , Request $request , Response $response , Document $project , Database $dbForProject ) {
2023-05-23 13:43:03 +00:00
$sessionLimit = $project -> getAttribute ( 'auths' , [])[ 'maxSessions' ] ? ? APP_LIMIT_USER_SESSIONS_DEFAULT ;
$session = $response -> getPayload ();
$userId = $session [ 'userId' ] ? ? '' ;
if ( empty ( $userId )) {
return ;
}
$user = $dbForProject -> getDocument ( 'users' , $userId );
if ( $user -> isEmpty ()) {
return ;
}
$sessions = $user -> getAttribute ( 'sessions' , []);
$count = \count ( $sessions );
if ( $count <= $sessionLimit ) {
return ;
}
for ( $i = 0 ; $i < ( $count - $sessionLimit ); $i ++ ) {
$session = array_shift ( $sessions );
$dbForProject -> deleteDocument ( 'sessions' , $session -> getId ());
}
2024-02-27 09:08:39 +00:00
2023-12-14 13:32:06 +00:00
$dbForProject -> purgeCachedDocument ( 'users' , $userId );
2023-05-23 13:43:03 +00:00
});
2026-02-04 05:30:22 +00:00
Http :: shutdown ()
2022-08-02 01:10:48 +00:00
-> groups ([ 'api' ])
2022-07-22 06:00:42 +00:00
-> inject ( 'utopia' )
-> inject ( 'request' )
-> inject ( 'response' )
-> inject ( 'project' )
2023-07-07 00:12:39 +00:00
-> inject ( 'user' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForEvents' )
-> inject ( 'queueForAudits' )
2026-03-11 14:01:26 +00:00
-> inject ( 'usage' )
-> inject ( 'publisherForUsage' )
2022-12-20 16:11:30 +00:00
-> inject ( 'queueForDeletes' )
-> inject ( 'queueForDatabase' )
2024-02-20 11:40:55 +00:00
-> inject ( 'queueForBuilds' )
2024-02-20 12:06:35 +00:00
-> inject ( 'queueForMessaging' )
2022-11-16 10:33:11 +00:00
-> inject ( 'queueForFunctions' )
2024-11-04 15:05:54 +00:00
-> inject ( 'queueForWebhooks' )
-> inject ( 'queueForRealtime' )
-> inject ( 'dbForProject' )
2026-01-07 07:04:28 +00:00
-> inject ( 'authorization' )
2026-01-05 21:05:00 +00:00
-> inject ( 'timelimit' )
2026-01-07 14:57:57 +00:00
-> inject ( 'eventProcessor' )
2026-02-23 20:01:54 +00:00
-> inject ( 'bus' )
2026-03-11 14:01:26 +00:00
-> inject ( 'apiKey' )
2026-03-19 03:55:22 +00:00
-> inject ( 'mode' )
-> action ( function ( Http $utopia , Request $request , Response $response , Document $project , User $user , Event $queueForEvents , Audit $queueForAudits , Context $usage , UsagePublisher $publisherForUsage , Delete $queueForDeletes , EventDatabase $queueForDatabase , Build $queueForBuilds , Messaging $queueForMessaging , Func $queueForFunctions , Event $queueForWebhooks , Realtime $queueForRealtime , Database $dbForProject , Authorization $authorization , callable $timelimit , EventProcessor $eventProcessor , Bus $bus , ? Key $apiKey , string $mode ) use ( $parseLabel ) {
2022-08-09 11:57:33 +00:00
2022-08-07 15:09:37 +00:00
$responsePayload = $response -> getPayload ();
2022-07-22 06:00:42 +00:00
2026-03-11 14:01:26 +00:00
if ( ! empty ( $queueForEvents -> getEvent ())) {
2024-11-04 15:20:43 +00:00
if ( empty ( $queueForEvents -> getPayload ())) {
$queueForEvents -> setPayload ( $responsePayload );
}
2026-01-07 14:57:57 +00:00
// Get project and function/webhook events (cached)
$functionsEvents = $eventProcessor -> getFunctionsEvents ( $project , $dbForProject );
$webhooksEvents = $eventProcessor -> getWebhooksEvents ( $project );
// Generate events for this operation
$generatedEvents = Event :: generateEvents (
$queueForEvents -> getEvent (),
$queueForEvents -> getParams ()
);
2021-02-28 18:36:13 +00:00
2024-11-04 15:20:43 +00:00
if ( $project -> getId () !== 'console' ) {
$queueForRealtime
-> from ( $queueForEvents )
-> trigger ();
}
2025-01-04 07:21:42 +00:00
2026-01-07 14:57:57 +00:00
// Only trigger functions if there are matching function events
2026-03-11 14:01:26 +00:00
if ( ! empty ( $functionsEvents )) {
2026-01-07 14:57:57 +00:00
foreach ( $generatedEvents as $event ) {
if ( isset ( $functionsEvents [ $event ])) {
$queueForFunctions
-> from ( $queueForEvents )
-> trigger ();
break ;
}
}
}
// Only trigger webhooks if there are matching webhook events
2026-03-11 14:01:26 +00:00
if ( ! empty ( $webhooksEvents )) {
2026-01-07 14:57:57 +00:00
foreach ( $generatedEvents as $event ) {
if ( isset ( $webhooksEvents [ $event ])) {
$queueForWebhooks
-> from ( $queueForEvents )
-> trigger ();
break ;
}
}
2025-01-04 07:21:42 +00:00
}
2024-11-04 15:20:43 +00:00
}
2024-11-04 15:05:54 +00:00
2023-02-19 11:04:12 +00:00
$route = $utopia -> getRoute ();
2022-08-14 19:27:43 +00:00
$requestParams = $route -> getParamsValues ();
2022-08-08 12:19:41 +00:00
2026-01-05 21:05:00 +00:00
/**
* Abuse labels
*/
$abuseEnabled = System :: getEnv ( '_APP_OPTIONS_ABUSE' , 'enabled' ) !== 'disabled' ;
$abuseResetCode = $route -> getLabel ( 'abuse-reset' , []);
$abuseResetCode = \is_array ( $abuseResetCode ) ? $abuseResetCode : [ $abuseResetCode ];
if ( $abuseEnabled && \count ( $abuseResetCode ) > 0 && \in_array ( $response -> getStatusCode (), $abuseResetCode )) {
$abuseKeyLabel = $route -> getLabel ( 'abuse-key' , 'url:{url},ip:{ip}' );
2026-03-11 14:01:26 +00:00
$abuseKeyLabel = ( ! is_array ( $abuseKeyLabel )) ? [ $abuseKeyLabel ] : $abuseKeyLabel ;
2026-01-05 21:05:00 +00:00
foreach ( $abuseKeyLabel as $abuseKey ) {
$start = $request -> getContentRangeStart ();
$end = $request -> getContentRangeEnd ();
$timeLimit = $timelimit ( $abuseKey , $route -> getLabel ( 'abuse-limit' , 0 ), $route -> getLabel ( 'abuse-time' , 3600 ));
$timeLimit
-> setParam ( '{projectId}' , $project -> getId ())
-> setParam ( '{userId}' , $user -> getId ())
-> setParam ( '{userAgent}' , $request -> getUserAgent ( '' ))
-> setParam ( '{ip}' , $request -> getIP ())
-> setParam ( '{url}' , $request -> getHostname () . $route -> getPath ())
-> setParam ( '{method}' , $request -> getMethod ())
2026-03-11 14:01:26 +00:00
-> setParam ( '{chunkId}' , ( int ) ( $start / ( $end + 1 - $start )));
2026-01-05 21:05:00 +00:00
foreach ( $request -> getParams () as $key => $value ) { // Set request params as potential abuse keys
2026-03-11 14:01:26 +00:00
if ( ! empty ( $value )) {
2026-01-05 21:05:00 +00:00
$timeLimit -> setParam ( '{param-' . $key . '}' , ( \is_array ( $value )) ? \json_encode ( $value ) : $value );
}
}
$abuse = new Abuse ( $timeLimit );
$abuse -> reset ();
}
}
2022-08-17 14:29:22 +00:00
/**
* Audit labels
*/
2022-08-16 12:28:30 +00:00
$pattern = $route -> getLabel ( 'audits.resource' , null );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $pattern )) {
2022-08-16 12:28:30 +00:00
$resource = $parseLabel ( $pattern , $responsePayload , $requestParams , $user );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $resource ) && $resource !== $pattern ) {
2022-12-20 16:11:30 +00:00
$queueForAudits -> setResource ( $resource );
2022-08-07 14:30:47 +00:00
}
2022-08-07 15:49:30 +00:00
}
2022-08-13 08:02:00 +00:00
2026-03-11 14:01:26 +00:00
if ( ! $user -> isEmpty ()) {
2025-01-21 06:41:20 +00:00
$userClone = clone $user ;
2025-01-14 08:17:01 +00:00
// $user doesn't support `type` and can cause unintended effects.
2026-03-22 02:31:46 +00:00
if ( empty ( $user -> getAttribute ( 'type' ))) {
$userClone -> setAttribute ( 'type' , $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER );
}
2025-01-21 06:41:20 +00:00
$queueForAudits -> setUser ( $userClone );
2025-01-14 07:48:11 +00:00
} elseif ( $queueForAudits -> getUser () === null || $queueForAudits -> getUser () -> isEmpty ()) {
/**
* User in the request is empty , and no user was set for auditing previously .
* This indicates :
* - No API Key was used .
* - No active session exists .
*
* Therefore , we consider this an anonymous request and create a relevant user .
*/
2025-11-27 11:48:32 +00:00
$user = new User ([
2025-01-14 07:48:11 +00:00
'$id' => '' ,
'status' => true ,
2025-11-04 06:08:35 +00:00
'type' => ACTIVITY_TYPE_GUEST ,
2025-01-14 12:15:49 +00:00
'email' => 'guest.' . $project -> getId () . '@service.' . $request -> getHostname (),
2025-01-14 07:48:11 +00:00
'password' => '' ,
2025-01-14 12:15:49 +00:00
'name' => 'Guest' ,
2025-01-14 07:48:11 +00:00
]);
2023-10-01 17:39:26 +00:00
$queueForAudits -> setUser ( $user );
2021-03-11 16:28:03 +00:00
}
2021-10-07 15:35:17 +00:00
2026-03-11 14:01:26 +00:00
if ( ! empty ( $queueForAudits -> getResource ()) && ! $queueForAudits -> getUser () -> isEmpty ()) {
2022-08-16 12:28:30 +00:00
/**
* audits . payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route -> getLabel ( 'audits.payload' , true );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $pattern )) {
2022-12-20 16:11:30 +00:00
$queueForAudits -> setPayload ( $responsePayload );
2022-08-16 12:28:30 +00:00
}
2022-12-20 16:11:30 +00:00
foreach ( $queueForEvents -> getParams () as $key => $value ) {
$queueForAudits -> setParam ( $key , $value );
2021-02-28 18:36:13 +00:00
}
2025-02-11 07:56:39 +00:00
2022-12-20 16:11:30 +00:00
$queueForAudits -> trigger ();
2022-04-18 16:21:45 +00:00
}
2021-10-07 15:35:17 +00:00
2026-03-11 14:01:26 +00:00
if ( ! empty ( $queueForDeletes -> getType ())) {
2022-12-20 16:11:30 +00:00
$queueForDeletes -> trigger ();
2022-04-13 12:39:31 +00:00
}
2021-10-07 15:35:17 +00:00
2026-03-11 14:01:26 +00:00
if ( ! empty ( $queueForDatabase -> getType ())) {
2022-12-20 16:11:30 +00:00
$queueForDatabase -> trigger ();
2022-07-22 06:00:42 +00:00
}
2021-10-07 15:35:17 +00:00
2026-03-11 14:01:26 +00:00
if ( ! empty ( $queueForBuilds -> getType ())) {
2024-02-20 11:40:55 +00:00
$queueForBuilds -> trigger ();
}
2026-03-11 14:01:26 +00:00
if ( ! empty ( $queueForMessaging -> getType ())) {
2024-02-20 13:20:09 +00:00
$queueForMessaging -> trigger ();
2024-02-20 12:06:35 +00:00
}
2025-02-11 07:56:39 +00:00
// Cache label
2022-08-16 15:02:17 +00:00
$useCache = $route -> getLabel ( 'cache' , false );
if ( $useCache ) {
2022-11-10 10:08:01 +00:00
$resource = $resourceType = null ;
2022-08-09 13:43:37 +00:00
$data = $response -> getPayload ();
2026-03-11 14:01:26 +00:00
if ( ! empty ( $data [ 'payload' ])) {
2022-08-16 15:02:17 +00:00
$pattern = $route -> getLabel ( 'cache.resource' , null );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $pattern )) {
2022-08-16 15:02:17 +00:00
$resource = $parseLabel ( $pattern , $responsePayload , $requestParams , $user );
}
2022-08-15 09:05:41 +00:00
2022-11-10 10:08:01 +00:00
$pattern = $route -> getLabel ( 'cache.resourceType' , null );
2026-03-11 14:01:26 +00:00
if ( ! empty ( $pattern )) {
2022-11-10 10:08:01 +00:00
$resourceType = $parseLabel ( $pattern , $responsePayload , $requestParams , $user );
}
2025-06-30 17:03:30 +00:00
$cache = new Cache (
new Filesystem ( APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project -> getId ())
);
2025-05-16 08:56:12 +00:00
$key = $request -> cacheIdentifier ();
2024-02-25 08:12:28 +00:00
$signature = md5 ( $data [ 'payload' ]);
2026-03-11 14:01:26 +00:00
$cacheLog = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'cache' , $key ));
2025-04-17 14:13:09 +00:00
$accessedAt = $cacheLog -> getAttribute ( 'accessedAt' , 0 );
2022-08-31 13:07:27 +00:00
$now = DateTime :: now ();
2022-08-16 15:02:17 +00:00
if ( $cacheLog -> isEmpty ()) {
2026-02-26 06:44:35 +00:00
try {
$authorization -> skip ( fn () => $dbForProject -> createDocument ( 'cache' , new Document ([
'$id' => $key ,
'resource' => $resource ,
'resourceType' => $resourceType ,
'mimeType' => $response -> getContentType (),
'accessedAt' => $now ,
'signature' => $signature ,
])));
} catch ( DuplicateException ) {
// Race condition: another concurrent request already created the cache document
2026-02-26 06:50:51 +00:00
$cacheLog = $authorization -> skip ( fn () => $dbForProject -> getDocument ( 'cache' , $key ));
2026-02-26 06:44:35 +00:00
}
2022-08-31 13:11:23 +00:00
} elseif ( DateTime :: formatTz ( DateTime :: addSeconds ( new \DateTime (), - APP_CACHE_UPDATE )) > $accessedAt ) {
2022-08-31 13:07:27 +00:00
$cacheLog -> setAttribute ( 'accessedAt' , $now );
2026-03-06 09:42:07 +00:00
$authorization -> skip ( fn () => $dbForProject -> updateDocument ( 'cache' , $cacheLog -> getId (), new Document ([
'accessedAt' => $cacheLog -> getAttribute ( 'accessedAt' )
])));
2025-06-30 17:03:30 +00:00
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
$cache -> save ( $key , $data [ 'payload' ]);
2022-08-16 15:02:17 +00:00
}
2022-08-14 15:01:34 +00:00
2022-08-16 15:02:17 +00:00
if ( $signature !== $cacheLog -> getAttribute ( 'signature' )) {
2024-02-25 08:12:28 +00:00
$cache -> save ( $key , $data [ 'payload' ]);
2022-08-16 15:02:17 +00:00
}
2022-08-15 12:16:32 +00:00
}
2022-07-23 17:42:42 +00:00
}
2023-10-25 07:39:59 +00:00
if ( $project -> getId () !== 'console' ) {
2026-03-16 06:26:07 +00:00
if ( ! $user -> isPrivileged ( $authorization -> getRoles ())) {
2026-02-23 20:01:54 +00:00
$bus -> dispatch ( new RequestCompleted (
project : $project -> getArrayCopy (),
request : $request ,
response : $response ,
));
2022-08-17 10:55:01 +00:00
}
2026-03-11 14:01:26 +00:00
// Publish usage metrics if context has data
if ( ! $usage -> isEmpty ()) {
$metrics = $usage -> getMetrics ();
// Filter out API key disabled metrics using suffix pattern matching
$disabledMetrics = $apiKey ? -> getDisabledMetrics () ? ? [];
if ( ! empty ( $disabledMetrics )) {
$metrics = array_values ( array_filter ( $metrics , function ( $metric ) use ( $disabledMetrics ) {
foreach ( $disabledMetrics as $pattern ) {
if ( str_ends_with ( $metric [ 'key' ], $pattern )) {
return false ;
}
}
return true ;
}));
}
$message = new UsageMessage (
project : $project ,
metrics : $metrics ,
reduce : $usage -> getReduce ()
);
$publisherForUsage -> enqueue ( $message );
}
2022-07-22 06:00:42 +00:00
}
});
2023-11-06 21:28:45 +00:00
2026-02-04 05:30:22 +00:00
Http :: init ()
2023-11-06 21:28:45 +00:00
-> groups ([ 'usage' ])
-> action ( function () {
2024-04-01 11:02:47 +00:00
if ( System :: getEnv ( '_APP_USAGE_STATS' , 'enabled' ) !== 'enabled' ) {
2023-11-06 21:28:45 +00:00
throw new Exception ( Exception :: GENERAL_USAGE_DISABLED );
}
});