Raw
  1package storage
  2
  3import (
  4	"crypto/rand"
  5	"encoding/hex"
  6	"fmt"
  7	"net/netip"
  8	"regexp"
  9	"strings"
 10	"time"
 11	"unicode"
 12	"unicode/utf8"
 13)
 14
 15const (
 16	firstUID = 10000
 17	firstGID = 20000
 18	lastUID  = 19999
 19	lastGID  = 2147483647
 20)
 21
 22var (
 23	usernamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,31}$`)
 24	groupPattern    = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,31}$`)
 25)
 26
 27type User struct {
 28	ID               string
 29	Username         string
 30	DisplayName      string
 31	PasswordHash     []byte
 32	Disabled         bool
 33	UIDNumber        int
 34	CreatedAt        time.Time
 35	LastLoginAt      *time.Time
 36	CredentialsEpoch int64
 37}
 38
 39func (u User) HomeDirectory() string {
 40	return "/home/" + u.Username
 41}
 42
 43type CreateUserInput struct {
 44	Username     string
 45	DisplayName  string
 46	PasswordHash []byte
 47	Disabled     bool
 48}
 49
 50type UpdateUserInput struct {
 51	DisplayName  string
 52	PasswordHash []byte
 53	Disabled     bool
 54	LastLoginAt  *time.Time
 55}
 56
 57type Group struct {
 58	ID          string
 59	Name        string
 60	Description string
 61	GIDNumber   int
 62	CreatedAt   time.Time
 63}
 64
 65type CreateGroupInput struct {
 66	Name        string
 67	Description string
 68}
 69
 70type GoogleLink struct {
 71	UserID    string
 72	GoogleSub string
 73	LinkedAt  time.Time
 74}
 75
 76type CreateGoogleLinkInput struct {
 77	UserID    string
 78	GoogleSub string
 79}
 80
 81type Invitation struct {
 82	ID        string
 83	TokenHash []byte
 84	CreatedBy *string
 85	CreatedAt time.Time
 86	ExpiresAt time.Time
 87	UsedBy    *string
 88	UsedAt    *time.Time
 89	RevokedAt *time.Time
 90}
 91
 92type CreateInvitationInput struct {
 93	TokenHash []byte
 94	CreatedBy *string
 95	ExpiresAt time.Time
 96}
 97
 98type AuditEvent struct {
 99	ID         int64
100	Actor      string
101	Action     string
102	Subject    string
103	OccurredAt time.Time
104	Detail     string
105	Address    netip.Addr
106}
107
108type AppendAuditInput struct {
109	Actor   string
110	Action  string
111	Subject string
112	Detail  string
113	Address netip.Addr
114}
115
116type AuditFilter struct {
117	Actor   string
118	Action  string
119	Subject string
120	After   *time.Time
121	Before  *time.Time
122	Limit   int
123}
124
125type ForwardSession struct {
126	ID                string
127	UserID            string
128	AppDomain         string
129	CreatedAt         time.Time
130	ExpiresAt         time.Time
131	Revoked           bool
132	ReauthenticatedAt *time.Time
133	Browser           string
134	ClientID          string
135	Address           netip.Addr
136	UserAgent         string
137	LastSeenAt        *time.Time
138}
139
140type CreateForwardSessionInput struct {
141	ID        string
142	UserID    string
143	AppDomain string
144	CreatedAt time.Time
145	ExpiresAt time.Time
146	Epoch     *int64
147	Browser   string
148	ClientID  string
149	Address   netip.Addr
150	UserAgent string
151}
152
153type Checkpoint struct {
154	Value string
155	Set   bool
156}
157
158func newUUID() (string, error) {
159	var value [16]byte
160	if _, err := rand.Read(value[:]); err != nil {
161		return "", fmt.Errorf("generate UUID: %w", err)
162	}
163	value[6] = value[6]&0x0f | 0x40
164	value[8] = value[8]&0x3f | 0x80
165	encoded := make([]byte, 32)
166	hex.Encode(encoded, value[:])
167	return string(encoded[0:8]) + "-" + string(encoded[8:12]) + "-" + string(encoded[12:16]) + "-" + string(encoded[16:20]) + "-" + string(encoded[20:32]), nil
168}
169
170func normalizeUsername(value string) (string, error) {
171	value = strings.ToLower(value)
172	if !usernamePattern.MatchString(value) {
173		return "", fmt.Errorf("username does not match required grammar")
174	}
175	return value, nil
176}
177
178func normalizeGroupName(value string) (string, error) {
179	value = strings.ToLower(value)
180	if !groupPattern.MatchString(value) {
181		return "", fmt.Errorf("group name does not match required grammar")
182	}
183	return value, nil
184}
185
186func validateDisplayName(value string) error {
187	if !utf8.ValidString(value) {
188		return fmt.Errorf("display name is not valid UTF-8")
189	}
190	if len(value) > 128 {
191		return fmt.Errorf("display name exceeds 128 bytes")
192	}
193	for _, r := range value {
194		if unicode.IsControl(r) {
195			return fmt.Errorf("display name contains a control character")
196		}
197	}
198	if value != "" && strings.IndexFunc(value, visibleRune) < 0 {
199		return fmt.Errorf("display name has no visible character")
200	}
201	return nil
202}
203
204func visibleRune(r rune) bool {
205	return !unicode.IsSpace(r) && !unicode.In(r, unicode.Cf, unicode.Variation_Selector, unicode.Other_Default_Ignorable_Code_Point)
206}
207
208func requireValue(name, value string) error {
209	if strings.TrimSpace(value) == "" {
210		return fmt.Errorf("%s is empty", name)
211	}
212	if !utf8.ValidString(value) {
213		return fmt.Errorf("%s is not valid UTF-8", name)
214	}
215	return nil
216}
217
218func validateGroupDescription(value string) error {
219	if !utf8.ValidString(value) {
220		return fmt.Errorf("group description is not valid UTF-8")
221	}
222	return nil
223}
224
225func timeToInt(value time.Time) int64 {
226	return value.UTC().UnixMicro()
227}
228
229func intToTime(value int64) time.Time {
230	return time.UnixMicro(value).UTC()
231}
232
233func canonicalTime(value time.Time) time.Time {
234	return intToTime(timeToInt(value))
235}
236
237func boolToInt(value bool) int {
238	if value {
239		return 1
240	}
241	return 0
242}