From cf54d5d60a38f445706c55e9ff1d0585f9e51bab Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Wed, 13 Nov 2024 16:26:12 +0100 Subject: [PATCH] # This is a combination of 9 commits. # This is the 1st commit message: feat: add option to force passwords in public conversations Signed-off-by: Anna Larch # The commit message #2 will be skipped: # fixup! feat: add option to force passwords in public conversations # The commit message #3 will be skipped: # fixup! feat: add option to force passwords in public conversations # The commit message #4 will be skipped: # fixup! feat: add option to force passwords in public conversations # The commit message #5 will be skipped: # fixup! fixup! feat: add option to force passwords in public conversations # The commit message #6 will be skipped: # fixup! fixup! feat: add option to force passwords in public conversations # The commit message #7 will be skipped: # fixup! feat: add option to force passwords in public conversations # The commit message #8 will be skipped: # fixup! feat: add option to force passwords in public conversations # The commit message #9 will be skipped: # fixup! feat: add option to force passwords in public conversations --- docs/capabilities.md | 4 + docs/conversation.md | 3 +- docs/settings.md | 1 + lib/Capabilities.php | 4 +- lib/Config.php | 4 + lib/Controller/RoomController.php | 45 ++++++-- 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, 333 insertions(+), 41 deletions(-) diff --git a/docs/capabilities.md b/docs/capabilities.md index 5ee8adda207c..9d4b20b8aa7e 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 13fe92972b08..53ccef43f388 100644 --- a/docs/conversation.md +++ b/docs/conversation.md @@ -115,7 +115,6 @@ ## Creating a new conversation *Note:* Creating a conversation as a child breakout room, will automatically set the lobby when breakout rooms are not started and will always overwrite the room type with the parent room type. Also, moderators of the parent conversation will be automatically added as moderators. - * Method: `POST` * Endpoint: `/room` * Data: @@ -128,6 +127,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 +135,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 diff --git a/docs/settings.md b/docs/settings.md index b828fe2cf333..e95738b25a31 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 91280aeb16c6..79bed2d1a484 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 ccf88382bc29..491d898def1e 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 1d3ca673b1e1..03ed7ae1fe32 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', 'hint' => $e->getHint()], Http::STATUS_BAD_REQUEST); } catch (\InvalidArgumentException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } @@ -1420,21 +1431,35 @@ 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', 'hint' => $this->l10n->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', 'hint' => $e->getHint()], Http::STATUS_BAD_REQUEST); } catch (TypeException $e) { return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST); } return new DataResponse(); + } /** diff --git a/lib/Manager.php b/lib/Manager.php index 28e311a68876..a4e87a89efb2 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 f2854d513620..dfb77517a239 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 1ccd4f06a04e..567aa82d3ee4 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 fb1cb56c78b5..2fa3374a6856 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 1dc3ce44918d..25359a023df5 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 745cf8193dc3..9b2c9fc6b64b 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 cc7882888c6f..dbd4f998202b 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 6c3840f55f01..0749a6e6f111 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 72ddd1a5249c..fefaa268686d 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 4509f6afe862..52cfd6e8f46b 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" + }, + "hint": { + "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" ] + }, + "hint": { + "type": "string", + "nullable": true } } } diff --git a/openapi.json b/openapi.json index efbc617aa59c..4aaf74005fe0 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" + }, + "hint": { + "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" ] + }, + "hint": { + "type": "string", + "nullable": true } } } diff --git a/src/__mocks__/capabilities.ts b/src/__mocks__/capabilities.ts index b6b4558d0c77..4ca38bc35b19 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 23546e2a8239..ca9292a0318d 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 f4f9c3bfff01..df314564d049 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 13f736e8d08a..4ec2f7721e1c 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 6401e017d1b1..6afa31e75bdd 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 cc58aa728c03..9be4e8d8a3fb 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 f2a820dc9978..27053a8ec6d2 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 c32f80af0a84..6b8b1568fa1e 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; + hint?: 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"; + hint?: string | null; }; }; }; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 5b8d795075cb..8141f56f8ea5 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; + hint?: 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"; + hint?: string | null; }; }; }; diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index d853bf0e5262..2669e433e881 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 8696ea30ff30..a24e01bbbd94 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(