1
0
Fork 0
mirror of synced 2024-06-29 19:50:26 +12:00

Merge branch 'feat-list-users-queries' into feat-list-user-logs-queries

This commit is contained in:
Steven 2022-08-19 22:00:54 +00:00
commit a5bc07c9e4
94 changed files with 5203 additions and 2042 deletions

5
.env
View file

@ -56,8 +56,8 @@ _APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_PHONE_PROVIDER=phone://mock
_APP_PHONE_FROM=+123456789
_APP_SMS_PROVIDER=sms://mock
_APP_SMS_FROM=+123456789
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_FUNCTIONS_SIZE_LIMIT=30000000
@ -72,6 +72,7 @@ OPEN_RUNTIMES_NETWORK=appwrite_runtimes
_APP_EXECUTOR_SECRET=your-secret-key
_APP_EXECUTOR_HOST=http://appwrite-executor/v1
_APP_MAINTENANCE_INTERVAL=86400
_APP_MAINTENANCE_RETENTION_CACHE=2592000
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
_APP_MAINTENANCE_RETENTION_ABUSE=86400
_APP_MAINTENANCE_RETENTION_AUDIT=1209600

View file

@ -123,6 +123,33 @@ RUN \
./configure && \
make && make install
# Rust Extensions Compile Image
FROM php:8.0.18-cli as rust_compile
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH=/root/.cargo/bin:$PATH
RUN apt-get update && apt-get install musl-tools build-essential clang-11 git -y
RUN rustup target add $(uname -m)-unknown-linux-musl
# Install ZigBuild for easier cross-compilation
RUN curl https://ziglang.org/builds/zig-linux-$(uname -m)-0.10.0-dev.2674+d980c6a38.tar.xz --output /tmp/zig.tar.xz
RUN tar -xf /tmp/zig.tar.xz -C /tmp/ && cp -r /tmp/zig-linux-$(uname -m)-0.10.0-dev.2674+d980c6a38 /tmp/zig/
ENV PATH=/tmp/zig:$PATH
RUN cargo install cargo-zigbuild
ENV RUSTFLAGS="-C target-feature=-crt-static"
FROM rust_compile as scrypt
WORKDIR /usr/local/lib/php/extensions/
RUN \
git clone --depth 1 https://github.com/appwrite/php-scrypt.git && \
cd php-scrypt && \
cargo zigbuild --workspace --all-targets --target $(uname -m)-unknown-linux-musl --release && \
mv target/$(uname -m)-unknown-linux-musl/release/libphp_scrypt.so target/libphp_scrypt.so
FROM php:8.0.18-cli-alpine3.15 as final
LABEL maintainer="team@appwrite.io"
@ -193,8 +220,8 @@ ENV _APP_SERVER=swoole \
_APP_SMTP_SECURE= \
_APP_SMTP_USERNAME= \
_APP_SMTP_PASSWORD= \
_APP_PHONE_PROVIDER= \
_APP_PHONE_FROM= \
_APP_SMS_PROVIDER= \
_APP_SMS_FROM= \
_APP_FUNCTIONS_SIZE_LIMIT=30000000 \
_APP_FUNCTIONS_TIMEOUT=900 \
_APP_FUNCTIONS_CONTAINERS=10 \
@ -263,6 +290,7 @@ COPY --from=imagick /usr/local/lib/php/extensions/no-debug-non-zts-20200930/imag
COPY --from=yaml /usr/local/lib/php/extensions/no-debug-non-zts-20200930/yaml.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=maxmind /usr/local/lib/php/extensions/no-debug-non-zts-20200930/maxminddb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=mongodb /usr/local/lib/php/extensions/no-debug-non-zts-20200930/mongodb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=scrypt /usr/local/lib/php/extensions/php-scrypt/target/libphp_scrypt.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
# Add Source Code
COPY ./app /usr/src/code/app
@ -319,6 +347,7 @@ RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini
RUN echo extension=imagick.so >> /usr/local/etc/php/conf.d/imagick.ini
RUN echo extension=yaml.so >> /usr/local/etc/php/conf.d/yaml.ini
RUN echo extension=maxminddb.so >> /usr/local/etc/php/conf.d/maxminddb.ini
RUN echo extension=libphp_scrypt.so >> /usr/local/etc/php/conf.d/libphp_scrypt.ini
RUN if [ "$DEBUG" == "true" ]; then printf "zend_extension=yasd \nyasd.debug_mode=remote \nyasd.init_file=/usr/local/dev/yasd_init.php \nyasd.remote_port=9005 \nyasd.log_level=-1" >> /usr/local/etc/php/conf.d/yasd.ini; fi
RUN if [ "$DEBUG" == "true" ]; then echo "opcache.enable=0" >> /usr/local/etc/php/conf.d/appwrite.ini; fi

View file

@ -1,5 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\ID;
@ -1160,8 +1161,30 @@ $collections = [
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => 'hash', // Hashing algorithm used to hash the password
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('hashOptions'), // Configuration of hashing algorithm
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO_OPTIONS,
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('passwordUpdate'),
'type' => Database::VAR_DATETIME,
@ -1274,6 +1297,13 @@ $collections = [
]
],
'indexes' => [
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [256],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_email'),
'type' => Database::INDEX_UNIQUE,
@ -1288,6 +1318,41 @@ $collections = [
'lengths' => [16],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_passwordUpdate'),
'type' => Database::INDEX_KEY,
'attributes' => ['passwordUpdate'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_registration'),
'type' => Database::INDEX_KEY,
'attributes' => ['registration'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_emailVerification'),
'type' => Database::INDEX_KEY,
'attributes' => ['emailVerification'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_phoneVerification'),
'type' => Database::INDEX_KEY,
'attributes' => ['phoneVerification'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
@ -2388,6 +2453,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('stdout'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 1000000,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('statusCode'),
'type' => Database::VAR_INTEGER,
@ -2766,6 +2842,62 @@ $collections = [
],
]
],
'cache' => [
'$collection' => Database::METADATA,
'$id' => 'cache',
'name' => 'Cache',
'attributes' => [
[
'$id' => 'resource',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 255,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'accessedAt',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'signature',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 255,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_accessedAt',
'type' => Database::INDEX_KEY,
'attributes' => ['accessedAt'],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_resource',
'type' => Database::INDEX_KEY,
'attributes' => ['resource'],
'lengths' => [],
'orders' => [],
],
],
],
'files' => [
'$collection' => ID::custom('buckets'),
'$id' => ID::custom('files'),

View file

@ -50,7 +50,7 @@ return [
],
Exception::GENERAL_PHONE_DISABLED => [
'name' => Exception::GENERAL_PHONE_DISABLED,
'description' => 'Phone provider is not configured. Please check the _APP_PHONE_PROVIDER environment variable of your Appwrite server.',
'description' => 'Phone provider is not configured. Please check the _APP_SMS_PROVIDER environment variable of your Appwrite server.',
'code' => 503,
],
Exception::GENERAL_ARGUMENT_INVALID => [
@ -88,11 +88,6 @@ return [
'description' => 'The request cannot be fulfilled with the current protocol. Please check the value of the _APP_OPTIONS_FORCE_HTTPS environment variable.',
'code' => 500,
],
Exception::GENERAL_PERMISSION_INVALID => [
'name' => Exception::GENERAL_PERMISSION_INVALID,
'description' => 'The provided permissions are invalid for this resource type. Documents and files cannot contain a `create` permission.',
'code' => 400,
],
/** User Errors */
Exception::USER_COUNT_EXCEEDED => [

View file

@ -81,7 +81,7 @@ return [
'scopes' => \array_merge($member, $admins, []),
],
Auth::USER_ROLE_APPS => [
'label' => 'Application',
'label' => 'Applications',
'scopes' => ['health.read'],
],
];

View file

@ -7,7 +7,7 @@
use Utopia\App;
use Appwrite\Runtimes\Runtimes;
$runtimes = new Runtimes('v1');
$runtimes = new Runtimes('v2');
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));

View file

@ -394,8 +394,8 @@ return [
'description' => '',
'variables' => [
[
'name' => '_APP_PHONE_PROVIDER',
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'phone://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.",
'name' => '_APP_SMS_PROVIDER',
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.",
'introduction' => '0.15.0',
'default' => '',
'required' => false,
@ -403,7 +403,7 @@ return [
'filter' => ''
],
[
'name' => '_APP_PHONE_FROM',
'name' => '_APP_SMS_FROM',
'description' => 'Phone number used for sending out messages. Must start with a leading \'+\' and maximum of 15 digits without spaces (+123456789).',
'introduction' => '0.15.0',
'default' => '',
@ -804,6 +804,15 @@ return [
'question' => '',
'filter' => ''
],
[
'name' => '_APP_MAINTENANCE_RETENTION_CACHE',
'description' => 'The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days).',
'introduction' => '0.16.0',
'default' => '2592000',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_MAINTENANCE_RETENTION_EXECUTION',
'description' => 'The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days).',

File diff suppressed because it is too large Load diff

View file

@ -7,8 +7,6 @@ use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Image\Image;
@ -37,8 +35,6 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
}
$output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v1/avatars/' . $type . '/:code-' . $code . $width . $height . $quality . $output);
$path = $set[$code];
$type = 'png';
@ -46,35 +42,15 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path);
}
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */);
if ($data) {
//$output = (empty($output)) ? $type : $output;
return $response
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
}
$image = new Image(\file_get_contents($path));
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$cache->save($key, $data);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data, null);
->file($data)
;
unset($image);
};
@ -82,6 +58,8 @@ App::get('/v1/avatars/credit-cards/:code')
->desc('Get Credit Card Icon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/credit-card')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getCreditCard')
@ -100,6 +78,8 @@ App::get('/v1/avatars/browsers/:code')
->desc('Get Browser Icon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/browser')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getBrowser')
@ -118,6 +98,8 @@ App::get('/v1/avatars/flags/:code')
->desc('Get Country Flag')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/flag')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getFlag')
@ -136,6 +118,8 @@ App::get('/v1/avatars/image')
->desc('Get Image from URL')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/image')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getImage')
@ -151,19 +135,7 @@ App::get('/v1/avatars/image')
$quality = 80;
$output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v2/avatars/images-' . $url . '-' . $width . '/' . $height . '/' . $quality);
$type = 'png';
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 7/* 1 week */);
if ($data) {
return $response
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
}
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@ -182,19 +154,14 @@ App::get('/v1/avatars/image')
}
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$cache->save($key, $data);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data);
->file($data)
;
unset($image);
});
@ -202,6 +169,8 @@ App::get('/v1/avatars/favicon')
->desc('Get Favicon')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/favicon')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getFavicon')
@ -217,19 +186,7 @@ App::get('/v1/avatars/favicon')
$height = 56;
$quality = 80;
$output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v2/avatars/favicon-' . $url);
$type = 'png';
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */);
if ($data) {
return $response
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
}
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@ -314,14 +271,11 @@ App::get('/v1/avatars/favicon')
if (empty($data) || (\mb_substr($data, 0, 5) === '<html') || \mb_substr($data, 0, 5) === '<!doc') {
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
}
$cache->save($key, $data);
return $response
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/x-icon')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data);
->file($data)
;
}
$fetch = @\file_get_contents($outputHref, false);
@ -331,21 +285,15 @@ App::get('/v1/avatars/favicon')
}
$image = new Image($fetch);
$image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$cache->save($key, $data);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data);
->file($data)
;
unset($image);
});
@ -381,19 +329,21 @@ App::get('/v1/avatars/qr')
}
$image = new Image($qrcode->render($text));
$image->crop((int) $size, (int) $size);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->setContentType('image/png')
->send($image->output('png', 9));
->send($image->output('png', 9))
;
});
App::get('/v1/avatars/initials')
->desc('Get User Initials')
->groups(['api', 'avatars'])
->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/initials')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getInitials')
@ -468,5 +418,6 @@ App::get('/v1/avatars/initials')
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->setContentType('image/png')
->send($image->getImageBlob());
->file($image->getImageBlob())
;
});

View file

