-
Notifications
You must be signed in to change notification settings - Fork 329
Implement permissions check for can-delete-message #1842
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
Conversation
Neat! I think the right home for this logic is on MessageStore. That class will just need to gain a reference to ChannelStore, in the same way the latter already has UserStore; see Git history for how that was added. |
lib/model/store.dart
Outdated
if (realmCanDeleteOwnMessageGroup != null) { | ||
// realmCanDeleteOwnMessageGroup always present as of FL 291. | ||
// TODO(server-10) make required, simplify, delete this FL-291 comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already covered by the TODO(server-10)
on the realmCanDeleteOwnMessageGroup field itself, right? Once that's made non-nullable, the analyzer will naturally point out that this condition can be simplified.
So I think this can just apply the condition without comment.
(Similarly for the other TODO-server comments in this method.)
7aaff57
to
41ba775
Compare
Thanks! Revision pushed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! One more comment on the substore code organization — otherwise I'll leave this for @rajveermalviya to do the initial round of review 🙂
lib/model/message.dart
Outdated
@@ -375,6 +385,113 @@ class MessageStoreImpl extends HasRealmStore with MessageStore, _OutboxMessageSt | |||
); | |||
} | |||
|
|||
@override | |||
bool selfCanDeleteMessage(int messageId, {required DateTime byDate}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this (and its private helper) move onto the MessageStore mixin? I think it can — it's just using the interface provided by MessageStore itself (and its ancestors), not any of the details of the data structures internal to MessageStoreImpl.
Compare the selfHasContentAccess and hasPostingPermission methods which live on the ChannelStore mixin.
41ba775
to
4e8b5c6
Compare
Thanks, done! Revision pushed. |
lib/model/message.dart
Outdated
@override | ||
bool selfCanDeleteMessage(int messageId, {required DateTime byDate}) => | ||
messageStore.selfCanDeleteMessage(messageId, byDate: byDate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: now redundant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this @chrisbobbe! One comment otherwise LGTM.
/// on supported servers below version 10. | ||
/// | ||
/// Removed in FL 291. | ||
final RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy; // TODO(server-10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be delete_own_message_policy
and not realm_delete_own_message_policy
?
(Docs only has mention of the former.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I should add a note that this relies on information that isn't in the current doc; I had to look at zulip/zulip@0cd51f2fe, which removed the entry for realm_delete_own_message_policy
in the register-response doc.
In general there's a pattern for realm attributes where the field in the register response is prefixed with realm_
but the field in the realm/update_dict
event isn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the added note (with the reference to that zulip/zulip commit) appears in a later commit
4e8b5c6
to
263e304
Compare
Thanks both 🙂, made those tweaks and pushed a revision. I'll mark this for Greg's review since Rajesh's comment was small and resolved simply. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @chrisbobbe, and thanks @rajveermalviya for the previous review!
Generally this all looks great. Comments below.
/// on supported servers below version 10. | ||
/// | ||
/// Removed in FL 291. | ||
final RealmDeleteOwnMessagePolicy? realmDeleteOwnMessagePolicy; // TODO(server-10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the added note (with the reference to that zulip/zulip commit) appears in a later commit
lib/api/model/initial_snapshot.dart
Outdated
|
||
const RealmDeleteOwnMessagePolicy({required this.apiValue}); | ||
|
||
final int? apiValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or just int
?
test/example_data.dart
Outdated
// no default; allow `null` to simulate servers without this | ||
realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// no default; allow `null` to simulate servers without this | |
realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy, | |
realmDeleteOwnMessagePolicy: realmDeleteOwnMessagePolicy, |
For this one, null is the natural default (since it's what modern servers always have), so no need to further justify it.
lib/api/model/model.dart
Outdated
@@ -627,6 +627,7 @@ class ZulipStream { | |||
|
|||
final int streamId; | |||
String name; | |||
bool? isArchived; // TODO(server-10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For servers that don't send this property, I believe they'll only send us non-archived channels. So we can make this non-nullable, and default to false. (And then a TODO-server comment just for removing the default.)
lib/model/message.dart
Outdated
/// Whether the user has permission to delete a message, as of [byDate]. | ||
/// | ||
/// For a value of [byDate], use [ZulipBinding.instance.utcNow]. | ||
bool selfCanDeleteMessage(int messageId, {required DateTime byDate}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: "by date" doesn't seem like the right description for this parameter — how about atDate
?
lib/model/message.dart
Outdated
if ( | ||
realmDeleteOwnMessagePolicy != null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It ought to be true that either realmCanDeleteOwnMessageGroup or realmDeleteOwnMessagePolicy is non-null, right?
Probably helpful to make that explicit: have an if/else-if chain for those, and then in the else
where both were null, have a TODO(log) and thereby clarify that that case isn't expected to happen.
lib/model/message.dart
Outdated
if (realmMessageContentDeleteLimitSeconds == null) { | ||
// i.e., no limit | ||
return true; | ||
} | ||
|
||
return byDate.millisecondsSinceEpoch ~/ 1000 - message.timestamp | ||
<= realmMessageContentDeleteLimitSeconds!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: join as one stanza, because closely related (interpreting the same one setting)
lib/model/message.dart
Outdated
switch (realmDeleteOwnMessagePolicy!) { | ||
case RealmDeleteOwnMessagePolicy.members: | ||
return true; | ||
case RealmDeleteOwnMessagePolicy.admins: | ||
return role.isAtLeast(UserRole.administrator); | ||
case RealmDeleteOwnMessagePolicy.fullMembers: { | ||
if (!role.isAtLeast(UserRole.member)) return false; | ||
if (role == UserRole.member) { | ||
return hasPassedWaitingPeriod(selfUser, byDate: byDate); | ||
} | ||
return true; | ||
} | ||
case RealmDeleteOwnMessagePolicy.moderators: | ||
return role.isAtLeast(UserRole.moderator); | ||
case RealmDeleteOwnMessagePolicy.everyone: | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: a bit clearer to understand and verify if the cases written in logical order (broad to narrow, or vice versa) rather than API-value order
The analyzer will check we've covered all the cases, so there's no worry about missing one by making it harder to compare with the enum's definition.
} | ||
|
||
void doTest(bool expected, CanDeleteMessageParams params) { | ||
test('params: ${params.describe()}', () async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's avoid newlines in the test names. It's unconventional for a name; and when I run flutter test
on this file, it seems to defeat the normal way that the output stays compact — there ends up being a separate line for each of these test cases, like this:
$ flutter test test/model/message_test.dart
…
51 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.
00:02 +39: selfCanDeleteMessage ... truedeleted while request in progress but we get success response
00:02 +40: selfCanDeleteMessage ... true
00:02 +40: selfCanDeleteMessage ... false
00:02 +41: selfCanDeleteMessage ... false
00:02 +41: selfCanDeleteMessage ... true
00:02 +42: selfCanDeleteMessage ... true
00:02 +42: selfCanDeleteMessage ... true
00:02 +43: selfCanDeleteMessage ... true
00:02 +43: selfCanDeleteMessage ... true
00:02 +44: selfCanDeleteMessage ... true
00:02 +44: selfCanDeleteMessage ... true
00:02 +45: selfCanDeleteMessage ... true
00:02 +45: selfCanDeleteMessage ... false
00:02 +46: selfCanDeleteMessage ... false
00:02 +46: selfCanDeleteMessage ... true
00:02 +47: selfCanDeleteMessage ... true
00:02 +47: selfCanDeleteMessage ... true
00:02 +48: selfCanDeleteMessage ... true
00:02 +48: selfCanDeleteMessage ... false
00:02 +49: selfCanDeleteMessage ... false
00:02 +49: selfCanDeleteMessage ... false
00:02 +50: selfCanDeleteMessage ... false
00:02 +50: selfCanDeleteMessage ... false
00:02 +51: selfCanDeleteMessage ... false
00:02 +51: selfCanDeleteMessage ... false
00:02 +52: selfCanDeleteMessage ... false
00:02 +52: selfCanDeleteMessage ... true
00:02 +53: selfCanDeleteMessage ... true
00:02 +53: selfCanDeleteMessage ... true
00:02 +54: selfCanDeleteMessage ... true
00:02 +54: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +55: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +55: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +56: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +56: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +57: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +57: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +58: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +58: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +59: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +59: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +60: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +60: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +61: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +61: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +62: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +62: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +63: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +63: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +64: selfCanDeleteMessage ... channel.canDeleteOwnMessageGroup?: N/A
00:02 +117: All tests passed!
unknown, | ||
self, | ||
otherHuman, | ||
botOwnedBySelf, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This value doesn't appear to get exercised.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, we should exercise this.
When you're not in the "can delete any message" group, there are 8 distinct paths to approval:
- self or self-owned bot
- own-message permission by realm or by channel
- time not limited or inside time limit
I have a hunch that we don't want to exercise all of them 🙂—
final permissiveSenderConfigs = [
CanDeleteMessageSenderConfig.self,
CanDeleteMessageSenderConfig.botOwnedBySelf,
];
final ownMessageReasons = [
(realm: false, channel: true ),
(realm: true, channel: false),
];
final permissiveTimeLimitConfigs = [
CanDeleteMessageTimeLimitConfig.notLimited,
CanDeleteMessageTimeLimitConfig.insideLimit,
];
for (final senderConfig in permissiveSenderConfigs) {
for (final ownMessageReason in ownMessageReasons) {
for (final timeLimitConfig in permissiveTimeLimitConfigs) {
doTest(true, CanDeleteMessageParams.restrictiveForChannelMessageExcept(
isChannelArchived: false,
senderConfig: senderConfig,
inRealmCanDeleteOwnMessageGroup: ownMessageReason.realm,
inChannelCanDeleteOwnMessageGroup: ownMessageReason.channel,
timeLimitConfig: timeLimitConfig));
}
}
}
and if that's right, maybe I can do just these two?:
- self, realm, time-not-limited
- self-owned bot, channel, inside time limit
(or some other subset)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't want to exercise all of them
(because of the code-complexity and computation costs, and we'd be unlikely to catch more bugs by doing so)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree.
…t doc And leave a comment so we remember to keep this order when adding more fields.
Like HasUserStore for data about users (added in 623bcb4), and HasRealmStore for data about the realm, this will help make it convenient for other substores to refer to data about channels.
This way, this substore becomes a valid home for methods that need to refer to data about both messages and channels. See also fab85ca, where we provided UserStore to ChannelStore.
263e304
to
b25b8b1
Compare
Thanks for the review! Revision pushed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! Just a few comments below.
if (realmCanDeleteAnyMessageGroup != null | ||
&& selfHasPermissionForGroupSetting(realmCanDeleteAnyMessageGroup!, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's file an issue to track that follow-up. Then also this TODO comment can refer to the issue.
lib/model/message.dart
Outdated
case RealmDeleteOwnMessagePolicy.everyone: | ||
case RealmDeleteOwnMessagePolicy.members: | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these cases really have the same behavior? The self-user could be a guest, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, I think I was confusing guests with the public-access option (which mobile doesn't offer yet)
lib/model/message.dart
Outdated
final message = messages[messageId]; | ||
if (message == null) return false; // TODO(log) | ||
|
||
final ZulipStream? channel; | ||
if (message is StreamMessage) { | ||
channel = streams[message.streamId]; | ||
if (channel == null) { | ||
assert(false); // TODO(log) | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it intentional that the "unknown message" and "unknown channel" cases end up returning opposite fallback answers? Seems at first thought like they should turn out the same.
unknown, | ||
self, | ||
otherHuman, | ||
botOwnedBySelf, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree.
b25b8b1
to
536cbb7
Compare
Thanks! Revision pushed. |
Thanks! Looks good; merging. |
The permissions logic for whether you can delete a message is pretty complicated, it turns out. Here's a PR with an implementation and tests—which are pretty long, despite trying to simplify where possible. Suggestions would be much appreciated. :)
Oh—and see this comment in the implementation:
Very happy to move the code somewhere else, just wasn't sure where would be best :)
Related: #1548