From ae2dae979d1e431859b9e9678c0ab4f008b49de6 Mon Sep 17 00:00:00 2001 From: Jumpy Squirrel Date: Sun, 11 Aug 2024 12:43:37 +0200 Subject: [PATCH 1/2] feat(#222): allow filtering fields for user permission finds --- internal/web/controller/adminctl/adminctl.go | 29 ++++++-- test/acceptance/admin_acc_test.go | 78 +++++++++++++++++++- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/internal/web/controller/adminctl/adminctl.go b/internal/web/controller/adminctl/adminctl.go index e9c02cb..0b1f8a3 100644 --- a/internal/web/controller/adminctl/adminctl.go +++ b/internal/web/controller/adminctl/adminctl.go @@ -114,11 +114,7 @@ func findAttendeesHandler(w http.ResponseWriter, r *http.Request) { } if limitedAccess { - criteria.FillFields = []string{"id", "nickname", "first_name", "last_name", "country", - "spoken_languages", "registration_language", "birthday", "pronouns", "tshirt_size", - "flags", "options", "packages", "status", - "total_dues", "payment_balance", "current_dues", - } + criteria.FillFields = limitToAllowedFields(criteria.FillFields) for i := range criteria.MatchAny { criteria.MatchAny[i].Status = limitToAttendingStatus(criteria.MatchAny[i].Status) } @@ -134,6 +130,29 @@ func findAttendeesHandler(w http.ResponseWriter, r *http.Request) { ctlutil.WriteJson(ctx, w, results) } +func limitToAllowedFields(desired []string) []string { + allowed := []string{"id", "nickname", "first_name", "last_name", "country", + "spoken_languages", "registration_language", "birthday", "pronouns", "tshirt_size", + "flags", "options", "packages", "status", + "total_dues", "payment_balance", "current_dues", + } + + result := make([]string, 0) + for _, d := range desired { + for _, a := range allowed { + if d == a { + result = append(result, d) + } + } + } + + if len(result) == 0 { + return allowed + } else { + return result + } +} + func limitToAttendingStatus(desired []status.Status) []status.Status { if len(desired) == 0 { return []status.Status{status.Approved, status.PartiallyPaid, status.Paid, status.CheckedIn} diff --git a/test/acceptance/admin_acc_test.go b/test/acceptance/admin_acc_test.go index a08bdbb..8c7fe25 100644 --- a/test/acceptance/admin_acc_test.go +++ b/test/acceptance/admin_acc_test.go @@ -1227,6 +1227,82 @@ func TestSearch_OtherPermissionsDeny(t *testing.T) { tstRequireErrorResponse(t, response, http.StatusForbidden, "auth.forbidden", "you are not authorized for this operation - the attempt has been logged") } +func TestSearch_PermissionAllowedFields_Ok(t *testing.T) { + docs.Given("given the configuration for standard registration") + tstSetup(false, false, true) + defer tstShutdown() + + docs.Given("given an existing attendee who has been given the sponsordesk permission") + loc, att := tstRegisterAttendeeAndTransitionToStatus(t, "search2d-", status.Paid) + permBody := admin.AdminInfoDto{ + Permissions: "sponsordesk", + } + permissionResponse := tstPerformPut(loc+"/admin", tstRenderJson(permBody), tstValidAdminToken(t)) + require.Equal(t, http.StatusNoContent, permissionResponse.status) + + docs.When("when they search for attendees and limit the fields to allowed fields") + token := tstValidUserToken(t, att.Id) + searchAll := attendee.AttendeeSearchCriteria{ + MatchAny: []attendee.AttendeeSearchSingleCriterion{ + {}, + }, + FillFields: []string{"id", "nickname", "spoken_languages"}, + } + response := tstPerformPost("/api/rest/v1/attendees/find", tstRenderJson(searchAll), token) + + docs.Then("then the request is successful and the list of attendees is returned with only the desired fields") + require.Equal(t, http.StatusOK, response.status, "unexpected http response status") + expected := `{ + "attendees": [ + { + "id": 1, + "badge_id": "1C", + "nickname": "BlackCheetah", + "spoken_languages": "de,en", + "spoken_languages_list": ["de","en"] + } + ] +}` + tstRequireSearchResultMatches(t, expected, response.body) +} + +func TestSearch_PermissionAllowedFields_Filtered(t *testing.T) { + docs.Given("given the configuration for standard registration") + tstSetup(false, false, true) + defer tstShutdown() + + docs.Given("given an existing attendee who has been given the sponsordesk permission") + loc, att := tstRegisterAttendeeAndTransitionToStatus(t, "search2d-", status.Paid) + permBody := admin.AdminInfoDto{ + Permissions: "sponsordesk", + } + permissionResponse := tstPerformPut(loc+"/admin", tstRenderJson(permBody), tstValidAdminToken(t)) + require.Equal(t, http.StatusNoContent, permissionResponse.status) + + docs.When("when they search for attendees and ask for forbidden fields") + token := tstValidUserToken(t, att.Id) + searchAll := attendee.AttendeeSearchCriteria{ + MatchAny: []attendee.AttendeeSearchSingleCriterion{ + {}, + }, + FillFields: []string{"id", "nickname", "email", "phone"}, + } + response := tstPerformPost("/api/rest/v1/attendees/find", tstRenderJson(searchAll), token) + + docs.Then("then the request is successful and the list of attendees is returned, but with the field list filtered to allowed fields") + require.Equal(t, http.StatusOK, response.status, "unexpected http response status") + expected := `{ + "attendees": [ + { + "id": 1, + "badge_id": "1C", + "nickname": "BlackCheetah" + } + ] +}` + tstRequireSearchResultMatches(t, expected, response.body) +} + func TestSearch_StaffDeny(t *testing.T) { docs.Given("given the configuration for staff registration") tstSetup(false, true, true) @@ -1268,7 +1344,7 @@ func TestSearch_AdminOk(t *testing.T) { } response := tstPerformPost("/api/rest/v1/attendees/find", tstRenderJson(searchAll), token) - docs.Then("then the request is successful and the list of attendees is returned") + docs.Then("then the request is successful and the list of attendees is returned with all fields") require.Equal(t, http.StatusOK, response.status, "unexpected http response status") expected := `{ "attendees": [ From c99adda6bbcaed19737f20356138670cc3c200c4 Mon Sep 17 00:00:00 2001 From: Jumpy Squirrel Date: Sun, 11 Aug 2024 12:50:07 +0200 Subject: [PATCH 2/2] fix(#222): fix api docs to describe what is really happening --- api/openapi-spec/openapi.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/api/openapi-spec/openapi.yaml b/api/openapi-spec/openapi.yaml index 3965912..a05ac9c 100644 --- a/api/openapi-spec/openapi.yaml +++ b/api/openapi-spec/openapi.yaml @@ -2126,13 +2126,17 @@ components: For a detailed description of the fields, please see the AttendeeSearchResult schema. - Keep in mind that your permissions may limit field visibility. + Keep in mind that your permissions may limit field visibility. + + Non-Admins cannot use field sets, are limited to attending statuses, and are limited to these fields: + id, nickname, first_name, last_name, country, spoken_languages, registration_language, birthday, pronouns, + tshirt_size, flags, options, packages, status, total_dues, payment_balance, current_dues. Also remember that even if you don't list the id field, you will still get it. You will not get fields that you don't have permission to see even if you request them. - If you do not specify any fields, only the id will be returned. + If you do not specify any fields, a default set of fields will be returned that depends on your permissions. items: type: string enum: