1package userbase
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "time"
8)
9
10type userV3 struct {
11 ID string `json:"id"`
12 Username string `json:"username"`
13 Email string `json:"email"`
14 EmailVerified bool `json:"email_verified"`
15 DisplayName string `json:"display_name"`
16 PasswordHash []byte `json:"password_hash"`
17 Disabled bool `json:"disabled"`
18 UIDNumber int64 `json:"uid_number"`
19 CreatedAt int64 `json:"created_at"`
20}
21
22type googleLinkV3 struct {
23 UserID string `json:"user_id"`
24 GoogleSub string `json:"google_sub"`
25 GoogleEmailSnapshot string `json:"google_email_snapshot"`
26 LinkedAt int64 `json:"linked_at"`
27}
28
29type invitationV3 struct {
30 ID string `json:"id"`
31 TokenHash []byte `json:"token_hash"`
32 Email *string `json:"email"`
33 CreatedBy *string `json:"created_by"`
34 CreatedAt int64 `json:"created_at"`
35 ExpiresAt int64 `json:"expires_at"`
36 UsedBy *string `json:"used_by"`
37 UsedAt *int64 `json:"used_at"`
38 RevokedAt *int64 `json:"revoked_at"`
39 Label string `json:"label"`
40 BatchID *string `json:"batch_id"`
41}
42
43type documentV3 struct {
44 ExportVersion int `json:"export_version"`
45 ExportedAt int64 `json:"exported_at"`
46 NextUIDNumber int64 `json:"next_uid_number"`
47 NextGIDNumber int64 `json:"next_gid_number"`
48 Users []userV3 `json:"users"`
49 Groups []group `json:"groups"`
50 Memberships []membership `json:"memberships"`
51 GoogleLinks []googleLinkV3 `json:"google_links"`
52 Invitations []invitationV3 `json:"invitations"`
53 InvitationGroups []invitationGroup `json:"invitation_groups"`
54 PendingConfirmations []pendingConfirmation `json:"pending_confirmations"`
55 SSHKeys []sshKey `json:"ssh_keys"`
56 DexKeys json.RawMessage `json:"dex_keys,omitempty"`
57}
58
59func userV3Key(u userV3) (string, string) { return u.ID, "" }
60func googleLinkV3Key(l googleLinkV3) (string, string) { return l.UserID, "" }
61func invitationV3Key(i invitationV3) (string, string) { return i.ID, "" }
62
63func (d documentV3) sorted() documentV3 {
64 d.Users = sortedRows(d.Users, userV3Key)
65 d.Groups = sortedRows(d.Groups, groupKey)
66 d.Memberships = sortedRows(d.Memberships, membershipKey)
67 d.GoogleLinks = sortedRows(d.GoogleLinks, googleLinkV3Key)
68 d.Invitations = sortedRows(d.Invitations, invitationV3Key)
69 d.InvitationGroups = sortedRows(d.InvitationGroups, invitationGroupKey)
70 d.PendingConfirmations = sortedRows(d.PendingConfirmations, pendingKey)
71 d.SSHKeys = sortedRows(d.SSHKeys, sshKeyKey)
72 return d
73}
74
75func (d documentV3) withoutAddresses() document {
76 doc := document{
77 ExportVersion: ExportVersion,
78 ExportedAt: d.ExportedAt,
79 NextUIDNumber: d.NextUIDNumber,
80 NextGIDNumber: d.NextGIDNumber,
81 Groups: d.Groups,
82 Memberships: d.Memberships,
83 InvitationGroups: d.InvitationGroups,
84 PendingConfirmations: d.PendingConfirmations,
85 SSHKeys: d.SSHKeys,
86 DexKeys: d.DexKeys,
87 }
88 for _, u := range d.Users {
89 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})
90 }
91 for _, l := range d.GoogleLinks {
92 doc.GoogleLinks = append(doc.GoogleLinks, googleLink{UserID: l.UserID, GoogleSub: l.GoogleSub, LinkedAt: l.LinkedAt})
93 }
94 for _, i := range d.Invitations {
95 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})
96 }
97 return doc
98}
99
100func decodeLegacy(data []byte, version int, doc any, canonical func() any, dexKeys func() json.RawMessage, rows func() document) (Document, error) {
101 decoder := json.NewDecoder(bytes.NewReader(data))
102 decoder.DisallowUnknownFields()
103 if err := decoder.Decode(doc); err != nil {
104 return Document{}, fmt.Errorf("userbase: decode: %w", err)
105 }
106 if keys := dexKeys(); len(keys) > 0 && keys[0] != '{' {
107 return Document{}, fmt.Errorf("userbase: decode: dex_keys is not a JSON object")
108 }
109 again, err := marshal(canonical())
110 if err != nil {
111 return Document{}, err
112 }
113 if !bytes.Equal(again, data) {
114 return Document{}, ErrNotCanonical
115 }
116 upgraded := rows()
117 return Document{Version: version, Snapshot: toSnapshot(upgraded), ExportedAt: time.UnixMicro(upgraded.ExportedAt).UTC(), DexKeys: upgraded.DexKeys}, nil
118}
119
120func decodeV3(data []byte) (Document, error) {
121 var doc documentV3
122 out, err := decodeLegacy(data, 3, &doc, func() any { return doc.sorted() }, func() json.RawMessage { return doc.DexKeys }, func() document { return doc.withoutAddresses() })
123 if err != nil {
124 return Document{}, err
125 }
126 if err := checkSSHKeys(doc.SSHKeys); err != nil {
127 return Document{}, err
128 }
129 return out, nil
130}