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: 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": [