Merge remote-tracking branch 'origin/0.15.x' into feat-password-hash-algos
This commit is contained in:
commit
001e69ab80
|
@ -801,7 +801,7 @@ $collections = [
|
||||||
'size' => Database::LENGTH_KEY,
|
'size' => Database::LENGTH_KEY,
|
||||||
'signed' => true,
|
'signed' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'default' => null,
|
'default' => 0,
|
||||||
'array' => false,
|
'array' => false,
|
||||||
'filters' => [],
|
'filters' => [],
|
||||||
],
|
],
|
||||||
|
@ -838,6 +838,17 @@ $collections = [
|
||||||
'array' => false,
|
'array' => false,
|
||||||
'filters' => ['encrypt'],
|
'filters' => ['encrypt'],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'$id' => 'expire',
|
||||||
|
'type' => Database::VAR_INTEGER,
|
||||||
|
'format' => '',
|
||||||
|
'size' => 0,
|
||||||
|
'signed' => false,
|
||||||
|
'required' => false,
|
||||||
|
'default' => 0,
|
||||||
|
'array' => false,
|
||||||
|
'filters' => [],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'indexes' => [
|
'indexes' => [
|
||||||
[
|
[
|
||||||
|
|
|
@ -988,7 +988,7 @@ App::post('/v1/account/jwt')
|
||||||
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
|
||||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
->label('sdk.response.model', Response::MODEL_JWT)
|
->label('sdk.response.model', Response::MODEL_JWT)
|
||||||
->label('abuse-limit', 10)
|
->label('abuse-limit', 100)
|
||||||
->label('abuse-key', 'url:{url},userId:{userId}')
|
->label('abuse-key', 'url:{url},userId:{userId}')
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('user')
|
->inject('user')
|
||||||
|
|
|
@ -26,6 +26,7 @@ use Appwrite\Extend\Exception;
|
||||||
use Utopia\Validator\ArrayList;
|
use Utopia\Validator\ArrayList;
|
||||||
use Utopia\Validator\Boolean;
|
use Utopia\Validator\Boolean;
|
||||||
use Utopia\Validator\Hostname;
|
use Utopia\Validator\Hostname;
|
||||||
|
use Utopia\Validator\Integer;
|
||||||
use Utopia\Validator\Range;
|
use Utopia\Validator\Range;
|
||||||
use Utopia\Validator\Text;
|
use Utopia\Validator\Text;
|
||||||
use Utopia\Validator\WhiteList;
|
use Utopia\Validator\WhiteList;
|
||||||
|
@ -782,9 +783,10 @@ App::post('/v1/projects/:projectId/keys')
|
||||||
->param('projectId', null, new UID(), 'Project unique ID.')
|
->param('projectId', null, new UID(), 'Project unique ID.')
|
||||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||||
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
|
||||||
|
->param('expire', 0, new Integer(), 'Key expiration time in Unix timestamp. Use 0 for unlimited expiration.', true)
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('dbForConsole')
|
->inject('dbForConsole')
|
||||||
->action(function (string $projectId, string $name, array $scopes, Response $response, Database $dbForConsole) {
|
->action(function (string $projectId, string $name, array $scopes, int $expire, Response $response, Database $dbForConsole) {
|
||||||
|
|
||||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||||
|
|
||||||
|
@ -799,6 +801,7 @@ App::post('/v1/projects/:projectId/keys')
|
||||||
'projectId' => $project->getId(),
|
'projectId' => $project->getId(),
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'scopes' => $scopes,
|
'scopes' => $scopes,
|
||||||
|
'expire' => $expire,
|
||||||
'secret' => \bin2hex(\random_bytes(128)),
|
'secret' => \bin2hex(\random_bytes(128)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -889,9 +892,10 @@ App::put('/v1/projects/:projectId/keys/:keyId')
|
||||||
->param('keyId', null, new UID(), 'Key unique ID.')
|
->param('keyId', null, new UID(), 'Key unique ID.')
|
||||||
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
|
||||||
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
|
||||||
|
->param('expire', 0, new Integer(), 'Key expiration time in Unix timestamp. Use 0 for unlimited expiration.', true)
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('dbForConsole')
|
->inject('dbForConsole')
|
||||||
->action(function (string $projectId, string $keyId, string $name, array $scopes, Response $response, Database $dbForConsole) {
|
->action(function (string $projectId, string $keyId, string $name, array $scopes, int $expire, Response $response, Database $dbForConsole) {
|
||||||
|
|
||||||
$project = $dbForConsole->getDocument('projects', $projectId);
|
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||||
|
|
||||||
|
@ -911,6 +915,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
|
||||||
$key
|
$key
|
||||||
->setAttribute('name', $name)
|
->setAttribute('name', $name)
|
||||||
->setAttribute('scopes', $scopes)
|
->setAttribute('scopes', $scopes)
|
||||||
|
->setAttribute('expire', $expire)
|
||||||
;
|
;
|
||||||
|
|
||||||
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
$dbForConsole->updateDocument('keys', $key->getId(), $key);
|
||||||
|
|
|
@ -279,6 +279,12 @@ App::init(function (App $utopia, Request $request, Response $response, Document
|
||||||
$role = Auth::USER_ROLE_APP;
|
$role = Auth::USER_ROLE_APP;
|
||||||
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
|
||||||
|
|
||||||
|
$expire = $key->getAttribute('expire', 0);
|
||||||
|
|
||||||
|
if (!empty($expire) && $expire < \time()) {
|
||||||
|
throw new AppwriteException('Project key expired', 401, AppwriteException:: PROJECT_KEY_EXPIRED);
|
||||||
|
}
|
||||||
|
|
||||||
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
|
Authorization::setRole('role:' . Auth::USER_ROLE_APP);
|
||||||
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
|
||||||
}
|
}
|
||||||
|
|
|
@ -434,7 +434,7 @@ App::post('/v1/execution')
|
||||||
->desc('Create an execution')
|
->desc('Create an execution')
|
||||||
->param('runtimeId', '', new Text(64), 'The runtimeID to execute.')
|
->param('runtimeId', '', new Text(64), 'The runtimeID to execute.')
|
||||||
->param('vars', [], new Assoc(), 'Environment variables required for the build.')
|
->param('vars', [], new Assoc(), 'Environment variables required for the build.')
|
||||||
->param('data', '{}', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true)
|
->param('data', '', new Text(8192), 'Data to be forwarded to the function, this is user specified.', true)
|
||||||
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.')
|
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.')
|
||||||
->inject('activeRuntimes')
|
->inject('activeRuntimes')
|
||||||
->inject('response')
|
->inject('response')
|
||||||
|
|
|
@ -132,6 +132,7 @@ const DELETE_TYPE_CERTIFICATES = 'certificates';
|
||||||
const DELETE_TYPE_USAGE = 'usage';
|
const DELETE_TYPE_USAGE = 'usage';
|
||||||
const DELETE_TYPE_REALTIME = 'realtime';
|
const DELETE_TYPE_REALTIME = 'realtime';
|
||||||
const DELETE_TYPE_BUCKETS = 'buckets';
|
const DELETE_TYPE_BUCKETS = 'buckets';
|
||||||
|
const DELETE_TYPE_SESSIONS = 'sessions';
|
||||||
// Mail Types
|
// Mail Types
|
||||||
const MAIL_TYPE_VERIFICATION = 'verification';
|
const MAIL_TYPE_VERIFICATION = 'verification';
|
||||||
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
|
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
global $cli;
|
global $cli;
|
||||||
global $register;
|
global $register;
|
||||||
|
|
||||||
|
use Appwrite\Auth\Auth;
|
||||||
use Appwrite\Event\Certificate;
|
use Appwrite\Event\Certificate;
|
||||||
use Appwrite\Event\Delete;
|
use Appwrite\Event\Delete;
|
||||||
use Utopia\App;
|
use Utopia\App;
|
||||||
|
@ -93,6 +94,14 @@ $cli
|
||||||
->trigger();
|
->trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notifyDeleteExpiredSessions()
|
||||||
|
{
|
||||||
|
(new Delete())
|
||||||
|
->setType(DELETE_TYPE_SESSIONS)
|
||||||
|
->setTimestamp(time() - Auth::TOKEN_EXPIRATION_LOGIN_LONG)
|
||||||
|
->trigger();
|
||||||
|
}
|
||||||
|
|
||||||
function renewCertificates($dbForConsole)
|
function renewCertificates($dbForConsole)
|
||||||
{
|
{
|
||||||
$time = date('d-m-Y H:i:s', time());
|
$time = date('d-m-Y H:i:s', time());
|
||||||
|
@ -136,6 +145,7 @@ $cli
|
||||||
notifyDeleteAuditLogs($auditLogRetention);
|
notifyDeleteAuditLogs($auditLogRetention);
|
||||||
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
|
notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d);
|
||||||
notifyDeleteConnections();
|
notifyDeleteConnections();
|
||||||
|
notifyDeleteExpiredSessions();
|
||||||
renewCertificates($database);
|
renewCertificates($database);
|
||||||
}, $interval);
|
}, $interval);
|
||||||
});
|
});
|
||||||
|
|
|
@ -100,6 +100,10 @@ class DeletesV1 extends Worker
|
||||||
$this->deleteRealtimeUsage($this->args['timestamp']);
|
$this->deleteRealtimeUsage($this->args['timestamp']);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case DELETE_TYPE_SESSIONS:
|
||||||
|
$this->deleteExpiredSessions($this->args['timestamp']);
|
||||||
|
break;
|
||||||
|
|
||||||
case DELETE_TYPE_CERTIFICATES:
|
case DELETE_TYPE_CERTIFICATES:
|
||||||
$document = new Document($this->args['document']);
|
$document = new Document($this->args['document']);
|
||||||
$this->deleteCertificates($document);
|
$this->deleteCertificates($document);
|
||||||
|
@ -250,6 +254,20 @@ class DeletesV1 extends Worker
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $timestamp
|
||||||
|
*/
|
||||||
|
protected function deleteExpiredSessions(int $timestamp): void
|
||||||
|
{
|
||||||
|
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
|
||||||
|
$dbForProject = $this->getProjectDB($projectId);
|
||||||
|
// Delete Sessions
|
||||||
|
$this->deleteByGroup('sessions', [
|
||||||
|
new Query('expire', Query::TYPE_LESSER, [$timestamp])
|
||||||
|
], $dbForProject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $timestamp
|
* @param int $timestamp
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -147,6 +147,7 @@ class Exception extends \Exception
|
||||||
public const PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
|
public const PROJECT_INVALID_FAILURE_URL = 'project_invalid_failure_url';
|
||||||
public const PROJECT_MISSING_USER_ID = 'project_missing_user_id';
|
public const PROJECT_MISSING_USER_ID = 'project_missing_user_id';
|
||||||
public const PROJECT_RESERVED_PROJECT = 'project_reserved_project';
|
public const PROJECT_RESERVED_PROJECT = 'project_reserved_project';
|
||||||
|
public const PROJECT_KEY_EXPIRED = 'project_key_expired';
|
||||||
|
|
||||||
/** Webhooks */
|
/** Webhooks */
|
||||||
public const WEBHOOK_NOT_FOUND = 'webhook_not_found';
|
public const WEBHOOK_NOT_FOUND = 'webhook_not_found';
|
||||||
|
|
|
@ -27,6 +27,12 @@ class Key extends Model
|
||||||
'default' => '',
|
'default' => '',
|
||||||
'example' => 'My API Key',
|
'example' => 'My API Key',
|
||||||
])
|
])
|
||||||
|
->addRule('expire', [
|
||||||
|
'type' => self::TYPE_INTEGER,
|
||||||
|
'description' => 'Key expiration in Unix timestamp.',
|
||||||
|
'default' => 0,
|
||||||
|
'example' => '1653990687',
|
||||||
|
])
|
||||||
->addRule('scopes', [
|
->addRule('scopes', [
|
||||||
'type' => self::TYPE_STRING,
|
'type' => self::TYPE_STRING,
|
||||||
'description' => 'Allowed permission scopes.',
|
'description' => 'Allowed permission scopes.',
|
||||||
|
|
|
@ -270,6 +270,7 @@ class AccountCustomClientTest extends Scope
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$this->assertEquals($response['headers']['status-code'], 201);
|
$this->assertEquals($response['headers']['status-code'], 201);
|
||||||
|
$this->assertEquals($response['headers']['x-ratelimit-remaining'], 99);
|
||||||
$this->assertNotEmpty($response['body']['jwt']);
|
$this->assertNotEmpty($response['body']['jwt']);
|
||||||
$this->assertIsString($response['body']['jwt']);
|
$this->assertIsString($response['body']['jwt']);
|
||||||
|
|
||||||
|
|
|
@ -1164,7 +1164,10 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
$this->assertContains('teams.write', $response['body']['scopes']);
|
$this->assertContains('teams.write', $response['body']['scopes']);
|
||||||
$this->assertNotEmpty($response['body']['secret']);
|
$this->assertNotEmpty($response['body']['secret']);
|
||||||
|
|
||||||
$data = array_merge($data, ['keyId' => $response['body']['$id']]);
|
$data = array_merge($data, [
|
||||||
|
'keyId' => $response['body']['$id'],
|
||||||
|
'secret' => $response['body']['secret']
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test for FAILURE
|
* Test for FAILURE
|
||||||
|
@ -1182,6 +1185,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @depends testCreateProjectKey
|
* @depends testCreateProjectKey
|
||||||
*/
|
*/
|
||||||
|
@ -1194,6 +1198,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
'x-appwrite-project' => $this->getProject()['$id'],
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
], $this->getHeaders()), []);
|
], $this->getHeaders()), []);
|
||||||
|
|
||||||
|
|
||||||
$this->assertEquals(200, $response['headers']['status-code']);
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
$this->assertEquals(1, $response['body']['total']);
|
$this->assertEquals(1, $response['body']['total']);
|
||||||
|
|
||||||
|
@ -1204,6 +1209,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @depends testCreateProjectKey
|
* @depends testCreateProjectKey
|
||||||
*/
|
*/
|
||||||
|
@ -1215,6 +1221,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys/' . $keyId, array_merge([
|
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys/' . $keyId, array_merge([
|
||||||
'content-type' => 'application/json',
|
'content-type' => 'application/json',
|
||||||
'x-appwrite-project' => $this->getProject()['$id'],
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
'x-appwrite-key' => $keyId
|
||||||
], $this->getHeaders()), []);
|
], $this->getHeaders()), []);
|
||||||
|
|
||||||
$this->assertEquals(200, $response['headers']['status-code']);
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
@ -1239,6 +1246,75 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @depends testCreateProject
|
||||||
|
*/
|
||||||
|
public function testValidateProjectKey($data): void
|
||||||
|
{
|
||||||
|
$id = $data['projectId'] ?? '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for SUCCESS
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'name' => 'Key Test',
|
||||||
|
'scopes' => ['health.read'],
|
||||||
|
'expire' => time() + 3600,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_GET, '/health', [
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $id,
|
||||||
|
'x-appwrite-key' => $response['body']['secret']
|
||||||
|
], []);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for SUCCESS
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'name' => 'Key Test',
|
||||||
|
'scopes' => ['health.read'],
|
||||||
|
'expire' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_GET, '/health', [
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $id,
|
||||||
|
'x-appwrite-key' => $response['body']['secret']
|
||||||
|
], []);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for FAILURE
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'name' => 'Key Test',
|
||||||
|
'scopes' => ['health.read'],
|
||||||
|
'expire' => time() - 3600,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_GET, '/health', [
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $id,
|
||||||
|
'x-appwrite-key' => $response['body']['secret']
|
||||||
|
], []);
|
||||||
|
|
||||||
|
$this->assertEquals(401, $response['headers']['status-code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @depends testCreateProjectKey
|
* @depends testCreateProjectKey
|
||||||
*/
|
*/
|
||||||
|
@ -1253,6 +1329,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
], $this->getHeaders()), [
|
], $this->getHeaders()), [
|
||||||
'name' => 'Key Test Update',
|
'name' => 'Key Test Update',
|
||||||
'scopes' => ['users.read', 'users.write', 'collections.read'],
|
'scopes' => ['users.read', 'users.write', 'collections.read'],
|
||||||
|
'expire' => time() + 360,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals(200, $response['headers']['status-code']);
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
|
Loading…
Reference in a new issue