-
Notifications
You must be signed in to change notification settings - Fork 332
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
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?
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.
2f0956d
to
8e1932d
Compare
Thanks for the review! Revision pushed. |
lib/widgets/action_sheet.dart
Outdated
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.
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 one cluster of comments below.
lib/widgets/all_channels.dart
Outdated
final channel = this.channel; | ||
final Subscription? subscription = channel is Subscription ? channel : null; | ||
final hasContentAccess = store.selfHasContentAccess(channel); | ||
final hasContentAccess = store.selfHasContentAccess(channel, atDate: 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.
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.
Hmm — there probably won't ever be any of these, though. So it's unfortunate to be making this API more awkward for the code across the app to use, when the extra complication only gets used (a) in a fallback for old servers (b) and only after a hypothetical future change we don't expect to happen.
lib/model/realm.dart
Outdated
case SystemGroupName.fullMembers: | ||
if (!_selfUserRole.isAtLeast(UserRole.member)) return false; | ||
if (_selfUserRole == UserRole.member) { | ||
return selfHasPassedWaitingPeriod(byDate: atDate); | ||
} | ||
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.
How about we instead just make this unimplemented, like .streamCreatorOrNobody
above?
The difference being that in this case, we don't expect to ever add code that triggers this case. (I think we may have never had a permission that defaulted to "full members". If we ever add one in the future, we'd probably make the permission configurable and group-based from the beginning, so there would still never be a fallback to such a default for it.)
/// callers must also check that the user's role is at least [UserRole.member]. | ||
// This used to be a method on [UserStore], but we needed it for permissions | ||
// here in [RealmStore]. | ||
bool selfHasPassedWaitingPeriod({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.
I think the refactor to move this here is still fine, though — logically its purpose is about permissions (that's the only reason to care about having passed the waiting period), so it makes sense for it to live where the permission checks are.
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.
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.
8e1932d
to
3730c90
Compare
Thanks for the review! Revision pushed. |
Thanks! Looks good; merging. |
Fixes #1850.
Also does the refactor Greg suggested here: #1842 (comment)