@ -42,7 +42,6 @@ use Appwrite\Utopia\Database\Validator\Queries as QueriesValidator;
use Appwrite\Utopia\Database\Validator\OrderAttributes;
use Appwrite\Utopia\Response;
use Appwrite\Detector\Detector;
use Appwrite\Event\Audit as EventAudit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Stats\Stats;
@ -56,7 +55,7 @@ use MaxMind\Db\Reader;
* @return Document Newly created attribute document
* @throws Exception
*/
function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Event $events, Stats $usage): Document
function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $database, Event $events, Stats $usage): Document
{
$key = $attribute->getAttribute('key');
$type = $attribute->getAttribute('type', '');
@ -147,11 +146,6 @@ function createAttribute(string $databaseId, string $collectionId, Document $att
->setParam('attributeId', $attribute->getId())
;
$audits
->setResource('database/' . $db->getId() . '/collection/' . $collectionId)
->setPayload($attribute->getArrayCopy())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
return $attribute;
@ -162,6 +156,7 @@ App::post('/v1/databases')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].create')
->label('scope', 'databases.write')
->label('audits.resource', 'database/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'create')
@ -173,10 +168,9 @@ App::post('/v1/databases')
->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId;
@ -224,11 +218,6 @@ App::post('/v1/databases')
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
}
$audits
->setResource('database/' . $databaseId)
->setPayload($database->getArrayCopy())
;
$events->setParam('databaseId', $database->getId());
$usage->setParam('databases.create', 1);
@ -398,6 +387,7 @@ App::put('/v1/databases/:databaseId')
->groups(['api', 'database'])
->label('scope', 'databases.write')
->label('event', 'databases.[databaseId].update')
->label('audits.resource', 'database/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'update')
@ -409,10 +399,9 @@ App::put('/v1/databases/:databaseId')
->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$database = $dbForProject->getDocument('databases', $databaseId);
@ -430,11 +419,6 @@ App::put('/v1/databases/:databaseId')
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Bad structure. ' . $exception->getMessage());
}
$audits
->setResource('database/' . $databaseId)
->setPayload($database->getArrayCopy())
;
$usage->setParam('databases.update', 1);
$events->setParam('databaseId', $database->getId());
@ -446,6 +430,7 @@ App::delete('/v1/databases/:databaseId')
->groups(['api', 'database'])
->label('scope', 'databases.write')
->label('event', 'databases.[databaseId].delete')
->label('audits.resource', 'database/{request.databaseId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'delete')
@ -456,10 +441,9 @@ App::delete('/v1/databases/:databaseId')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('audits')
->inject('deletes')
->inject('usage')
->action(function (string $databaseId, Response $response, Database $dbForProject, Event $events, EventAudit $audits, Delete $deletes, Stats $usage) {
->action(function (string $databaseId, Response $response, Database $dbForProject, Event $events, Delete $deletes, Stats $usage) {
$database = $dbForProject->getDocument('databases', $databaseId);
@ -483,11 +467,6 @@ App::delete('/v1/databases/:databaseId')
->setPayload($response->output($database, Response::MODEL_DATABASE))
;
$audits
->setResource('database/' . $databaseId)
->setPayload($database->getArrayCopy())
;
$usage->setParam('databases.delete', 1);
$response->noContent();
@ -499,6 +478,7 @@ App::post('/v1/databases/:databaseId/collections')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'createCollection')
@ -513,10 +493,9 @@ App::post('/v1/databases/:databaseId/collections')
->param('documentSecurity', false, new Boolean(true), 'Specifies the permissions model used in this collection, which accepts either \'collection\' or \'document\'. For \'collection\' level permission, the permissions specified in read and write params are applied to all documents in the collection. For \'document\' level permissions, read and write permissions are specified in each document. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -535,9 +514,9 @@ App::post('/v1/databases/:databaseId/collections')
try {
$dbForProject->createDocument('database_' . $database->getInternalId(), new Document([
'$id' => $collectionId,
'$permissions' => $permissions ?? [],
'databaseInternalId' => $database->getInternalId(),
'databaseId' => $databaseId,
'$permissions' => $permissions ?? [],
'documentSecurity' => $documentSecurity,
'enabled' => true,
'name' => $name,
@ -552,11 +531,6 @@ App::post('/v1/databases/:databaseId/collections')
throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED);
}
$audits
->setResource('database/' . $databaseId . '/collection/' . $collectionId)
->setPayload($collection->getArrayCopy())
;
$events
->setContext('database', $database)
->setParam('databaseId', $databaseId)
@ -766,6 +740,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
->groups(['api', 'database'])
->label('scope', 'collections.write')
->label('event', 'databases.[databaseId].collections.[collectionId].update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'updateCollection')
@ -781,10 +756,9 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
->param('enabled', true, new Boolean(), 'Is collection enabled?', true)
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -820,11 +794,6 @@ App::put('/v1/databases/:databaseId/collections/:collectionId')
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, 'Bad structure. ' . $exception->getMessage());
}
$audits
->setResource('database/' . $databaseId . '/collection/' . $collectionId)
->setPayload($collection->getArrayCopy())
;
$usage
->setParam('databaseId', $databaseId)
->setParam('databases.collections.update', 1);
@ -843,6 +812,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
->groups(['api', 'database'])
->label('scope', 'collections.write')
->label('event', 'databases.[databaseId].collections.[collectionId].delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'deleteCollection')
@ -854,10 +824,9 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('audits')
->inject('deletes')
->inject('usage')
->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, Event $events, EventAudit $audits, Delete $deletes, Stats $usage) {
->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, Event $events, Delete $deletes, Stats $usage) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -889,11 +858,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId')
->setPayload($response->output($collection, Response::MODEL_COLLECTION))
;
$audits
->setResource('database/' . $databaseId . '/collection/' . $collectionId)
->setPayload($collection->getArrayCopy())
;
$usage
->setParam('databaseId', $databaseId)
->setParam('databases.collections.delete', 1);
@ -907,6 +871,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'createStringAttribute')
@ -915,7 +880,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_STRING)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Range::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.')
->param('required', null, new Boolean(), 'Is attribute required?')
@ -924,10 +889,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
// Ensure attribute default is within required size
$validator = new Text($size);
@ -942,7 +906,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string
'required' => $required,
'default' => $default,
'array' => $array,
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_STRING);
@ -954,6 +918,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email'
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.namespace', 'databases')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.method', 'createEmailAttribute')
@ -962,7 +927,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email'
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_EMAIL)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Email(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
@ -970,10 +935,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email'
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
$attribute = createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
@ -983,7 +947,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email'
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_EMAIL,
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL);
@ -995,6 +959,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.namespace', 'databases')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.method', 'createEnumAttribute')
@ -1003,7 +968,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_ENUM)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('elements', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of elements in enumerated type. Uses length of longest element to determine size. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' elements are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.')
->param('required', null, new Boolean(), 'Is attribute required?')
@ -1012,10 +977,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
// use length of longest string as attribute size
$size = 0;
@ -1040,7 +1004,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_ENUM,
'formatOptions' => ['elements' => $elements],
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM);
@ -1052,6 +1016,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.namespace', 'databases')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.method', 'createIpAttribute')
@ -1060,7 +1025,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_IP)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new IP(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
@ -1068,10 +1033,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
$attribute = createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
@ -1081,7 +1045,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_IP,
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_IP);
@ -1093,6 +1057,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.namespace', 'databases')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.method', 'createUrlAttribute')
@ -1101,7 +1066,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_URL)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new URL(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
@ -1109,10 +1074,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
$attribute = createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
@ -1122,7 +1086,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_URL,
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_URL);
@ -1134,6 +1098,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.namespace', 'databases')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.method', 'createIntegerAttribute')
@ -1142,7 +1107,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_INTEGER)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true)
@ -1152,10 +1117,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
// Ensure attribute default is within range
$min = (is_null($min)) ? PHP_INT_MIN : \intval($min);
@ -1185,7 +1149,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege
'min' => $min,
'max' => $max,
],
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$formatOptions = $attribute->getAttribute('formatOptions', []);
@ -1204,6 +1168,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.namespace', 'databases')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.method', 'createFloatAttribute')
@ -1212,7 +1177,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_FLOAT)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true)
@ -1222,10 +1187,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('events')
->inject('usage')
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Event $events, Stats $usage) {
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Event $events, Stats $usage) {
// Ensure attribute default is within range
$min = (is_null($min)) ? -PHP_FLOAT_MAX : \floatval($min);
@ -1258,7 +1222,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float'
'min' => $min,
'max' => $max,
],
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$formatOptions = $attribute->getAttribute('formatOptions', []);
@ -1277,6 +1241,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.namespace', 'databases')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.method', 'createBooleanAttribute')
@ -1285,7 +1250,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_BOOLEAN)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Boolean(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
@ -1293,10 +1258,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
$attribute = createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
@ -1305,7 +1269,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea
'required' => $required,
'default' => $default,
'array' => $array,
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_BOOLEAN);
@ -1318,6 +1282,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.namespace', 'databases')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.method', 'createDatetimeAttribute')
@ -1334,10 +1299,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
$attribute = createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
@ -1347,7 +1311,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
'default' => $default,
'array' => $array,
'filters' => ['datetime']
]), $response, $dbForProject, $database, $audits, $events, $usage);
]), $response, $dbForProject, $database, $events, $usage);
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($attribute, Response::MODEL_ATTRIBUTE_DATETIME);
@ -1367,7 +1331,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_ATTRIBUTE_LIST)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->inject('response')
->inject('dbForProject')
->inject('usage')
@ -1419,7 +1383,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
Response::MODEL_ATTRIBUTE_DATETIME,
Response::MODEL_ATTRIBUTE_STRING])// needs to be last, since its condition would dominate any other string attribute
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->inject('response')
->inject('dbForProject')
@ -1476,6 +1440,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
->groups(['api', 'database'])
->label('scope', 'collections.write')
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'deleteAttribute')
@ -1483,15 +1448,14 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('events')
->inject('audits')
->inject('usage')
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events, EventAudit $audits, Stats $usage) {
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events, Stats $usage) {
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -1557,11 +1521,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key
->setPayload($response->output($attribute, $model))
;
$audits
->setResource('database/' . $databaseId . '/collection/' . $collectionId)
->setPayload($attribute->getArrayCopy())
;
$response->noContent();
});
@ -1571,6 +1530,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].create')
->label('scope', 'collections.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'createIndex')
@ -1579,7 +1539,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_INDEX)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', null, new Key(), 'Index Key.')
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL, Database::INDEX_ARRAY]), 'Index type.')
->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.')
@ -1587,10 +1547,9 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $database, EventAudit $audits, Stats $usage, Event $events) {
->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, Response $response, Database $dbForProject, EventDatabase $database, Stats $usage, Event $events) {
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -1712,11 +1671,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes')
->setContext('database', $db)
;
$audits
->setResource('database/' . $databaseId . '/collection/' . $collection->getId())
->setPayload($index->getArrayCopy())
;
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
$response->dynamic($index, Response::MODEL_INDEX);
});
@ -1734,7 +1688,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_INDEX_LIST)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->inject('response')
->inject('dbForProject')
->inject('usage')
@ -1776,7 +1730,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_INDEX)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', null, new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
@ -1820,6 +1774,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->groups(['api', 'database'])
->label('scope', 'collections.write')
->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'deleteIndex')
@ -1827,15 +1782,14 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('key', '', new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
->inject('database')
->inject('events')
->inject('audits')
->inject('usage')
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events, EventAudit $audits, Stats $usage) {
->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $database, Event $events, Stats $usage) {
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -1881,11 +1835,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->setPayload($response->output($index, Response::MODEL_INDEX))
;
$audits
->setResource('database/' . $databaseId . '/collection/' . $collection->getId())
->setPayload($index->getArrayCopy())
;
$response->noContent();
});
@ -1895,6 +1844,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].create')
->label('scope', 'documents.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'createDocument')
@ -1904,17 +1854,16 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->label('sdk.response.model', Response::MODEL_DOCUMENT)
->param('databaseId', '', new UID(), 'Database ID.')
->param('documentId', '', new CustomId(), 'Document ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection). Make sure to define attributes before creating documents.')
->param('data', [], new JSON(), 'Document data as JSON object.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE]), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('audits')
->inject('usage')
->inject('events')
->inject('mode')
->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, EventAudit $audits, Stats $usage, Event $events, string $mode) {
->action(function (string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@ -1968,7 +1917,10 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
}
} else {
foreach ($allowedPermissions as $permission) {
// If the permission is not set, add it for the current user.
/**
* If an allowed permission was not passed in the request,
* and there is a current user, add it for the current user.
*/
if (empty(\preg_grep("#^{$permission}\(.+\)$#", $permissions)) && !empty($user->getId())) {
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
}
@ -2024,11 +1976,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents')
->setParam('collectionId', $collectionId)
;
$audits
->setResource('database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId())
->setPayload($document->getArrayCopy())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($document, Response::MODEL_DOCUMENT);
});
@ -2046,8 +1993,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/database#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response. By default will return maximum 25 results. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' results allowed per request.', true)
->param('offset', 0, new Range(0, APP_LIMIT_COUNT), 'Offset value. The default value is 0. Use this value to manage pagination. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
->param('cursor', '', new UID(), 'ID of the document used as the starting point for the query, excluding the document itself. Should be used for efficient pagination when working with large sets of data. [learn more about pagination](https://appwrite.io/docs/pagination)', true)
@ -2159,7 +2106,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_DOCUMENT)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('documentId', null, new UID(), 'Document ID.')
->inject('response')
->inject('dbForProject')
@ -2315,6 +2262,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update')
->label('scope', 'documents.write')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'updateDocument')
@ -2329,11 +2277,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of strings with permissions. By default no user is granted with any permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, EventAudit $audits, Stats $usage, Event $events, string $mode) {
->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, Response $response, Database $dbForProject, Stats $usage, Event $events, string $mode) {
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@ -2441,11 +2388,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
->setParam('collectionId', $collectionId)
;
$audits
->setResource('database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId())
->setPayload($document->getArrayCopy())
;
$response->dynamic($document, Response::MODEL_DOCUMENT);
});
@ -2455,6 +2397,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'databases')
->label('sdk.method', 'deleteDocument')
@ -2462,16 +2405,15 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('collectionId', null, new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/database#createCollection).')
->param('documentId', null, new UID(), 'Document ID.')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('audits')
->inject('deletes')
->inject('usage')
->inject('mode')
->action(function (string $databaseId, string $collectionId, string $documentId, Response $response, Database $dbForProject, Event $events, EventAudit $audits, Delete $deletes, Stats $usage, string $mode) {
->action(function (string $databaseId, string $collectionId, string $documentId, Response $response, Database $dbForProject, Event $events, Delete $deletes, Stats $usage, string $mode) {
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -2536,11 +2478,6 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
->setPayload($response->output($document, Response::MODEL_DOCUMENT))
;
$audits
->setResource('database/' . $databaseId . '/collection/' . $collectionId . '/document/' . $document->getId())
->setPayload($document->getArrayCopy())
;
$response->noContent();
});

View file

@ -47,6 +47,7 @@ App::post('/v1/functions')
->desc('Create Function')
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].create')
->label('audits.resource', 'function/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'create')
@ -293,6 +294,7 @@ App::put('/v1/functions/:functionId')
->desc('Update Function')
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].update')
->label('audits.resource', 'function/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'update')
@ -356,6 +358,7 @@ App::patch('/v1/functions/:functionId/deployments/:deploymentId')
->desc('Update Function Deployment')
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'updateDeployment')
@ -421,6 +424,7 @@ App::delete('/v1/functions/:functionId')
->desc('Delete Function')
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'delete')
@ -458,6 +462,7 @@ App::post('/v1/functions/:functionId/deployments')
->desc('Create Deployment')
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].deployments.[deploymentId].create')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'createDeployment')
@ -751,6 +756,7 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId')
->desc('Delete Deployment')
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].deployments.[deploymentId].delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'deleteDeployment')
@ -966,6 +972,7 @@ App::post('/v1/functions/:functionId/executions')
$execution->setAttribute('status', $executionResponse['status']);
$execution->setAttribute('statusCode', $executionResponse['statusCode']);
$execution->setAttribute('response', $executionResponse['response']);
$execution->setAttribute('stdout', $executionResponse['stdout']);
$execution->setAttribute('stderr', $executionResponse['stderr']);
$execution->setAttribute('time', $executionResponse['time']);
} catch (\Throwable $th) {
@ -986,6 +993,14 @@ App::post('/v1/functions/:functionId/executions')
->setParam('functionStatus', $execution->getAttribute('status', ''))
->setParam('functionExecutionTime', $execution->getAttribute('time') * 1000); // ms
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$isPrivilegedUser && !$isAppUser) {
$execution->setAttribute('stdout', '');
$execution->setAttribute('stderr', '');
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($execution, Response::MODEL_EXECUTION);
@ -1043,6 +1058,17 @@ App::get('/v1/functions/:functionId/executions')
$results = $dbForProject->find('executions', \array_merge($filterQueries, $queries));
$total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT);
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$isPrivilegedUser && !$isAppUser) {
$results = array_map(function ($execution) {
$execution->setAttribute('stdout', '');
$execution->setAttribute('stderr', '');
return $execution;
}, $results);
}
$response->dynamic(new Document([
'executions' => $results,
'total' => $total,
@ -1082,6 +1108,14 @@ App::get('/v1/functions/:functionId/executions/:executionId')
throw new Exception(Exception::EXECUTION_NOT_FOUND);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
if (!$isPrivilegedUser && !$isAppUser) {
$execution->setAttribute('stdout', '');
$execution->setAttribute('stderr', '');
}
$response->dynamic($execution, Response::MODEL_EXECUTION);
});
@ -1090,6 +1124,7 @@ App::post('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->desc('Retry Build')
->label('scope', 'functions.write')
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'functions')
->label('sdk.method', 'retryBuild')

View file

@ -136,6 +136,7 @@ App::post('/v1/projects')
if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue;
}
$attributes = [];
$indexes = [];
@ -162,7 +163,6 @@ App::post('/v1/projects')
'orders' => $index['orders'],
]);
}
$dbForProject->createCollection($key, $attributes, $indexes);
}
@ -548,7 +548,7 @@ App::delete('/v1/projects/:projectId')
->inject('deletes')
->action(function (string $projectId, string $password, Response $response, Document $user, Database $dbForConsole, Delete $deletes) {
if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password
if (!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}

View file

@ -2,7 +2,6 @@
use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\Event\Audit;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Permissions\PermissionsProcessor;
@ -11,8 +10,6 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -51,6 +48,7 @@ App::post('/v1/storage/buckets')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
->label('event', 'buckets.[bucketId].create')
->label('audits.resource', 'buckets/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'createBucket')
@ -69,10 +67,9 @@ App::post('/v1/storage/buckets')
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
@ -136,11 +133,6 @@ App::post('/v1/storage/buckets')
throw new Exception(Exception::STORAGE_BUCKET_ALREADY_EXISTS);
}
$audits
->setResource('storage/buckets/' . $bucket->getId())
->setPayload($bucket->getArrayCopy())
;
$events
->setParam('bucketId', $bucket->getId())
;
@ -234,6 +226,7 @@ App::put('/v1/storage/buckets/:bucketId')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
->label('event', 'buckets.[bucketId].update')
->label('audits.resource', 'buckets/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'updateBucket')
@ -252,10 +245,9 @@ App::put('/v1/storage/buckets/:bucketId')
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('usage')
->inject('events')
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
->action(function (string $bucketId, string $name, ?array $permissions, string $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@ -285,11 +277,6 @@ App::put('/v1/storage/buckets/:bucketId')
->setAttribute('encryption', (bool) filter_var($encryption, FILTER_VALIDATE_BOOLEAN))
->setAttribute('antivirus', (bool) filter_var($antivirus, FILTER_VALIDATE_BOOLEAN)));
$audits
->setResource('storage/buckets/' . $bucket->getId())
->setPayload($bucket->getArrayCopy())
;
$events
->setParam('bucketId', $bucket->getId())
;
@ -304,6 +291,7 @@ App::delete('/v1/storage/buckets/:bucketId')
->groups(['api', 'storage'])
->label('scope', 'buckets.write')
->label('event', 'buckets.[bucketId].delete')
->label('audits.resource', 'buckets/{request.bucketId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'deleteBucket')
@ -313,11 +301,10 @@ App::delete('/v1/storage/buckets/:bucketId')
->param('bucketId', '', new UID(), 'Bucket unique ID.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('deletes')
->inject('events')
->inject('usage')
->action(function (string $bucketId, Response $response, Database $dbForProject, Audit $audits, Delete $deletes, Event $events, Stats $usage) {
->action(function (string $bucketId, Response $response, Database $dbForProject, Delete $deletes, Event $events, Stats $usage) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
@ -337,11 +324,6 @@ App::delete('/v1/storage/buckets/:bucketId')
->setPayload($response->output($bucket, Response::MODEL_BUCKET))
;
$audits
->setResource('storage/buckets/' . $bucket->getId())
->setPayload($bucket->getArrayCopy())
;
$usage->setParam('storage.buckets.delete', 1);
$response->noContent();
@ -353,6 +335,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'buckets.[bucketId].files.[fileId].create')
->label('audits.resource', 'files/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'createFile')
@ -370,13 +353,13 @@ App::post('/v1/storage/buckets/:bucketId/files')
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('audits')
->inject('usage')
->inject('events')
->inject('mode')
->inject('deviceFiles')
->inject('deviceLocal')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) {
->inject('deletes')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal, Delete $deletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -412,7 +395,10 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
} else {
foreach ($allowedPermissions as $permission) {
// If the permission is not set, add it for the current user
/**
* If an allowed permission was not passed in the request,
* and there is a current user, add it for the current user.
*/
if (empty(\preg_grep("#^{$permission}\(.+\)$#", $permissions)) && !empty($user->getId())) {
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
}
@ -488,8 +474,8 @@ App::post('/v1/storage/buckets/:bucketId/files')
}
/**
* Validators
*/
* Validators
*/
// Check if file type is allowed
$allowedFileExtensions = $bucket->getAttribute('allowedFileExtensions', []);
$fileExt = new FileExt($allowedFileExtensions);
@ -625,10 +611,6 @@ App::post('/v1/storage/buckets/:bucketId/files')
throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS);
}
$audits
->setResource('storage/files/' . $file->getId())
;
$usage
->setParam('storage', $sizeActual ?? 0)
->setParam('storage.files.create', 1)
@ -676,6 +658,11 @@ App::post('/v1/storage/buckets/:bucketId/files')
->setContext('bucket', $bucket)
;
$deletes
->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
->setResource('file/' . $file->getId())
;
$metadata = null; // was causing leaks as it was passed by reference
$response->setStatusCode(Response::STATUS_CODE_CREATED);
@ -816,6 +803,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->desc('Get File Preview')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('cache', true)
->label('cache.resource', 'file/{request.fileId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFilePreview')
@ -874,14 +863,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5($fileId . $width . $height . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output);
$file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId);
$file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
if ($fileSecurity) {
$valid = $validator->isValid($file->getRead());
$valid |= $validator->isValid($file->getRead());
if (!$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@ -892,7 +881,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$algorithm = $file->getAttribute('algorithm');
$cipher = $file->getAttribute('openSSLCipher');
$mime = $file->getAttribute('mimeType');
if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) {
if (!\in_array($mime, $inputs)) {
$path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default'];
@ -905,7 +893,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$cipher = null;
$background = (empty($background)) ? 'eceff1' : $background;
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
$key = \md5($path . $width . $height . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output);
$deviceFiles = $deviceLocal;
}
@ -916,23 +903,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId() . DIRECTORY_SEPARATOR . $bucketId . DIRECTORY_SEPARATOR . $fileId)); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */);
if (empty($output)) {
// when file extension is not provided and the mime type is not one of our supported outputs
// we fallback to `jpg` output format
$output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type;
}
if ($data) {
return $response
->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'])
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data)
;
}
$source = $deviceFiles->read($path);
@ -977,20 +953,18 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$data = $image->output($output, $quality);
$cache->save($key, $data);
$usage
->setParam('storage.files.read', 1)
->setParam('bucketId', $bucketId)
;
$response
->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'])
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data)
;
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType($contentType)
->file($data)
;
unset($image);
});
@ -1287,6 +1261,7 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'buckets.[bucketId].files.[fileId].update')
->label('audits.resource', 'files/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'updateFile')
@ -1300,11 +1275,10 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('audits')
->inject('usage')
->inject('mode')
->inject('events')
->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, Audit $audits, Stats $usage, string $mode, Event $events) {
->action(function (string $bucketId, string $fileId, ?array $permissions, Response $response, Database $dbForProject, Document $user, Stats $usage, string $mode, Event $events) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@ -1370,8 +1344,6 @@ App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->setContext('bucket', $bucket)
;
$audits->setResource('file/' . $file->getId());
$usage
->setParam('storage.files.update', 1)
->setParam('bucketId', $bucketId)
@ -1386,6 +1358,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'buckets.[bucketId].files.[fileId].delete')
->label('audits.resource', 'file/{request.fileId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'deleteFile')
@ -1397,12 +1370,11 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('audits')
->inject('usage')
->inject('mode')
->inject('deviceFiles')
->inject('project')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Audit $audits, Stats $usage, string $mode, Device $deviceFiles, Document $project) {
->inject('deletes')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Stats $usage, string $mode, Device $deviceFiles, Delete $deletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && $mode !== APP_MODE_ADMIN)) {
@ -1440,10 +1412,10 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
}
if ($deviceDeleted) {
//delete related cache
$cacheDir = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId() . DIRECTORY_SEPARATOR . $bucketId . DIRECTORY_SEPARATOR . $fileId;
$deviceLocal = new Local($cacheDir);
$deviceLocal->delete($cacheDir, true);
$deletes
->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
->setResource('file/' . $fileId)
;
$deleted = $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId);
@ -1454,8 +1426,6 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to delete file from device');
}
$audits->setResource('file/' . $file->getId());
$usage
->setParam('storage', $file->getAttribute('size', 0) * -1)
->setParam('storage.files.delete', 1)

