diff options
author | Shankar Kalidindi <94879708+skalidindi53@users.noreply.github.com> | 2024-07-10 11:03:55 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-10 11:03:55 -0500 |
commit | 193b70eff9bb867e4611fe38ea06ff7407c4f03c (patch) | |
tree | 4c11d334dbd3664fdcff80cb920728048d172f1d | |
parent | aeab37a4827039778d29823aa9685e650388162f (diff) | |
parent | 49741ae91aa3c336b1e4f5c05e308aaecf830ff9 (diff) |
Merge pull request #12558 from nextcloud/skalidindi53/12292/Banning-users-and-guests
feat: Added database migration for banning users
-rw-r--r-- | appinfo/info.xml | 2 | ||||
-rw-r--r-- | appinfo/routes/routesBanController.php | 2 | ||||
-rw-r--r-- | lib/Controller/BanController.php | 74 | ||||
-rw-r--r-- | lib/Migration/Version20000Date20240621150333.php | 70 | ||||
-rw-r--r-- | lib/Model/Ban.php | 68 | ||||
-rw-r--r-- | lib/Model/BanMapper.php | 64 | ||||
-rw-r--r-- | lib/Service/BanService.php | 78 | ||||
-rw-r--r-- | openapi-full.json | 30 | ||||
-rw-r--r-- | openapi.json | 30 | ||||
-rw-r--r-- | src/services/banService.ts | 7 | ||||
-rw-r--r-- | src/types/openapi/openapi-full.ts | 26 | ||||
-rw-r--r-- | src/types/openapi/openapi.ts | 26 | ||||
-rw-r--r-- | tests/integration/features/bootstrap/FeatureContext.php | 117 | ||||
-rw-r--r-- | tests/integration/features/conversation-1/ban.feature | 92 |
14 files changed, 602 insertions, 84 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index de2513b58..bf4c0e5a0 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -18,7 +18,7 @@ * 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa. ]]></description> - <version>20.0.0-dev.2</version> + <version>20.0.0-dev.3</version> <licence>agpl</licence> <author>Daniel Calviño Sánchez</author> diff --git a/appinfo/routes/routesBanController.php b/appinfo/routes/routesBanController.php index d38bb6397..cdda427a8 100644 --- a/appinfo/routes/routesBanController.php +++ b/appinfo/routes/routesBanController.php @@ -17,6 +17,6 @@ return [ /** @see \OCA\Talk\Controller\BanController::listBans() */ ['name' => 'Ban#listBans', 'url' => '/api/{apiVersion}/ban/{token}', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\BanController::unbanActor() */ - ['name' => 'Ban#unbanActor', 'url' => '/api/{apiVersion}/ban/{token}', 'verb' => 'DELETE', 'requirements' => $requirements], + ['name' => 'Ban#unbanActor', 'url' => '/api/{apiVersion}/ban/{token}/{banId}', 'verb' => 'DELETE', 'requirements' => $requirements], ], ]; diff --git a/lib/Controller/BanController.php b/lib/Controller/BanController.php index ed39a9dd5..43cf0bca1 100644 --- a/lib/Controller/BanController.php +++ b/lib/Controller/BanController.php @@ -11,10 +11,13 @@ namespace OCA\Talk\Controller; use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant; use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Ban; use OCA\Talk\ResponseDefinitions; +use OCA\Talk\Service\BanService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; /** @@ -24,6 +27,8 @@ class BanController extends AEnvironmentAwareController { public function __construct( string $appName, IRequest $request, + protected BanService $banService, + protected ITimeFactory $timeFactory, ) { parent::__construct($appName, $request); } @@ -37,7 +42,7 @@ class BanController extends AEnvironmentAwareController { * @psalm-param Attendee::ACTOR_*|'ip' $actorType Type of actor to ban, or `ip` when banning a clients remote address * @param string $actorId Actor ID or the IP address or range in case of type `ip` * @param string $internalNote Optional internal note - * @return DataResponse<Http::STATUS_OK, TalkBan, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}> + * @return DataResponse<Http::STATUS_OK, TalkBan, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'bannedActor'|'internalNote'}, array{}> * * 200: Ban successfully * 400: Actor information is invalid @@ -45,25 +50,29 @@ class BanController extends AEnvironmentAwareController { #[PublicPage] #[RequireModeratorParticipant] public function banActor(string $actorType, string $actorId, string $internalNote = ''): DataResponse { - if ($actorId === 'wrong') { + try { + $moderator = $this->participant->getAttendee(); + $moderatorActorType = $moderator->getActorType(); + $moderatorActorId = $moderator->getActorId(); + + $ban = $this->banService->createBan( + $moderatorActorId, + $moderatorActorType, + $this->room->getId(), + $actorId, + $actorType, + $this->timeFactory->getDateTime(), + $internalNote + ); + + return new DataResponse($ban->jsonSerialize(), Http::STATUS_OK); + } catch (\InvalidArgumentException $e) { + /** @var 'bannedActor'|'internalNote' $message */ + $message = $e->getMessage(); return new DataResponse([ - 'error' => 'actor', + 'error' => $message, ], Http::STATUS_BAD_REQUEST); } - - - return new DataResponse( - [ - 'id' => random_int(1, 1337), - 'actorType' => $this->participant->getAttendee()->getActorType(), - 'actorId' => $this->participant->getAttendee()->getActorId(), - 'bannedType' => $actorType, - 'bannedId' => $actorId, - 'bannedTime' => time(), - 'internalNote' => $internalNote ?: 'Lorem ipsum', - ], - Http::STATUS_OK - ); } /** @@ -71,23 +80,16 @@ class BanController extends AEnvironmentAwareController { * * Required capability: `ban-v1` * - * @return DataResponse<Http::STATUS_OK, list<TalkBan>, array{}> + * @return DataResponse<Http::STATUS_OK, TalkBan[], array{}> * * 200: List all bans */ #[PublicPage] #[RequireModeratorParticipant] public function listBans(): DataResponse { - return new DataResponse([ - $this->randomBan(Attendee::ACTOR_USERS, 'test'), - $this->randomBan(Attendee::ACTOR_USERS, '123456'), - $this->randomBan(Attendee::ACTOR_FEDERATED_USERS, 'admin@nextcloud.local'), - $this->randomBan('ip', '127.0.0.1'), - $this->randomBan('ip', '127.0.0.1/32'), - $this->randomBan('ip', '127.0.0.0/24'), - $this->randomBan('ip', '::1/24'), - $this->randomBan('ip', '2001:0db8:85a3::/48'), - ], Http::STATUS_OK); + $bans = $this->banService->getBansForRoom($this->room->getId()); + $result = array_map(static fn (Ban $ban): array => $ban->jsonSerialize(), $bans); + return new DataResponse($result, Http::STATUS_OK); } /** @@ -103,21 +105,7 @@ class BanController extends AEnvironmentAwareController { #[PublicPage] #[RequireModeratorParticipant] public function unbanActor(int $banId): DataResponse { + $this->banService->findAndDeleteBanByIdForRoom($banId, $this->room->getId()); return new DataResponse([], Http::STATUS_OK); } - - /** - * @psalm-return TalkBan - */ - protected function randomBan(string $actorType, string $actorId): array { - return [ - 'id' => random_int(1, 1337), - 'actorType' => $this->participant->getAttendee()->getActorType(), - 'actorId' => $this->participant->getAttendee()->getActorId(), - 'bannedType' => $actorType, - 'bannedId' => $actorId, - 'bannedTime' => random_int(1514747958, 1714747958), - 'internalNote' => '#NOTE#' . $actorType . '#' . $actorId . '#' . sha1($actorType . $actorId), - ]; - } } diff --git a/lib/Migration/Version20000Date20240621150333.php b/lib/Migration/Version20000Date20240621150333.php new file mode 100644 index 000000000..ba7addbef --- /dev/null +++ b/lib/Migration/Version20000Date20240621150333.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Talk\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version20000Date20240621150333 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('talk_bans')) { + $table = $schema->createTable('talk_bans'); + $table->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 20, + ]); + $table->addColumn('actor_type', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('actor_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('room_id', Types::BIGINT, [ + 'notnull' => true, + 'unsigned' => true, + ]); + $table->addColumn('banned_type', Types::STRING, [ + 'length' => 64, + 'notnull' => true, + ]); + $table->addColumn('banned_id', Types::STRING, [ + 'length' => 64, + 'notnull' => true, + ]); + $table->addColumn('banned_time', Types::DATETIME, [ + 'notnull' => false, + ]); + $table->addColumn('internal_note', Types::TEXT, [ + 'notnull' => false, + ]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['banned_type', 'banned_id', 'room_id'], 'talk_bans_unique_actor_room'); //A user should not be banned from the same room more than once + $table->addIndex(['room_id']); + return $schema; + } + + return null; + } +} diff --git a/lib/Model/Ban.php b/lib/Model/Ban.php new file mode 100644 index 000000000..b2f1d686d --- /dev/null +++ b/lib/Model/Ban.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Talk\Model; + +use OCA\Talk\ResponseDefinitions; +use OCP\AppFramework\Db\Entity; + +/** + * @psalm-import-type TalkBan from ResponseDefinitions + * + * @method void setId(int $id) + * @method int getId() + * @method void setActorType(string $actorType) + * @method string getActorType() + * @method void setActorId(string $actorId) + * @method string getActorId() + * @method void setRoomId(int $roomId) + * @method int getRoomId() + * @method void setBannedType(string $bannedType) + * @method string getBannedType() + * @method void setBannedId(string $bannedId) + * @method string getBannedId() + * @method void setBannedTime(\DateTime $bannedTime) + * @method \DateTime getBannedTime() + * @method void setInternalNote(null|string $internalNote) + * @method null|string getInternalNote() + */ +class Ban extends Entity implements \JsonSerializable { + protected string $actorType = ''; + protected string $actorId = ''; + protected int $roomId = 0; + protected string $bannedType = ''; + protected string $bannedId = ''; + protected ?\DateTime $bannedTime = null; + protected ?string $internalNote = null; + + public function __construct() { + $this->addType('id', 'int'); + $this->addType('actorType', 'string'); + $this->addType('actorId', 'string'); + $this->addType('roomId', 'int'); + $this->addType('bannedType', 'string'); + $this->addType('bannedId', 'string'); + $this->addType('bannedTime', 'datetime'); + $this->addType('internalNote', 'string'); + } + + /** + * @return TalkBan + */ + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'actorType' => $this->getActorType(), + 'actorId' => $this->getActorId(), + 'bannedType' => $this->getBannedType(), + 'bannedId' => $this->getBannedId(), + 'bannedTime' => $this->getBannedTime()->getTimestamp(), + 'internalNote' => $this->getInternalNote() ?? '', + ]; + } +} diff --git a/lib/Model/BanMapper.php b/lib/Model/BanMapper.php new file mode 100644 index 000000000..01c979990 --- /dev/null +++ b/lib/Model/BanMapper.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Talk\Model; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @method Ban mapRowToEntity(array $row) + * @method Ban findEntity(IQueryBuilder $query) + * @method Ban[] findEntities(IQueryBuilder $query) + * @template-extends QBMapper<Ban> + */ +class BanMapper extends QBMapper { + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'talk_bans', Ban::class); + } + + /** + * @throws DoesNotExistException + */ + public function findForActorAndRoom(string $actorId, string $actorType, int $roomId): Ban { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('actor_id', $query->createNamedParameter($actorId, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); + + return $this->findEntity($query); + } + + public function findByRoomId(int $roomId): array { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))) + ->orderBy('id', 'ASC'); + + return $this->findEntities($query); + } + + /** + * @throws DoesNotExistException + */ + public function findByBanIdAndRoom(int $banId, int $roomId): Ban { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('id', $query->createNamedParameter($banId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); + + return $this->findEntity($query); + } +} diff --git a/lib/Service/BanService.php b/lib/Service/BanService.php new file mode 100644 index 000000000..f36f9013e --- /dev/null +++ b/lib/Service/BanService.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Talk\Service; + +use DateTime; +use OCA\Talk\Model\Ban; +use OCA\Talk\Model\BanMapper; +use OCP\AppFramework\Db\DoesNotExistException; + +class BanService { + + public function __construct( + protected BanMapper $banMapper, + ) { + } + + /** + * Create a new ban + * + * @throws \InvalidArgumentException + */ + public function createBan(string $actorId, string $actorType, int $roomId, string $bannedId, string $bannedType, DateTime $bannedTime, string $internalNote): Ban { + if (empty($bannedId) || empty($bannedType)) { + throw new \InvalidArgumentException('bannedActor'); + } + + if (empty($internalNote)) { + throw new \InvalidArgumentException('internalNote'); + } + + $ban = new Ban(); + $ban->setActorId($actorId); + $ban->setActorType($actorType); + $ban->setRoomId($roomId); + $ban->setBannedId($bannedId); + $ban->setBannedType($bannedType); + $ban->setBannedTime($bannedTime); + $ban->setInternalNote($internalNote); + + return $this->banMapper->insert($ban); + } + + /** + * Retrieve a ban for a specific actor and room. + * + * @throws DoesNotExistException + */ + public function getBanForActorAndRoom(string $actorId, string $actorType, int $roomId): Ban { + return $this->banMapper->findForActorAndRoom($actorId, $actorType, $roomId); + } + + /** + * Retrieve all bans for a specific room. + * + * @return Ban[] + */ + public function getBansForRoom(int $roomId): array { + return $this->banMapper->findByRoomId($roomId); + } + + /** + * Retrieve a ban by its ID and delete it. + */ + public function findAndDeleteBanByIdForRoom(int $banId, int $roomId): void { + try { + $ban = $this->banMapper->findByBanIdAndRoom($banId, $roomId); + $this->banMapper->delete($ban); + } catch (DoesNotExistException) { + // Ban does not exist + } + } +} diff --git a/openapi-full.json b/openapi-full.json index 2faa008ca..f9f21adc8 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -2099,7 +2099,11 @@ ], "properties": { "error": { - "type": "string" + "type": "string", + "en |