diff --git a/internal/restrict/collection/user.go b/internal/restrict/collection/user.go index d45ce42f..803a0495 100644 --- a/internal/restrict/collection/user.go +++ b/internal/restrict/collection/user.go @@ -19,7 +19,6 @@ import ( // Y has the OML can_manage_users or higher. // There exists a committee where Y has the CML can_manage and X is in committee/user_ids. // X is in a group of a meeting where Y has user.can_see. -// There exists a meeting where Y has the CML can_manage for the meeting's committee X is in meeting/user_ids. // There is a related object: // There exists a motion which Y can see and X is a submitter/supporter. // There exists an option which Y can see and X is the linked content object. @@ -32,6 +31,13 @@ import ( // // Mode A: Y can see X. // +// Mode B: +// +// Y==X +// Y has the OML can_manage_users or higher. +// There exists a committee where Y has the CML can_manage and X is in committee/user_ids. +// X is in a group of a meeting where Y has user.can_see_sensitive_data. +// // Mode D: Y can see these fields if at least one condition is true: // // Y has the OML can_manage_users or higher. @@ -67,6 +73,8 @@ func (u User) Modes(mode string) FieldRestricter { switch mode { case "A": return u.see + case "B": + return u.modeB case "D": return u.modeD case "E": @@ -303,6 +311,74 @@ func (User) RequiredObjects(ctx context.Context, ds *dsfetch.Fetch) []UserRequir } } +func (u User) modeB(ctx context.Context, ds *dsfetch.Fetch, userIDs ...int) ([]int, error) { + requestUserID, err := perm.RequestUserFromContext(ctx) + if err != nil { + return nil, fmt.Errorf("getting request user: %w", err) + } + + isUserManager, err := perm.HasOrganizationManagementLevel(ctx, ds, requestUserID, perm.OMLCanManageUsers) + if err != nil { + return nil, fmt.Errorf("check organization management level: %w", err) + } + + if isUserManager { + return userIDs, nil + } + + // Precalculated list of userIDs, that the user can see. + allowedUserIDs := set.New[int]() + if requestUserID != 0 { + allowedUserIDs.Add(requestUserID) + + // Get all userIDs of committees, where the request user is manager. + commiteeIDs, err := perm.ManagementLevelCommittees(ctx, ds, requestUserID) + if err != nil { + return nil, fmt.Errorf("getting committee ids: %w", err) + } + + for _, committeeID := range commiteeIDs { + userIDs, err := ds.Committee_UserIDs(committeeID).Value(ctx) + if err != nil { + return nil, fmt.Errorf("fetching users from committee %d: %w", committeeID, err) + } + allowedUserIDs.Add(userIDs...) + } + + } + + return eachCondition(userIDs, func(otherUserID int) (bool, error) { + if allowedUserIDs.Has(otherUserID) { + return true, nil + } + + // Check if the user is in a meeting, where the request user can + // user.can_see_sensitive_data. + otherUserMeetingUserIDs, err := ds.User_MeetingUserIDs(otherUserID).Value(ctx) + if err != nil { + return false, fmt.Errorf("fetch meeting ids from requested user %d: %w", otherUserID, err) + } + + for _, meetingUserID := range otherUserMeetingUserIDs { + meetingID, err := ds.MeetingUser_MeetingID(meetingUserID).Value(ctx) + if err != nil { + return false, fmt.Errorf("getting meeting: %w", err) + } + + perms, err := perm.FromContext(ctx, meetingID) + if err != nil { + return false, fmt.Errorf("checking permissions of meeting %d: %w", meetingID, err) + } + + if perms.Has(perm.UserCanSeeSensitiveData) { + return true, nil + } + } + + return false, nil + }) +} + func (User) modeD(ctx context.Context, ds *dsfetch.Fetch, userIDs ...int) ([]int, error) { requestUser, err := perm.RequestUserFromContext(ctx) if err != nil { diff --git a/internal/restrict/collection/user_test.go b/internal/restrict/collection/user_test.go index 78af78b9..190b8fac 100644 --- a/internal/restrict/collection/user_test.go +++ b/internal/restrict/collection/user_test.go @@ -351,6 +351,364 @@ func TestUserModeA(t *testing.T) { ) } +func TestUserModeB(t *testing.T) { + f := collection.User{}.Modes("B") + + testCase( + "No perms", + t, + f, + false, + `user/2/id: 2`, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "With anonymous", + t, + f, + false, + `user/2/id: 2`, + withRequestUser(0), + withElementID(2), + ) + + testCase( + "Request user", + t, + f, + true, + `user/2/id: 2`, + withRequestUser(1), + withElementID(1), + ) + + testCase( + "Can manage users", + t, + f, + true, + `--- + user/2/id: 2 + user/1/organization_management_level: can_manage_users + `, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "Committee Manager", + t, + f, + true, + `--- + user/2/committee_ids: [5] + user/1: + committee_management_ids: [5] + committee/5/user_ids: [2] + `, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "Committee Manager user not in it", + t, + f, + false, + `--- + user/2/committee_ids: [5] + user/1: + committee_management_ids: [5] + committee/5/user_ids: [] + `, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "user.can_see in meeting", + t, + f, + false, + `--- + user/2/meeting_user_ids: [20] + meeting_user/20/meeting_id: 5 + `, + withRequestUser(1), + withElementID(2), + withPerms(5, perm.UserCanSee), + ) + + testCase( + "user.can_see_sensitive_data in meeting", + t, + f, + true, + `--- + user/2/meeting_user_ids: [20] + meeting_user/20/meeting_id: 5 + `, + withRequestUser(1), + withElementID(2), + withPerms(5, perm.UserCanSeeSensitiveData), + ) + + testCase( + "user.can_see not in meeting", + t, + f, + false, + `--- + user/2/meeting_user_ids: [] + `, + withRequestUser(1), + withElementID(2), + withPerms(5, perm.UserCanSee), + ) + + testCase( + "committee can manage", + t, + f, + true, + `--- + user/1: + committee_management_ids: [7] + committee/7/user_ids: [2] + + meeting/5/committee_id: 7 + `, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "committee can manage user not in meeting", + t, + f, + false, + `--- + user/2/meeting_user_ids: [] + meeting/5/committee_id: 7 + user/1: + committee_management_ids: [7] + committee/7/id: 7 + `, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "Vote delegated to", + t, + f, + false, + `--- + user/1/meeting_user_ids: [10] + user/2/id: 2 + + meeting_user: + 10: + vote_delegated_to_id: 20 + user_id: 1 + 20: + user_id: 2 + `, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "Vote delegated from", + t, + f, + false, + `--- + user/1/meeting_user_ids: [10] + user/2/id: 2 + + meeting_user: + 10: + vote_delegations_from_ids: [20] + user_id: 1 + 20: + user_id: 2 + `, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "motion submitter", + t, + f, + false, + `--- + user/1/meeting_user_ids: [10] + user/2/meeting_user_ids: [20] + + meeting_user/10: + meeting_id: 30 + meeting_user/20: + motion_submitter_ids: [4] + meeting_id: 30 + + motion_submitter/4: + motion_id: 7 + + motion/7: + meeting_id: 30 + state_id: 5 + + motion_state/5/id: 5 + `, + withRequestUser(1), + withElementID(2), + withPerms(30, perm.MotionCanSee), + ) + + testCase( + "motion supporter", + t, + f, + false, + `--- + user/1/meeting_user_ids: [10] + user/2/meeting_user_ids: [20] + + meeting_user/10: + meeting_id: 30 + meeting_user/20: + supported_motion_ids: [7] + meeting_id: 30 + + motion/7: + meeting_id: 30 + state_id: 5 + + motion_state/5/id: 5 + `, + withRequestUser(1), + withElementID(2), + withPerms(30, perm.MotionCanSee), + ) + + testCase( + "assignment candidate", + t, + f, + false, + `--- + user/1/meeting_user_ids: [10] + user/2/meeting_user_ids: [20] + + meeting_user/10: + meeting_id: 30 + meeting_user/20: + assignment_candidate_ids: [4] + meeting_id: 30 + + assignment_candidate/4/assignment_id: 5 + assignment/5/meeting_id: 30 + `, + withRequestUser(1), + withElementID(2), + withPerms(30, perm.AssignmentCanSee), + ) + + testCase( + "speaker", + t, + f, + false, + `--- + user/1/meeting_user_ids: [10] + user/2/meeting_user_ids: [20] + + meeting_user/10: + meeting_id: 30 + meeting_user/20: + speaker_ids: [4] + meeting_id: 30 + + speaker/4: + list_of_speakers_id: 5 + meeting_id: 30 + + list_of_speakers/5: + meeting_id: 30 + content_object_id: topic/10 + + topic/10/meeting_id: 30 + `, + withRequestUser(1), + withElementID(2), + withPerms(30, perm.ListOfSpeakersCanSee, perm.AgendaItemCanSee), + ) + + testCase( + "vote delegated ids", + t, + f, + false, + `--- + user/1/meeting_user_ids: [10] + user/2/meeting_user_ids: [20] + + meeting_user/10: + meeting_id: 30 + meeting_user/20: + vote_delegations_from_ids: [4] + meeting_id: 30 + + vote/4/option_id: 5 + option/5/poll_id: 6 + poll/6: + state: published + meeting_id: 30 + + meeting/30/enable_anonymous: true + `, + withRequestUser(1), + withElementID(2), + ) + + testCase( + "chat messages", + t, + f, + false, + `--- + user/1/meeting_user_ids: [10] + user/2/meeting_user_ids: [20] + + meeting_user/10: + meeting_id: 30 + meeting_user/20: + chat_message_ids: [4] + meeting_id: 30 + + meeting_user/10/group_ids: [5] + + meeting/30/id: 30 + + chat_message/4: + meeting_user_id: 20 + chat_group_id: 3 + + chat_group/3: + read_group_ids: [5] + meeting_id: 30 + + group/5/id: 5 + `, + withRequestUser(1), + withElementID(2), + ) +} + func TestUserModeD(t *testing.T) { var u collection.User diff --git a/internal/restrict/field_def.go b/internal/restrict/field_def.go index 9adcf29d..c7ccd351 100644 --- a/internal/restrict/field_def.go +++ b/internal/restrict/field_def.go @@ -1277,12 +1277,12 @@ var restrictionModes = map[string]string{ "user/title": "A", "user/username": "A", "user/vote_ids": "A", + "user/email": "B", "user/can_change_own_password": "D", "user/is_active": "D", "user/last_email_sent": "D", "user/committee_ids": "E", "user/committee_management_ids": "E", - "user/email": "E", "user/forwarding_committee_ids": "E", "user/meeting_ids": "E", "user/organization_management_level": "E", diff --git a/internal/restrict/perm/generated.go b/internal/restrict/perm/generated.go index e8b7e743..0ea22012 100644 --- a/internal/restrict/perm/generated.go +++ b/internal/restrict/perm/generated.go @@ -39,6 +39,8 @@ const ( UserCanManage TPermission = "user.can_manage" UserCanManagePresence TPermission = "user.can_manage_presence" UserCanSee TPermission = "user.can_see" + UserCanSeeSensitiveData TPermission = "user.can_see_sensitive_data" + UserCanUpdate TPermission = "user.can_update" ) var derivatePerms = map[TPermission][]TPermission{ @@ -76,7 +78,9 @@ var derivatePerms = map[TPermission][]TPermission{ "projector.can_manage": {"projector.can_see"}, "projector.can_see": {}, "tag.can_manage": {}, - "user.can_manage": {"user.can_manage_presence", "user.can_see"}, + "user.can_manage": {"user.can_manage_presence", "user.can_see", "user.can_see", "user.can_see_sensitive_data", "user.can_update"}, "user.can_manage_presence": {"user.can_see"}, "user.can_see": {}, + "user.can_see_sensitive_data": {"user.can_see"}, + "user.can_update": {"user.can_see", "user.can_see_sensitive_data"}, } diff --git a/meta b/meta index fb66a293..8ed4b0af 160000 --- a/meta +++ b/meta @@ -1 +1 @@ -Subproject commit fb66a293e5e9c7130b208dd71298f124fed5a4da +Subproject commit 8ed4b0af23719ac2c737164733deb6506bda602b diff --git a/pkg/datastore/dskey/gen_collection_fields.go b/pkg/datastore/dskey/gen_collection_fields.go index 2ef8d533..3e1feb86 100644 --- a/pkg/datastore/dskey/gen_collection_fields.go +++ b/pkg/datastore/dskey/gen_collection_fields.go @@ -862,6 +862,7 @@ var collectionFields = [...]collectionField{ {"topic", "text"}, {"topic", "title"}, {"user", "A"}, + {"user", "B"}, {"user", "D"}, {"user", "E"}, {"user", "F"}, @@ -2631,100 +2632,102 @@ func collectionFieldToID(cf string) int { return 858 case "user/A": return 859 - case "user/D": + case "user/B": return 860 - case "user/E": + case "user/D": return 861 - case "user/F": + case "user/E": return 862 - case "user/G": + case "user/F": return 863 - case "user/H": + case "user/G": return 864 - case "user/can_change_own_password": + case "user/H": return 865 - case "user/committee_ids": + case "user/can_change_own_password": return 866 - case "user/committee_management_ids": + case "user/committee_ids": return 867 - case "user/default_number": + case "user/committee_management_ids": return 868 - case "user/default_password": + case "user/default_number": return 869 - case "user/default_vote_weight": + case "user/default_password": return 870 - case "user/delegated_vote_ids": + case "user/default_vote_weight": return 871 - case "user/email": + case "user/delegated_vote_ids": return 872 - case "user/first_name": + case "user/email": return 873 - case "user/forwarding_committee_ids": + case "user/first_name": return 874 - case "user/gender": + case "user/forwarding_committee_ids": return 875 - case "user/id": + case "user/gender": return 876 - case "user/is_active": + case "user/id": return 877 - case "user/is_demo_user": + case "user/is_active": return 878 - case "user/is_physical_person": + case "user/is_demo_user": return 879 - case "user/is_present_in_meeting_ids": + case "user/is_physical_person": return 880 - case "user/last_email_sent": + case "user/is_present_in_meeting_ids": return 881 - case "user/last_login": + case "user/last_email_sent": return 882 - case "user/last_name": + case "user/last_login": return 883 - case "user/meeting_ids": + case "user/last_name": return 884 - case "user/meeting_user_ids": + case "user/meeting_ids": return 885 - case "user/option_ids": + case "user/meeting_user_ids": return 886 - case "user/organization_id": + case "user/option_ids": return 887 - case "user/organization_management_level": + case "user/organization_id": return 888 - case "user/password": + case "user/organization_management_level": return 889 - case "user/poll_candidate_ids": + case "user/password": return 890 - case "user/poll_voted_ids": + case "user/poll_candidate_ids": return 891 - case "user/pronoun": + case "user/poll_voted_ids": return 892 - case "user/saml_id": + case "user/pronoun": return 893 - case "user/title": + case "user/saml_id": return 894 - case "user/username": + case "user/title": return 895 - case "user/vote_ids": + case "user/username": return 896 - case "vote/A": + case "user/vote_ids": return 897 - case "vote/B": + case "vote/A": return 898 - case "vote/delegated_user_id": + case "vote/B": return 899 - case "vote/id": + case "vote/delegated_user_id": return 900 - case "vote/meeting_id": + case "vote/id": return 901 - case "vote/option_id": + case "vote/meeting_id": return 902 - case "vote/user_id": + case "vote/option_id": return 903 - case "vote/user_token": + case "vote/user_id": return 904 - case "vote/value": + case "vote/user_token": return 905 - case "vote/weight": + case "vote/value": return 906 + case "vote/weight": + return 907 default: return -1 }