View file

@ -2,7 +2,6 @@
use Appwrite\Auth\Auth;
use Appwrite\Detector\Detector;
use Appwrite\Event\Audit as EventAudit;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
@ -41,6 +40,7 @@ App::post('/v1/teams')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].create')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'create')
@ -55,8 +55,7 @@ App::post('/v1/teams')
->inject('user')
->inject('dbForProject')
->inject('events')
->inject('audits')
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $events, Event $audits) {
->action(function (string $teamId, string $name, array $roles, Response $response, Document $user, Database $dbForProject, Event $events) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
@ -108,12 +107,6 @@ App::post('/v1/teams')
$events->setParam('userId', $user->getId());
}
$audits
->setParam('event', 'teams.create')
->setParam('resource', 'team/' . $teamId)
->setParam('data', $team->getArrayCopy())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($team, Response::MODEL_TEAM);
});
@ -198,6 +191,7 @@ App::put('/v1/teams/:teamId')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].update')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'update')
@ -210,8 +204,7 @@ App::put('/v1/teams/:teamId')
->inject('response')
->inject('dbForProject')
->inject('events')
->inject('audits')
->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events, EventAudit $audits) {
->action(function (string $teamId, string $name, Response $response, Database $dbForProject, Event $events) {
$team = $dbForProject->getDocument('teams', $teamId);
@ -224,7 +217,6 @@ App::put('/v1/teams/:teamId')
->setAttribute('search', implode(' ', [$teamId, $name])));
$events->setParam('teamId', $team->getId());
$audits->setResource('team/' . $team->getId());
$response->dynamic($team, Response::MODEL_TEAM);
});
@ -234,6 +226,7 @@ App::delete('/v1/teams/:teamId')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].delete')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{request.teamId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'delete')
@ -245,8 +238,7 @@ App::delete('/v1/teams/:teamId')
->inject('dbForProject')
->inject('events')
->inject('deletes')
->inject('audits')
->action(function (string $teamId, Response $response, Database $dbForProject, Event $events, Delete $deletes, EventAudit $audits) {
->action(function (string $teamId, Response $response, Database $dbForProject, Event $events, Delete $deletes) {
$team = $dbForProject->getDocument('teams', $teamId);
@ -279,12 +271,6 @@ App::delete('/v1/teams/:teamId')
->setPayload($response->output($team, Response::MODEL_TEAM))
;
$audits
->setParam('event', 'teams.delete')
->setParam('resource', 'team/' . $teamId)
->setParam('data', $team->getArrayCopy())
;
$response->noContent();
});
@ -294,6 +280,8 @@ App::post('/v1/teams/:teamId/memberships')
->label('event', 'teams.[teamId].memberships.[membershipId].create')
->label('scope', 'teams.write')
->label('auth.type', 'invites')
->label('audits.resource', 'team/{request.teamId}')
->label('audits.userId', '{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'createMembership')
@ -312,10 +300,9 @@ App::post('/v1/teams/:teamId/memberships')
->inject('user')
->inject('dbForProject')
->inject('locale')
->inject('audits')
->inject('mails')
->inject('events')
->action(function (string $teamId, string $email, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, EventAudit $audits, Mail $mails, Event $events) {
->action(function (string $teamId, string $email, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, Event $events) {
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles());
@ -358,7 +345,9 @@ App::post('/v1/teams/:teamId/memberships')
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash(Auth::passwordGenerator()),
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an
@ -444,10 +433,6 @@ App::post('/v1/teams/:teamId/memberships')
;
}
$audits
->setResource('team/' . $teamId)
;
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())
@ -587,6 +572,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].update')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{request.teamId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'updateMembershipRoles')
@ -601,9 +587,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, EventAudit $audits, Event $events) {
->action(function (string $teamId, string $membershipId, array $roles, Request $request, Response $response, Document $user, Database $dbForProject, Event $events) {
$team = $dbForProject->getDocument('teams', $teamId);
if ($team->isEmpty()) {
@ -639,8 +624,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
*/
$dbForProject->deleteCachedDocument('users', $profile->getId());
$audits->setResource('team/' . $teamId);
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId());
@ -659,6 +642,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].update.status')
->label('scope', 'public')
->label('audits.resource', 'team/{request.teamId}')
->label('audits.userId', '{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'updateMembershipStatus')
@ -675,9 +660,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->inject('user')
->inject('dbForProject')
->inject('geodb')
->inject('audits')
->inject('events')
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Reader $geodb, EventAudit $audits, Event $events) {
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Reader $geodb, Event $events) {
$protocol = $request->getProtocol();
$membership = $dbForProject->getDocument('memberships', $membershipId);
@ -721,13 +705,11 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
->setAttribute('confirm', true)
;
$user
->setAttribute('emailVerification', true)
;
$user->setAttribute('emailVerification', true);
// Log user in
Authorization::setRole('user:' . $user->getId());
Authorization::setRole(Role::user($user->getId())->toString());
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
@ -755,7 +737,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$dbForProject->deleteCachedDocument('users', $user->getId());
Authorization::setRole('user:' . $userId);
Authorization::setRole(Role::user($userId)->toString());
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
@ -763,8 +745,6 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$team = Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team->setAttribute('total', $team->getAttribute('total', 0) + 1)));
$audits->setResource('team/' . $teamId);
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())
@ -795,6 +775,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].memberships.[membershipId].delete')
->label('scope', 'teams.write')
->label('audits.resource', 'team/{request.teamId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'teams')
->label('sdk.method', 'deleteMembership')
@ -805,9 +786,8 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
->param('membershipId', '', new UID(), 'Membership ID.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
->action(function (string $teamId, string $membershipId, Response $response, Database $dbForProject, Event $events) {
$membership = $dbForProject->getDocument('memberships', $membershipId);
@ -854,8 +834,6 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
}
$audits->setResource('team/' . $teamId);
$events
->setParam('teamId', $team->getId())
->setParam('membershipId', $membership->getId())

View file

@ -6,7 +6,6 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Audit as EventAudit;
use Appwrite\Network\Validator\Email;
use Appwrite\Stats\Stats;
use Appwrite\Utopia\Database\Validator\CustomId;
@ -34,12 +33,64 @@ use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\Boolean;
use MaxMind\Db\Reader;
use Utopia\Validator\Integer;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Database $dbForProject, Stats $usage, Event $events): Document
{
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
if (!empty($email)) {
$email = \strtolower($email);
}
try {
$userId = $userId == 'unique()'
? ID::unique()
: ID::custom($userId);
$user = $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'emailVerification' => false,
'phone' => $phone,
'phoneVerification' => false,
'status' => true,
'password' => (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null,
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptions,
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name])
]));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
$usage->setParam('users.create', 1);
$events->setParam('userId', $user->getId());
return $user;
}
App::post('/v1/users')
->desc('Create User')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'create')
@ -48,51 +99,235 @@ App::post('/v1/users')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', null, new Email(), 'User email.', true)
->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('password', null, new Password(), 'Plain text user password. Must be at least 8 chars.', true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/bcrypt')
->desc('Create User with Bcrypt Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createBcryptUser')
->label('sdk.description', '/docs/references/users/create-bcrypt-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->param('password', '', new Password(), 'User password hashed using Bcrypt.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$email = \strtolower($email);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
try {
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$user = $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash($password),
'passwordUpdate' => DateTime::now(),
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name])
]));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
App::post('/v1/users/md5')
->desc('Create User with MD5 Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createMD5User')
->label('sdk.description', '/docs/references/users/create-md5-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using MD5.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/argon2')
->desc('Create User with Argon2 Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createArgon2User')
->label('sdk.description', '/docs/references/users/create-argon2-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Argon2.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/sha')
->desc('Create User with SHA Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createSHAUser')
->label('sdk.description', '/docs/references/users/create-sha-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$options = '{}';
if (!empty($passwordVersion)) {
$options = '{"version":"' . $passwordVersion . '"}';
}
$usage
->setParam('users.create', 1)
;
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$events
->setParam('userId', $user->getId())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/phpass')
->desc('Create User with PHPass Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createPHPassUser')
->label('sdk.description', '/docs/references/users/create-phpass-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using PHPass.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/scrypt')
->desc('Create User with Scrypt Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createScryptUser')
->label('sdk.description', '/docs/references/users/create-scrypt-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt.')
->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.')
->param('passwordCpu', '', new Integer(), 'Optional CPU cost used to hash password.')
->param('passwordMemory', '', new Integer(), 'Optional memory cost used to hash password.')
->param('passwordParallel', '', new Integer(), 'Optional parallelization cost used to hash password.')
->param('passwordLength', '', new Integer(), 'Optional hash length used to hash password.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
'costMemory' => $passwordMemory,
'costParallel' => $passwordParallel,
'length' => $passwordLength
];
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/scrypt-modified')
->desc('Create User with Scrypt Modified Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createScryptModifiedUser')
->label('sdk.description', '/docs/references/users/create-scrypt-modified-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt Modified.')
->param('passwordSalt', '', new Text(128), 'Salt used to hash password.')
->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.')
->param('passwordSignerKey', '', new Text(128), 'Signer key used to hash password.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -141,9 +376,7 @@ App::get('/v1/users')
$filterQueries = Query::groupByType($queries)['filters'];
$usage
->setParam('users.read', 1)
;
$usage->setParam('users.read', 1);
$response->dynamic(new Document([
'users' => $dbForProject->find('users', $queries),
@ -174,12 +407,48 @@ App::get('/v1/users/:userId')
throw new Exception(Exception::USER_NOT_FOUND);
}
$usage
->setParam('users.read', 1)
;
$usage->setParam('users.read', 1);
$response->dynamic($user, Response::MODEL_USER);
});
App::patch('/v1/users/:userId/prefs')
->desc('Update User Preferences')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.prefs')
->label('scope', 'users.write')
->label('audits.resource', 'user/{request.userId}')
->label('audits.userId', '{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePrefs')
->label('sdk.description', '/docs/references/users/update-user-prefs.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PREFERENCES)
->param('userId', '', new UID(), 'User ID.')
->param('prefs', '', new Assoc(), 'Prefs key-value JSON object.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, array $prefs, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::get('/v1/users/:userId/prefs')
->desc('Get User Preferences')
->groups(['api', 'users'])
@ -205,9 +474,8 @@ App::get('/v1/users/:userId/prefs')
$prefs = $user->getAttribute('prefs', new \stdClass());
$usage
->setParam('users.read', 1)
;
$usage->setParam('users.read', 1);
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
@ -247,9 +515,8 @@ App::get('/v1/users/:userId/sessions')
$sessions[$key] = $session;
}
$usage
->setParam('users.read', 1)
;
$usage->setParam('users.read', 1);
$response->dynamic(new Document([
'sessions' => $sessions,
'total' => count($sessions),
@ -371,9 +638,7 @@ App::get('/v1/users/:userId/logs')
}
}
$usage
->setParam('users.read', 1)
;
$usage->setParam('users.read', 1);
$response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getId()),
@ -386,6 +651,8 @@ App::patch('/v1/users/:userId/status')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.status')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateStatus')
@ -409,13 +676,9 @@ App::patch('/v1/users/:userId/status')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status));
$usage
->setParam('users.update', 1)
;
$usage->setParam('users.update', 1);
$events
->setParam('userId', $user->getId())
;
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -425,6 +688,7 @@ App::patch('/v1/users/:userId/verification')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.verification')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateEmailVerification')
@ -448,13 +712,9 @@ App::patch('/v1/users/:userId/verification')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
$usage
->setParam('users.update', 1)
;
$usage->setParam('users.update', 1);
$events
->setParam('userId', $user->getId())
;
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -464,6 +724,7 @@ App::patch('/v1/users/:userId/verification/phone')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.verification')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePhoneVerification')
@ -487,13 +748,9 @@ App::patch('/v1/users/:userId/verification/phone')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
$usage
->setParam('users.update', 1)
;
$usage->setParam('users.update', 1);
$events
->setParam('userId', $user->getId())
;
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -503,6 +760,8 @@ App::patch('/v1/users/:userId/name')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.name')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateName')
@ -514,9 +773,8 @@ App::patch('/v1/users/:userId/name')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $userId, string $name, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
->action(function (string $userId, string $name, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -531,13 +789,7 @@ App::patch('/v1/users/:userId/name')
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$audits
->setResource('user/' . $user->getId())
;
$events
->setParam('userId', $user->getId())
;
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -547,6 +799,8 @@ App::patch('/v1/users/:userId/password')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.password')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePassword')
@ -558,9 +812,8 @@ App::patch('/v1/users/:userId/password')
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $userId, string $password, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
->action(function (string $userId, string $password, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -569,18 +822,14 @@ App::patch('/v1/users/:userId/password')
}
$user
->setAttribute('password', Auth::passwordHash($password))
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', DateTime::now());
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$audits
->setResource('user/' . $user->getId())
;
$events
->setParam('userId', $user->getId())
;
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -590,6 +839,8 @@ App::patch('/v1/users/:userId/email')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.email')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateEmail')
@ -601,9 +852,8 @@ App::patch('/v1/users/:userId/email')
->param('email', '', new Email(), 'User email.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $userId, string $email, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -625,14 +875,7 @@ App::patch('/v1/users/:userId/email')
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
$audits
->setResource('user/' . $user->getId())
;
$events
->setParam('userId', $user->getId())
;
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -642,6 +885,7 @@ App::patch('/v1/users/:userId/phone')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.phone')
->label('scope', 'users.write')
->label('audits.resource', 'user/{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updatePhone')
@ -653,9 +897,8 @@ App::patch('/v1/users/:userId/phone')
->param('number', '', new Phone(), 'User phone number.')
->inject('response')
->inject('dbForProject')
->inject('audits')
->inject('events')
->action(function (string $userId, string $number, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
->action(function (string $userId, string $number, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
@ -675,14 +918,44 @@ App::patch('/v1/users/:userId/phone')
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
$events->setParam('userId', $user->getId());
$audits
->setResource('user/' . $user->getId())
;
$response->dynamic($user, Response::MODEL_USER);
});
$events
->setParam('userId', $user->getId())
;
App::patch('/v1/users/:userId/verification')
->desc('Update Email Verification')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.verification')
->label('scope', 'users.write')
->label('audits.resource', 'user/{request.userId}')
->label('audits.userId', '{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateEmailVerification')
->label('sdk.description', '/docs/references/users/update-user-email-verification.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new UID(), 'User ID.')
->param('emailVerification', false, new Boolean(), 'User email verification status.')
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, bool $emailVerification, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
@ -715,13 +988,9 @@ App::patch('/v1/users/:userId/prefs')
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$usage
->setParam('users.update', 1)
;
$usage->setParam('users.update', 1);
$events
->setParam('userId', $user->getId())
;
$events->setParam('userId', $user->getId());
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
@ -731,6 +1000,7 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('audits.resource', 'user/{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteSession')
@ -779,6 +1049,7 @@ App::delete('/v1/users/:userId/sessions')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('audits.resource', 'user/{user.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteSessions')
@ -825,6 +1096,7 @@ App::delete('/v1/users/:userId')
->groups(['api', 'users'])
->label('event', 'users.[userId].delete')
->label('scope', 'users.write')
->label('audits.resource', 'user/{request.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'delete')
@ -860,9 +1132,7 @@ App::delete('/v1/users/:userId')
->setPayload($response->output($clone, Response::MODEL_USER))
;
$usage
->setParam('users.delete', 1)
;
$usage->setParam('users.delete', 1);
$response->noContent();
});

View file

@ -3,6 +3,7 @@
require_once __DIR__ . '/../init.php';
use Utopia\App;
use Utopia\Database\Role;
use Utopia\Locale\Locale;
use Utopia\Logger\Logger;
use Utopia\Logger\Log;
@ -246,7 +247,9 @@ App::init()
/*
* ACL Check
*/
$role = ($user->isEmpty()) ? Auth::USER_ROLE_GUESTS : Auth::USER_ROLE_USERS;
$role = ($user->isEmpty())
? Role::guests()->toString()
: Role::users()->toString();
// Add user roles
$memberships = $user->find('teamId', $project->getAttribute('teamId', null), 'memberships');

View file

@ -14,10 +14,37 @@ use Utopia\App;
use Appwrite\Extend\Exception;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, Document $user) {
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
$find = $matches[0][$pos];
$parts = explode('.', $match);
if (count($parts) !== 2) {
throw new Exception('Too less or too many parts', 400, Exception::GENERAL_ARGUMENT_INVALID);
}
$namespace = $parts[0] ?? '';
$replace = $parts[1] ?? '';
$params = match ($namespace) {
'user' => (array)$user,
'request' => $requestParams,
default => $responsePayload,
};
if (array_key_exists($replace, $params)) {
$label = \str_replace($find, $params[$replace], $label);
}
}
return $label;
};
App::init()
->groups(['api'])
@ -42,9 +69,9 @@ App::init()
throw new Exception(Exception::PROJECT_UNKNOWN);
}
/*
* Abuse Check
*/
/*
* Abuse Check
*/
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$timeLimitArray = [];
@ -89,7 +116,7 @@ App::init()
if (
(App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' // Route is rate-limited
&& $abuse->check()) // Abuse is not disabled
&& $abuse->check()) // Abuse is not disabled
&& (!$isAppUser && !$isPrivilegedUser)
) { // User is not an admin or API key
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
@ -100,15 +127,13 @@ App::init()
* Background Jobs
*/
$events
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user)
;
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user);
$mails
->setProject($project)
->setUser($user)
;
->setUser($user);
$audits
->setMode($mode)
@ -116,8 +141,7 @@ App::init()
->setIP($request->getIP())
->setEvent($route->getLabel('event', ''))
->setProject($project)
->setUser($user)
;
->setUser($user);
$usage
->setParam('projectId', $project->getId())
@ -127,11 +151,35 @@ App::init()
->setParam('httpPath', $route->getPath())
->setParam('networkRequestSize', 0)
->setParam('networkResponseSize', 0)
->setParam('storage', 0)
;
->setParam('storage', 0);
$deletes->setProject($project);
$database->setProject($project);
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$key = md5($request->getURI() . implode('*', $request->getParams()));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$timestamp = 60 * 60 * 24 * 30;
$data = $cache->load($key, $timestamp);
if (!empty($data)) {
$data = json_decode($data, true);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT')
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($data['content-type'])
->send(base64_decode($data['payload']))
;
$route->setIsActive(false);
} else {
$response->addHeader('X-Appwrite-Cache', 'miss');
}
}
});
App::init()
@ -201,11 +249,13 @@ App::shutdown()
->inject('database')
->inject('mode')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) {
->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload();
if (!empty($events->getEvent())) {
if (empty($events->getPayload())) {
$events->setPayload($response->getPayload());
$events->setPayload($responsePayload);
}
/**
* Trigger functions.
@ -235,7 +285,7 @@ App::shutdown()
$bucket = $events->getContext('bucket');
$target = Realtime::fromPayload(
// Pass first, most verbose event pattern
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $payload,
project: $project,
@ -258,7 +308,38 @@ App::shutdown()
}
}
if (!empty($audits->getResource())) {
$route = $utopia->match($request);
$requestParams = $route->getParamsValues();
$user = $audits->getUser();
/**
* Audit labels
*/
$pattern = $route->getLabel('audits.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
if (!empty($resource) && $resource !== $pattern) {
$audits->setResource($resource);
}
}
$pattern = $route->getLabel('audits.userId', null);
if (!empty($pattern)) {
$userId = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$user = $dbForProject->getDocument('users', $userId);
$audits->setUser($user);
}
if (!empty($audits->getResource()) && !empty($audits->getUser()->getId())) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (!empty($pattern)) {
$audits->setPayload($responsePayload);
}
foreach ($events->getParams() as $key => $value) {
$audits->setParam($key, $value);
}
@ -273,16 +354,58 @@ App::shutdown()
$database->trigger();
}
$route = $utopia->match($request);
/**
* Cache label
*/
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$resource = null;
$data = $response->getPayload();
if (!empty($data['payload'])) {
$pattern = $route->getLabel('cache.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
$key = md5($request->getURI() . implode('*', $request->getParams()));
$data = json_encode([
'content-type' => $response->getContentType(),
'payload' => base64_encode($data['payload']),
]) ;
$signature = md5($data);
$cacheLog = $dbForProject->getDocument('cache', $key);
if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
'resource' => $resource,
'accessedAt' => \time(),
'signature' => $signature,
])));
} elseif (date('Y/m/d', \time()) > date('Y/m/d', $cacheLog->getAttribute('accessedAt'))) {
$cacheLog->setAttribute('accessedAt', \time());
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
}
if ($signature !== $cacheLog->getAttribute('signature')) {
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$cache->save($key, $data);
}
}
}
if (
App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled'
&& $project->getId()
&& $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin
&& !empty($route->getLabel('sdk.namespace', null))
) { // Don't calculate console usage on admin mode
) {
$usage
->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage'))
->setParam('networkResponseSize', $response->getSize())
->submit();
->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage'))
->setParam('networkResponseSize', $response->getSize())
->submit();
}
});

