Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(conversations): add option to force passwords #13767

Merged
merged 1 commit into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions docs/conversation.md
miaulalala marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,15 @@
| `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:
+ `200 OK` When the "one to one" conversation already exists
+ `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

Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,6 @@ Legend:
| `conversations_files` | string<br>`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<br>`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<br>`1` or `0` | `0` | No | 🖌️ | Whether the Matterbridge integration is enabled and can be configured |
| `force_passwords` | string<br>`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<br>`1` or `0` | `0` | No | | Additionally enable the lobby for inactive rooms so they can only be read by moderators. |
4 changes: 3 additions & 1 deletion lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class Capabilities implements IPublicCapability {
'talk-polls-drafts',
'download-call-participants',
'email-csv-import',
'conversation-creation-password',
];

public const CONDITIONAL_FEATURES = [
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
44 changes: 34 additions & 10 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Http::STATUS_OK|Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @param string $password The room password (only available with `conversation-creation-password` capability)
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string, message?: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, 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);
Expand All @@ -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);
Expand Down Expand Up @@ -645,10 +654,10 @@ protected function createCircleRoom(string $targetCircleId): DataResponse {
}

/**
* @return DataResponse<Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @return DataResponse<Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error?: string, message?: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, 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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -1420,16 +1431,29 @@ public function removeAttendeeFromRoom(int $attendeeId): DataResponse {
/**
* Allowed guests to join conversation
*
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'}, array{}>
* Required capability: `conversation-creation-password` for `string $password` parameter
*
* @param string $password New password (only available with `conversation-creation-password` capability)
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'|'password', message?: null|string}, array{}>
*
* 200: Allowed guests successfully
* 400: Allowing guests is not possible
*/
#[NoAdminRequired]
#[RequireLoggedInModeratorParticipant]
public function makePublic(): DataResponse {
public function makePublic(string $password = ''): DataResponse {
miaulalala marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Expand Down
19 changes: 18 additions & 1 deletion lib/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
miaulalala marked this conversation as resolved.
Show resolved Hide resolved
$token = $this->getNewToken();

$insert = $this->db->getQueryBuilder();
Expand All @@ -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),
]
);

Expand All @@ -1135,6 +1137,7 @@ public function createRoom(int $type, string $name = '', string $objectType = ''
'token' => $token,
'object_type' => $objectType,
'object_id' => $objectId,
'password' => $password
miaulalala marked this conversation as resolved.
Show resolved Hide resolved
]);

$event = new RoomCreatedEvent($room);
Expand Down Expand Up @@ -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();
}
}
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@
* },
* conversations: array{
* can-create: bool,
* force-passwords: bool,
* },
* federation: array{
* enabled: bool,
Expand Down
103 changes: 95 additions & 8 deletions lib/Service/RoomService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -80,6 +81,7 @@ public function __construct(
protected IEventDispatcher $dispatcher,
protected IJobList $jobList,
protected LoggerInterface $logger,
protected IL10N $l10n,
) {
}

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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, [[
Expand All @@ -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 {
Expand Down Expand Up @@ -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'));
}
miaulalala marked this conversation as resolved.
Show resolved Hide resolved

$event = new ValidatePasswordPolicyEvent($password);
try {
$this->dispatcher->dispatchTyped($event);
} catch (HintException $e) {
throw new PasswordException(PasswordException::REASON_VALUE, $e->getHint());
}
miaulalala marked this conversation as resolved.
Show resolved Hide resolved

$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);
miaulalala marked this conversation as resolved.
Show resolved Hide resolved

$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);
miaulalala marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @param Room $room
* @param int $newState Currently it is only allowed to change between
Expand Down Expand Up @@ -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);
}
}

}
Loading
Loading