Raw
  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}