mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +05:30
Implementation of the backend for the OAuth2 clients management
This commit is contained in:
@@ -60,4 +60,13 @@ final class Error {
|
||||
const OTP_ALREADY_ENABLED = 'error.otp_already_enabled';
|
||||
const OTP_NOT_ENABLED = 'error.otp_not_enabled';
|
||||
|
||||
const NAME_REQUIRED = 'error.name_required';
|
||||
|
||||
const REDIRECT_URI_REQUIRED = 'error.redirectUri_required';
|
||||
const REDIRECT_URI_INVALID = 'error.redirectUri_invalid';
|
||||
|
||||
const WEBSITE_URL_INVALID = 'error.websiteUrl_invalid';
|
||||
|
||||
const MINECRAFT_SERVER_IP_INVALID = 'error.minecraftServerIp_invalid';
|
||||
|
||||
}
|
||||
|
@@ -34,6 +34,7 @@ use const common\LATEST_RULES_VERSION;
|
||||
* Отношения:
|
||||
* @property EmailActivation[] $emailActivations
|
||||
* @property OauthSession[] $oauthSessions
|
||||
* @property OauthClient[] $oauthClients
|
||||
* @property UsernameHistory[] $usernameHistory
|
||||
* @property AccountSession[] $sessions
|
||||
* @property MinecraftAccessKey[] $minecraftAccessKeys
|
||||
@@ -93,6 +94,11 @@ class Account extends ActiveRecord {
|
||||
return $this->hasMany(OauthSession::class, ['owner_id' => 'id'])->andWhere(['owner_type' => 'user']);
|
||||
}
|
||||
|
||||
public function getOauthClients(): OauthClientQuery {
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||
return $this->hasMany(OauthClient::class, ['account_id' => 'id']);
|
||||
}
|
||||
|
||||
public function getUsernameHistory(): ActiveQuery {
|
||||
return $this->hasMany(UsernameHistory::class, ['account_id' => 'id']);
|
||||
}
|
||||
|
@@ -1,48 +1,62 @@
|
||||
<?php
|
||||
namespace common\models;
|
||||
|
||||
use Yii;
|
||||
use yii\behaviors\TimestampBehavior;
|
||||
use yii\db\ActiveQuery;
|
||||
use yii\db\ActiveRecord;
|
||||
|
||||
/**
|
||||
* Поля модели:
|
||||
* @property string $id
|
||||
* @property string $secret
|
||||
* @property string $type
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property string $redirect_uri
|
||||
* @property string $website_url
|
||||
* @property string $minecraft_server_ip
|
||||
* @property integer $account_id
|
||||
* @property bool $is_trusted
|
||||
* @property bool $is_deleted
|
||||
* @property integer $created_at
|
||||
*
|
||||
* Отношения:
|
||||
* @property Account $account
|
||||
* @property Account|null $account
|
||||
* @property OauthSession[] $sessions
|
||||
*/
|
||||
class OauthClient extends ActiveRecord {
|
||||
|
||||
public static function tableName() {
|
||||
public const TYPE_APPLICATION = 'application';
|
||||
public const TYPE_MINECRAFT_SERVER = 'minecraft-server';
|
||||
|
||||
public static function tableName(): string {
|
||||
return '{{%oauth_clients}}';
|
||||
}
|
||||
|
||||
public function rules() {
|
||||
public function behaviors(): array {
|
||||
return [
|
||||
[['id'], 'required', 'when' => function(self $model) {
|
||||
return $model->isNewRecord;
|
||||
}],
|
||||
[['id'], 'unique', 'when' => function(self $model) {
|
||||
return $model->isNewRecord;
|
||||
}],
|
||||
[['name', 'description'], 'required'],
|
||||
[['name', 'description'], 'string', 'max' => 255],
|
||||
[
|
||||
'class' => TimestampBehavior::class,
|
||||
'updatedAtAttribute' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount() {
|
||||
public function generateSecret(): void {
|
||||
$this->secret = Yii::$app->security->generateRandomString(64);
|
||||
}
|
||||
|
||||
public function getAccount(): ActiveQuery {
|
||||
return $this->hasOne(Account::class, ['id' => 'account_id']);
|
||||
}
|
||||
|
||||
public function getSessions() {
|
||||
public function getSessions(): ActiveQuery {
|
||||
return $this->hasMany(OauthSession::class, ['client_id' => 'id']);
|
||||
}
|
||||
|
||||
public static function find(): OauthClientQuery {
|
||||
return Yii::createObject(OauthClientQuery::class, [static::class]);
|
||||
}
|
||||
|
||||
}
|
||||
|
34
common/models/OauthClientQuery.php
Normal file
34
common/models/OauthClientQuery.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\models;
|
||||
|
||||
use yii\db\ActiveQuery;
|
||||
use yii\db\Command;
|
||||
|
||||
/**
|
||||
* @see OauthClient
|
||||
*/
|
||||
class OauthClientQuery extends ActiveQuery {
|
||||
|
||||
private $showDeleted = false;
|
||||
|
||||
public function includeDeleted(): self {
|
||||
$this->showDeleted = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onlyDeleted(): self {
|
||||
$this->showDeleted = true;
|
||||
return $this->andWhere(['is_deleted' => true]);
|
||||
}
|
||||
|
||||
public function createCommand($db = null): Command {
|
||||
if ($this->showDeleted === false) {
|
||||
$this->andWhere(['is_deleted' => false]);
|
||||
}
|
||||
|
||||
return parent::createCommand($db);
|
||||
}
|
||||
|
||||
}
|
@@ -4,6 +4,7 @@ namespace common\models;
|
||||
use common\components\Redis\Set;
|
||||
use Yii;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\behaviors\TimestampBehavior;
|
||||
use yii\db\ActiveQuery;
|
||||
use yii\db\ActiveRecord;
|
||||
|
||||
@@ -14,6 +15,7 @@ use yii\db\ActiveRecord;
|
||||
* @property string|null $owner_id
|
||||
* @property string $client_id
|
||||
* @property string $client_redirect_uri
|
||||
* @property integer $created_at
|
||||
*
|
||||
* Отношения
|
||||
* @property OauthClient $client
|
||||
@@ -26,6 +28,15 @@ class OauthSession extends ActiveRecord {
|
||||
return '{{%oauth_sessions}}';
|
||||
}
|
||||
|
||||
public function behaviors() {
|
||||
return [
|
||||
[
|
||||
'class' => TimestampBehavior::class,
|
||||
'updatedAtAttribute' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getClient(): ActiveQuery {
|
||||
return $this->hasOne(OauthClient::class, ['id' => 'client_id']);
|
||||
}
|
||||
|
@@ -12,6 +12,9 @@ final class Permissions {
|
||||
public const MANAGE_TWO_FACTOR_AUTH = 'manage_two_factor_auth';
|
||||
public const BLOCK_ACCOUNT = 'block_account';
|
||||
public const COMPLETE_OAUTH_FLOW = 'complete_oauth_flow';
|
||||
public const CREATE_OAUTH_CLIENTS = 'create_oauth_clients';
|
||||
public const VIEW_OAUTH_CLIENTS = 'view_oauth_clients';
|
||||
public const MANAGE_OAUTH_CLIENTS = 'manage_oauth_clients';
|
||||
|
||||
// Personal level controller permissions
|
||||
public const OBTAIN_OWN_ACCOUNT_INFO = 'obtain_own_account_info';
|
||||
@@ -23,6 +26,8 @@ final class Permissions {
|
||||
public const CHANGE_OWN_ACCOUNT_EMAIL = 'change_own_account_email';
|
||||
public const MANAGE_OWN_TWO_FACTOR_AUTH = 'manage_own_two_factor_auth';
|
||||
public const MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
|
||||
public const VIEW_OWN_OAUTH_CLIENTS = 'view_own_oauth_clients';
|
||||
public const MANAGE_OWN_OAUTH_CLIENTS = 'manage_own_oauth_clients';
|
||||
|
||||
// Data permissions
|
||||
public const OBTAIN_ACCOUNT_EMAIL = 'obtain_account_email';
|
||||
|
@@ -3,7 +3,6 @@ namespace common\rbac\rules;
|
||||
|
||||
use common\models\Account;
|
||||
use Yii;
|
||||
use yii\base\InvalidParamException;
|
||||
use yii\rbac\Rule;
|
||||
|
||||
class AccountOwner extends Rule {
|
||||
@@ -26,7 +25,7 @@ class AccountOwner extends Rule {
|
||||
public function execute($accessToken, $item, $params): bool {
|
||||
$accountId = $params['accountId'] ?? null;
|
||||
if ($accountId === null) {
|
||||
throw new InvalidParamException('params don\'t contain required key: accountId');
|
||||
return false;
|
||||
}
|
||||
|
||||
$identity = Yii::$app->user->findIdentityByAccessToken($accessToken);
|
||||
|
61
common/rbac/rules/OauthClientOwner.php
Normal file
61
common/rbac/rules/OauthClientOwner.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\rbac\rules;
|
||||
|
||||
use common\models\OauthClient;
|
||||
use common\rbac\Permissions as P;
|
||||
use Yii;
|
||||
use yii\rbac\Rule;
|
||||
|
||||
class OauthClientOwner extends Rule {
|
||||
|
||||
public $name = 'oauth_client_owner';
|
||||
|
||||
/**
|
||||
* Accepts 2 params:
|
||||
* - clientId - it's the client id, that user want access to.
|
||||
* - accountId - if it is passed to check the VIEW_OAUTH_CLIENTS permission, then it will
|
||||
* check, that current user have access to the provided account.
|
||||
*
|
||||
* @param string|int $accessToken
|
||||
* @param \yii\rbac\Item $item
|
||||
* @param array $params
|
||||
*
|
||||
* @return bool a value indicating whether the rule permits the auth item it is associated with.
|
||||
*/
|
||||
public function execute($accessToken, $item, $params): bool {
|
||||
$accountId = $params['accountId'] ?? null;
|
||||
if ($accountId !== null && $item->name === P::VIEW_OWN_OAUTH_CLIENTS) {
|
||||
return (new AccountOwner())->execute($accessToken, $item, ['accountId' => $accountId]);
|
||||
}
|
||||
|
||||
$clientId = $params['clientId'] ?? null;
|
||||
if ($clientId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var OauthClient|null $client */
|
||||
$client = OauthClient::findOne($clientId);
|
||||
if ($client === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$identity = Yii::$app->user->findIdentityByAccessToken($accessToken);
|
||||
if ($identity === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = $identity->getAccount();
|
||||
if ($account === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($account->id !== $client->account_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
69
common/tasks/ClearOauthSessions.php
Normal file
69
common/tasks/ClearOauthSessions.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\tasks;
|
||||
|
||||
use common\models\OauthClient;
|
||||
use Yii;
|
||||
use yii\queue\RetryableJobInterface;
|
||||
|
||||
class ClearOauthSessions implements RetryableJobInterface {
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $clientId;
|
||||
|
||||
/**
|
||||
* @var int unix timestamp, that allows to limit this task to clear only some old sessions
|
||||
*/
|
||||
public $notSince;
|
||||
|
||||
public static function createFromOauthClient(OauthClient $client, int $notSince = null): self {
|
||||
$result = new static();
|
||||
$result->clientId = $client->id;
|
||||
if ($notSince !== null) {
|
||||
$result->notSince = $notSince;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getTtr(): int {
|
||||
return 60/*sec*/ * 5/*min*/;
|
||||
}
|
||||
|
||||
public function canRetry($attempt, $error): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\queue\Queue $queue which pushed and is handling the job
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
* @throws \yii\db\StaleObjectException
|
||||
*/
|
||||
public function execute($queue): void {
|
||||
Yii::$app->statsd->inc('queue.clearOauthSessions.attempt');
|
||||
/** @var OauthClient|null $client */
|
||||
$client = OauthClient::find()
|
||||
->includeDeleted()
|
||||
->andWhere(['id' => $this->clientId])
|
||||
->one();
|
||||
if ($client === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionsQuery = $client->getSessions();
|
||||
if ($this->notSince !== null) {
|
||||
$sessionsQuery->andWhere(['<=', 'created_at', $this->notSince]);
|
||||
}
|
||||
|
||||
foreach ($sessionsQuery->each(100, Yii::$app->unbufferedDb) as $session) {
|
||||
/** @var \common\models\OauthSession $session */
|
||||
$session->delete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
24
common/validators/MinecraftServerAddressValidator.php
Normal file
24
common/validators/MinecraftServerAddressValidator.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace common\validators;
|
||||
|
||||
use yii\validators\Validator;
|
||||
|
||||
class MinecraftServerAddressValidator extends Validator {
|
||||
|
||||
protected function validateValue($value) {
|
||||
// we will add minecraft protocol to help parse_url understand all another parts
|
||||
$urlParts = parse_url('minecraft://' . $value);
|
||||
$cnt = count($urlParts);
|
||||
// scheme will be always presented, so we need to increase expected $cnt by 1
|
||||
if (($cnt === 3 && isset($urlParts['host'], $urlParts['port']))
|
||||
|| ($cnt === 2 && isset($urlParts['host']))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$this->message, []];
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user