package userbase import ( "bytes" "encoding/json" "fmt" "time" ) type userV3 struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` DisplayName string `json:"display_name"` PasswordHash []byte `json:"password_hash"` Disabled bool `json:"disabled"` UIDNumber int64 `json:"uid_number"` CreatedAt int64 `json:"created_at"` } type googleLinkV3 struct { UserID string `json:"user_id"` GoogleSub string `json:"google_sub"` GoogleEmailSnapshot string `json:"google_email_snapshot"` LinkedAt int64 `json:"linked_at"` } type invitationV3 struct { ID string `json:"id"` TokenHash []byte `json:"token_hash"` Email *string `json:"email"` CreatedBy *string `json:"created_by"` CreatedAt int64 `json:"created_at"` ExpiresAt int64 `json:"expires_at"` UsedBy *string `json:"used_by"` UsedAt *int64 `json:"used_at"` RevokedAt *int64 `json:"revoked_at"` Label string `json:"label"` BatchID *string `json:"batch_id"` } type documentV3 struct { ExportVersion int `json:"export_version"` ExportedAt int64 `json:"exported_at"` NextUIDNumber int64 `json:"next_uid_number"` NextGIDNumber int64 `json:"next_gid_number"` Users []userV3 `json:"users"` Groups []group `json:"groups"` Memberships []membership `json:"memberships"` GoogleLinks []googleLinkV3 `json:"google_links"` Invitations []invitationV3 `json:"invitations"` InvitationGroups []invitationGroup `json:"invitation_groups"` PendingConfirmations []pendingConfirmation `json:"pending_confirmations"` SSHKeys []sshKey `json:"ssh_keys"` DexKeys json.RawMessage `json:"dex_keys,omitempty"` } func userV3Key(u userV3) (string, string) { return u.ID, "" } func googleLinkV3Key(l googleLinkV3) (string, string) { return l.UserID, "" } func invitationV3Key(i invitationV3) (string, string) { return i.ID, "" } func (d documentV3) sorted() documentV3 { d.Users = sortedRows(d.Users, userV3Key) d.Groups = sortedRows(d.Groups, groupKey) d.Memberships = sortedRows(d.Memberships, membershipKey) d.GoogleLinks = sortedRows(d.GoogleLinks, googleLinkV3Key) d.Invitations = sortedRows(d.Invitations, invitationV3Key) d.InvitationGroups = sortedRows(d.InvitationGroups, invitationGroupKey) d.PendingConfirmations = sortedRows(d.PendingConfirmations, pendingKey) d.SSHKeys = sortedRows(d.SSHKeys, sshKeyKey) return d } func (d documentV3) withoutAddresses() document { doc := document{ ExportVersion: ExportVersion, ExportedAt: d.ExportedAt, NextUIDNumber: d.NextUIDNumber, NextGIDNumber: d.NextGIDNumber, Groups: d.Groups, Memberships: d.Memberships, InvitationGroups: d.InvitationGroups, PendingConfirmations: d.PendingConfirmations, SSHKeys: d.SSHKeys, DexKeys: d.DexKeys, } for _, u := range d.Users { doc.Users = append(doc.Users, user{ID: u.ID, Username: u.Username, DisplayName: u.DisplayName, PasswordHash: u.PasswordHash, Disabled: u.Disabled, UIDNumber: u.UIDNumber, CreatedAt: u.CreatedAt}) } for _, l := range d.GoogleLinks { doc.GoogleLinks = append(doc.GoogleLinks, googleLink{UserID: l.UserID, GoogleSub: l.GoogleSub, LinkedAt: l.LinkedAt}) } for _, i := range d.Invitations { doc.Invitations = append(doc.Invitations, invitation{ID: i.ID, TokenHash: i.TokenHash, CreatedBy: i.CreatedBy, CreatedAt: i.CreatedAt, ExpiresAt: i.ExpiresAt, UsedBy: i.UsedBy, UsedAt: i.UsedAt, RevokedAt: i.RevokedAt, Label: i.Label, BatchID: i.BatchID}) } return doc } func decodeLegacy(data []byte, version int, doc any, canonical func() any, dexKeys func() json.RawMessage, rows func() document) (Document, error) { decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() if err := decoder.Decode(doc); err != nil { return Document{}, fmt.Errorf("userbase: decode: %w", err) } if keys := dexKeys(); len(keys) > 0 && keys[0] != '{' { return Document{}, fmt.Errorf("userbase: decode: dex_keys is not a JSON object") } again, err := marshal(canonical()) if err != nil { return Document{}, err } if !bytes.Equal(again, data) { return Document{}, ErrNotCanonical } upgraded := rows() return Document{Version: version, Snapshot: toSnapshot(upgraded), ExportedAt: time.UnixMicro(upgraded.ExportedAt).UTC(), DexKeys: upgraded.DexKeys}, nil } func decodeV3(data []byte) (Document, error) { var doc documentV3 out, err := decodeLegacy(data, 3, &doc, func() any { return doc.sorted() }, func() json.RawMessage { return doc.DexKeys }, func() document { return doc.withoutAddresses() }) if err != nil { return Document{}, err } if err := checkSSHKeys(doc.SSHKeys); err != nil { return Document{}, err } return out, nil }