Skip to content

Commit

Permalink
Merge pull request #223 from eurofurence/issue-222-find-api-expansion
Browse files Browse the repository at this point in the history
Issue 222 find api expansion
  • Loading branch information
Jumpy-Squirrel authored Aug 10, 2024
2 parents c3b5452 + d7d28d7 commit d32f0c9
Show file tree
Hide file tree
Showing 20 changed files with 344 additions and 38 deletions.
72 changes: 64 additions & 8 deletions api/openapi-spec/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1228,8 +1228,10 @@ paths:
Returns all matching attendees.
This is a privileged operation, subject must either have the admin group or
have "regdesk" or "sponsordesk" permission in their admin info. In the latter case,
have one of the configured permissions in their admin info. In the latter case,
only a suitable subset of fields are returned and non-attending registrations are always omitted.
The list of permissions is configured under "security.find_api_access.permissions".
operationId: findAttendees
requestBody:
content:
Expand Down Expand Up @@ -1668,7 +1670,7 @@ components:
according to the IANA language registry (https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry).
The list of allowed values is specified in the service configuration, and is used to select email language templates as well as represented on the badge.
example: 'de-DE,en-US'
example: 'de,en'
registration_language:
type: string
description: |-
Expand Down Expand Up @@ -1801,7 +1803,19 @@ components:
according to the IANA language registry (https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry).
The list of allowed values is specified in the service configuration, and is used to select email language templates as well as represented on the badge.
example: 'de-DE,en-US'
example: 'de,en'
spoken_languages_list:
type: array
items:
type: string
description: |-
List of a configurable subset of RFC 5646 locales (language-REGION) that specify the spoken languages, in order of preference,
according to the IANA language registry (https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry).
The list of allowed values is specified in the service configuration, and is used to select email language templates as well as represented on the badge.
example:
- 'de'
- 'en'
registration_language:
type: string
description: |-
Expand All @@ -1814,16 +1828,58 @@ components:
example: en-US
flags:
type: string
description: A comma separated list of flags as declared in configuration. Flags are used to store yes/no-style information about an attendee, and displayed as checkboxes. Flags can be configured with respect to their visibility and who may change them (admin only, normal user). Flags are used to represent properties of the attendee, such as "is staff", "does not wish their name to appear in the convention booklet", etc.
description: DEPRECATED - use flags_list for new implementations. A comma separated list of flags as declared in configuration. Flags are used to store yes/no-style information about an attendee, and displayed as checkboxes. Flags can be configured with respect to their visibility and who may change them (admin only, normal user). Flags are used to represent properties of the attendee, such as "is staff", "does not wish their name to appear in the convention booklet", etc.
example: anon,ev
flags_list:
type: array
items:
type: string
description: A sorted, unique list of flags as declared in configuration. Flags are used to store yes/no-style information about an attendee, and displayed as checkboxes. Flags can be configured with respect to their visibility and who may change them (admin only, normal user). Flags are used to represent properties of the attendee, such as "is staff", "does not wish their name to appear in the convention booklet", etc.
example:
- anon
- ev
options:
type: string
description: A comma separated list of options as declared in configuration. Options are used to store yes/no-style choices an attendee makes that do not cost money, and displayed as checkboxes. With Options, you cannot control the visibility in the configuration, as they are basically options given to the attendee that do not cost money. Use these for "I wish to receive emails about x subject" or "I am a fursuiter" etc.
description: DEPRECATED - use options_list for new implementations. A comma separated list of options as declared in configuration. Options are used to store yes/no-style choices an attendee makes that do not cost money, and displayed as checkboxes. With Options, you cannot control the visibility in the configuration, as they are basically options given to the attendee that do not cost money. Use these for "I wish to receive emails about x subject" or "I am a fursuiter" etc.
example: art,anim,music,suit
options_list:
type: array
items:
type: string
description: A sorted, unique list of options as declared in configuration. Options are used to store yes/no-style choices an attendee makes that do not cost money, and displayed as checkboxes. With Options, you cannot control the visibility in the configuration, as they are basically options given to the attendee that do not cost money. Use these for "I wish to receive emails about x subject" or "I am a fursuiter" etc.
example:
- anim
- art
- music
- suit
packages:
type: string
description: A comma separated list of packages as declared in configuration. Packages are the things that cost money, like being a supersponsor or a day guest for a certain day. They can be configured with respect to who may add / remove them, if they are on by default, and whether they are visible if not selected (admin only, normal user, completely disabled). There is also configuration as to which packages are mutually exclusive, such as sponsor and supersponsor.
description: DEPRECATED - use packages_list for new implementations. A comma separated list of packages as declared in configuration. Packages are the things that cost money, like being a supersponsor or a day guest for a certain day. They can be configured with respect to who may add / remove them, if they are on by default, and whether they are visible if not selected (admin only, normal user, completely disabled). There is also configuration as to which packages are mutually exclusive, such as sponsor and supersponsor.
example: room-none,attendance,sponsor
packages_list:
type: array
items:
type: object
required:
- name
- count
properties:
name:
type: string
example: attendance
description: the code for the package
count:
type: integer
example: 1
description: the number of times the package was purchased (at the moment, only the value 1 is supported, but this may change in the future).
description: A sorted list of packages as declared in configuration. Packages are the things that cost money, like being a supersponsor or a day guest for a certain day. They can be configured with respect to who may add / remove them, if they are on by default, and whether they are visible if not selected (admin only, normal user, completely disabled). There is also configuration as to which packages are mutually exclusive, such as sponsor and supersponsor.
example:
- name: attendance
count: 1
- name: room-none
count: 1
- name: sponsor
count: 1
user_comments:
type: string
description: Optional comments the attendee wishes to make regarding their registration. Not processed in any way.
Expand Down Expand Up @@ -1970,8 +2026,8 @@ components:
additionalProperties:
$ref: '#/components/schemas/ChoiceStateCondition'
example:
de-DE: 1
en-US: 0
de: 1
en: 0
registration_language:
description: desired state of registration language locales (as declared in configuration). If specified at all, locale must be present the given number of times (0 or 1). Example would select all attendees that use the non-English registration ui.
type: object
Expand Down
13 changes: 13 additions & 0 deletions docs/config-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ security:
require_login_for_reg: true
# set this to true temporarily to use the load testing command with constant tokens. Never use in production.
# anonymize_identity: true
find_api_access:
# the list of permissions in the "permissions" field that grant access to the (limited) findAttendees API
permissions:
- regdesk
- sponsordesk
- staffradio
logging:
severity: INFO
style: plain # or ecs (elastic common schema), the default
Expand Down Expand Up @@ -89,6 +95,13 @@ additional_info_areas:
permissions:
- regdesk
- sponsordesk
fursuitbadge:
self_read: true
permissions:
- fursuitbadge
staffradio:
permissions:
- staffradio
choices:
flags:
hc:
Expand Down
11 changes: 10 additions & 1 deletion internal/api/v1/attendee/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type AttendeeDto struct {
Gender string `json:"gender"` // optional, one of male,female,other,notprovided
Pronouns string `json:"pronouns"` // optional
TshirtSize string `json:"tshirt_size"`
SpokenLanguages string `json:"spoken_languages"` // configurable subset of RFC 5646 locales, comma separated (de-DE,en-US)
SpokenLanguages string `json:"spoken_languages"` // configurable subset of configured language codes, comma separated (de,en)
RegistrationLanguage string `json:"registration_language"` // one out of configurable subset of RFC 5646 locales (default en-US)

// comma separated lists, allowed choices are convention dependent
Expand Down Expand Up @@ -111,10 +111,14 @@ type AttendeeSearchResult struct {
Pronouns *string `json:"pronouns,omitempty"`
TshirtSize *string `json:"tshirt_size,omitempty"`
SpokenLanguages *string `json:"spoken_languages,omitempty"`
SpokenLanguagesList []string `json:"spoken_languages_list,omitempty"`
RegistrationLanguage *string `json:"registration_language,omitempty"`
Flags *string `json:"flags,omitempty"`
FlagsList []string `json:"flags_list,omitempty"`
Options *string `json:"options,omitempty"`
OptionsList []string `json:"options_list,omitempty"`
Packages *string `json:"packages,omitempty"`
PackagesList []PackageState `json:"packages_list,omitempty"`
UserComments *string `json:"user_comments,omitempty"`
Status *status.Status `json:"status,omitempty"`
TotalDues *int64 `json:"total_dues,omitempty"`
Expand All @@ -130,3 +134,8 @@ type AttendeeSearchResult struct {
type ChoiceState struct {
Present bool `json:"present"`
}

type PackageState struct {
Name string `json:"name"`
Count int `json:"count"`
}
12 changes: 10 additions & 2 deletions internal/repository/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ func AdditionalInfoFieldNames() []string {
return result
}

// AllowedPermissions returns a sorted unique map of all permissions referenced in
// additional info configurations.
// AllowedPermissions returns a sorted unique list of all permissions referenced in
// additional info configurations or configured to have access to the find api.
func AllowedPermissions() []string {
resultMap := make(map[string]bool)
for _, v := range Configuration().AdditionalInfo {
Expand All @@ -155,6 +155,10 @@ func AllowedPermissions() []string {
}
}

for _, perm := range Configuration().Security.FindApiAccess.Permissions {
resultMap[perm] = true
}

result := make([]string, 0)
for k := range resultMap {
result = append(result, k)
Expand All @@ -176,6 +180,10 @@ func AdditionalInfoConfiguration(fieldName string) AddInfoConfig {
}
}

func PermissionsAllowingFindAttendees() []string {
return Configuration().Security.FindApiAccess.Permissions
}

func AllowedTshirtSizes() []string {
return Configuration().TShirtSizes
}
Expand Down
1 change: 1 addition & 0 deletions internal/repository/config/loading.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func parseAndOverwriteConfig(yamlFile []byte) error {
validateBirthdayConfiguration(errs, newConfigurationData.Birthday)
validateRegistrationStartTime(errs, newConfigurationData.GoLive, newConfigurationData.Security)
validateDuesConfiguration(errs, newConfigurationData.Dues)
validateAdditionalInfoConfiguration(errs, newConfigurationData.AdditionalInfo)

if len(errs) != 0 {
var keys []string
Expand Down
5 changes: 5 additions & 0 deletions internal/repository/config/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type (
Cors CorsConfig `yaml:"cors"`
RequireLogin bool `yaml:"require_login_for_reg"`
AnonymizeIdentity bool `yaml:"anonymize_identity"`
FindApiAccess FindApiAccessConfig `yaml:"find_api_access"`
}

FixedTokenConfig struct {
Expand All @@ -97,6 +98,10 @@ type (
AllowOrigin string `yaml:"allow_origin"`
}

FindApiAccessConfig struct {
Permissions []string `yaml:"permissions"` // the list of permissions that grant access to the FindAttendees endpoint
}

// LoggingConfig configures logging
LoggingConfig struct {
Style LogStyle `yaml:"style"`
Expand Down
28 changes: 28 additions & 0 deletions internal/repository/config/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func setConfigurationDefaults(c *Application) {
if c.Dues.DueDays == 0 {
c.Dues.DueDays = 14
}
if len(c.Security.FindApiAccess.Permissions) == 0 {
c.Security.FindApiAccess.Permissions = []string{"regdesk", "sponsordesk"}
}
}

const (
Expand Down Expand Up @@ -102,6 +105,12 @@ func validateSecurityConfiguration(errs url.Values, c SecurityConfig) {
parsedKeySet = append(parsedKeySet, publicKeyPtr)
}
}

for _, perm := range c.FindApiAccess.Permissions {
if validation.ViolatesPattern(attendeePermissionPattern, perm) {
errs.Add("security.find_api_access.permissions", "permissions for find api access must match [a-z]+, no other characters allowed")
}
}
}

var allowedDatabases = []DatabaseType{Mysql, Inmemory}
Expand Down Expand Up @@ -252,3 +261,22 @@ func validateServiceConfiguration(errs url.Values, c ServiceConfig) {
errs.Add("service.mail_service", "base url must be empty (enables in-memory simulator) or start with http:// or https:// and may not end in a /")
}
}

const addInfoAreaPattern = "^[a-z]+$"
const attendeePermissionPattern = "^[a-z]+$"

func validateAdditionalInfoConfiguration(errs url.Values, areas map[string]AddInfoConfig) {
for area, config := range areas {
if validation.ViolatesPattern(addInfoAreaPattern, area) {
errs.Add("additional_info_areas", "keys for additional info fields must match [a-z]+, no other characters allowed")
}
if area == "overdue" {
errs.Add("additional_info_areas.overdue", "this key is reserved for internal use by the admin frontend, you may not configure it")
}
for _, perm := range config.Permissions {
if validation.ViolatesPattern(attendeePermissionPattern, perm) {
errs.Add(fmt.Sprintf("additional_info_areas.%s.permissions", perm), "permissions for additional info access must match [a-z]+, no other characters allowed")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func tstBuildValidAttendee() *entity.Attendee {
Telegram: "@ihopethisuserdoesnotexist",
Birthday: "1998-11-23",
Gender: "other",
SpokenLanguages: ",de-DE,en-US,",
SpokenLanguages: ",de,en,",
RegistrationLanguage: ",en-US,",
Flags: ",anon,ev,",
Packages: ",room-none,attendance,stage,sponsor2,",
Expand Down
16 changes: 8 additions & 8 deletions internal/repository/database/mysqldb/searchquery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ func TestTwoFullSearchQueries(t *testing.T) {
Email: "ee*ee@ff*ff",
Telegram: "@abc",
SpokenLanguages: map[string]int8{
"en-US": 0,
"de-DE": 1,
"en": 0,
"de": 1,
},
RegistrationLanguage: map[string]int8{
"en-US": 0,
Expand Down Expand Up @@ -86,8 +86,8 @@ func TestTwoFullSearchQueries(t *testing.T) {
Email: "gg@hh",
Telegram: "@def",
SpokenLanguages: map[string]int8{
"en-GB": 0,
"de-AT": 1,
"fr": 0,
"en": 1,
},
RegistrationLanguage: map[string]int8{
"en-GB": 0,
Expand Down Expand Up @@ -140,8 +140,8 @@ func TestTwoFullSearchQueries(t *testing.T) {
"param_1_4": "DE",
"param_1_5": "%ee%ee@ff%ff%",
"param_1_6": "%@abc%",
"param_1_7": "%,de-DE,%",
"param_1_8": "%,en-US,%",
"param_1_7": "%,de,%",
"param_1_8": "%,en,%",
"param_1_9": "%,de-DE,%",
"param_1_10": "%,en-US,%",
"param_1_11": "%,flagone,%",
Expand All @@ -160,8 +160,8 @@ func TestTwoFullSearchQueries(t *testing.T) {
"param_2_4": "CH",
"param_2_5": "%gg@hh%",
"param_2_6": "%@def%",
"param_2_7": "%,de-AT,%",
"param_2_8": "%,en-GB,%",
"param_2_7": "%,en,%",
"param_2_8": "%,fr,%",
"param_2_9": "%,de-AT,%",
"param_2_10": "%,en-GB,%",
"param_2_11": "%,fone,%",
Expand Down
13 changes: 12 additions & 1 deletion internal/service/attendeesrv/addinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (s *AttendeeServiceImplData) CanAccessAdditionalInfoArea(ctx context.Contex
}

loggedInSubject := ctxvalues.Subject(ctx)
allowed, err := s.subjectHasAdminPermissionEntry(ctx, loggedInSubject, area...)
allowed, err := s.subjectHasAreaPermissionEntry(ctx, loggedInSubject, area...)
return allowed, err
}

Expand All @@ -53,3 +53,14 @@ func (s *AttendeeServiceImplData) CanAccessOwnAdditionalInfoArea(ctx context.Con

return false, nil
}

func (s *AttendeeServiceImplData) CanUseFindAttendee(ctx context.Context) (bool, error) {
if ctxvalues.HasApiToken(ctx) || ctxvalues.IsAuthorizedAsGroup(ctx, config.OidcAdminGroup()) {
return true, nil
}

permissions := config.PermissionsAllowingFindAttendees()
loggedInSubject := ctxvalues.Subject(ctx)
allowed, err := s.subjectHasDirectPermissionEntry(ctx, loggedInSubject, permissions...)
return allowed, err
}
12 changes: 12 additions & 0 deletions internal/service/attendeesrv/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,25 @@ type AttendeeService interface {
//
// Normal users (loaded by identity) need a matching permissions entry in their admin info.
// Admins and Api Token can see all areas.
//
// Returns true if access is allowed, and an error if the check could not be performed.
CanAccessAdditionalInfoArea(ctx context.Context, area ...string) (bool, error)

// CanAccessOwnAdditionalInfoArea checks permission to access ones own additional info for a given area
//
// This is only allowed for areas which have self_read or self_write configured.
//
// Returns true if access is allowed, and an error if the check could not be performed.
CanAccessOwnAdditionalInfoArea(ctx context.Context, attendeeId uint, wantWriteAccess bool, area string) (bool, error)

// CanUseFindAttendee checks permission to use the find attendees API
//
// Normal users (loaded by identity) need a permissions entry in their admin info that is listed in the security configuration,
// Admins and Api Token can always use find attendee.
//
// Returns true if access is allowed, and an error if the check could not be performed.
CanUseFindAttendee(ctx context.Context) (bool, error)

// GenerateFakeRegistrations creates the specified number of fake registrations in the database.
//
// Only for use on test systems.
Expand Down
Loading

0 comments on commit d32f0c9

Please sign in to comment.