View file

@ -494,6 +494,7 @@ App::post('/v1/execution')
$executionStart = \microtime(true);
$stdout = '';
$stderr = '';
$res = '';
$statusCode = 0;
$errNo = -1;
$executorResponse = '';
@ -521,6 +522,7 @@ App::post('/v1/execution')
]);
$executorResponse = \curl_exec($ch);
$executorResponse = json_decode($executorResponse, true);
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
@ -544,13 +546,19 @@ App::post('/v1/execution')
switch (true) {
case $statusCode >= 500:
$stderr = $executorResponse ?? 'Internal Runtime error.';
$stderr = ($executorResponse ?? [])['stderr'] ?? 'Internal Runtime error.';
$stdout = ($executorResponse ?? [])['stdout'] ?? 'Internal Runtime error.';
break;
case $statusCode >= 100:
$stdout = $executorResponse;
$stdout = $executorResponse['stdout'];
$res = $executorResponse['response'];
if (is_array($res)) {
$res = json_encode($res, JSON_UNESCAPED_UNICODE);
}
break;
default:
$stderr = $executorResponse ?? 'Execution failed.';
$stderr = ($executorResponse ?? [])['stderr'] ?? 'Execution failed.';
$stdout = ($executorResponse ?? [])['stdout'] ?? '';
break;
}
@ -563,7 +571,8 @@ App::post('/v1/execution')
$execution = [
'status' => $functionStatus,
'statusCode' => $statusCode,
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
'response' => \mb_strcut($res, 0, 1000000), // Limit to 1MB
'stdout' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
'time' => $executionTime,
];
@ -654,7 +663,7 @@ $http->on('start', function ($http) {
/**
* Warmup: make sure images are ready to run fast 🚀
*/
$runtimes = new Runtimes('v1');
$runtimes = new Runtimes('v2');
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
$runtimes = $runtimes->getAll(true, $allowList);
foreach ($runtimes as $runtime) {

View file

@ -258,7 +258,7 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
try {
Authorization::cleanRoles();
Authorization::setRole('any');
Authorization::setRole(Role::any()->toString());
$app->run($request, $response);
} catch (\Throwable $th) {

View file

@ -23,12 +23,12 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Phone\Mock;
use Appwrite\Auth\Phone\Telesign;
use Appwrite\Auth\Phone\TextMagic;
use Appwrite\Auth\Phone\Twilio;
use Appwrite\Auth\Phone\Msg91;
use Appwrite\Auth\Phone\Vonage;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\SMS\Adapter\Telesign;
use Appwrite\SMS\Adapter\TextMagic;
use Appwrite\SMS\Adapter\Twilio;
use Appwrite\SMS\Adapter\Msg91;
use Appwrite\SMS\Adapter\Vonage;
use Appwrite\DSN\DSN;
use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
@ -146,6 +146,8 @@ const DELETE_TYPE_USAGE = 'usage';
const DELETE_TYPE_REALTIME = 'realtime';
const DELETE_TYPE_BUCKETS = 'buckets';
const DELETE_TYPE_SESSIONS = 'sessions';
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
@ -998,8 +1000,8 @@ App::setResource('geodb', function ($register) {
return $register->get('geodb');
}, ['register']);
App::setResource('phone', function () {
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
App::setResource('sms', function () {
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();

View file

@ -434,7 +434,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$realtime->subscribe($project->getId(), $connection, $roles, $channels);
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_USER);
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
'type' => 'connected',
@ -549,7 +549,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
$user = $response->output($user, Response::MODEL_USER);
$user = $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
'type' => 'response',
'data' => [

View file

@ -129,6 +129,15 @@ $cli
}
}
function notifyDeleteCache($interval)
{
(new Delete())
->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP)
->setTimestamp(time() - $interval)
->trigger();
}
// # of days in seconds (1 day = 86400s)
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
$executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600');
@ -136,8 +145,9 @@ $cli
$abuseLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', '86400');
$usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600'); //36 hours
$usageStatsRetention1d = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_1D', '8640000'); // 100 days
$cacheRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days
Console::loop(function () use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) {
Console::loop(function () use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d, $cacheRetention) {
$database = getConsoleDB();
$time = DateTime::now();
@ -150,5 +160,6 @@ $cli
notifyDeleteConnections();
notifyDeleteExpiredSessions();
renewCertificates($database);
notifyDeleteCache($cacheRetention);
}, $interval);
});

View file

@ -392,10 +392,10 @@ sort($patterns);
<tr>
<th width="30"></th>
<th width="160">Created</th>
<th width="150">Status</th>
<th width="120">Trigger</th>
<th width="80">Runtime</th>
<th></th>
<th width="100">Status</th>
<th width="80">Trigger</th>
<th width="60">Runtime</th>
<th width=""></th>
</tr>
</thead>
<tbody data-ls-loop="project-function-executions.executions" data-ls-as="execution">
@ -416,29 +416,44 @@ sort($patterns);
<td data-title="Trigger: ">
<span data-ls-bind="{{execution.trigger}}"></span>
</td>
<td data-title="Runtime: ">
<td data-title="Time: ">
<span data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-ls-bind="{{execution.time|seconds2hum}}"></span>
<span data-ls-if="{{execution.status}} === 'waiting' || {{execution.status}} === 'processing'">-</span>
</td>
<td data-title="">
<div data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-title="">
<div data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-title="" style="display: flex;">
<button class="desktops-only pull-end link margin-start text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Stderr</button>
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Stdout</button>
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-response-{{execution.$id}}">Response</button>
<button class="desktops-only pull-end link margin-start text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Errors</button>
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Output</button>
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-response-{{execution.$id}}">Response</button>
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Stdout</button>
<button class="phones-only-inline tablets-only-inline link text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Stderr</button>
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Output</button>
<button class="phones-only-inline tablets-only-inline link text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Errors</button>
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-stdout-{{execution.$id}}">
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-response-{{execution.$id}}">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>STDOUT</h1>
<h1>RESPONSE</h1>
<div class="margin-bottom ide" data-ls-if="({{execution.response.length}})">
<pre data-ls-bind="{{execution.response}}"></pre>
</div>
<div class="margin-bottom" data-ls-if="(!{{execution.response.length}})">
<p>No Response was logged.</p>
</div>
</div>
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-stdout-{{execution.$id}}">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1>STDOUT</h1>
<div class="margin-bottom ide" data-ls-if="({{execution.stdout.length}})">
<pre data-ls-bind="{{execution.stdout}}"></pre>
</div>
<div class="margin-bottom" data-ls-if="(!{{execution.stdout.length}})">
<p>No output was logged.</p>
</div>
</div>

View file

@ -147,10 +147,11 @@ services:
- _APP_STATSD_PORT
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_SECRET
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
appwrite-realtime:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@ -514,8 +515,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
@ -545,6 +546,7 @@ services:
- _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
@ -595,7 +597,7 @@ services:
- _APP_REDIS_PASS
mariadb:
image: mariadb:10.8.3 # fix issues when upgrading using: mysql_upgrade -u root -p
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
restart: unless-stopped

View file

@ -1,6 +1,8 @@
<?php
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
@ -32,7 +34,6 @@ class DeletesV1 extends Worker
{
$project = new Document($this->args['project'] ?? []);
$type = $this->args['type'] ?? '';
switch (strval($type)) {
case DELETE_TYPE_DOCUMENT:
$document = new Document($this->args['document'] ?? []);
@ -106,6 +107,13 @@ class DeletesV1 extends Worker
case DELETE_TYPE_USAGE:
$this->deleteUsageStats($this->args['dateTime1d'], $this->args['dateTime30m']);
break;
case DELETE_TYPE_CACHE_BY_RESOURCE:
$this->deleteCacheByResource($project->getId());
break;
case DELETE_TYPE_CACHE_BY_TIMESTAMP:
$this->deleteCacheByTimestamp();
break;
default:
Console::error('No delete operation for type: ' . $type);
break;
@ -116,6 +124,49 @@ class DeletesV1 extends Worker
{
}
/**
* @param string $projectId
*/
protected function deleteCacheByResource(string $projectId): void
{
$this->deleteCacheFiles([
new Query('resource', Query::TYPE_EQUAL, [$this->args['resource']])
]);
}
protected function deleteCacheByTimestamp(): void
{
$this->deleteCacheFiles([
new Query('accessedAt', Query::TYPE_LESSER, [$this->args['timestamp']])
]);
}
protected function deleteCacheFiles($query): void
{
$this->deleteForProjectIds(function (string $projectId) use ($query) {
$dbForProject = $this->getProjectDB($projectId);
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId)
);
$this->deleteByGroup(
'cache',
$query,
$dbForProject,
function (Document $document) use ($cache, $projectId) {
$path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId();
if ($cache->purge($document->getId())) {
Console::success('Deleting cache file: ' . $path);
} else {
Console::error('Failed to delete cache file: ' . $path);
}
}
);
});
}
/**
* @param Document $document database document
* @param string $projectId

View file

@ -295,6 +295,7 @@ class FunctionsV1 extends Worker
->setAttribute('status', $executionResponse['status'])
->setAttribute('statusCode', $executionResponse['statusCode'])
->setAttribute('response', $executionResponse['response'])
->setAttribute('stdout', $executionResponse['stdout'])
->setAttribute('stderr', $executionResponse['stderr'])
->setAttribute('time', $executionResponse['time']);
} catch (\Throwable $th) {

View file

@ -1,12 +1,12 @@
<?php
use Appwrite\Auth\Phone;
use Appwrite\Auth\Phone\Mock;
use Appwrite\Auth\Phone\Telesign;
use Appwrite\Auth\Phone\TextMagic;
use Appwrite\Auth\Phone\Twilio;
use Appwrite\Auth\Phone\Msg91;
use Appwrite\Auth\Phone\Vonage;
use Appwrite\Auth\SMS;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\SMS\Adapter\Telesign;
use Appwrite\SMS\Adapter\TextMagic;
use Appwrite\SMS\Adapter\Twilio;
use Appwrite\SMS\Adapter\Msg91;
use Appwrite\SMS\Adapter\Vonage;
use Appwrite\DSN\DSN;
use Appwrite\Resque\Worker;
use Utopia\App;
@ -19,7 +19,7 @@ Console::success(APP_NAME . ' messaging worker v1 has started' . "\n");
class MessagingV1 extends Worker
{
protected ?Phone $phone = null;
protected ?SMS $sms = null;
protected ?string $from = null;
public function getName(): string
@ -29,11 +29,11 @@ class MessagingV1 extends Worker
public function init(): void
{
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
$this->phone = match ($dsn->getHost()) {
$this->sms = match ($dsn->getHost()) {
'mock' => new Mock('', ''), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
@ -43,12 +43,12 @@ class MessagingV1 extends Worker
default => null
};
$this->from = App::getEnv('_APP_PHONE_FROM');
$this->from = App::getEnv('_APP_SMS_FROM');
}
public function run(): void
{
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
Console::info('Skipped sms processing. No Phone provider has been set.');
return;
}
@ -62,7 +62,7 @@ class MessagingV1 extends Worker
$message = $this->args['message'];
try {
$this->phone->send($this->from, $recipient, $message);
$this->sms->send($this->from, $recipient, $message);
} catch (\Exception $error) {
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
}

View file

@ -42,16 +42,16 @@
"ext-zlib": "*",
"ext-sockets": "*",
"appwrite/php-clamav": "1.1.*",
"appwrite/php-runtimes": "0.10.*",
"utopia-php/framework": "0.20.*",
"appwrite/php-runtimes": "0.11.*",
"utopia-php/framework": "0.21.*",
"utopia-php/logger": "0.3.*",
"utopia-php/abuse": "0.10.*",
"utopia-php/abuse": "0.11.*",
"utopia-php/analytics": "0.2.*",
"utopia-php/audit": "0.11.*",
"utopia-php/audit": "0.12.*",
"utopia-php/cache": "0.6.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.23.0 as 0.22.0",
"utopia-php/database": "0.23.*",
"utopia-php/locale": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/preloader": "0.2.*",

58
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1858c544a5b5ae59bc6ff146a375a683",
"content-hash": "5d5b72c7940a8376e187cce1b33d327a",
"packages": [
{
"name": "adhocore/jwt",
@ -115,11 +115,11 @@
},
{
"name": "appwrite/php-runtimes",
"version": "0.10.0",
"version": "0.11.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
"reference": "09874846c6bdb7be58c97b12323d2b35ec995409"
"reference": "547fc026e11c0946846a8ac690898f5bf53be101"
},
"require": {
"php": ">=8.0",
@ -154,7 +154,7 @@
"php",
"runtimes"
],
"time": "2022-06-28T05:26:20+00:00"
"time": "2022-08-15T14:03:36+00:00"
},
{
"name": "chillerlan/php-qrcode",
@ -1733,23 +1733,23 @@
},
{
"name": "utopia-php/abuse",
"version": "0.10.0",
"version": "0.11.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "b5beadce6581291e4385b0cc86f1be2a79bb2ef0"
"reference": "f1096b92a8c47b19b0c55096775c186cab0b0a97"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/b5beadce6581291e4385b0cc86f1be2a79bb2ef0",
"reference": "b5beadce6581291e4385b0cc86f1be2a79bb2ef0",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/f1096b92a8c47b19b0c55096775c186cab0b0a97",
"reference": "f1096b92a8c47b19b0c55096775c186cab0b0a97",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.22.0"
"utopia-php/database": "0.23.0"
},
"require-dev": {
"phpunit/phpunit": "^9.4",
@ -1781,9 +1781,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/0.10.0"
"source": "https://github.com/utopia-php/abuse/tree/0.11.0"
},
"time": "2022-08-17T14:31:54+00:00"
"time": "2022-08-19T08:47:17+00:00"
},
{
"name": "utopia-php/analytics",
@ -1842,22 +1842,22 @@
},
{
"name": "utopia-php/audit",
"version": "0.11.0",
"version": "0.12.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "a06f784f8e8b69bcae4f1a5bca58d41bda76c250"
"reference": "fe5d2372d9c7f0e1abcf85eaf59ebeaa6f572168"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/a06f784f8e8b69bcae4f1a5bca58d41bda76c250",
"reference": "a06f784f8e8b69bcae4f1a5bca58d41bda76c250",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/fe5d2372d9c7f0e1abcf85eaf59ebeaa6f572168",
"reference": "fe5d2372d9c7f0e1abcf85eaf59ebeaa6f572168",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/database": "0.22.0"
"utopia-php/database": "0.23.0"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
@ -1889,9 +1889,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/0.11.0"
"source": "https://github.com/utopia-php/audit/tree/0.12.0"
},
"time": "2022-08-17T15:08:58+00:00"
"time": "2022-08-19T08:47:16+00:00"
},
{
"name": "utopia-php/cache",
@ -2170,16 +2170,16 @@
},
{
"name": "utopia-php/framework",
"version": "0.20.0",
"version": "0.21.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/framework.git",
"reference": "beb5e861c7d0a6256a1272e6b9d70b060ca8629a"
"reference": "5aa5431788460a782065e42b0e8a35e7f139af2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/framework/zipball/beb5e861c7d0a6256a1272e6b9d70b060ca8629a",
"reference": "beb5e861c7d0a6256a1272e6b9d70b060ca8629a",
"url": "https://api.github.com/repos/utopia-php/framework/zipball/5aa5431788460a782065e42b0e8a35e7f139af2f",
"reference": "5aa5431788460a782065e42b0e8a35e7f139af2f",
"shasum": ""
},
"require": {
@ -2213,9 +2213,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/framework/issues",
"source": "https://github.com/utopia-php/framework/tree/0.20.0"
"source": "https://github.com/utopia-php/framework/tree/0.21.0"
},
"time": "2022-07-30T09:55:28+00:00"
"time": "2022-08-12T11:37:21+00:00"
},
{
"name": "utopia-php/image",
@ -2833,12 +2833,12 @@
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "987f2933b97bd04f702ea08685f2be28a08a841c"
"reference": "1a67d9dcd2884a6a708176955f83e319ac53059e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/987f2933b97bd04f702ea08685f2be28a08a841c",
"reference": "987f2933b97bd04f702ea08685f2be28a08a841c",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/1a67d9dcd2884a6a708176955f83e319ac53059e",
"reference": "1a67d9dcd2884a6a708176955f83e319ac53059e",
"shasum": ""
},
"require": {
@ -2876,7 +2876,7 @@
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/master"
},
"time": "2022-08-17T12:50:44+00:00"
"time": "2022-08-19T10:03:22+00:00"
},
{
"name": "doctrine/instantiator",
@ -5389,5 +5389,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.2.0"
"plugin-api-version": "2.3.0"
}

View file

@ -103,11 +103,9 @@ services:
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
# - ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
- ./docs:/usr/src/code/docs
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
# - ./debug:/tmp
- ./dev:/usr/local/dev
depends_on:
- mariadb
@ -173,10 +171,11 @@ services:
- _APP_STATSD_PORT
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_SECRET
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
appwrite-realtime:
entrypoint: realtime
@ -207,7 +206,6 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
# - ./vendor:/usr/src/code/vendor
depends_on:
- mariadb
- redis
@ -330,7 +328,7 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
# - ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
#- ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
depends_on:
- redis
- mariadb
@ -545,8 +543,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
@ -580,6 +578,7 @@ services:
- _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
@ -639,7 +638,7 @@ services:
- _APP_REDIS_PASS
mariadb:
image: mariadb:10.8.3 # fix issues when upgrading using: mysql_upgrade -u root -p
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
networks:

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [Argon2](https://en.wikipedia.org/wiki/Argon2) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [MD5](https://en.wikipedia.org/wiki/MD5) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [PHPass](https://www.openwall.com/phpass/) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [Scrypt Modified](https://gist.github.com/Meldiron/eecf84a0225eccb5a378d45bb27462cc) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [Scrypt](https://github.com/Tarsnap/scrypt) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [SHA](https://en.wikipedia.org/wiki/Secure_Hash_Algorithm) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

2539
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,8 +6,8 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="true"
>
stopOnFailure="false"
>
<extensions>
<extension class="Appwrite\Tests\TestHook" />
</extensions>

View file

@ -866,6 +866,79 @@ if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createArgon2User(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/argon2';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createBcryptUser(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/bcrypt';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createMD5User(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/md5';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createPHPassUser(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/phpass';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createScryptUser(userId,email,password,passwordSalt,passwordCpu,passwordMemory,passwordParallel,passwordLength,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
if(typeof passwordSalt==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSalt"');}
if(typeof passwordCpu==='undefined'){throw new AppwriteException('Missing required parameter: "passwordCpu"');}
if(typeof passwordMemory==='undefined'){throw new AppwriteException('Missing required parameter: "passwordMemory"');}
if(typeof passwordParallel==='undefined'){throw new AppwriteException('Missing required parameter: "passwordParallel"');}
if(typeof passwordLength==='undefined'){throw new AppwriteException('Missing required parameter: "passwordLength"');}
let path='/users/scrypt';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordSalt!=='undefined'){payload['passwordSalt']=passwordSalt;}
if(typeof passwordCpu!=='undefined'){payload['passwordCpu']=passwordCpu;}
if(typeof passwordMemory!=='undefined'){payload['passwordMemory']=passwordMemory;}
if(typeof passwordParallel!=='undefined'){payload['passwordParallel']=passwordParallel;}
if(typeof passwordLength!=='undefined'){payload['passwordLength']=passwordLength;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createScryptModifiedUser(userId,email,password,passwordSalt,passwordSaltSeparator,passwordSignerKey,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
if(typeof passwordSalt==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSalt"');}
if(typeof passwordSaltSeparator==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSaltSeparator"');}
if(typeof passwordSignerKey==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSignerKey"');}
let path='/users/scrypt-modified';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordSalt!=='undefined'){payload['passwordSalt']=passwordSalt;}
if(typeof passwordSaltSeparator!=='undefined'){payload['passwordSaltSeparator']=passwordSaltSeparator;}
if(typeof passwordSignerKey!=='undefined'){payload['passwordSignerKey']=passwordSignerKey;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createSHAUser(userId,email,password,passwordVersion,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/sha';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordVersion!=='undefined'){payload['passwordVersion']=passwordVersion;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getUsage(range,provider){return __awaiter(this,void 0,void 0,function*(){let path='/users/usage';let payload={};if(typeof range!=='undefined'){payload['range']=range;}
if(typeof provider!=='undefined'){payload['provider']=provider;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}

View file

@ -866,6 +866,79 @@ if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createArgon2User(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/argon2';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createBcryptUser(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/bcrypt';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createMD5User(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/md5';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createPHPassUser(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/phpass';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createScryptUser(userId,email,password,passwordSalt,passwordCpu,passwordMemory,passwordParallel,passwordLength,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
if(typeof passwordSalt==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSalt"');}
if(typeof passwordCpu==='undefined'){throw new AppwriteException('Missing required parameter: "passwordCpu"');}
if(typeof passwordMemory==='undefined'){throw new AppwriteException('Missing required parameter: "passwordMemory"');}
if(typeof passwordParallel==='undefined'){throw new AppwriteException('Missing required parameter: "passwordParallel"');}
if(typeof passwordLength==='undefined'){throw new AppwriteException('Missing required parameter: "passwordLength"');}
let path='/users/scrypt';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordSalt!=='undefined'){payload['passwordSalt']=passwordSalt;}
if(typeof passwordCpu!=='undefined'){payload['passwordCpu']=passwordCpu;}
if(typeof passwordMemory!=='undefined'){payload['passwordMemory']=passwordMemory;}
if(typeof passwordParallel!=='undefined'){payload['passwordParallel']=passwordParallel;}
if(typeof passwordLength!=='undefined'){payload['passwordLength']=passwordLength;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createScryptModifiedUser(userId,email,password,passwordSalt,passwordSaltSeparator,passwordSignerKey,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
if(typeof passwordSalt==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSalt"');}
if(typeof passwordSaltSeparator==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSaltSeparator"');}
if(typeof passwordSignerKey==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSignerKey"');}
let path='/users/scrypt-modified';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordSalt!=='undefined'){payload['passwordSalt']=passwordSalt;}
if(typeof passwordSaltSeparator!=='undefined'){payload['passwordSaltSeparator']=passwordSaltSeparator;}
if(typeof passwordSignerKey!=='undefined'){payload['passwordSignerKey']=passwordSignerKey;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createSHAUser(userId,email,password,passwordVersion,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/sha';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordVersion!=='undefined'){payload['passwordVersion']=passwordVersion;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getUsage(range,provider){return __awaiter(this,void 0,void 0,function*(){let path='/users/usage';let payload={};if(typeof range!=='undefined'){payload['range']=range;}
if(typeof provider!=='undefined'){payload['provider']=provider;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}

View file

@ -695,7 +695,7 @@
* stored as is, and replaces any previous value. The maximum allowed prefs
* size is 64kB and throws error if exceeded.
*
* @param {Partial<Preferences>} prefs
* @param {object} prefs
* @throws {AppwriteException}
* @returns {Promise}
*/
@ -6199,6 +6199,388 @@
}, payload);
});
}
/**
* Create User with Argon2 Password
*
* Create a new user. Password provided must be hashed with the
* [Argon2](https://en.wikipedia.org/wiki/Argon2) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createArgon2User(userId, email, password, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/argon2';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with Bcrypt Password
*
* Create a new user. Password provided must be hashed with the
* [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createBcryptUser(userId, email, password, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/bcrypt';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with MD5 Password
*
* Create a new user. Password provided must be hashed with the
* [MD5](https://en.wikipedia.org/wiki/MD5) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createMD5User(userId, email, password, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/md5';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with PHPass Password
*
* Create a new user. Password provided must be hashed with the
* [PHPass](https://www.openwall.com/phpass/) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createPHPassUser(userId, email, password, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/phpass';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with Scrypt Password
*
* Create a new user. Password provided must be hashed with the
* [Scrypt](https://github.com/Tarsnap/scrypt) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} passwordSalt
* @param {number} passwordCpu
* @param {number} passwordMemory
* @param {number} passwordParallel
* @param {number} passwordLength
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createScryptUser(userId, email, password, passwordSalt, passwordCpu, passwordMemory, passwordParallel, passwordLength, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
if (typeof passwordSalt === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordSalt"');
}
if (typeof passwordCpu === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordCpu"');
}
if (typeof passwordMemory === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordMemory"');
}
if (typeof passwordParallel === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordParallel"');
}
if (typeof passwordLength === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordLength"');
}
let path = '/users/scrypt';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof passwordSalt !== 'undefined') {
payload['passwordSalt'] = passwordSalt;
}
if (typeof passwordCpu !== 'undefined') {
payload['passwordCpu'] = passwordCpu;
}
if (typeof passwordMemory !== 'undefined') {
payload['passwordMemory'] = passwordMemory;
}
if (typeof passwordParallel !== 'undefined') {
payload['passwordParallel'] = passwordParallel;
}
if (typeof passwordLength !== 'undefined') {
payload['passwordLength'] = passwordLength;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with Scrypt Modified Password
*
* Create a new user. Password provided must be hashed with the [Scrypt
* Modified](https://gist.github.com/Meldiron/eecf84a0225eccb5a378d45bb27462cc)
* algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint
* to create users with a plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} passwordSalt
* @param {string} passwordSaltSeparator
* @param {string} passwordSignerKey
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createScryptModifiedUser(userId, email, password, passwordSalt, passwordSaltSeparator, passwordSignerKey, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
if (typeof passwordSalt === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordSalt"');
}
if (typeof passwordSaltSeparator === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordSaltSeparator"');
}
if (typeof passwordSignerKey === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordSignerKey"');
}
let path = '/users/scrypt-modified';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof passwordSalt !== 'undefined') {
payload['passwordSalt'] = passwordSalt;
}
if (typeof passwordSaltSeparator !== 'undefined') {
payload['passwordSaltSeparator'] = passwordSaltSeparator;
}
if (typeof passwordSignerKey !== 'undefined') {
payload['passwordSignerKey'] = passwordSignerKey;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with SHA Password
*
* Create a new user. Password provided must be hashed with the
* [SHA](https://en.wikipedia.org/wiki/Secure_Hash_Algorithm) algorithm. Use
* the [POST /users](/docs/server/users#usersCreate) endpoint to create users
* with a plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} passwordVersion
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createSHAUser(userId, email, password, passwordVersion, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/sha';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof passwordVersion !== 'undefined') {
payload['passwordVersion'] = passwordVersion;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Get usage stats for the users API
*

View file

@ -2,12 +2,34 @@
namespace Appwrite\Auth;
use Appwrite\Auth\Hash\Argon2;
use Appwrite\Auth\Hash\Bcrypt;
use Appwrite\Auth\Hash\Md5;
use Appwrite\Auth\Hash\Phpass;
use Appwrite\Auth\Hash\Scrypt;
use Appwrite\Auth\Hash\Scryptmodified;
use Appwrite\Auth\Hash\Sha;
use Utopia\Database\Document;
use Utopia\Database\DateTime;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
class Auth
{
public const SUPPORTED_ALGOS = [
'argon2',
'bcrypt',
'md5',
'sha',
'phpass',
'scrypt',
'scryptMod',
'plaintext'
];
public const DEFAULT_ALGO = 'argon2';
public const DEFAULT_ALGO_OPTIONS = ['memoryCost' => 2048, 'timeCost' => 4, 'threads' => 3];
/**
* User Roles.
*/
@ -134,26 +156,98 @@ class Auth
*
* One way string hashing for user passwords
*
* @param $string
* @param string $string
* @param string $algo hashing algorithm to use
* @param array $options algo-specific options
*
* @return bool|string|null
*/
public static function passwordHash($string)
public static function passwordHash(string $string, string $algo, array $options = [])
{
return \password_hash($string, PASSWORD_BCRYPT, array('cost' => 8));
// Plain text not supported, just an alias. Switch to recommended algo
if ($algo === 'plaintext') {
$algo = Auth::DEFAULT_ALGO;
$options = Auth::DEFAULT_ALGO_OPTIONS;
}
if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) {
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
switch ($algo) {
case 'argon2':
$hasher = new Argon2($options);
return $hasher->hash($string);
case 'bcrypt':
$hasher = new Bcrypt($options);
return $hasher->hash($string);
case 'md5':
$hasher = new Md5($options);
return $hasher->hash($string);
case 'sha':
$hasher = new Sha($options);
return $hasher->hash($string);
case 'phpass':
$hasher = new Phpass($options);
return $hasher->hash($string);
case 'scrypt':
$hasher = new Scrypt($options);
return $hasher->hash($string);
case 'scryptMod':
$hasher = new Scryptmodified($options);
return $hasher->hash($string);
default:
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
}
/**
* Password verify.
*
* @param $plain
* @param $hash
* @param string $plain
* @param string $hash
* @param string $algo hashing algorithm used to hash
* @param array $options algo-specific options
*
* @return bool
*/
public static function passwordVerify($plain, $hash)
public static function passwordVerify(string $plain, string $hash, string $algo, array $options = [])
{
return \password_verify($plain, $hash);
// Plain text not supported, just an alias. Switch to recommended algo
if ($algo === 'plaintext') {
$algo = Auth::DEFAULT_ALGO;
$options = Auth::DEFAULT_ALGO_OPTIONS;
}
if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) {
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
switch ($algo) {
case 'argon2':
$hasher = new Argon2($options);
return $hasher->verify($plain, $hash);
case 'bcrypt':
$hasher = new Bcrypt($options);
return $hasher->verify($plain, $hash);
case 'md5':
$hasher = new Md5($options);
return $hasher->verify($plain, $hash);
case 'sha':
$hasher = new Sha($options);
return $hasher->verify($plain, $hash);
case 'phpass':
$hasher = new Phpass($options);
return $hasher->verify($plain, $hash);
case 'scrypt':
$hasher = new Scrypt($options);
return $hasher->verify($plain, $hash);
case 'scryptMod':
$hasher = new Scryptmodified($options);
return $hasher->verify($plain, $hash);
default:
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
}
/**
@ -164,8 +258,6 @@ class Auth
* @param int $length
*
* @return string
*
* @throws \Exception
*/
public static function passwordGenerator(int $length = 20): string
{
@ -180,14 +272,32 @@ class Auth
* @param int $length
*
* @return string
*
* @throws \Exception
*/
public static function tokenGenerator(int $length = 128): string
{
return \bin2hex(\random_bytes($length));
}
/**
* Code Generator.
*
* Generate random code string
*
* @param int $length
*
* @return string
*/
public static function codeGenerator(int $length = 6): string
{
$value = '';
for ($i = 0; $i < $length; $i++) {
$value .= random_int(0, 9);
}
return $value;
}
/**
* Verify token and check that its not expired.
*
@ -309,19 +419,19 @@ class Auth
if (!self::isPrivilegedUser(Authorization::getRoles()) && !self::isAppUser(Authorization::getRoles())) {
if ($user->getId()) {
$roles[] = 'user:' . $user->getId();
$roles[] = Auth::USER_ROLE_USERS;
$roles[] = Role::user($user->getId())->toString();
$roles[] = Role::users()->toString();
} else {
return [Auth::USER_ROLE_GUESTS];
return [Role::guests()->toString()];
}
}
foreach ($user->getAttribute('memberships', []) as $node) {
if (isset($node['teamId']) && isset($node['roles'])) {
$roles[] = 'team:' . $node['teamId'];
$roles[] = Role::team($node['teamId'])->toString();
foreach ($node['roles'] as $nodeRole) { // Set all team roles
$roles[] = 'team:' . $node['teamId'] . '/' . $nodeRole;
$roles[] = Role::team($node['teamId'], $nodeRole)->toString();
}
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Appwrite\Auth;
abstract class Hash
{
/**
* @var array $options Hashing-algo specific options
*/
protected array $options = [];
/**
* @param array $options Hashing-algo specific options
*/
public function __construct(array $options = [])
{
$this->setOptions($options);
}
/**
* Set hashing algo options
*
* @param array $options Hashing-algo specific options
*/
public function setOptions(array $options): self
{
$this->options = \array_merge([], $this->getDefaultOptions(), $options);
return $this;
}
/**
* Get hashing algo options
*
* @return array $options Hashing-algo specific options
*/
public function getOptions(): array
{
return $this->options;
}
/**
* @param string $password Input password to hash
*
* @return string hash
*/
abstract public function hash(string $password): string;
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
abstract public function verify(string $password, string $hash): bool;
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
abstract public function getDefaultOptions(): array;
}

View file

@ -0,0 +1,47 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* Argon2 accepted options:
* int threads
* int time_cost
* int memory_cost
*
* Reference: https://www.php.net/manual/en/function.password-hash.php#example-983
*/
class Argon2 extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
return \password_hash($password, PASSWORD_ARGON2ID, $this->getOptions());
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return \password_verify($password, $hash);
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3];
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* Bcrypt accepted options:
* int cost
* string? salt; auto-generated if empty
*
* Reference: https://www.php.net/manual/en/password.constants.php
*/
class Bcrypt extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
return \password_hash($password, PASSWORD_BCRYPT, $this->getOptions());
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return \password_verify($password, $hash);
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [ 'cost' => 8 ];
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* MD5 does not accept any options.
*
* Reference: https://www.php.net/manual/en/function.md5.php
*/
class Md5 extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
return \md5($password);
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return $this->hash($password) === $hash;
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [];
}
}

