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}