From d77753d1ceae75bcb02726a4bcd0d0053ed106be Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Wed, 13 Nov 2024 16:26:12 +0100 Subject: [PATCH] feat(conversations): add option to force passwords in public conversations Signed-off-by: Anna Larch --- docs/capabilities.md | 4 + docs/conversation.md | 7 ++ docs/settings.md | 1 + lib/Capabilities.php | 4 +- lib/Config.php | 4 + lib/Controller/RoomController.php | 44 ++++++-- lib/Manager.php | 19 +++- lib/ResponseDefinitions.php | 1 + lib/Service/RoomService.php | 103 ++++++++++++++++-- openapi-administration.json | 6 +- openapi-backend-recording.json | 6 +- openapi-backend-signaling.json | 6 +- openapi-backend-sipbridge.json | 6 +- openapi-bots.json | 6 +- openapi-federation.json | 6 +- openapi-full.json | 41 ++++++- openapi.json | 41 ++++++- src/__mocks__/capabilities.ts | 1 + src/types/openapi/openapi-administration.ts | 1 + .../openapi/openapi-backend-recording.ts | 1 + .../openapi/openapi-backend-signaling.ts | 1 + .../openapi/openapi-backend-sipbridge.ts | 1 + src/types/openapi/openapi-bots.ts | 1 + src/types/openapi/openapi-federation.ts | 1 + src/types/openapi/openapi-full.ts | 29 ++++- src/types/openapi/openapi.ts | 29 ++++- tests/php/CapabilitiesTest.php | 2 + tests/php/Service/RoomServiceTest.php | 5 + 28 files changed, 337 insertions(+), 40 deletions(-) diff --git a/docs/capabilities.md b/docs/capabilities.md index 5ee8adda207..9d4b20b8aa7 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -166,3 +166,7 @@ * `config => call => start-without-media` (local) - Boolean, whether media should be disabled when starting or joining a conversation * `config => call => max-duration` - Integer, maximum call duration in seconds. Please note that this should only be used with system cron and with a reasonable high value, due to the expended duration until the background job ran. * `config => call => blur-virtual-background` (local) - Boolean, whether blur background is set by default when joining a conversation + +## 21 +* `config => conversations => force-passwords` - Whether passwords are enforced for public rooms +* `conversation-creation-password` - Whether the endpoints for creating public conversations or making a conversation public support setting a password diff --git a/docs/conversation.md b/docs/conversation.md index 13fe92972b0..948b2dae35d 100644 --- a/docs/conversation.md +++ b/docs/conversation.md @@ -128,6 +128,7 @@ | `roomName` | string | Conversation name up to 255 characters (Not available for `roomType = 1`) | | `objectType` | string | Type of an object this room references, currently only allowed value is `room` to indicate the parent of a breakout room (See [Object types](constants.md#object-types)) | | `objectId` | string | Id of an object this room references, room token is used for the parent of a breakout room | +| `password` | string | Password for the room (only available with `conversation-creation-password` capability) | * Response: - Status code: @@ -135,6 +136,7 @@ + `201 Created` When the conversation was created + `400 Bad Request` When an invalid conversation type was given + `400 Bad Request` When the conversation name is empty for `type = 3` + + `400 Bad Request` When a password is required for a public room or when the password is invalid according to the password policy + `401 Unauthorized` When the user is not logged in + `404 Not Found` When the target to invite does not exist @@ -283,6 +285,11 @@ Get all (for moderators and in case of "free selection") or the assigned breakou * Method: `POST` * Endpoint: `/room/{token}/public` +* Data: + +| field | type | Description | +|------------|---------|-------------------------------------------------------------------------------------------------| +| `password` | ?string | Password for the conversation (only available with `conversation-creation-password` capability) | * Response: - Status code: diff --git a/docs/settings.md b/docs/settings.md index b828fe2cf33..e95738b25a3 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -113,5 +113,6 @@ Legend: | `conversations_files` | string
`1` or `0` | `1` | No | 🖌️ | Whether the files app integration is enabled allowing to start conversations in the right sidebar | | `conversations_files_public_shares` | string
`1` or `0` | `1` | No | 🖌️ | Whether the public share integration is enabled allowing to start conversations in the right sidebar on the public share page (Requires `conversations_files` also to be enabled) | | `enable_matterbridge` | string
`1` or `0` | `0` | No | 🖌️ | Whether the Matterbridge integration is enabled and can be configured | +| `force_passwords` | string
`1` or `0` | `0` | No | ️ | Whether public chats are forced to use a password | | `inactivity_lock_after_days` | int | `0` | No | | A duration (in days) after which rooms are locked. Calculated from the last activity in the room. | | `inactivity_enable_lobby` | string
`1` or `0` | `0` | No | | Additionally enable the lobby for inactive rooms so they can only be read by moderators. | diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 91280aeb16c..79bed2d1a48 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -109,6 +109,7 @@ class Capabilities implements IPublicCapability { 'talk-polls-drafts', 'download-call-participants', 'email-csv-import', + 'conversation-creation-password', ]; public const CONDITIONAL_FEATURES = [ @@ -224,7 +225,8 @@ public function getCapabilities(): array { 'summary-threshold' => 100, ], 'conversations' => [ - 'can-create' => $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user) + 'can-create' => $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user), + 'force-passwords' => $this->talkConfig->isPasswordEnforced(), ], 'federation' => [ 'enabled' => false, diff --git a/lib/Config.php b/lib/Config.php index ccf88382bc2..491d898def1 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -705,4 +705,8 @@ public function getInactiveLockTime(): int { public function enableLobbyOnLockedRooms(): bool { return $this->appConfig->getAppValueBool('inactivity_enable_lobby'); } + + public function isPasswordEnforced(): bool { + return $this->appConfig->getAppValueBool('force_passwords'); + } } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 1d3ca673b1e..6c4384bdc04 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -502,16 +502,25 @@ protected function formatRoom(Room $room, ?Participant $currentParticipant, ?arr * @param 'groups'|'circles'|'' $source Source of the invite ID ('circles' to create a room with a circle, etc.) * @param string $objectType Type of the object * @param string $objectId ID of the object - * @return DataResponse|DataResponse|DataResponse, array{}> + * @param string $password The room password (only available with `conversation-creation-password` capability) + * @return DataResponse|DataResponse|DataResponse, array{}> * * 200: Room already existed * 201: Room created successfully - * 400: Room type invalid + * 400: Room type invalid or missing or invalid password * 403: Missing permissions to create room * 404: User, group or other target to invite was not found */ #[NoAdminRequired] - public function createRoom(int $roomType, string $invite = '', string $roomName = '', string $source = '', string $objectType = '', string $objectId = ''): DataResponse { + public function createRoom( + int $roomType, + string $invite = '', + string $roomName = '', + string $source = '', + string $objectType = '', + string $objectId = '', + string $password = '', + ): DataResponse { if ($roomType !== Room::TYPE_ONE_TO_ONE) { /** @var IUser $user */ $user = $this->userManager->get($this->userId); @@ -533,7 +542,7 @@ public function createRoom(int $roomType, string $invite = '', string $roomName } return $this->createGroupRoom($invite); case Room::TYPE_PUBLIC: - return $this->createEmptyRoom($roomName, true, $objectType, $objectId); + return $this->createEmptyRoom($roomName, true, $objectType, $objectId, $password); } return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -645,10 +654,10 @@ protected function createCircleRoom(string $targetCircleId): DataResponse { } /** - * @return DataResponse|DataResponse|DataResponse, array{}> + * @return DataResponse|DataResponse|DataResponse, array{}> */ #[NoAdminRequired] - protected function createEmptyRoom(string $roomName, bool $public = true, string $objectType = '', string $objectId = ''): DataResponse { + protected function createEmptyRoom(string $roomName, bool $public = true, string $objectType = '', string $objectId = '', string $password = ''): DataResponse { $currentUser = $this->userManager->get($this->userId); if (!$currentUser instanceof IUser) { return new DataResponse([], Http::STATUS_NOT_FOUND); @@ -686,7 +695,9 @@ protected function createEmptyRoom(string $roomName, bool $public = true, string // Create the room try { - $room = $this->roomService->createConversation($roomType, $roomName, $currentUser, $objectType, $objectId); + $room = $this->roomService->createConversation($roomType, $roomName, $currentUser, $objectType, $objectId, $password); + } catch (PasswordException $e) { + return new DataResponse(['error' => 'password', 'message' => $e->getHint()], Http::STATUS_BAD_REQUEST); } catch (\InvalidArgumentException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } @@ -1420,16 +1431,29 @@ public function removeAttendeeFromRoom(int $attendeeId): DataResponse { /** * Allowed guests to join conversation * - * @return DataResponse, array{}>|DataResponse + * Required capability: `conversation-creation-password` for `string $password` parameter + * + * @param string $password New password (only available with `conversation-creation-password` capability) + * @return DataResponse, array{}>|DataResponse * * 200: Allowed guests successfully * 400: Allowing guests is not possible */ #[NoAdminRequired] #[RequireLoggedInModeratorParticipant] - public function makePublic(): DataResponse { + public function makePublic(string $password = ''): DataResponse { + if ($this->talkConfig->isPasswordEnforced() && $password === '') { + return new DataResponse(['error' => 'password', 'message' => $this->l->t('Password needs to be set')], Http::STATUS_BAD_REQUEST); + } + try { - $this->roomService->setType($this->room, Room::TYPE_PUBLIC); + if ($password !== '') { + $this->roomService->makePublicWithPassword($this->room, $password); + } else { + $this->roomService->setType($this->room, Room::TYPE_PUBLIC); + } + } catch (PasswordException $e) { + return new DataResponse(['error' => 'password', 'message' => $e->getHint()], Http::STATUS_BAD_REQUEST); } catch (TypeException $e) { return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST); } diff --git a/lib/Manager.php b/lib/Manager.php index 28e311a6887..a4e87a89efb 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -25,6 +25,7 @@ use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException; +use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; use OCP\ICache; @@ -1108,7 +1109,7 @@ public function getChangelogRoom(string $userId): Room { * @param string $objectId * @return Room */ - public function createRoom(int $type, string $name = '', string $objectType = '', string $objectId = ''): Room { + public function createRoom(int $type, string $name = '', string $objectType = '', string $objectId = '', string $password = ''): Room { $token = $this->getNewToken(); $insert = $this->db->getQueryBuilder(); @@ -1118,6 +1119,7 @@ public function createRoom(int $type, string $name = '', string $objectType = '' 'name' => $insert->createNamedParameter($name), 'type' => $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT), 'token' => $insert->createNamedParameter($token), + 'password' => $insert->createNamedParameter($password), ] ); @@ -1135,6 +1137,7 @@ public function createRoom(int $type, string $name = '', string $objectType = '' 'token' => $token, 'object_type' => $objectType, 'object_id' => $objectId, + 'password' => $password ]); $event = new RoomCreatedEvent($room); @@ -1409,4 +1412,18 @@ protected function loadLastMessageInfo(IQueryBuilder $query): void { $query->selectAlias('c.expire_date', 'comment_expire_date'); $query->selectAlias('c.meta_data', 'comment_meta_data'); } + + /** + * @param int $roomId + * @param string $password + * @throws Exception + */ + public function setPublic(int $roomId, string $password = ''): void { + $update = $this->db->getQueryBuilder(); + $update->update('talk_rooms') + ->set('type', $update->createNamedParameter(Room::TYPE_PUBLIC, IQueryBuilder::PARAM_INT)) + ->set('password', $update->createNamedParameter($password, IQueryBuilder::PARAM_STR)) + ->where($update->expr()->eq('id', $update->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); + $update->executeStatement(); + } } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index f2854d51362..dfb77517a23 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -361,6 +361,7 @@ * }, * conversations: array{ * can-create: bool, + * force-passwords: bool, * }, * federation: array{ * enabled: bool, diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 1ccd4f06a04..567aa82d3ee 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -57,6 +57,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\HintException; use OCP\IDBConnection; +use OCP\IL10N; use OCP\IUser; use OCP\Log\Audit\CriticalActionPerformedEvent; use OCP\Security\Events\ValidatePasswordPolicyEvent; @@ -80,6 +81,7 @@ public function __construct( protected IEventDispatcher $dispatcher, protected IJobList $jobList, protected LoggerInterface $logger, + protected IL10N $l10n, ) { } @@ -127,17 +129,13 @@ public function createOneToOneConversation(IUser $actor, IUser $targetUser): Roo } /** - * @param int $type - * @param string $name - * @param IUser|null $owner - * @param string $objectType - * @param string $objectId * @return Room * @throws InvalidArgumentException on too long or empty names * @throws InvalidArgumentException unsupported type * @throws InvalidArgumentException invalid object data + * @throws PasswordException empty or invalid password */ - public function createConversation(int $type, string $name, ?IUser $owner = null, string $objectType = '', string $objectId = ''): Room { + public function createConversation(int $type, string $name, ?IUser $owner = null, string $objectType = '', string $objectId = '', string $password = ''): Room { $name = trim($name); if ($name === '' || mb_strlen($name) > 255) { throw new InvalidArgumentException('name'); @@ -167,7 +165,20 @@ public function createConversation(int $type, string $name, ?IUser $owner = null throw new InvalidArgumentException('object'); } - $room = $this->manager->createRoom($type, $name, $objectType, $objectId); + if ($type !== Room::TYPE_PUBLIC || !$this->config->isPasswordEnforced()) { + $room = $this->manager->createRoom($type, $name, $objectType, $objectId); + } elseif ($password === '') { + throw new PasswordException(PasswordException::REASON_VALUE, $this->l10n->t('Password needs to be set')); + } else { + $event = new ValidatePasswordPolicyEvent($password); + try { + $this->dispatcher->dispatchTyped($event); + } catch (HintException $e) { + throw new PasswordException(PasswordException::REASON_VALUE, $e->getHint()); + } + $passwordHash = $this->hasher->hash($password); + $room = $this->manager->createRoom($type, $name, $objectType, $objectId, $passwordHash); + } if ($owner instanceof IUser) { $this->participantService->addUsers($room, [[ @@ -177,8 +188,8 @@ public function createConversation(int $type, string $name, ?IUser $owner = null 'participantType' => Participant::OWNER, ]], null); } - return $room; + } public function prepareConversationName(string $objectName): string { @@ -545,6 +556,44 @@ public function setType(Room $room, int $newType, bool $allowSwitchingOneToOne = $this->dispatcher->dispatchTyped($event); } + /** + * @throws PasswordException|TypeException + */ + public function makePublicWithPassword(Room $room, string $password): void { + if ($room->getType() === Room::TYPE_PUBLIC) { + return; + } + + if ($room->getType() !== Room::TYPE_GROUP) { + throw new TypeException(TypeException::REASON_TYPE); + } + + if ($password === '') { + throw new PasswordException(PasswordException::REASON_VALUE, $this->l10n->t('Password needs to be set')); + } + + $event = new ValidatePasswordPolicyEvent($password); + try { + $this->dispatcher->dispatchTyped($event); + } catch (HintException $e) { + throw new PasswordException(PasswordException::REASON_VALUE, $e->getHint()); + } + + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_TYPE, Room::TYPE_PUBLIC, $room->getType()); + $this->dispatcher->dispatchTyped($event); + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_PASSWORD, $password); + $this->dispatcher->dispatchTyped($event); + + $passwordHash = $this->hasher->hash($password); + $this->manager->setPublic($room->getId(), $passwordHash); + $room->setType(Room::TYPE_PUBLIC); + + $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_TYPE, Room::TYPE_PUBLIC, $room->getType()); + $this->dispatcher->dispatchTyped($event); + $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_PASSWORD, $password); + $this->dispatcher->dispatchTyped($event); + } + /** * @param Room $room * @param int $newState Currently it is only allowed to change between @@ -1221,4 +1270,42 @@ public function deleteRoom(Room $room): void { public function getInactiveRooms(\DateTime $inactiveSince): array { return $this->manager->getInactiveRooms($inactiveSince); } + + /** + * @param Room $room + * @param int $oldType + * @param int $newType + * @param bool $allowSwitchingOneToOne + * @return void + */ + public function validateRoomTypeSwitch(Room $room, int $oldType, int $newType, bool $allowSwitchingOneToOne): void { + if (!$allowSwitchingOneToOne && $oldType === Room::TYPE_ONE_TO_ONE) { + throw new TypeException(TypeException::REASON_TYPE); + } + + if ($oldType === Room::TYPE_ONE_TO_ONE_FORMER) { + throw new TypeException(TypeException::REASON_TYPE); + } + + if ($oldType === Room::TYPE_NOTE_TO_SELF) { + throw new TypeException(TypeException::REASON_TYPE); + } + + if (!in_array($newType, [Room::TYPE_GROUP, Room::TYPE_PUBLIC, Room::TYPE_ONE_TO_ONE_FORMER], true)) { + throw new TypeException(TypeException::REASON_VALUE); + } + + if ($newType === Room::TYPE_ONE_TO_ONE_FORMER && $oldType !== Room::TYPE_ONE_TO_ONE) { + throw new TypeException(TypeException::REASON_VALUE); + } + + if ($room->getBreakoutRoomMode() !== BreakoutRoom::MODE_NOT_CONFIGURED) { + throw new TypeException(TypeException::REASON_BREAKOUT_ROOM); + } + + if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) { + throw new TypeException(TypeException::REASON_BREAKOUT_ROOM); + } + } + } diff --git a/openapi-administration.json b/openapi-administration.json index fb1cb56c78b..2fa3374a685 100644 --- a/openapi-administration.json +++ b/openapi-administration.json @@ -237,11 +237,15 @@ "conversations": { "type": "object", "required": [ - "can-create" + "can-create", + "force-passwords" ], "properties": { "can-create": { "type": "boolean" + }, + "force-passwords": { + "type": "boolean" } } }, diff --git a/openapi-backend-recording.json b/openapi-backend-recording.json index 1dc3ce44918..25359a023df 100644 --- a/openapi-backend-recording.json +++ b/openapi-backend-recording.json @@ -170,11 +170,15 @@ "conversations": { "type": "object", "required": [ - "can-create" + "can-create", + "force-passwords" ], "properties": { "can-create": { "type": "boolean" + }, + "force-passwords": { + "type": "boolean" } } }, diff --git a/openapi-backend-signaling.json b/openapi-backend-signaling.json index 745cf8193dc..9b2c9fc6b64 100644 --- a/openapi-backend-signaling.json +++ b/openapi-backend-signaling.json @@ -170,11 +170,15 @@ "conversations": { "type": "object", "required": [ - "can-create" + "can-create", + "force-passwords" ], "properties": { "can-create": { "type": "boolean" + }, + "force-passwords": { + "type": "boolean" } } }, diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json index cc7882888c6..dbd4f998202 100644 --- a/openapi-backend-sipbridge.json +++ b/openapi-backend-sipbridge.json @@ -213,11 +213,15 @@ "conversations": { "type": "object", "required": [ - "can-create" + "can-create", + "force-passwords" ], "properties": { "can-create": { "type": "boolean" + }, + "force-passwords": { + "type": "boolean" } } }, diff --git a/openapi-bots.json b/openapi-bots.json index 6c3840f55f0..0749a6e6f11 100644 --- a/openapi-bots.json +++ b/openapi-bots.json @@ -170,11 +170,15 @@ "conversations": { "type": "object", "required": [ - "can-create" + "can-create", + "force-passwords" ], "properties": { "can-create": { "type": "boolean" + }, + "force-passwords": { + "type": "boolean" } } }, diff --git a/openapi-federation.json b/openapi-federation.json index 72ddd1a5249..fefaa268686 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -213,11 +213,15 @@ "conversations": { "type": "object", "required": [ - "can-create" + "can-create", + "force-passwords" ], "properties": { "can-create": { "type": "boolean" + }, + "force-passwords": { + "type": "boolean" } } }, diff --git a/openapi-full.json b/openapi-full.json index 4509f6afe86..b500f03f0aa 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -389,11 +389,15 @@ "conversations": { "type": "object", "required": [ - "can-create" + "can-create", + "force-passwords" ], "properties": { "can-create": { "type": "boolean" + }, + "force-passwords": { + "type": "boolean" } } }, @@ -11252,6 +11256,11 @@ "type": "string", "default": "", "description": "ID of the object" + }, + "password": { + "type": "string", + "default": "", + "description": "The room password (only available with `conversation-creation-password` capability)" } } } @@ -11344,7 +11353,7 @@ } }, "400": { - "description": "Room type invalid", + "description": "Room type invalid or missing or invalid password", "content": { "application/json": { "schema": { @@ -11368,6 +11377,9 @@ "properties": { "error": { "type": "string" + }, + "message": { + "type": "string" } } } @@ -11882,6 +11894,7 @@ "post": { "operationId": "room-make-public", "summary": "Allowed guests to join conversation", + "description": "Required capability: `conversation-creation-password` for `string $password` parameter", "tags": [ "room" ], @@ -11893,6 +11906,23 @@ "basic_auth": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "default": "", + "description": "New password (only available with `conversation-creation-password` capability)" + } + } + } + } + } + }, "parameters": [ { "name": "apiVersion", @@ -11986,8 +12016,13 @@ "enum": [ "breakout-room", "type", - "value" + "value", + "password" ] + }, + "message": { + "type": "string", + "nullable": true } } } diff --git a/openapi.json b/openapi.json index efbc617aa59..377819b9e45 100644 --- a/openapi.json +++ b/openapi.json @@ -330,11 +330,15 @@ "conversations": { "type": "object", "required": [ - "can-create" + "can-create", + "force-passwords" ], "properties": { "can-create": { "type": "boolean" + }, + "force-passwords": { + "type": "boolean" } } }, @@ -11139,6 +11143,11 @@ "type": "string", "default": "", "description": "ID of the object" + }, + "password": { + "type": "string", + "default": "", + "description": "The room password (only available with `conversation-creation-password` capability)" } } } @@ -11231,7 +11240,7 @@ } }, "400": { - "description": "Room type invalid", + "description": "Room type invalid or missing or invalid password", "content": { "application/json": { "schema": { @@ -11255,6 +11264,9 @@ "properties": { "error": { "type": "string" + }, + "message": { + "type": "string" } } } @@ -12016,6 +12028,7 @@ "post": { "operationId": "room-make-public", "summary": "Allowed guests to join conversation", + "description": "Required capability: `conversation-creation-password` for `string $password` parameter", "tags": [ "room" ], @@ -12027,6 +12040,23 @@ "basic_auth": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "default": "", + "description": "New password (only available with `conversation-creation-password` capability)" + } + } + } + } + } + }, "parameters": [ { "name": "apiVersion", @@ -12120,8 +12150,13 @@ "enum": [ "breakout-room", "type", - "value" + "value", + "password" ] + }, + "message": { + "type": "string", + "nullable": true } } } diff --git a/src/__mocks__/capabilities.ts b/src/__mocks__/capabilities.ts index b6b4558d0c7..4ca38bc35b1 100644 --- a/src/__mocks__/capabilities.ts +++ b/src/__mocks__/capabilities.ts @@ -130,6 +130,7 @@ export const mockedCapabilities: Capabilities = { }, conversations: { 'can-create': true, + 'force-passwords': false, }, federation: { enabled: false, diff --git a/src/types/openapi/openapi-administration.ts b/src/types/openapi/openapi-administration.ts index 23546e2a823..ca9292a0318 100644 --- a/src/types/openapi/openapi-administration.ts +++ b/src/types/openapi/openapi-administration.ts @@ -246,6 +246,7 @@ export type components = { }; conversations: { "can-create": boolean; + "force-passwords": boolean; }; federation: { enabled: boolean; diff --git a/src/types/openapi/openapi-backend-recording.ts b/src/types/openapi/openapi-backend-recording.ts index f4f9c3bfff0..df314564d04 100644 --- a/src/types/openapi/openapi-backend-recording.ts +++ b/src/types/openapi/openapi-backend-recording.ts @@ -80,6 +80,7 @@ export type components = { }; conversations: { "can-create": boolean; + "force-passwords": boolean; }; federation: { enabled: boolean; diff --git a/src/types/openapi/openapi-backend-signaling.ts b/src/types/openapi/openapi-backend-signaling.ts index 13f736e8d08..4ec2f7721e1 100644 --- a/src/types/openapi/openapi-backend-signaling.ts +++ b/src/types/openapi/openapi-backend-signaling.ts @@ -66,6 +66,7 @@ export type components = { }; conversations: { "can-create": boolean; + "force-passwords": boolean; }; federation: { enabled: boolean; diff --git a/src/types/openapi/openapi-backend-sipbridge.ts b/src/types/openapi/openapi-backend-sipbridge.ts index 6401e017d1b..6afa31e75bd 100644 --- a/src/types/openapi/openapi-backend-sipbridge.ts +++ b/src/types/openapi/openapi-backend-sipbridge.ts @@ -161,6 +161,7 @@ export type components = { }; conversations: { "can-create": boolean; + "force-passwords": boolean; }; federation: { enabled: boolean; diff --git a/src/types/openapi/openapi-bots.ts b/src/types/openapi/openapi-bots.ts index cc58aa728c0..9be4e8d8a3f 100644 --- a/src/types/openapi/openapi-bots.ts +++ b/src/types/openapi/openapi-bots.ts @@ -84,6 +84,7 @@ export type components = { }; conversations: { "can-create": boolean; + "force-passwords": boolean; }; federation: { enabled: boolean; diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index f2a820dc997..27053a8ec6d 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -192,6 +192,7 @@ export type components = { }; conversations: { "can-create": boolean; + "force-passwords": boolean; }; federation: { enabled: boolean; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index c32f80af0a8..4ff1bdc426a 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -855,7 +855,10 @@ export type paths = { }; get?: never; put?: never; - /** Allowed guests to join conversation */ + /** + * Allowed guests to join conversation + * @description Required capability: `conversation-creation-password` for `string $password` parameter + */ post: operations["room-make-public"]; /** Disallowed guests to join conversation */ delete: operations["room-make-private"]; @@ -1989,6 +1992,7 @@ export type components = { }; conversations: { "can-create": boolean; + "force-passwords": boolean; }; federation: { enabled: boolean; @@ -6215,6 +6219,11 @@ export interface operations { * @default */ objectId?: string; + /** + * @description The room password (only available with `conversation-creation-password` capability) + * @default + */ + password?: string; }; }; }; @@ -6247,7 +6256,7 @@ export interface operations { }; }; }; - /** @description Room type invalid */ + /** @description Room type invalid or missing or invalid password */ 400: { headers: { [name: string]: unknown; @@ -6258,6 +6267,7 @@ export interface operations { meta: components["schemas"]["OCSMeta"]; data: { error?: string; + message?: string; }; }; }; @@ -6478,7 +6488,17 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody?: { + content: { + "application/json": { + /** + * @description New password (only available with `conversation-creation-password` capability) + * @default + */ + password?: string; + }; + }; + }; responses: { /** @description Allowed guests successfully */ 200: { @@ -6505,7 +6525,8 @@ export interface operations { meta: components["schemas"]["OCSMeta"]; data: { /** @enum {string} */ - error: "breakout-room" | "type" | "value"; + error: "breakout-room" | "type" | "value" | "password"; + message?: string | null; }; }; }; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 5b8d795075c..ee766969fa0 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -857,7 +857,10 @@ export type paths = { }; get?: never; put?: never; - /** Allowed guests to join conversation */ + /** + * Allowed guests to join conversation + * @description Required capability: `conversation-creation-password` for `string $password` parameter + */ post: operations["room-make-public"]; /** Disallowed guests to join conversation */ delete: operations["room-make-private"]; @@ -1486,6 +1489,7 @@ export type components = { }; conversations: { "can-create": boolean; + "force-passwords": boolean; }; federation: { enabled: boolean; @@ -5696,6 +5700,11 @@ export interface operations { * @default */ objectId?: string; + /** + * @description The room password (only available with `conversation-creation-password` capability) + * @default + */ + password?: string; }; }; }; @@ -5728,7 +5737,7 @@ export interface operations { }; }; }; - /** @description Room type invalid */ + /** @description Room type invalid or missing or invalid password */ 400: { headers: { [name: string]: unknown; @@ -5739,6 +5748,7 @@ export interface operations { meta: components["schemas"]["OCSMeta"]; data: { error?: string; + message?: string; }; }; }; @@ -6059,7 +6069,17 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody?: { + content: { + "application/json": { + /** + * @description New password (only available with `conversation-creation-password` capability) + * @default + */ + password?: string; + }; + }; + }; responses: { /** @description Allowed guests successfully */ 200: { @@ -6086,7 +6106,8 @@ export interface operations { meta: components["schemas"]["OCSMeta"]; data: { /** @enum {string} */ - error: "breakout-room" | "type" | "value"; + error: "breakout-room" | "type" | "value" | "password"; + message?: string | null; }; }; }; diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index d853bf0e526..2669e433e88 100644 --- a/tests/php/CapabilitiesTest.php +++ b/tests/php/CapabilitiesTest.php @@ -152,6 +152,7 @@ public function testGetCapabilitiesGuest(): void { ], 'conversations' => [ 'can-create' => false, + 'force-passwords' => false, ], 'federation' => [ 'enabled' => false, @@ -284,6 +285,7 @@ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCrea ], 'conversations' => [ 'can-create' => $canCreate, + 'force-passwords' => false, ], 'federation' => [ 'enabled' => false, diff --git a/tests/php/Service/RoomServiceTest.php b/tests/php/Service/RoomServiceTest.php index 8696ea30ff3..a24e01bbbd9 100644 --- a/tests/php/Service/RoomServiceTest.php +++ b/tests/php/Service/RoomServiceTest.php @@ -26,6 +26,7 @@ use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; +use OCP\IL10N; use OCP\IUser; use OCP\Security\IHasher; use OCP\Share\IManager as IShareManager; @@ -46,6 +47,7 @@ class RoomServiceTest extends TestCase { protected IEventDispatcher&MockObject $dispatcher; protected IJobList&MockObject $jobList; protected LoggerInterface&MockObject $logger; + protected IL10N&MockObject $l10n; protected ?RoomService $service = null; public function setUp(): void { @@ -60,6 +62,7 @@ public function setUp(): void { $this->dispatcher = $this->createMock(IEventDispatcher::class); $this->jobList = $this->createMock(IJobList::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->l10n = $this->createMock(IL10N::class); $this->service = new RoomService( $this->manager, $this->participantService, @@ -71,6 +74,7 @@ public function setUp(): void { $this->dispatcher, $this->jobList, $this->logger, + $this->l10n, ); } @@ -332,6 +336,7 @@ public function testVerifyPassword(): void { $dispatcher, $this->jobList, $this->logger, + $this->l10n, ); $room = new Room(