View file

@ -0,0 +1,290 @@
<?php
/**
* Portable PHP password hashing framework.
* source Version 0.5 / genuine.
* Written by Solar Designer <solar at openwall.com> in 2004-2017 and placed in
* the public domain. Revised in subsequent years, still public domain.
* There's absolutely no warranty.
* The homepage URL for the source framework is: http://www.openwall.com/phpass/
* Please be sure to update the Version line if you edit this file in any way.
* It is suggested that you leave the main version number intact, but indicate
* your project name (after the slash) and add your own revision information.
* Please do not change the "private" password hashing method implemented in
* here, thereby making your hashes incompatible. However, if you must, please
* change the hash type identifier (the "$P$") to something different.
* Obviously, since this code is in the public domain, the above are not
* requirements (there can be none), but merely suggestions.
*
* @author Solar Designer <solar@openwall.com>
* @copyright Copyright (C) 2017 All rights reserved.
* @license http://www.opensource.org/licenses/mit-license.html MIT License; see LICENSE.txt
*/
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* PHPass accepted options:
* int iteration_count_log2; The Logarithmic cost value used when generating hash values indicating the number of rounds used to generate hashes
* string portable_hashes
* string random_state; The cached random state
*
* Reference: https://github.com/photodude/phpass
*/
class Phpass extends Hash
{
/**
* Alphabet used in itoa64 conversions.
*
* @var string
* @since 0.1.0
*/
protected string $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
$randomState = \microtime();
if (\function_exists('getmypid')) {
$randomState .= getmypid();
}
return ['iteration_count_log2' => 8, 'portable_hashes' => false, 'random_state' => $randomState];
}
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
$options = $this->getDefaultOptions();
$random = '';
if (CRYPT_BLOWFISH === 1 && !$options['portable_hashes']) {
$random = $this->getRandomBytes(16, $options);
$hash = crypt($password, $this->gensaltBlowfish($random, $options));
if (strlen($hash) === 60) {
return $hash;
}
}
if (strlen($random) < 6) {
$random = $this->getRandomBytes(6, $options);
}
$hash = $this->cryptPrivate($password, $this->gensaltPrivate($random, $options));
if (strlen($hash) === 34) {
return $hash;
}
/**
* Returning '*' on error is safe here, but would _not_ be safe
* in a crypt(3)-like function used _both_ for generating new
* hashes and for validating passwords against existing hashes.
*/
return '*';
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
$verificationHash = $this->cryptPrivate($password, $hash);
if ($verificationHash[0] === '*') {
$verificationHash = crypt($password, $hash);
}
/**
* This is not constant-time. In order to keep the code simple,
* for timing safety we currently rely on the salts being
* unpredictable, which they are at least in the non-fallback
* cases (that is, when we use /dev/urandom and bcrypt).
*/
return $hash === $verificationHash;
}
/**
* @param int $count
*
* @return String $output
* @since 0.1.0
* @throws Exception Thows an Exception if the $count parameter is not a positive integer.
*/
protected function getRandomBytes(int $count, array $options): string
{
if (!is_int($count) || $count < 1) {
throw new \Exception('Argument count must be a positive integer');
}
$output = '';
if (@is_readable('/dev/urandom') && ($fh = @fopen('/dev/urandom', 'rb'))) {
$output = fread($fh, $count);
fclose($fh);
}
if (strlen($output) < $count) {
$output = '';
for ($i = 0; $i < $count; $i += 16) {
$options['iteration_count_log2'] = md5(microtime() . $options['iteration_count_log2']);
$output .= md5($options['iteration_count_log2'], true);
}
$output = substr($output, 0, $count);
}
return $output;
}
/**
* @param String $input
* @param int $count
*
* @return String $output
* @since 0.1.0
* @throws Exception Thows an Exception if the $count parameter is not a positive integer.
*/
protected function encode64($input, $count)
{
if (!is_int($count) || $count < 1) {
throw new \Exception('Argument count must be a positive integer');
}
$output = '';
$i = 0;
do {
$value = ord($input[$i++]);
$output .= $this->itoa64[$value & 0x3f];
if ($i < $count) {
$value |= ord($input[$i]) << 8;
}
$output .= $this->itoa64[($value >> 6) & 0x3f];
if ($i++ >= $count) {
break;
}
if ($i < $count) {
$value |= ord($input[$i]) << 16;
}
$output .= $this->itoa64[($value >> 12) & 0x3f];
if ($i++ >= $count) {
break;
}
$output .= $this->itoa64[($value >> 18) & 0x3f];
} while ($i < $count);
return $output;
}
/**
* @param String $input
*
* @return String $output
* @since 0.1.0
*/
private function gensaltPrivate($input, $options)
{
$output = '$P$';
$output .= $this->itoa64[min($options['iteration_count_log2'] + ((PHP_VERSION >= '5') ? 5 : 3), 30)];
$output .= $this->encode64($input, 6);
return $output;
}
/**
* @param String $password
* @param String $setting
*
* @return String $output
* @since 0.1.0
*/
private function cryptPrivate($password, $setting)
{
$output = '*0';
if (substr($setting, 0, 2) === $output) {
$output = '*1';
}
$id = substr($setting, 0, 3);
// We use "$P$", phpBB3 uses "$H$" for the same thing
if ($id !== '$P$' && $id !== '$H$') {
return $output;
}
$count_log2 = strpos($this->itoa64, $setting[3]);
if ($count_log2 < 7 || $count_log2 > 30) {
return $output;
}
$count = 1 << $count_log2;
$salt = substr($setting, 4, 8);
if (strlen($salt) !== 8) {
return $output;
}
/**
* We were kind of forced to use MD5 here since it's the only
* cryptographic primitive that was available in all versions of PHP
* in use. To implement our own low-level crypto in PHP
* would have result in much worse performance and
* consequently in lower iteration counts and hashes that are
* quicker to crack (by non-PHP code).
*/
$hash = md5($salt . $password, true);
do {
$hash = md5($hash . $password, true);
} while (--$count);
$output = substr($setting, 0, 12);
$output .= $this->encode64($hash, 16);
return $output;
}
/**
* @param String $input
*
* @return String $output
* @since 0.1.0
*/
private function gensaltBlowfish($input, $options)
{
/**
* This one needs to use a different order of characters and a
* different encoding scheme from the one in encode64() above.
* We care because the last character in our encoded string will
* only represent 2 bits. While two known implementations of
* bcrypt will happily accept and correct a salt string which
* has the 4 unused bits set to non-zero, we do not want to take
* chances and we also do not want to waste an additional byte
* of entropy.
*/
$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$output = '$2a$';
$output .= chr(ord('0') + $options['iteration_count_log2'] / 10);
$output .= chr(ord('0') + $options['iteration_count_log2'] % 10);
$output .= '$';
$i = 0;
do {
$c1 = ord($input[$i++]);
$output .= $itoa64[$c1 >> 2];
$c1 = ($c1 & 0x03) << 4;
if ($i >= 16) {
$output .= $itoa64[$c1];
break;
}
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 4;
$output .= $itoa64[$c1];
$c1 = ($c2 & 0x0f) << 2;
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 6;
$output .= $itoa64[$c1];
$output .= $itoa64[$c2 & 0x3f];
} while (1);
return $output;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* Scrypt accepted options:
* string? salt; auto-generated if empty
* int costCpu
* int costMemory
* int costParallel
* int length
*
* Reference: https://github.com/DomBlack/php-scrypt/blob/master/scrypt.php#L112-L116
*/
class Scrypt extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
$options = $this->getOptions();
return \scrypt($password, $options['salt'], $options['costCpu'], $options['costMemory'], $options['costParallel'], $options['length']);
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return $hash === $this->hash($password);
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [ 'costCpu' => 8, 'costMemory' => 14, 'costParallel' => 1, 'length' => 64 ];
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* This is Scrypt hash with some additional steps added by Google.
*
* string salt
* string saltSeparator
* strin signerKey
*
* Reference: https://github.com/DomBlack/php-scrypt/blob/master/scrypt.php#L112-L116
*/
class Scryptmodified extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
$options = $this->getOptions();
$derivedKeyBytes = $this->generateDerivedKey($password);
$signerKeyBytes = \base64_decode($options['signerKey']);
$hashedPassword = $this->hashKeys($signerKeyBytes, $derivedKeyBytes);
return \base64_encode($hashedPassword);
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return $this->hash($password) === $hash;
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [ ];
}
private function generateDerivedKey(string $password)
{
$options = $this->getOptions();
$saltBytes = \base64_decode($options['salt']);
$saltSeparatorBytes = \base64_decode($options['saltSeparator']);
$derivedKey = \scrypt(\utf8_encode($password), $saltBytes . $saltSeparatorBytes, 16384, 8, 1, 64);
$derivedKey = \hex2bin($derivedKey);
return $derivedKey;
}
private function hashKeys($signerKeyBytes, $derivedKeyBytes): string
{
$key = \substr($derivedKeyBytes, 0, 32);
$iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
$hash = \openssl_encrypt($signerKeyBytes, 'aes-256-ctr', $key, OPENSSL_RAW_DATA, $iv);
return $hash;
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* SHA accepted options:
* string? version. Allowed:
* - Version 1: sha1
* - Version 2: sha224, sha256, sha384, sha512/224, sha512/256, sha512
* - Version 3: sha3-224, sha3-256, sha3-384, sha3-512
*
* Reference: https://www.php.net/manual/en/function.hash-algos.php
*/
class Sha extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
$algo = $this->getOptions()['version'];
return \hash($algo, $password);
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return $this->hash($password) === $hash;
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [ 'version' => 'sha3-512' ];
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace Appwrite\Auth\Phone;
use Appwrite\Auth\Phone;
class Mock extends Phone
{
/**
* @var string
*/
public static string $defaultDigits = '123456';
/**
* @param string $from
* @param string $to
* @param string $message
* @return void
*/
public function send(string $from, string $to, string $message): void
{
return;
}
/**
* @param int $digits
* @return string
*/
public function generateSecretDigits(int $digits = 6): string
{
return self::$defaultDigits;
}
}

View file

@ -9,6 +9,7 @@ class Delete extends Event
{
protected string $type = '';
protected ?Document $document = null;
protected ?string $resource = null;
protected ?string $datetime = null;
protected ?string $datetime1d = null;
protected ?string $datetime30m = null;
@ -90,6 +91,29 @@ class Delete extends Event
return $this;
}
/**
* Returns the resource for the delete event.
*
* @return string
*/
public function getResource(): string
{
return $this->resource;
}
/**
* Sets the resource for the delete event.
*
* @param string $resource
* @return self
*/
public function setResource(string $resource): self
{
$this->resource = $resource;
return $this;
}
/**
* Returns the set document for the delete event.
*
@ -100,6 +124,7 @@ class Delete extends Event
return $this->document;
}
/**
* Executes this event and sends it to the deletes worker.
*
@ -112,9 +137,10 @@ class Delete extends Event
'project' => $this->project,
'type' => $this->type,
'document' => $this->document,
'resource' => $this->resource,
'datetime' => $this->datetime,
'datetime1d' => $this->datetime1d,
'datetime30m' => $this->datetime30m
'datetime30m' => $this->datetime30m,
]);
}
}

View file

@ -50,7 +50,6 @@ class Exception extends \Exception
public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found';
public const GENERAL_SERVER_ERROR = 'general_server_error';
public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported';
public const GENERAL_PERMISSION_INVALID = 'general_permission_invalid';
/** Users */
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';

View file

@ -6,6 +6,8 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Appwrite\Messaging\Adapter;
use Utopia\App;
use Utopia\Database\ID;
use Utopia\Database\Role;
class Realtime extends Adapter
{
@ -187,7 +189,7 @@ class Realtime extends Adapter
*/
if (
\array_key_exists($channel, $this->subscriptions[$event['project']][$role])
&& (\in_array($role, $event['roles']) || \in_array('any', $event['roles']))
&& (\in_array($role, $event['roles']) || \in_array(Role::any()->toString(), $event['roles']))
) {
/**
* Saving all connections that are allowed to receive this event.
@ -256,27 +258,25 @@ class Realtime extends Adapter
case 'users':
$channels[] = 'account';
$channels[] = 'account.' . $parts[1];
$roles = ['user:' . $parts[1]];
$roles = [Role::user(ID::custom($parts[1]))->toString()];
break;
case 'teams':
if ($parts[2] === 'memberships') {
$permissionsChanged = $parts[4] ?? false;
$channels[] = 'memberships';
$channels[] = 'memberships.' . $parts[3];
$roles = ['team:' . $parts[1]];
} else {
$permissionsChanged = $parts[2] === 'create';
$channels[] = 'teams';
$channels[] = 'teams.' . $parts[1];
$roles = ['team:' . $parts[1]];
}
$roles = [Role::team(ID::custom($parts[1]))->toString()];
break;
case 'databases':
if (in_array($parts[4] ?? [], ['attributes', 'indexes'])) {
$channels[] = 'console';
$projectId = 'console';
$roles = ['team:' . $project->getAttribute('teamId')];
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
} elseif (($parts[4] ?? '') === 'documents') {
if ($database->isEmpty()) {
throw new \Exception('Database needs to be passed to Realtime for Document events in the Database.');
@ -288,7 +288,8 @@ class Realtime extends Adapter
$channels[] = 'documents';
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getCollection() . '.documents';
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getCollection() . '.documents.' . $payload->getId();
$roles = ($collection->getAttribute('documentSecurity', false))
$roles = $collection->getAttribute('documentSecurity', false)
? \array_merge($collection->getRead(), $payload->getRead())
: $collection->getRead();
}
@ -301,6 +302,7 @@ class Realtime extends Adapter
$channels[] = 'files';
$channels[] = 'buckets.' . $payload->getAttribute('bucketId') . '.files';
$channels[] = 'buckets.' . $payload->getAttribute('bucketId') . '.files.' . $payload->getId();
$roles = $bucket->getAttribute('fileSecurity', false)
? \array_merge($bucket->getRead(), $payload->getRead())
: $bucket->getRead();
@ -319,7 +321,8 @@ class Realtime extends Adapter
}
} elseif ($parts[2] === 'deployments') {
$channels[] = 'console';
$roles = ['team:' . $project->getAttribute('teamId')];
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;

View file

@ -1,10 +1,8 @@
<?php
namespace Appwrite\Auth;
namespace Appwrite\SMS;
use Appwrite\Extend\Exception;
abstract class Phone
abstract class Adapter
{
/**
* @var string
@ -69,20 +67,9 @@ abstract class Phone
\curl_close($ch);
if ($code >= 400) {
throw new Exception($response);
throw new \Exception($response);
}
return $response;
}
/**
* Generate 6 random digits for phone verification.
*
* @param int $digits
* @return string
*/
public function generateSecretDigits(int $digits = 6): string
{
return substr(str_shuffle("0123456789"), 0, $digits);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Appwrite\SMS\Adapter;
use Appwrite\SMS\Adapter;
class Mock extends Adapter
{
/**
* @var string
*/
public static string $digits = '123456';
/**
* @param string $from
* @param string $to
* @param string $message
* @return void
*/
public function send(string $from, string $to, string $message): void
{
return;
}
}

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://docs.msg91.com/p/tf9GTextN/e/Irz7-x1PK/MSG91
class Msg91 extends Phone
class Msg91 extends Adapter
{
/**
* @var string
@ -16,10 +16,10 @@ class Msg91 extends Phone
/**
* For Flow based sending SMS sender ID should not be set in flow
* In environment _APP_PHONE_PROVIDER format is 'phone://[senderID]:[authKey]@msg91'.
* _APP_PHONE_FROM value is flow ID created in Msg91
* Eg. _APP_PHONE_PROVIDER = phone://DINESH:5e1e93cad6fc054d8e759a5b@msg91
* _APP_PHONE_FROM = 3968636f704b303135323339
* In environment _APP_SMS_PROVIDER format is 'sms://[senderID]:[authKey]@msg91'.
* _APP_SMS_FROM value is flow ID created in Msg91
* Eg. _APP_SMS_PROVIDER = sms://DINESH:5e1e93cad6fc054d8e759a5b@msg91
* _APP_SMS_FROM = 3968636f704b303135323339
* @param string $from-> utilized from for flow id
* @param string $to
* @param string $message

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://developer.telesign.com/enterprise/docs/sms-api-send-an-sms
class Telesign extends Phone
class Telesign extends Adapter
{
/**
* @var string

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://www.textmagic.com/docs/api/start/
class TextMagic extends Phone
class TextMagic extends Adapter
{
/**
* @var string

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://www.twilio.com/docs/sms/api
class Twilio extends Phone
class Twilio extends Adapter
{
/**
* @var string

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://developer.vonage.com/api/sms
class Vonage extends Phone
class Vonage extends Adapter
{
/**
* @var string

View file

@ -17,15 +17,32 @@ class OpenAPI3 extends Format
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (
in_array($model->getType(), $usedModels)
&& !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double'])
) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
return;
if (!in_array($model->getType(), $usedModels)) {
continue;
}
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $ruleType;
foreach ($this->models as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
} else {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}

View file

@ -5,6 +5,8 @@ namespace Appwrite\Specification\Format;
use Appwrite\Specification\Format;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Permission;
use Utopia\Database\Role;
use Utopia\Validator;
class Swagger2 extends Format
@ -17,15 +19,32 @@ class Swagger2 extends Format
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (
in_array($model->getType(), $usedModels)
&& !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double'])
) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
return;
if (!in_array($model->getType(), $usedModels)) {
continue;
}
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $ruleType;
foreach ($this->models as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
} else {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
@ -317,7 +336,7 @@ class Swagger2 extends Format
$node['items'] = [
'type' => 'string',
];
$node['x-example'] = '["read(any)"]';
$node['x-example'] = '["' . Permission::read(Role::any()) . '"]';
break;
case 'Utopia\Database\Validator\Roles':
$node['type'] = $validator->getType();
@ -325,7 +344,7 @@ class Swagger2 extends Format
$node['items'] = [
'type' => 'string',
];
$node['x-example'] = '["any"]';
$node['x-example'] = '["' . Role::any()->toString() . '"]';
break;
case 'Appwrite\Auth\Validator\Password':
$node['type'] = $validator->getType();

View file

@ -8,6 +8,14 @@ use Swoole\Http\Response as SwooleHTTPResponse;
use Utopia\Database\Document;
use Appwrite\Utopia\Response\Filter;
use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\Account;
use Appwrite\Utopia\Response\Model\AlgoArgon2;
use Appwrite\Utopia\Response\Model\AlgoBcrypt;
use Appwrite\Utopia\Response\Model\AlgoMd5;
use Appwrite\Utopia\Response\Model\AlgoPhpass;
use Appwrite\Utopia\Response\Model\AlgoScrypt;
use Appwrite\Utopia\Response\Model\AlgoScryptModified;
use Appwrite\Utopia\Response\Model\AlgoSha;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
@ -120,6 +128,7 @@ class Response extends SwooleResponse
public const MODEL_ATTRIBUTE_DATETIME = 'attributeDatetime';
// Users
public const MODEL_ACCOUNT = 'account';
public const MODEL_USER = 'user';
public const MODEL_USER_LIST = 'userList';
public const MODEL_SESSION = 'session';
@ -128,6 +137,15 @@ class Response extends SwooleResponse
public const MODEL_JWT = 'jwt';
public const MODEL_PREFERENCES = 'preferences';
// Users password algos
public const MODEL_ALGO_MD5 = 'algoMd5';
public const MODEL_ALGO_SHA = 'algoSha';
public const MODEL_ALGO_SCRYPT = 'algoScrypt';
public const MODEL_ALGO_SCRYPT_MODIFIED = 'algoScryptModified';
public const MODEL_ALGO_BCRYPT = 'algoBcrypt';
public const MODEL_ALGO_ARGON2 = 'algoArgon2';
public const MODEL_ALGO_PHPASS = 'algoPhpass';
// Storage
public const MODEL_FILE = 'file';
public const MODEL_FILE_LIST = 'fileList';
@ -262,6 +280,14 @@ class Response extends SwooleResponse
->setModel(new ModelDocument())
->setModel(new Log())
->setModel(new User())
->setModel(new AlgoMd5())
->setModel(new AlgoSha())
->setModel(new AlgoPhpass())
->setModel(new AlgoBcrypt())
->setModel(new AlgoScrypt())
->setModel(new AlgoScryptModified())
->setModel(new AlgoArgon2())
->setModel(new Account())
->setModel(new Preferences())
->setModel(new Session())
->setModel(new Token())
@ -452,6 +478,24 @@ class Response extends SwooleResponse
return $this->payload;
}
/**
* Output response
*
* Generate HTTP response output including the response header (+cookies) and body and prints them.
*
* @param string $body
*
* @return void
*/
public function file(string $body = ''): void
{
$this->payload = [
'payload' => $body
];
$this->send($body);
}
/**
* YAML
*
@ -483,7 +527,6 @@ class Response extends SwooleResponse
return $this->payload;
}
/**
* Function to set a response filter
*

View file

@ -93,6 +93,22 @@ abstract class Model
return $this;
}
/**
* Delete an existing Rule
* If rule exists, it will be removed
*
* @param string $key
* @param array $options
*/
protected function removeRule(string $key): self
{
if (isset($this->rules[$key])) {
unset($this->rules[$key]);
}
return $this;
}
public function getRequired()
{
$list = [];

View file

@ -0,0 +1,38 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class Account extends User
{
public function __construct()
{
parent::__construct();
$this
->removeRule('password')
->removeRule('hash')
->removeRule('hashOptions');
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Account';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ACCOUNT;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoArgon2 extends Model
{
public function __construct()
{
// No options if imported. If hashed by Appwrite, following configuration is available:
$this
->addRule('memoryCost', [
'type' => self::TYPE_INTEGER,
'description' => 'Memory used to compute hash.',
'default' => '',
'example' => 65536,
])
->addRule('timeCost', [
'type' => self::TYPE_INTEGER,
'description' => 'Amount of time consumed to compute hash',
'default' => '',
'example' => 4,
])
->addRule('threads', [
'type' => self::TYPE_INTEGER,
'description' => 'Number of threads used to compute hash.',
'default' => '',
'example' => 3,
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoArgon2';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_ARGON2;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoBcrypt extends Model
{
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoBcrypt';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_BCRYPT;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoMd5 extends Model
{
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoMD5';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_MD5;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoPhpass extends Model
{
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoPHPass';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_PHPASS;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoScrypt extends Model
{
public function __construct()
{
$this
->addRule('costCpu', [
'type' => self::TYPE_INTEGER,
'description' => 'CPU complexity of computed hash.',
'default' => '',
'example' => 8,
])
->addRule('costMemory', [
'type' => self::TYPE_INTEGER,
'description' => 'Memory complexity of computed hash.',
'default' => '',
'example' => 14,
])
->addRule('costParallel', [
'type' => self::TYPE_INTEGER,
'description' => 'Parallelization of computed hash.',
'default' => '',
'example' => 1,
])
->addRule('length', [
'type' => self::TYPE_INTEGER,
'description' => 'Length used to compute hash.',
'default' => '',
'example' => 1,
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoScrypt';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_SCRYPT;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoScryptModified extends Model
{
public function __construct()
{
$this
->addRule('salt', [
'type' => self::TYPE_STRING,
'description' => 'Salt used to compute hash.',
'default' => '',
'example' => 'UxLMreBr6tYyjQ==',
])
->addRule('saltSeparator', [
'type' => self::TYPE_STRING,
'description' => 'Separator used to compute hash.',
'default' => '',
'example' => 'Bw==',
])
->addRule('signerKey', [
'type' => self::TYPE_STRING,
'description' => 'Key used to compute hash.',
'default' => '',
'example' => 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoScryptModified';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_SCRYPT_MODIFIED;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoSha extends Model
{
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoSHA';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_SHA;
}
}

View file

@ -29,8 +29,7 @@ class AttributeEmail extends Attribute
'description' => 'String format.',
'default' => 'email',
'example' => 'email',
'array' => false,
'require' => true,
'required' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,

View file

@ -36,8 +36,7 @@ class AttributeEnum extends Attribute
'description' => 'String format.',
'default' => 'enum',
'example' => 'enum',
'array' => false,
'require' => true,
'required' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,

View file

@ -29,8 +29,7 @@ class AttributeIP extends Attribute
'description' => 'String format.',
'default' => 'ip',
'example' => 'ip',
'array' => false,
'require' => true,
'required' => true,
])
->addRule('default', [
'type' => self::TYPE_STRING,

View file

@ -29,7 +29,6 @@ class AttributeURL extends Attribute
'description' => 'String format.',
'default' => 'url',
'example' => 'url',
'array' => false,
'required' => true,
])
->addRule('default', [

View file

@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Role;
class Execution extends Model
{
@ -32,7 +33,7 @@ class Execution extends Model
'type' => self::TYPE_STRING,
'description' => 'Execution roles.',
'default' => '',
'example' => ['any'],
'example' => [Role::any()->toString()],
'array' => true,
])
->addRule('functionId', [
@ -65,9 +66,15 @@ class Execution extends Model
'default' => '',
'example' => '',
])
->addRule('stdout', [
'type' => self::TYPE_STRING,
'description' => 'The script stdout output string. Logs the last 4,000 characters of the execution stdout output. This will return an empty string unless the response is returned using an API key or as part of a webhook payload.',
'default' => '',
'example' => '',
])
->addRule('stderr', [
'type' => self::TYPE_STRING,
'description' => 'The script stderr output string. Logs the last 4,000 characters of the execution stderr output',
'description' => 'The script stderr output string. Logs the last 4,000 characters of the execution stderr output. This will return an empty string unless the response is returned using an API key or as part of a webhook payload.',
'default' => '',
'example' => '',
])

View file

@ -35,6 +35,33 @@ class User extends Model
'default' => '',
'example' => 'John Doe',
])
->addRule('password', [
'type' => self::TYPE_STRING,
'description' => 'Hashed user password.',
'default' => '',
'example' => '$argon2id$v=19$m=2048,t=4,p=3$aUZjLnliVWRINmFNTWMudg$5S+x+7uA31xFnrHFT47yFwcJeaP0w92L/4LdgrVRXxE',
])
->addRule('hash', [
'type' => self::TYPE_STRING,
'description' => 'Password hashing algorithm.',
'default' => '',
'example' => 'argon2',
])
->addRule('hashOptions', [
'type' => [
Response::MODEL_ALGO_ARGON2,
Response::MODEL_ALGO_SCRYPT,
Response::MODEL_ALGO_SCRYPT_MODIFIED,
Response::MODEL_ALGO_BCRYPT,
Response::MODEL_ALGO_PHPASS,
Response::MODEL_ALGO_SHA,
Response::MODEL_ALGO_MD5, // keep least secure at the bottom. this order will be used in docs
],
'description' => 'Password hashing algorithm configuration.',
'default' => [],
'example' => new \stdClass(),
'array' => false,
])
->addRule('registration', [
'type' => self::TYPE_DATETIME,
'description' => 'User registration date in Datetime.',

View file

@ -2,7 +2,7 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Auth\Phone\Mock;
use Appwrite\SMS\Adapter\Mock;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
@ -693,7 +693,7 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'number' => $number,
'phone' => $number,
]);
$this->assertEquals(201, $response['headers']['status-code']);
@ -716,7 +716,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
$data['token'] = Mock::$defaultDigits;
$data['token'] = Mock::$digits;
$data['id'] = $userId;
$data['number'] = $number;
@ -872,7 +872,7 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'number' => $newPhone,
'phone' => $newPhone,
'password' => 'new-password'
]);
@ -952,7 +952,7 @@ class AccountCustomClientTest extends Scope
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'userId' => $id,
'secret' => Mock::$defaultDigits,
'secret' => Mock::$digits,
]);
$this->assertEquals(200, $response['headers']['status-code']);
@ -967,7 +967,7 @@ class AccountCustomClientTest extends Scope
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'userId' => ID::custom('ewewe'),
'secret' => Mock::$defaultDigits,
'secret' => Mock::$digits,
]);
$this->assertEquals(404, $response['headers']['status-code']);

View file

@ -95,10 +95,11 @@ class DatabasesPermissionsGuestTest extends Scope
foreach ($documents['body']['documents'] as $document) {
foreach ($document['$permissions'] as $permission) {
if (!\str_starts_with($permission, 'read')) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != 'read') {
continue;
}
$this->assertTrue(\str_contains($permission, 'any'));
$this->assertEquals($permission->getRole(), Role::any()->toString());
}
}
}

View file

@ -155,12 +155,17 @@ class DatabasesPermissionsMemberTest extends Scope
]);
foreach ($documents['body']['documents'] as $document) {
$hasPermissions = \array_reduce(['any', 'users', 'user:' . $users['user1']['$id']], function (bool $carry, string $role) use ($document) {
$hasPermissions = \array_reduce([
Role::any()->toString(),
Role::users()->toString(),
Role::user($users['user1']['$id'])->toString(),
], function (bool $carry, string $role) use ($document) {
if ($carry) {
return true;
}
foreach ($document['$permissions'] as $permission) {
if (\str_starts_with($permission, 'read') && \str_contains($permission, $role)) {
$permission = Permission::parse($permission);
if ($permission->getPermission() == 'read' && $permission->getRole() == $role) {
return true;
}
}
@ -181,12 +186,17 @@ class DatabasesPermissionsMemberTest extends Scope
]);
foreach ($documents['body']['documents'] as $document) {
$hasPermissions = \array_reduce(['any', 'users', 'user:' . $users['user1']['$id']], function (bool $carry, string $role) use ($document) {
$hasPermissions = \array_reduce([
Role::any()->toString(),
Role::users()->toString(),
Role::user($users['user1']['$id'])->toString(),
], function (bool $carry, string $role) use ($document) {
if ($carry) {
return true;
}
foreach ($document['$permissions'] as $permission) {
if (\str_starts_with($permission, 'read') && \str_contains($permission, $role)) {
$permission = Permission::parse($permission);
if ($permission->getPermission() == 'read' && $permission->getRole() == $role) {
return true;
}
}

View file

@ -10,6 +10,7 @@ use Tests\E2E\Scopes\SideClient;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\ID;
use Utopia\Database\Role;
class FunctionsCustomClientTest extends Scope
{
@ -148,7 +149,7 @@ class FunctionsCustomClientTest extends Scope
], [
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['any'],
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',
@ -333,7 +334,7 @@ class FunctionsCustomClientTest extends Scope
], [
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['any'],
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'vars' => [
'funcKey1' => 'funcValue1',
@ -398,6 +399,9 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']);
$this->assertNotEmpty($output['APPWRITE_FUNCTION_JWT']);
$this->assertEquals($projectId, $output['APPWRITE_FUNCTION_PROJECT_ID']);
// Client should never see logs and errors
$this->assertEmpty($execution['body']['stdout']);
$this->assertEmpty($execution['body']['stderr']);
// Cleanup : Delete function
$response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [

View file

@ -868,6 +868,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals('', $output['APPWRITE_FUNCTION_USER_ID']);
$this->assertEmpty($output['APPWRITE_FUNCTION_JWT']);
$this->assertEquals($this->getProject()['$id'], $output['APPWRITE_FUNCTION_PROJECT_ID']);
$this->assertStringContainsString('Amazing Function Log', $executions['body']['stdout']);
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json',
@ -895,7 +896,7 @@ class FunctionsCustomServerTest extends Scope
public function testCreateCustomNodeExecution()
{
$name = 'node-17.0';
$name = 'node-18.0';
$folder = 'node';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
$this->packageCode($folder);
@ -966,7 +967,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
$this->assertEquals('Node.js', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('17.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('18.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT_DATA']);
$this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']);

View file

@ -520,7 +520,7 @@ trait StorageBase
$this->assertEquals(204, $file['headers']['status-code']);
$this->assertEmpty($file['body']);
sleep(1);
//upload again using the same ID
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
'content-type' => 'multipart/form-data',

View file

@ -55,14 +55,313 @@ trait UsersBase
$this->assertEquals(true, $res['body']['status']);
$this->assertGreaterThan('2000-01-01 00:00:00', $res['body']['registration']);
/**
* Test Create with hashed passwords
*/
$res = $this->client->call(Client::METHOD_POST, '/users/md5', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'md5',
'email' => 'md5@appwrite.io',
'password' => '144fa7eaa4904e8ee120651997f70dcc', // appwrite
'name' => 'MD5 User',
]);
$res = $this->client->call(Client::METHOD_POST, '/users/bcrypt', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'bcrypt',
'email' => 'bcrypt@appwrite.io',
'password' => '$2a$15$xX/myGbFU.ZSKHSi6EHdBOySTdYm8QxBLXmOPHrYMwV0mHRBBSBOq', // appwrite (15 rounds)
'name' => 'Bcrypt User',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '$2a$15$xX/myGbFU.ZSKHSi6EHdBOySTdYm8QxBLXmOPHrYMwV0mHRBBSBOq');
$this->assertEquals($res['body']['hash'], 'bcrypt');
$res = $this->client->call(Client::METHOD_POST, '/users/argon2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'argon2',
'email' => 'argon2@appwrite.io',
'password' => '$argon2i$v=19$m=20,t=3,p=2$YXBwd3JpdGU$A/54i238ed09ZR4NwlACU5XnkjNBZU9QeOEuhjLiexI', // appwrite (salt appwrite, parallel 2, memory 20, iterations 3, length 32)
'name' => 'Argon2 User',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '$argon2i$v=19$m=20,t=3,p=2$YXBwd3JpdGU$A/54i238ed09ZR4NwlACU5XnkjNBZU9QeOEuhjLiexI');
$this->assertEquals($res['body']['hash'], 'argon2');
$res = $this->client->call(Client::METHOD_POST, '/users/sha', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'sha512',
'email' => 'sha512@appwrite.io',
'password' => '4243da0a694e8a2f727c8060fe0507c8fa01ca68146c76d2c190805b638c20c6bf6ba04e21f11ae138785d0bff63c416e6f87badbffad37f6dee50094cc38c70', // appwrite (sha512)
'name' => 'SHA512 User',
'passwordVersion' => 'sha512'
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '4243da0a694e8a2f727c8060fe0507c8fa01ca68146c76d2c190805b638c20c6bf6ba04e21f11ae138785d0bff63c416e6f87badbffad37f6dee50094cc38c70');
$this->assertEquals($res['body']['hash'], 'sha');
$this->assertEquals($res['body']['hashOptions']['version'], 'sha512');
$res = $this->client->call(Client::METHOD_POST, '/users/scrypt', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'scrypt',
'email' => 'scrypt@appwrite.io',
'password' => '3fdef49701bc4cfaacd551fe017283513284b4731e6945c263246ef948d3cf63b5d269c31fd697246085111a428245e24a4ddc6b64c687bc60a8910dbafc1d5b', // appwrite (salt appwrite, cpu 16384, memory 13, parallel 2, length 64)
'name' => 'Scrypt User',
'passwordSalt' => 'appwrite',
'passwordCpu' => 16384,
'passwordMemory' => 13,
'passwordParallel' => 2,
'passwordLength' => 64
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '3fdef49701bc4cfaacd551fe017283513284b4731e6945c263246ef948d3cf63b5d269c31fd697246085111a428245e24a4ddc6b64c687bc60a8910dbafc1d5b');
$this->assertEquals($res['body']['hash'], 'scrypt');
$this->assertEquals($res['body']['hashOptions']['salt'], 'appwrite');
$this->assertEquals($res['body']['hashOptions']['costCpu'], 16384);
$this->assertEquals($res['body']['hashOptions']['costMemory'], 13);
$this->assertEquals($res['body']['hashOptions']['costParallel'], 2);
$this->assertEquals($res['body']['hashOptions']['length'], 64);
$res = $this->client->call(Client::METHOD_POST, '/users/phpass', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'phpass',
'email' => 'phpass@appwrite.io',
'password' => '$P$Br387rwferoKN7uwHZqNMu98q3U8RO.',
'name' => 'PHPass User',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '$P$Br387rwferoKN7uwHZqNMu98q3U8RO.');
$res = $this->client->call(Client::METHOD_POST, '/users/scrypt-modified', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'scrypt-modified',
'email' => 'scrypt-modified@appwrite.io',
'password' => 'UlM7JiXRcQhzAGlaonpSqNSLIz475WMddOgLjej5De9vxTy48K6WtqlEzrRFeK4t0COfMhWCb8wuMHgxOFCHFQ==', // appwrite
'name' => 'Scrypt Modified User',
'passwordSalt' => 'UxLMreBr6tYyjQ==',
'passwordSaltSeparator' => 'Bw==',
'passwordSignerKey' => 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], 'UlM7JiXRcQhzAGlaonpSqNSLIz475WMddOgLjej5De9vxTy48K6WtqlEzrRFeK4t0COfMhWCb8wuMHgxOFCHFQ==');
$this->assertEquals($res['body']['hash'], 'scryptMod');
$this->assertEquals($res['body']['hashOptions']['salt'], 'UxLMreBr6tYyjQ==');
$this->assertEquals($res['body']['hashOptions']['signerKey'], 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==');
$this->assertEquals($res['body']['hashOptions']['saltSeparator'], 'Bw==');
return ['userId' => $body['$id']];
}
/**
* Tries to login into all accounts created with hashed password. Ensures hash veifying logic.
*
* @depends testCreateUser
*/
public function testCreateUserSessionHashed(array $data): void
{
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'md5@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'md5');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'bcrypt@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'bcrypt');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'argon2@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'argon2');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'sha512@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'sha512');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'scrypt@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'scrypt');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'phpass@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'phpass');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'scrypt-modified@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'scrypt-modified');
}
/**
* Tests all optional parameters of createUser (email, phone, anonymous..)
*
* @depends testCreateUser
*/
public function testCreateUserTypes(array $data): void
{
/**
* Test for SUCCESS
*/
// Email + password
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'email' => 'emailuser@appwrite.io',
'password' => 'emailUserPassword',
]);
$this->assertNotEmpty($response['body']['email']);
$this->assertNotEmpty($response['body']['password']);
$this->assertEmpty($response['body']['phone']);
// Phone
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'phone' => '+123456789012',
]);
$this->assertEmpty($response['body']['email']);
$this->assertEmpty($response['body']['password']);
$this->assertNotEmpty($response['body']['phone']);
// Anonymous
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
]);
$this->assertEmpty($response['body']['email']);
$this->assertEmpty($response['body']['password']);
$this->assertEmpty($response['body']['phone']);
// Email-only
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'email' => 'emailonlyuser@appwrite.io',
]);
$this->assertNotEmpty($response['body']['email']);
$this->assertEmpty($response['body']['password']);
$this->assertEmpty($response['body']['phone']);
// Password-only
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'password' => 'passwordOnlyUser',
]);
$this->assertEmpty($response['body']['email']);
$this->assertNotEmpty($response['body']['password']);
$this->assertEmpty($response['body']['phone']);
// Password and phone
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'password' => 'passwordOnlyUser',
'phone' => '+123456789013',
]);
$this->assertEmpty($response['body']['email']);
$this->assertNotEmpty($response['body']['password']);
$this->assertNotEmpty($response['body']['phone']);
}
/**
* @depends testCreateUser
*/
public function testListUsers(array $data): void
{
$totalUsers = 15;
/**
* Test for SUCCESS listUsers
*/
@ -74,7 +373,7 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(2, $response['body']['users']);
$this->assertCount($totalUsers, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$this->assertEquals($response['body']['users'][1]['$id'], 'user1');
@ -117,7 +416,7 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(2, $response['body']['users']);
$this->assertCount($totalUsers, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$this->assertEquals($response['body']['users'][0]['status'], $user1['status']);
$this->assertEquals($response['body']['users'][1]['$id'], $user1['$id']);
@ -171,7 +470,7 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(2, $response['body']['users']);
$this->assertCount($totalUsers, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$this->assertEquals($response['body']['users'][0]['status'], $user1['status']);
$this->assertEquals($response['body']['users'][1]['$id'], $user1['$id']);
@ -198,8 +497,8 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertEmpty($response['body']['users']);
$this->assertCount(0, $response['body']['users']);
$this->assertIsArray($response['body']['users']);
$this->assertCount($totalUsers, $response['body']['users']);
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
@ -223,7 +522,7 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertCount($totalUsers - 1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], 'user1');
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([

View file

@ -398,7 +398,7 @@ class WebhooksCustomServerTest extends Scope
], $this->getHeaders()), [
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => ['any'],
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'timeout' => 10,
]);
@ -447,7 +447,7 @@ class WebhooksCustomServerTest extends Scope
], $this->getHeaders()), [
'name' => 'Test',
'runtime' => 'php-8.0',
'execute' => ['any'],
'execute' => [Role::any()->toString()],
'vars' => [
'key1' => 'value1',
]

View file

@ -1,6 +1,8 @@
<?php
return function ($request, $response) {
\var_dump("Amazing Function Log"); // We test logs (stdout) visibility with this
$response->json([
'APPWRITE_FUNCTION_ID' => $request['env']['APPWRITE_FUNCTION_ID'],
'APPWRITE_FUNCTION_NAME' => $request['env']['APPWRITE_FUNCTION_NAME'],

View file

@ -6,6 +6,7 @@ use Appwrite\Auth\Auth;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\ID;
use Utopia\Database\Role;
use Utopia\Database\Validator\Authorization;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Database;
@ -18,7 +19,7 @@ class AuthTest extends TestCase
public function tearDown(): void
{
Authorization::cleanRoles();
Authorization::setRole('any');
Authorization::setRole(Role::any()->toString());
}
public function testCookieName(): void
@ -47,12 +48,137 @@ class AuthTest extends TestCase
public function testPassword(): void
{
$secret = 'secret';
$static = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy';
$dynamic = Auth::passwordHash($secret);
/*
General tests, using pre-defined hashes generated by online tools
*/
$this->assertEquals(Auth::passwordVerify($secret, $dynamic), true);
$this->assertEquals(Auth::passwordVerify($secret, $static), true);
// Bcrypt - Version Y
$plain = 'secret';
$hash = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// Bcrypt - Version A
$plain = 'test123';
$hash = '$2a$12$3f2ZaARQ1AmhtQWx2nmQpuXcWfTj1YV2/Hl54e8uKxIzJe3IfwLiu';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// Bcrypt - Cost 5
$plain = 'hello-world';
$hash = '$2a$05$IjrtSz6SN7UJ6Sh3l.b5jODEvEG2LMJTPAHIaLWRvlWx7if3VMkFO';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// Bcrypt - Cost 15
$plain = 'super-secret-password';
$hash = '$2a$15$DS0ZzbsFZYumH/E4Qj5oeOHnBcM3nCCsCA2m4Goigat/0iMVQC4Na';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// MD5 - Short
$plain = 'appwrite';
$hash = '144fa7eaa4904e8ee120651997f70dcc';
$generatedHash = Auth::passwordHash($plain, 'md5');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
// MD5 - Long
$plain = 'AppwriteIsAwesomeBackendAsAServiceThatIsAlsoOpenSourced';
$hash = '8410e96cf7ac64e0b84c3f8517a82616';
$generatedHash = Auth::passwordHash($plain, 'md5');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
// PHPass
$plain = 'pass123';
$hash = '$P$BVKPmJBZuLch27D4oiMRTEykGLQ9tX0';
$generatedHash = Auth::passwordHash($plain, 'phpass');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
// SHA
$plain = 'developersAreAwesome!';
$hash = '2455118438cb125354b89bb5888346e9bd23355462c40df393fab514bf2220b5a08e4e2d7b85d7327595a450d0ac965cc6661152a46a157c66d681bed20a4735';
$generatedHash = Auth::passwordHash($plain, 'sha');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'sha'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'sha'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'sha'));
// Argon2
$plain = 'safe-argon-password';
$hash = '$argon2id$v=19$m=2048,t=3,p=4$MWc5NWRmc2QxZzU2$41mp7rSgBZ49YxLbbxIac7aRaxfp5/e1G45ckwnK0g8';
$generatedHash = Auth::passwordHash($plain, 'argon2');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'argon2'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'argon2'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'argon2'));
// Scrypt
$plain = 'some-scrypt-password';
$hash = 'b448ad7ba88b653b5b56b8053a06806724932d0751988bc9cd0ef7ff059e8ba8a020e1913b7069a650d3f99a1559aba0221f2c277826919513a054e76e339028';
$generatedHash = Auth::passwordHash($plain, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
$this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-wrong-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
$this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 10, 'costParallel' => 2]));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
// ScryptModified tested are in provider-specific tests below
/*
Provider-specific tests, ensuring functionality of specific use-cases
*/
// Provider #1 (Database)
$plain = 'example-password';
$hash = '$2a$10$3bIGRWUes86CICsuchGLj.e.BqdCdg2/1Ud9LvBhJr0j7Dze8PBdS';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// Provider #2 (Blog)
$plain = 'your-password';
$hash = '$P$BkiNDJTpAWXtpaMhEUhUdrv7M0I1g6.';
$generatedHash = Auth::passwordHash($plain, 'phpass');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
// Provider #2 (Google)
$plain = 'users-password';
$hash = 'EPKgfALpS9Tvgr/y1ki7ubY4AEGJeWL3teakrnmOacN4XGiyD00lkzEHgqCQ71wGxoi/zb7Y9a4orOtvMV3/Jw==';
$salt = '56dFqW+kswqktw==';
$saltSeparator = 'Bw==';
$signerKey = 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==';
$options = [ 'salt' => $salt, 'saltSeparator' => $saltSeparator, 'signerKey' => $signerKey ];
$generatedHash = Auth::passwordHash($plain, 'scryptMod', $options);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scryptMod', $options));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scryptMod', $options));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scryptMod', $options));
}
public function testUnknownAlgo()
{
$this->expectExceptionMessage('Hashing algorithm \'md8\' is not supported.');
// Bcrypt - Cost 5
$plain = 'whatIsMd8?!?';
$generatedHash = Auth::passwordHash($plain, 'md8');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md8'));
}
public function testPasswordGenerator(): void
@ -67,6 +193,14 @@ class AuthTest extends TestCase
$this->assertEquals(\mb_strlen(Auth::tokenGenerator(5)), 10);
}
public function testCodeGenerator(): void
{
$this->assertEquals(6, \strlen(Auth::codeGenerator()));
$this->assertEquals(\mb_strlen(Auth::codeGenerator(256)), 256);
$this->assertEquals(\mb_strlen(Auth::codeGenerator(10)), 10);
$this->assertTrue(is_numeric(Auth::codeGenerator(5)));
}
public function testSessionVerify(): void
{
$secret = 'secret1';
@ -171,8 +305,8 @@ class AuthTest extends TestCase
public function testIsPrivilegedUser(): void
{
$this->assertEquals(false, Auth::isPrivilegedUser([]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_GUESTS]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_USERS]));
$this->assertEquals(false, Auth::isPrivilegedUser([Role::guests()->toString()]));
$this->assertEquals(false, Auth::isPrivilegedUser([Role::users()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_ADMIN]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER]));
@ -180,16 +314,16 @@ class AuthTest extends TestCase
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_SYSTEM]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_GUESTS]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
}
public function testIsAppUser(): void
{
$this->assertEquals(false, Auth::isAppUser([]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_GUESTS]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_USERS]));
$this->assertEquals(false, Auth::isAppUser([Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Role::users()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_ADMIN]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER]));
@ -197,8 +331,8 @@ class AuthTest extends TestCase
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_SYSTEM]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_GUESTS]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
}
@ -210,7 +344,7 @@ class AuthTest extends TestCase
$roles = Auth::getRoles($user);
$this->assertCount(1, $roles);
$this->assertContains('guests', $roles);
$this->assertContains(Role::guests()->toString(), $roles);
}
public function testUserRoles(): void

