package storage import ( "crypto/rand" "encoding/hex" "fmt" "net/netip" "regexp" "strings" "time" "unicode" "unicode/utf8" ) const ( firstUID = 10000 firstGID = 20000 lastUID = 19999 lastGID = 2147483647 ) var ( usernamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,31}$`) groupPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,31}$`) ) type User struct { ID string Username string DisplayName string PasswordHash []byte Disabled bool UIDNumber int CreatedAt time.Time LastLoginAt *time.Time CredentialsEpoch int64 } func (u User) HomeDirectory() string { return "/home/" + u.Username } type CreateUserInput struct { Username string DisplayName string PasswordHash []byte Disabled bool } type UpdateUserInput struct { DisplayName string PasswordHash []byte Disabled bool LastLoginAt *time.Time } type Group struct { ID string Name string Description string GIDNumber int CreatedAt time.Time } type CreateGroupInput struct { Name string Description string } type GoogleLink struct { UserID string GoogleSub string LinkedAt time.Time } type CreateGoogleLinkInput struct { UserID string GoogleSub string } type Invitation struct { ID string TokenHash []byte CreatedBy *string CreatedAt time.Time ExpiresAt time.Time UsedBy *string UsedAt *time.Time RevokedAt *time.Time } type CreateInvitationInput struct { TokenHash []byte CreatedBy *string ExpiresAt time.Time } type AuditEvent struct { ID int64 Actor string Action string Subject string OccurredAt time.Time Detail string Address netip.Addr } type AppendAuditInput struct { Actor string Action string Subject string Detail string Address netip.Addr } type AuditFilter struct { Actor string Action string Subject string After *time.Time Before *time.Time Limit int } type ForwardSession struct { ID string UserID string AppDomain string CreatedAt time.Time ExpiresAt time.Time Revoked bool ReauthenticatedAt *time.Time Browser string ClientID string Address netip.Addr UserAgent string LastSeenAt *time.Time } type CreateForwardSessionInput struct { ID string UserID string AppDomain string CreatedAt time.Time ExpiresAt time.Time Epoch *int64 Browser string ClientID string Address netip.Addr UserAgent string } type Checkpoint struct { Value string Set bool } func newUUID() (string, error) { var value [16]byte if _, err := rand.Read(value[:]); err != nil { return "", fmt.Errorf("generate UUID: %w", err) } value[6] = value[6]&0x0f | 0x40 value[8] = value[8]&0x3f | 0x80 encoded := make([]byte, 32) hex.Encode(encoded, value[:]) return string(encoded[0:8]) + "-" + string(encoded[8:12]) + "-" + string(encoded[12:16]) + "-" + string(encoded[16:20]) + "-" + string(encoded[20:32]), nil } func normalizeUsername(value string) (string, error) { value = strings.ToLower(value) if !usernamePattern.MatchString(value) { return "", fmt.Errorf("username does not match required grammar") } return value, nil } func normalizeGroupName(value string) (string, error) { value = strings.ToLower(value) if !groupPattern.MatchString(value) { return "", fmt.Errorf("group name does not match required grammar") } return value, nil } func validateDisplayName(value string) error { if !utf8.ValidString(value) { return fmt.Errorf("display name is not valid UTF-8") } if len(value) > 128 { return fmt.Errorf("display name exceeds 128 bytes") } for _, r := range value { if unicode.IsControl(r) { return fmt.Errorf("display name contains a control character") } } if value != "" && strings.IndexFunc(value, visibleRune) < 0 { return fmt.Errorf("display name has no visible character") } return nil } func visibleRune(r rune) bool { return !unicode.IsSpace(r) && !unicode.In(r, unicode.Cf, unicode.Variation_Selector, unicode.Other_Default_Ignorable_Code_Point) } func requireValue(name, value string) error { if strings.TrimSpace(value) == "" { return fmt.Errorf("%s is empty", name) } if !utf8.ValidString(value) { return fmt.Errorf("%s is not valid UTF-8", name) } return nil } func validateGroupDescription(value string) error { if !utf8.ValidString(value) { return fmt.Errorf("group description is not valid UTF-8") } return nil } func timeToInt(value time.Time) int64 { return value.UTC().UnixMicro() } func intToTime(value int64) time.Time { return time.UnixMicro(value).UTC() } func canonicalTime(value time.Time) time.Time { return intToTime(timeToInt(value)) } func boolToInt(value bool) int { if value { return 1 } return 0 }