-
Notifications
You must be signed in to change notification settings - Fork 331
initial_snapshot: Centralize group-permission defaults; fix admin permission for legacy servers #1853
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
base: main
Are you sure you want to change the base?
initial_snapshot: Centralize group-permission defaults; fix admin permission for legacy servers #1853
Conversation
b6fa709
to
2f0956d
Compare
lib/api/model/initial_snapshot.dart
Outdated
/// A known special string | ||
/// that [PermissionSettingsItem.defaultGroupName] might be. | ||
/// | ||
/// See server implementation, e.g. | ||
/// `can_administer_channel_group` in zerver/models/streams.py. | ||
@JsonEnum(valueField: 'apiValue', alwaysCreate: true) | ||
enum PseudoSystemGroupName implements SettableAsDefaultGroupName { | ||
streamCreatorOrNobody(apiValue: 'stream_creator_or_nobody'), |
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.
On CZO I proposed documenting this: #api design > stream_creator_or_nobody @ 💬
From that discussion, it looks like it might also get renamed.
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.
Cool. Let's include a link to that thread in a comment, too.
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! LGTM, moving over to Greg's review.
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 building this! Comments below.
lib/api/model/initial_snapshot.dart
Outdated
enum SystemGroupName implements SettableAsDefaultGroupName { | ||
everyoneOnInternet(apiValue: 'role:internet'), | ||
everyone(apiValue: 'role:everyone'), |
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 feels like it's not specifically about the initial snapshot — it'll eventually be relevant to other parts of the API too. It could go in model.dart
… but perhaps better:
Let's have a prep commit move SupportedPermissionSettings and PermissionSettingsItem to their own file, say permission.dart
. Then these new types can go there too.
lib/api/model/initial_snapshot.dart
Outdated
sealed class SettableAsDefaultGroupName { | ||
String toJson(); | ||
} | ||
|
||
class DefaultGroupNameConverter extends JsonConverter<SettableAsDefaultGroupName, String> { | ||
const DefaultGroupNameConverter(); | ||
|
||
@override | ||
SettableAsDefaultGroupName fromJson(String json) { |
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 converter be merged into the class? Seems like any time we'd want this class, we'd want this same conversion, right?
(The fromJson
method then becomes a factory constructor on that class. See other non-generated fromJson
implementations, like on GroupSettingValue.)
lib/api/model/initial_snapshot.dart
Outdated
/// A value of [PermissionSettingsItem.defaultGroupName]. | ||
/// | ||
/// Can be any of these: | ||
/// - a known system group [SystemGroupName] | ||
/// - a known special string [PseudoSystemGroupName] | ||
/// - an unknown system group or special string [DefaultGroupNameUnknown] | ||
sealed class SettableAsDefaultGroupName { |
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 there some information that the "settable as" part of the name is meant to convey here? How does the expressed meaning differ from if we called it DefaultGroupName
?
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 nope 🙂 I think I'd had the idea that it should be considered an interface rather than a base class, but I don't really remember why, and I don't see anything wrong with DefaultGroupName
when I look at it again :)
lib/api/model/initial_snapshot.dart
Outdated
/// A known special string | ||
/// that [PermissionSettingsItem.defaultGroupName] might be. | ||
/// | ||
/// See server implementation, e.g. | ||
/// `can_administer_channel_group` in zerver/models/streams.py. | ||
@JsonEnum(valueField: 'apiValue', alwaysCreate: true) | ||
enum PseudoSystemGroupName implements SettableAsDefaultGroupName { | ||
streamCreatorOrNobody(apiValue: 'stream_creator_or_nobody'), |
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.
Cool. Let's include a link to that thread in a comment, too.
switch (config.defaultGroupName) { | ||
case DefaultGroupNameUnknown(): | ||
// When we know about a permission, we should also know about the group | ||
// we've said is the default value for it. | ||
assert(false); | ||
return true; | ||
case PseudoSystemGroupName.streamCreatorOrNobody: | ||
// TODO implement | ||
return true; | ||
case SystemGroupName.everyoneOnInternet: |
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: let's move all this logic to its own method, even if it's a private method located in this same place.
That way selfHasPermissionForGroupSetting
gets to stay focused on the main logic it's about, rather than these details which are for a fallback case. As a bonus, this case can go into an early return, so that the method ends with the call to selfInGroupSetting
which is the thrust of the method's main narrative.
case PseudoSystemGroupName.streamCreatorOrNobody: | ||
// TODO implement | ||
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.
Hmm — this is a concerning-looking type of TODO comment, because it feels like it's not something we'd necessarily see when the time comes that we need to carry it out.
Is there an issue for a feature where we'd expect to start hitting this case?
Oh also: these defaults come from our code and not the server, so it would take a change to our code for us to start hitting this. Let's add an assert — that should catch any such change while in dev, to remind us this isn't yet implemented.
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.
Good points, yep—
Is there an issue for a feature where we'd expect to start hitting this case?
lib/model/realm.dart
Outdated
case SystemGroupName.fullMembers: | ||
// TODO check waiting period (nontrivial to get access to that logic | ||
// here in RealmStore; it's on UserStore, which depends on RealmStore) | ||
return _selfUserRole.isAtLeast(UserRole.member); |
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.
Hmm. That's the hasPassedWaitingPeriod
method, right?
We can just move that to the RealmStore mixin instead of UserStore — it's not using anything from the user store itself, just a User
object. Can include a comment justifying that placement by saying it's needed for permissions.
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.
Sure, I can do that. RealmStore
doesn't have access to the self-user User
object, and it would feel odd to ask selfHasPermissionForGroupSetting
's callers to provide it. But the passed-waiting-period method just uses .dateJoined
from the user, and we can store that in RealmStoreImpl
as ._selfUserDateJoined
, the way we already have ._selfUserRole
there, if that makes sense.
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.
Hmm true. But yeah, that solution works. The implementation will then live on RealmStoreImpl rather than on the mixin, which is fine.
case SystemGroupName.everyoneOnInternet: | ||
case SystemGroupName.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.
Do we have an issue for spectator view? If not, probably good to file one for tracking. (Can be terse and dashed off — it'll be MXB.) Then the everyoneOnInternet
line here can get a TODO line pointing to that… or perhaps better, have such a comment at the definition of that enum value, saying to audit all its references.
case SystemGroupName.everyone: | ||
test('everyone', () { | ||
prepare(); | ||
doCheck(GroupSettingType.realm, 'can_access_all_users_group', 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: strengthen the test
case SystemGroupName.everyone: | |
test('everyone', () { | |
prepare(); | |
doCheck(GroupSettingType.realm, 'can_access_all_users_group', true); | |
case SystemGroupName.everyone: | |
test('everyone', () { | |
prepare(selfUserRole: UserRole.guest); | |
doCheck(GroupSettingType.realm, 'can_access_all_users_group', true); |
and similarly .owner
for the .nobody
case at the end
And we'll use the new selfUserRole param for testing zulip#1850, coming up.
The "role:nobody" group is the default value for both of these group settings (see defaultGroupName in upcoming commits), so it's helpful to be explicit here.
This doesn't change our parsing of server data because we don't currently look at server_supported_permission_settings in the initial snapshot, as noted in the added TODO. It just adds data to the fixture, modeled on current server code.
This will support full-member permissions checks in the logic we're about to add, for defaults when a server doesn't know about a given permission. There aren't currently any defaults with the "role:fullmembers" value, so this won't control any behavior now. But some might be added in the future, and we might as well include it for completeness.
Now these are explicit and it's clear where they come from. Thanks Greg for this suggestion: zulip#1842 (comment) The exception is the pre-291 fallback, which doesn't fit into a static fixture because it depends on the realm setting realmDeleteOwnMessagePolicy.
2f0956d
to
8e1932d
Compare
Thanks for the review! Revision pushed. |
required int channelId, | ||
bool showTopicListButton = true, | ||
}) { | ||
final now = DateTime.now(); |
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 think I should change this to ZulipBinding.instance.utcNow()
; will do that in my next revision.
Fixes #1850.
Also does the refactor Greg suggested here: #1842 (comment)