View file

@ -7,6 +7,7 @@ use Utopia\Database\Document;
use Appwrite\Messaging\Adapter\Realtime;
use PHPUnit\Framework\TestCase;
use Utopia\Database\ID;
use Utopia\Database\Role;
class MessagingChannelsTest extends TestCase
{
@ -55,7 +56,9 @@ class MessagingChannelsTest extends TestCase
[
'teamId' => ID::custom('team' . $i),
'roles' => [
empty($index % 2) ? 'admin' : 'member'
empty($index % 2)
? Auth::USER_ROLE_ADMIN
: Role::users()->toString(),
]
]
]
@ -154,7 +157,7 @@ class MessagingChannelsTest extends TestCase
foreach ($this->allChannels as $index => $channel) {
$event = [
'project' => '1',
'roles' => ['any'],
'roles' => [Role::any()->toString()],
'data' => [
'channels' => [
0 => $channel,
@ -180,7 +183,10 @@ class MessagingChannelsTest extends TestCase
public function testRolePermissions(): void
{
$roles = ['guests', 'users'];
$roles = [
Role::guests()->toString(),
Role::users()->toString()
];
foreach ($this->allChannels as $index => $channel) {
foreach ($roles as $role) {
$permissions = [$role];
@ -217,7 +223,7 @@ class MessagingChannelsTest extends TestCase
foreach ($this->allChannels as $index => $channel) {
$permissions = [];
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
$permissions[] = 'user:user' . (!empty($i) ? $i : '') . $index;
$permissions[] = Role::user(ID::custom('user' . (!empty($i) ? $i : '') . $index))->toString();
}
$event = [
'project' => '1',
@ -251,7 +257,7 @@ class MessagingChannelsTest extends TestCase
$permissions = [];
for ($i = 0; $i < $this->connectionsPerChannel; $i++) {
$permissions[] = 'team:team' . $i;
$permissions[] = Role::team(ID::custom('team' . $i))->toString();
}
$event = [
'project' => '1',
@ -277,7 +283,14 @@ class MessagingChannelsTest extends TestCase
$this->assertStringEndsWith($index, $receiver);
}
$permissions = ['team:team' . $index . '/' . (empty($index % 2) ? 'admin' : 'member')];
$permissions = [
Role::team(
ID::custom('team' . $index),
(empty($index % 2)
? Auth::USER_ROLE_ADMIN
: Role::users()->toString())
)->toString()
];
$event = [
'project' => '1',

View file

@ -4,6 +4,8 @@ namespace Tests\Unit\Messaging;
use Appwrite\Messaging\Adapter\Realtime;
use PHPUnit\Framework\TestCase;
use Utopia\Database\ID;
use Utopia\Database\Role;
class MessagingGuestTest extends TestCase
{
@ -14,13 +16,13 @@ class MessagingGuestTest extends TestCase
$realtime->subscribe(
'1',
1,
['guests'],
[Role::guests()->toString()],
['files' => 0, 'documents' => 0, 'documents.789' => 0, 'account.123' => 0]
);
$event = [
'project' => '1',
'roles' => ['any'],
'roles' => [Role::any()->toString()],
'data' => [
'channels' => [
0 => 'documents',
@ -34,68 +36,68 @@ class MessagingGuestTest extends TestCase
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['guests'];
$event['roles'] = [Role::guests()->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['users'];
$event['roles'] = [Role::users()->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['user:123'];
$event['roles'] = [Role::user(ID::custom('123'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:abc'];
$event['roles'] = [Role::team(ID::custom('abc'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:abc/administrator'];
$event['roles'] = [Role::team(ID::custom('abc'), 'administrator')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:abc/god'];
$event['roles'] = [Role::team(ID::custom('abc'), 'god')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:def'];
$event['roles'] = [Role::team(ID::custom('def'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:def/guest'];
$event['roles'] = [Role::team(ID::custom('def'), 'guest')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['user:456'];
$event['roles'] = [Role::user(ID::custom('456'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:def/member'];
$event['roles'] = [Role::team(ID::custom('def'), 'member')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['any'];
$event['roles'] = [Role::any()->toString()];
$event['data']['channels'] = ['documents.123'];
$receivers = $realtime->getSubscribers($event);

View file

@ -26,13 +26,21 @@ class MessagingTest extends TestCase
$realtime->subscribe(
'1',
1,
['user:123', 'users', 'team:abc', 'team:abc/administrator', 'team:abc/moderator', 'team:def', 'team:def/guest'],
[
Role::user(ID::custom('123'))->toString(),
Role::users()->toString(),
Role::team(ID::custom('abc'))->toString(),
Role::team(ID::custom('abc'), 'administrator')->toString(),
Role::team(ID::custom('abc'), 'moderator')->toString(),
Role::team(ID::custom('def'))->toString(),
Role::team(ID::custom('def'), 'guest')->toString(),
],
['files' => 0, 'documents' => 0, 'documents.789' => 0, 'account.123' => 0]
);
$event = [
'project' => '1',
'roles' => ['any'],
'roles' => [Role::any()->toString()],
'data' => [
'channels' => [
0 => 'account.123',
@ -45,68 +53,68 @@ class MessagingTest extends TestCase
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['users'];
$event['roles'] = [Role::users()->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['user:123'];
$event['roles'] = [Role::user(ID::custom('123'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['team:abc'];
$event['roles'] = [Role::team(ID::custom('abc'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['team:abc/administrator'];
$event['roles'] = [Role::team(ID::custom('abc'), 'administrator')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['team:abc/moderator'];
$event['roles'] = [Role::team(ID::custom('abc'), 'moderator')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['team:def'];
$event['roles'] = [Role::team(ID::custom('def'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['team:def/guest'];
$event['roles'] = [Role::team(ID::custom('def'), 'guest')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertCount(1, $receivers);
$this->assertEquals(1, $receivers[0]);
$event['roles'] = ['user:456'];
$event['roles'] = [Role::user(ID::custom('456'))->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['team:def/member'];
$event['roles'] = [Role::team(ID::custom('def'), 'member')->toString()];
$receivers = $realtime->getSubscribers($event);
$this->assertEmpty($receivers);
$event['roles'] = ['any'];
$event['roles'] = [Role::any()->toString()];
$event['data']['channels'] = ['documents.123'];
$receivers = $realtime->getSubscribers($event);
@ -199,7 +207,7 @@ class MessagingTest extends TestCase
$this->assertArrayNotHasKey('account.456', $channels);
}
public function testFromPayloadCollectionLevelPermissions(): void
public function testFromPayloadPermissions(): void
{
/**
* Test Collection Level Permissions
@ -210,9 +218,9 @@ class MessagingTest extends TestCase
'$id' => ID::custom('test'),
'$collection' => ID::custom('collection'),
'$permissions' => [
'read(admin)',
'update(admin)',
'delete(admin)',
Permission::read(Role::team('123abc')),
Permission::update(Role::team('123abc')),
Permission::delete(Role::team('123abc')),
],
]),
database: new Document([
@ -228,8 +236,8 @@ class MessagingTest extends TestCase
])
);
$this->assertContains('any', $result['roles']);
$this->assertNotContains('role:admin', $result['roles']);
$this->assertContains(Role::any()->toString(), $result['roles']);
$this->assertNotContains(Role::team('123abc')->toString(), $result['roles']);
/**
* Test Document Level Permissions
@ -251,16 +259,16 @@ class MessagingTest extends TestCase
collection: new Document([
'$id' => ID::custom('collection'),
'$permissions' => [
'read(admin)',
'update(admin)',
'delete(admin)',
Permission::read(Role::team('123abc')),
Permission::update(Role::team('123abc')),
Permission::delete(Role::team('123abc')),
],
'documentSecurity' => true,
])
);
$this->assertContains('any', $result['roles']);
$this->assertContains('admin', $result['roles']);
$this->assertContains(Role::any()->toString(), $result['roles']);
$this->assertContains(Role::team('123abc')->toString(), $result['roles']);
}
public function testFromPayloadBucketLevelPermissions(): void
@ -274,9 +282,9 @@ class MessagingTest extends TestCase
'$id' => ID::custom('test'),
'$collection' => ID::custom('bucket'),
'$permissions' => [
'read(admin)',
'update(admin)',
'delete(admin)',
Permission::read(Role::team('123abc')),
Permission::update(Role::team('123abc')),
Permission::delete(Role::team('123abc')),
],
]),
bucket: new Document([
@ -289,8 +297,8 @@ class MessagingTest extends TestCase
])
);
$this->assertContains('any', $result['roles']);
$this->assertNotContains('admin', $result['roles']);
$this->assertContains(Role::any()->toString(), $result['roles']);
$this->assertNotContains(Role::team('123abc')->toString(), $result['roles']);
/**
* Test File Level Permissions
@ -309,15 +317,15 @@ class MessagingTest extends TestCase
bucket: new Document([
'$id' => ID::custom('bucket'),
'$permissions' => [
'read(admin)',
'update(admin)',
'delete(admin)',
Permission::read(Role::team('123abc')),
Permission::update(Role::team('123abc')),
Permission::delete(Role::team('123abc')),
],
'fileSecurity' => true
])
);
$this->assertContains('any', $result['roles']);
$this->assertContains('admin', $result['roles']);
$this->assertContains(Role::any()->toString(), $result['roles']);
$this->assertContains(Role::team('123abc')->toString(), $result['roles']);
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Tests\Unit\Validator;
namespace Tests\Unit\Task\Validator;
use Appwrite\Task\Validator\Cron;
use PHPUnit\Framework\TestCase;