-
Notifications
You must be signed in to change notification settings - Fork 374
Group-based permissions #1822
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
Group-based permissions #1822
Conversation
| // TODO(perf), TODO(#814): memoize which groups the self-user is in, | ||
| // to save doing this depth-first search on each permission check | ||
| return group.members.contains(selfUserId) | ||
| || group.directSubgroupIds.any(_selfInGroup); |
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 have a draft branch for this optimization. But it's a bit tricky to get right, so I've separated it out in order to let this PR focus on all the other aspects of the semantics of these permissions.
chrisbobbe
left a 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.
Thanks, this will be great to have! Comments below.
| void handleRealmUserUpdateEvent(RealmUserUpdateEvent event) { | ||
| // TODO(#1687): test handling user deactivation | ||
| for (final group in _groups.values) { | ||
| group.members.remove(event.userId); | ||
| } | ||
| } |
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 looks like it'll touch the groups on any user update, not just deactivation: so when a user changes their name, avatar, etc. 🙂
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.
Oof indeed, good catch, thanks.
lib/api/model/model.dart
Outdated
| return switch (json) { | ||
| int() => GroupSettingValueNamed.fromJson(json), | ||
| Map<String, dynamic>() => GroupSettingValueNameless.fromJson(json), | ||
| _ => throw TypeError(), |
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.
throw FormatException(), maybe? This would be the only result of a git grep 'throw TypeError'
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
| return group.members.contains(selfUserId) | ||
| || group.directSubgroupIds.any(_selfInGroup); |
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 I hope a group is never a subgroup of itself :)
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.
Indeed, that's an invariant of the system. The subgroup relation is acyclic: no group is a subgroup of itself, nor of any of its transitive subgroups.
I'm not finding that documented, though — looked here:
https://zulip.com/api/group-setting-values
https://zulip.com/api/update-user-group-subgroups
— so it should probably get written down more explicitly in the API docs.
lib/api/model/initial_snapshot.dart
Outdated
| @JsonKey(readValue: _readUsersIsActiveFallbackTrue) | ||
| final List<User> crossRealmBots; | ||
|
|
||
| final SupportedPermissionSettings serverSupportedPermissionSettings; |
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.
The API doc says
New in Zulip 8.0 (feature level 221)
so we should make this optional.
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.
Oops, good catch, thanks.
lib/api/model/model.dart
Outdated
| GroupSettingValue canAddSubscribersGroup; | ||
| GroupSettingValue canSubscribeGroup; |
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.
These are documented as new in Zulip 10.0, so we should make them optional.
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.
Also we should add these fields to ChannelUpdateEvent if they can change with that event, or leave a comment there if they can'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.
Indeed, thanks for those catches.
| return _selfHasContentAccessViaGroupPermissions(channel); | ||
| } | ||
|
|
||
| bool _selfHasContentAccessViaGroupPermissions(ZulipStream channel) { |
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.
channel: Add selfHasContentAccess, using canSubscribeGroup et al
[…]
This particular permission is one we don't yet have any logic that
should consume […]
I think it does actually :) see
which has corresponding TODO(#1786)s in the code.
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 indeed, thanks for the tip.
One of those two is directly covered by the permission logic here, so I revised this commit to take care of that. The other requires a bit of a variation, so I think I'll leave #1786 open for handling that one.
898844d to
d0b8bf5
Compare
gnprice
left a 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.
Thanks for the review! Seems I was a bit hasty in sending this yesterday; thanks for those catches. Revision pushed — PTAL.
lib/api/model/model.dart
Outdated
| return switch (json) { | ||
| int() => GroupSettingValueNamed.fromJson(json), | ||
| Map<String, dynamic>() => GroupSettingValueNameless.fromJson(json), | ||
| _ => throw TypeError(), |
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
lib/api/model/initial_snapshot.dart
Outdated
| @JsonKey(readValue: _readUsersIsActiveFallbackTrue) | ||
| final List<User> crossRealmBots; | ||
|
|
||
| final SupportedPermissionSettings serverSupportedPermissionSettings; |
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.
Oops, good catch, thanks.
lib/api/model/model.dart
Outdated
| GroupSettingValue canAddSubscribersGroup; | ||
| GroupSettingValue canSubscribeGroup; |
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.
Indeed, thanks for those catches.
| return _selfHasContentAccessViaGroupPermissions(channel); | ||
| } | ||
|
|
||
| bool _selfHasContentAccessViaGroupPermissions(ZulipStream channel) { |
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 indeed, thanks for the tip.
One of those two is directly covered by the permission logic here, so I revised this commit to take care of that. The other requires a bit of a variation, so I think I'll leave #1786 open for handling that one.
|
I haven't made any revision yet for the point you raised at #api design > server_supported_permission_settings @ 💬: this |
|
(I plan to leave this PR out of today's release, pending that API question. I hope to merge a version tomorrow before I go on vacation, though.) |
|
Thanks, this looks great! So I guess hopefully that API design thread resolves soon: #api design > server_supported_permission_settings @ 💬 but as you mentioned there
Otherwise LGTM. |
|
OK, revised with that change! PTAL. |
|
For the record, if we were to decide the existing API is stable and so just start using that, the change to make would be the diff below, plus a couple more items:
That diff's diffstat: and details: Diffdiff --git lib/api/model/initial_snapshot.dart lib/api/model/initial_snapshot.dart
index 5edc04afd..3e019cb9a 100644
--- lib/api/model/initial_snapshot.dart
+++ lib/api/model/initial_snapshot.dart
@@ -110,6 +110,5 @@ class InitialSnapshot {
final List<User> crossRealmBots;
- // TODO(server): Get this API stabilized, to replace [SupportedPermissionSettings.fixture].
- // final SupportedPermissionSettings? serverSupportedPermissionSettings;
+ final SupportedPermissionSettings? serverSupportedPermissionSettings; // TODO(server-8)
// TODO etc., etc.
@@ -173,4 +172,5 @@ class InitialSnapshot {
required this.realmNonActiveUsers,
required this.crossRealmBots,
+ required this.serverSupportedPermissionSettings,
});
diff --git lib/api/model/initial_snapshot.g.dart lib/api/model/initial_snapshot.g.dart
index 481b3184b..05e270a85 100644
Binary files lib/api/model/initial_snapshot.g.dart and lib/api/model/initial_snapshot.g.dart differ
diff --git lib/model/realm.dart lib/model/realm.dart
index 02db21c56..b62973e93 100644
--- lib/model/realm.dart
+++ lib/model/realm.dart
@@ -124,4 +124,6 @@ mixin RealmStore on PerAccountStoreBase, UserGroupStore {
/// Whether the self-user has the given (group-based) permission.
+ ///
+ /// Throws if the server does not have the concept of the given permission.
bool selfHasPermissionForGroupSetting(GroupSettingValue value,
GroupSettingType type, String name);
@@ -190,4 +192,5 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
}) :
_selfUserRole = selfUser.role,
+ _serverSupportedPermissionSettings = initialSnapshot.serverSupportedPermissionSettings,
serverPresencePingIntervalSeconds = initialSnapshot.serverPresencePingIntervalSeconds,
serverPresenceOfflineThresholdSeconds = initialSnapshot.serverPresenceOfflineThresholdSeconds,
@@ -226,7 +229,14 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
}
- /// The metadata for how to interpret the given group-based permission setting.
+ /// The config the server supplied for how to interpret the given
+ /// group-based permission setting.
+ ///
+ /// Throws if there is no such config from the server.
+ /// This method should therefore only be called if the server is one that
+ /// actually has the concept of the given permission.
PermissionSettingsItem _groupSettingConfig(GroupSettingType type, String name) {
- final supportedSettings = SupportedPermissionSettings.fixture;
+ // If the server has the particular permission being asked about, then it
+ // must have this bit of general infrastructure for group-based permissions.
+ final supportedSettings = _serverSupportedPermissionSettings!;
// Compare web's group_permission_settings.get_group_permission_setting_config.
@@ -247,4 +257,6 @@ class RealmStoreImpl extends HasUserGroupStore with RealmStore {
UserRole _selfUserRole;
+ final SupportedPermissionSettings? _serverSupportedPermissionSettings; // TODO(server-8)
+
@override
final int serverPresencePingIntervalSeconds;
diff --git test/example_data.dart test/example_data.dart
index 84d1abf6a..d3518ebbb 100644
--- test/example_data.dart
+++ test/example_data.dart
@@ -1251,4 +1251,5 @@ InitialSnapshot initialSnapshot({
List<User>? realmNonActiveUsers,
List<User>? crossRealmBots,
+ SupportedPermissionSettings? serverSupportedPermissionSettings,
}) {
return InitialSnapshot(
@@ -1300,4 +1301,6 @@ InitialSnapshot initialSnapshot({
realmNonActiveUsers: realmNonActiveUsers ?? [],
crossRealmBots: crossRealmBots ?? [],
+ serverSupportedPermissionSettings: serverSupportedPermissionSettings
+ ?? SupportedPermissionSettings.fixture,
);
} |
chrisbobbe
left a 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.
Thanks, LGTM! One nit below, then please merge at will.
lib/api/model/initial_snapshot.dart
Outdated
| /// This is the type that [InitialSnapshot.serverSupportedPermissionSettings], | ||
| /// would have, according to the API as it exists as of 2025-08; | ||
| /// but that API is documented as unstable and subject to change. |
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:
| /// This is the type that [InitialSnapshot.serverSupportedPermissionSettings], | |
| /// would have, according to the API as it exists as of 2025-08; | |
| /// but that API is documented as unstable and subject to change. | |
| /// This is the type that [InitialSnapshot.serverSupportedPermissionSettings] | |
| /// would have, according to the API as it exists as of 2025-08; | |
| /// but that API is documented as unstable and subject to change. |
Fixes zulip#1687. We'll add tests for this after an upcoming commit provides a way of accessing group membership that's more relevant for the app's needs. That way we can write the tests to that interface.
This is the way we'll be inspecting group membership for checking permissions (zulip#814), which is the main way we'll be using group membership in general. Because this method is how most of the app will consume group membership, adding this method also lets us write natural tests for the logic that maintains group membership on events. At this stage we stick with a very straightforward implementation. We might optimize it later with more data structures.
Fixes zulip#814. Fixes part of zulip#1786. The general implementation of group-based permissions (zulip#814) was added in the commits leading up to this one. This commit demonstrates implementing a given such permission. Then we also use the new permission in one place where we already want to use it, toward zulip#1786. (There's another part of zulip#1786 which calls for a variation: determining whether the user would be able to re-subscribe to a channel if they unsubscribe. We'll leave that for a follow-up.) We'll also be adding a more prominent bit of UI that wants this permission soon: the list of channels, zulip#188. To exercise this method on real data, I added some debugging prints that printed a list of the channels where this returned true, and a list of the channels where it returned false. I also manually tested the change for zulip#1786, by visiting an unsubscribed private channel (via the link in a #-mention), long-pressing the app bar to get the channel action sheet, and then (as another user, on web) adding and removing permission for the test user to subscribe themself.
|
Thanks! Done. |
…cess This tests the change in cf83c9e / zulip#1822.
…cess This tests the change in cf83c9e / zulip#1822.
Fixes #1687.
Fixes #814.
Stacked atop #1814.
This adds all the generic infrastructure needed for implementing any given group-based permission. The last commit (or pair of commits) then adds the permission "has content access (to a channel)" (which we'll want for #188), to demonstrate end-to-end how to add individual permissions using the new infrastructure.