Skip to content
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

Issue 240 246 impl #253

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions api/openapi-spec/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2043,6 +2043,12 @@ components:
admin_comments:
type: string
description: Optional comments only visible to admins. Not processed in any way.
identity_subject:
type: string
description: the OIDC subject the registration was made or last updated with.
avatar:
type: string
description: the avatar URL from when the registration was last updated by the user in the registration system. May be empty or sometimes outdated.
AttendeeSearchCriteria:
type: object
required:
Expand Down Expand Up @@ -2225,6 +2231,21 @@ components:
sponsor-items: 1
key-hand-out: 0
overdue: 1
identity_subjects:
description: |-
exact match to search for a set of OIDC subjects as cached in registrations. No condition if left empty.

IMPORTANT: Use of this field is limited to 8 identities, any additional identities in you list will be silently ignored!
If you need to check a large number of identities, please request the identity_subject search result field,
and filter on your end!

Note that caching of identities is configuration dependent, if disabled, no registrations will ever match if this is set.
type: array
maxItems: 8
items:
type: string
example:
- '1234567890'
ChoiceStateCondition:
type: integer
format: int64
Expand Down Expand Up @@ -2313,6 +2334,8 @@ components:
- due_date
- registered
- admin_comments
- identity_subject
- avatar
# and the field sets
- name
- address
Expand Down
1 change: 1 addition & 0 deletions docs/config-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ security:
audience: 'only-allowed-audience-in-tokens'
# optional, but will be checked if set
issuer: 'only-allowed-issuer-in-tokens'
avatar_base_url: 'http://localhost:10000/identity-avatars/'
cors:
# set this to true to send disable cors headers - not for production - local/test instances only - will log lots of warnings
disable: false
Expand Down
3 changes: 3 additions & 0 deletions internal/api/v1/attendee/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type AttendeeSearchSingleCriterion struct {
Permissions map[string]int8 `json:"permissions"`
AdminComments string `json:"admin_comments"`
AddInfo map[string]int8 `json:"add_info"` // can only search for presence of a value for each area, Note: special area 'overdue'
IdentitySubjects []string `json:"identity_subjects"`
}

// --- search result ---
Expand Down Expand Up @@ -128,6 +129,8 @@ type AttendeeSearchResult struct {
DueDate *string `json:"due_date,omitempty"`
Registered *string `json:"registered,omitempty"`
AdminComments *string `json:"admin_comments,omitempty"`
IdentitySubject *string `json:"identity_subject"`
Avatar *string `json:"avatar"`
}

// --- flags/options/packages result ---
Expand Down
1 change: 1 addition & 0 deletions internal/entity/attendee.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Attendee struct {
Options string `gorm:"type:varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"` // comma-separated choice field with leading and trailing comma
UserComments string `gorm:"type:text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" testdiff:"ignore"`
Identity string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;uniqueIndex:att_attendees_identity_uidx"`
Avatar string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"`
CacheTotalDues int64 `testdiff:"ignore"` // cache for search functionality only: valid dues balance
CachePaymentBalance int64 `testdiff:"ignore"` // cache for search functionality only: valid payments balance
CacheOpenBalance int64 `testdiff:"ignore"` // cache for search functionality only: tentative + pending payments balance
Expand Down
4 changes: 4 additions & 0 deletions internal/repository/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,7 @@ func VatPercent() float64 {
func RegsysPublicUrl() string {
return Configuration().Service.RegsysPublicUrl
}

func AvatarBaseUrl() string {
return Configuration().Security.Oidc.AvatarBaseUrl
}
1 change: 1 addition & 0 deletions internal/repository/config/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ type (
EarlyRegGroup string `yaml:"early_reg_group"` // optional, the group claim that turns on early registration
Audience string `yaml:"audience"`
Issuer string `yaml:"issuer"`
AvatarBaseUrl string `yaml:"avatar_base_url"` // optional, prefix for the avatar from the JWT id token
}

CorsConfig struct {
Expand Down
11 changes: 10 additions & 1 deletion internal/repository/database/inmemorydb/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/eurofurence/reg-attendee-service/internal/repository/config"
"github.com/eurofurence/reg-attendee-service/internal/web/util/validation"
"github.com/ryanuber/go-glob"
"slices"
"strconv"
"strings"
)
Expand Down Expand Up @@ -48,7 +49,8 @@ func (r *InMemoryRepository) matches(cond *attendee.AttendeeSearchSingleCriterio
matchesSubstringGlobOrEmpty(cond.AdminComments, adm.AdminComments) &&
matchesAddInfoPresence(cond.AddInfo, addInf) &&
matchesOverdue(cond.AddInfo, a.CacheDueDate, r.Now().Format(config.IsoDateFormat), st.Status) &&
matchesIsoDateRange(cond.BirthdayFrom, cond.BirthdayTo, a.Birthday)
matchesIsoDateRange(cond.BirthdayFrom, cond.BirthdayTo, a.Birthday) &&
matchesIdentitySubjects(cond.IdentitySubjects, a.Identity)
}

func matchesUintSliceOrEmpty(cond []uint, value uint) bool {
Expand Down Expand Up @@ -183,3 +185,10 @@ func matchesIsoDateRange(condFrom string, condTo string, value string) bool {
}
return true
}

func matchesIdentitySubjects(cond []string, value string) bool {
if len(cond) > 8 {
cond = cond[:8]
}
return len(cond) == 0 || slices.Contains(cond, value)
}
20 changes: 20 additions & 0 deletions internal/repository/database/mysqldb/searchquery.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ func (r *MysqlRepository) addSingleCondition(cond *attendee.AttendeeSearchSingle
if cond.BirthdayTo != "" {
query.WriteString(stringLessThanOrEqual("a.birthday", cond.BirthdayTo, params, paramBaseName, &paramNo))
}
if len(cond.IdentitySubjects) > 0 {
query.WriteString(stringSliceMatch("a.identity", cond.IdentitySubjects, params, paramBaseName, &paramNo))
}

return query.String()
}
Expand Down Expand Up @@ -315,6 +318,23 @@ func safeStatusSliceMatch(field string, values []status.Status) string {
return fmt.Sprintf(" AND ( %s IN (%s) )\n", field, strings.Join(mappedValues, ","))
}

func stringSliceMatch(field string, values []string, params map[string]interface{}, paramBaseName string, idx *int) string {
var result strings.Builder
result.WriteString(fmt.Sprintf(" AND ( %s IN ( ", field))
cond := make([]string, 0)
for i, v := range values {
if i < 8 {
pName := fmt.Sprintf("%s_%d_%d", paramBaseName, *idx, i+1)
params[pName] = v
cond = append(cond, "@"+pName)
}
}
*idx++
result.WriteString(strings.Join(cond, " , "))
result.WriteString(" ) )\n")
return result.String()
}

func substringMatch(field string, condition string, params map[string]interface{}, paramBaseName string, idx *int) string {
return fullstringMatch(field, "*"+condition+"*", params, paramBaseName, idx)
}
Expand Down
8 changes: 6 additions & 2 deletions internal/repository/database/mysqldb/searchquery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ func TestTwoFullSearchQueries(t *testing.T) {
"overdue": 1,
"sponsor-items": 1,
},
BirthdayFrom: "1970-10-24",
BirthdayTo: "1980-12-24",
BirthdayFrom: "1970-10-24",
BirthdayTo: "1980-12-24",
IdentitySubjects: []string{"Q1E4D2", "R1E5DD"},
},
},
MinId: 1,
Expand Down Expand Up @@ -158,6 +159,8 @@ func TestTwoFullSearchQueries(t *testing.T) {
"param_2_1": "small%bird",
"param_2_2": "Johnny",
"param_2_20": "1980-12-24",
"param_2_21_1": "Q1E4D2",
"param_2_21_2": "R1E5DD",
"param_2_3": "%Berlin%",
"param_2_4": "CH",
"param_2_5": "%gg@hh%",
Expand Down Expand Up @@ -238,6 +241,7 @@ WHERE (
AND ( ( SELECT COUNT(*) FROM att_additional_infos WHERE attendee_id = a.id AND area = @param_2_18_2 ) > 0 )
AND ( STRCMP(a.birthday,@param_2_19) >= 0 )
AND ( STRCMP(a.birthday,@param_2_20) <= 0 )
AND ( a.identity IN ( @param_2_21_1 , @param_2_21_2 ) )
)
) AND a.id >= @param_0_1 AND a.id <= @param_0_2 ORDER BY CONCAT(a.first_name, ' ', a.last_name) DESC`

Expand Down
1 change: 1 addition & 0 deletions internal/service/attendeesrv/attendeesrv.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (s *AttendeeServiceImplData) RegisterNewAttendee(ctx context.Context, atten
} else {
// record which user owns this attendee
attendee.Identity = ctxvalues.Subject(ctx)
attendee.Avatar = ctxvalues.Avatar(ctx) // if an admin registers a guest, they'll get the admin's avatar, but that's "correct", as the admin is now also the owner of the registration
}

if config.RequireLoginForReg() {
Expand Down
20 changes: 17 additions & 3 deletions internal/service/attendeesrv/dues.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/eurofurence/reg-attendee-service/internal/repository/config"
"github.com/eurofurence/reg-attendee-service/internal/repository/database"
"github.com/eurofurence/reg-attendee-service/internal/repository/paymentservice"
"github.com/eurofurence/reg-attendee-service/internal/web/util/ctxvalues"
"strconv"
"strings"
)
Expand Down Expand Up @@ -207,13 +208,15 @@ func (s *AttendeeServiceImplData) UpdateAttendeeCacheAndCalculateResultingStatus
identity := s.suffixForDeletedAttendees(attendee, newStatus, attendee.Identity)
zip := s.suffixForDeletedAttendees(attendee, newStatus, attendee.Zip)

avatar := s.avatarIfMatchingUser(ctx, identity)

dues, payments, open, dueDate := s.balances(updatedTransactionHistory)
// never move due date back in time (allows manual override)
if attendee.CacheDueDate != "" && attendee.CacheDueDate > dueDate {
dueDate = attendee.CacheDueDate
}

duesInformationChanged, err := s.updateCachedValuesAndIdentityInAttendee(ctx, attendee, dues, payments, open, dueDate, identity, zip)
duesInformationChanged, err := s.updateCachedValuesAndIdentityInAttendee(ctx, attendee, dues, payments, open, dueDate, identity, avatar, zip)
if err != nil {
return newStatus, false, err
}
Expand All @@ -239,15 +242,23 @@ func (s *AttendeeServiceImplData) suffixForDeletedAttendees(attendee *entity.Att
return value
}

func (s *AttendeeServiceImplData) updateCachedValuesAndIdentityInAttendee(ctx context.Context, attendee *entity.Attendee, dues int64, payments int64, open int64, dueDate string, identity string, zip string) (bool, error) {
func (s *AttendeeServiceImplData) avatarIfMatchingUser(ctx context.Context, identity string) string {
if identity == ctxvalues.Subject(ctx) {
return ctxvalues.Avatar(ctx)
}
return ""
}

func (s *AttendeeServiceImplData) updateCachedValuesAndIdentityInAttendee(ctx context.Context, attendee *entity.Attendee, dues int64, payments int64, open int64, dueDate string, identity string, avatar string, zip string) (bool, error) {
duesRelevantUpdate := attendee.CacheTotalDues != dues ||
attendee.CachePaymentBalance != payments ||
attendee.CacheDueDate != dueDate

needsUpdate := duesRelevantUpdate ||
attendee.CacheOpenBalance != open ||
attendee.Identity != identity ||
attendee.Zip != zip
attendee.Zip != zip ||
(avatar != "" && attendee.Avatar != avatar)

if needsUpdate {
attendee.CacheTotalDues = dues
Expand All @@ -256,6 +267,9 @@ func (s *AttendeeServiceImplData) updateCachedValuesAndIdentityInAttendee(ctx co
attendee.CacheDueDate = dueDate
attendee.Identity = identity
attendee.Zip = zip
if avatar != "" {
attendee.Avatar = avatar
}
err := database.GetRepository().UpdateAttendee(ctx, attendee)
return duesRelevantUpdate, err
}
Expand Down
13 changes: 12 additions & 1 deletion internal/service/attendeesrv/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/eurofurence/reg-attendee-service/internal/api/v1/attendee"
"github.com/eurofurence/reg-attendee-service/internal/api/v1/status"
"github.com/eurofurence/reg-attendee-service/internal/entity"
"github.com/eurofurence/reg-attendee-service/internal/repository/config"
"github.com/eurofurence/reg-attendee-service/internal/repository/database"
Expand Down Expand Up @@ -31,7 +32,7 @@ func (s *AttendeeServiceImplData) mapToAttendeeSearchResult(att *entity.Attendee
if len(fillFields) == 0 {
fillFields = []string{"nickname", "name", "country", "spoken_languages", "email", "telegram", "birthday", "pronouns",
"tshirt_size", "flags", "options", "packages", "user_comments", "status",
"total_dues", "payment_balance", "current_dues", "due_date", "registered", "admin_comments"}
"total_dues", "payment_balance", "current_dues", "due_date", "registered", "admin_comments", "avatar"}
}

var currentDues = att.CacheTotalDues - att.CachePaymentBalance
Expand All @@ -41,6 +42,14 @@ func (s *AttendeeServiceImplData) mapToAttendeeSearchResult(att *entity.Attendee
options := removeWrappingCommas(att.Options)
packagesList := sortedPackageListFromCommaSeparatedWithCounts(removeWrappingCommas(att.Packages))
packages := packagesFromPackagesList(packagesList)
identity := ""
if att.Status != status.Deleted {
identity = att.Identity
}
avatar := att.Avatar
if avatar != "" {
avatar = config.AvatarBaseUrl() + avatar
}
return attendee.AttendeeSearchResult{
Id: att.ID,
BadgeId: s.badgeId(att.ID),
Expand Down Expand Up @@ -77,6 +86,8 @@ func (s *AttendeeServiceImplData) mapToAttendeeSearchResult(att *entity.Attendee
DueDate: contains(n(att.CacheDueDate), fillFields, "all", "balances", "due_date"),
Registered: contains(n(registered), fillFields, "all", "registered"),
AdminComments: contains(n(att.AdminComments), fillFields, "all", "admin_comments"),
IdentitySubject: contains(n(identity), fillFields, "all", "identity_subject"),
Avatar: contains(n(avatar), fillFields, "all", "avatar"),
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/web/controller/adminctl/adminctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ 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",
"total_dues", "payment_balance", "current_dues", "identity_subject", "avatar",
}

result := make([]string, 0)
Expand Down
2 changes: 2 additions & 0 deletions internal/web/middleware/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ type CustomClaims struct {
EmailVerified bool `json:"email_verified"`
Groups []string `json:"groups,omitempty"`
Name string `json:"name"`
Avatar string `json:"avatar"`
}

type AllClaims struct {
Expand Down Expand Up @@ -153,6 +154,7 @@ func checkIdToken_MustReturnOnError(ctx context.Context, idTokenValue string) (s
ctxvalues.SetEmailVerified(ctx, parsedClaims.EmailVerified)
ctxvalues.SetName(ctx, parsedClaims.Name)
ctxvalues.SetSubject(ctx, parsedClaims.Subject)
ctxvalues.SetAvatar(ctx, parsedClaims.Avatar)
for _, group := range parsedClaims.Groups {
ctxvalues.SetAuthorizedAsGroup(ctx, group)
}
Expand Down
9 changes: 9 additions & 0 deletions internal/web/util/ctxvalues/accessors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const ContextEmail = "email"
const ContextEmailVerified = "emailverified"
const ContextName = "name"
const ContextSubject = "subject"
const ContextAvatar = "avatar"

func CreateContextWithValueMap(ctx context.Context) context.Context {
// this is so we can add values to our context, like ... I don't know ... the http status from the response!
Expand Down Expand Up @@ -108,6 +109,14 @@ func SetSubject(ctx context.Context, Subject string) {
setValue(ctx, ContextSubject, Subject)
}

func Avatar(ctx context.Context) string {
return valueOrDefault(ctx, ContextAvatar, "")
}

func SetAvatar(ctx context.Context, avatar string) {
setValue(ctx, ContextAvatar, avatar)
}

func HasApiToken(ctx context.Context) bool {
v := valueOrDefault(ctx, ContextApiToken, "")
return v == config.FixedApiToken()
Expand Down
9 changes: 6 additions & 3 deletions test/acceptance/admin_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,8 @@ func TestSearch_RegdeskOk(t *testing.T) {
"status": "approved",
"total_dues": 25500,
"payment_balance": 0,
"current_dues": 25500
"current_dues": 25500,
"identity_subject": "1234567890"
}
]
}`
Expand Down Expand Up @@ -1194,7 +1195,8 @@ func TestSearch_SponsordeskOk(t *testing.T) {
"status": "paid",
"total_dues": 25500,
"payment_balance": 25500,
"current_dues": 0
"current_dues": 0,
"identity_subject": "1234567890"
}
]
}`
Expand Down Expand Up @@ -1396,7 +1398,8 @@ func TestSearch_AdminOk(t *testing.T) {
"status": "new",
"total_dues": 0,
"payment_balance": 0,
"current_dues": 0
"current_dues": 0,
"identity_subject": "1234567890"
}
]
}`
Expand Down
Loading