1package user
2
3import (
4 "errors"
5 "strings"
6)
7
8// User is an entity representing a system user account
9type User struct {
10 username string
11 groups []string
12 shell string
13 home string
14}
15
16// NewUser creates a new User entity with validation
17func NewUser(username string, shell string) (*User, error) {
18 // Validate username
19 if err := ValidateUsername(username); err != nil {
20 return nil, err
21 }
22
23 // Validate shell
24 if err := ValidateShell(shell); err != nil {
25 return nil, err
26 }
27
28 user := &User{
29 username: username,
30 shell: shell,
31 groups: []string{},
32 home: "/home/" + username,
33 }
34
35 return user, nil
36}
37
38// Username returns the username
39func (u *User) Username() string {
40 return u.username
41}
42
43// Shell returns the login shell
44func (u *User) Shell() string {
45 return u.shell
46}
47
48// Home returns the home directory path
49func (u *User) Home() string {
50 return u.home
51}
52
53// Groups returns a copy of the user groups list
54func (u *User) Groups() []string {
55 groups := make([]string, len(u.groups))
56 copy(groups, u.groups)
57 return groups
58}
59
60// AddGroup adds a user to a group
61func (u *User) AddGroup(group string) error {
62 if group == "" {
63 return errors.New("group name cannot be empty")
64 }
65
66 // Check if already a member
67 for _, g := range u.groups {
68 if g == group {
69 return errors.New("user already in group " + group)
70 }
71 }
72
73 u.groups = append(u.groups, group)
74 return nil
75}
76
77// HasGroup checks if user is in a group
78func (u *User) HasGroup(group string) bool {
79 for _, g := range u.groups {
80 if g == group {
81 return true
82 }
83 }
84 return false
85}
86
87// String returns human-readable representation
88func (u *User) String() string {
89 return "User(username=" + u.username + ", shell=" + u.shell + ", groups=" + strings.Join(u.groups, ",") + ")"
90}
91
92// ValidateUsername validates username format
93// Usernames: lowercase letters, digits, underscore, hyphen (3-32 chars)
94func ValidateUsername(username string) error {
95 if username == "" {
96 return errors.New("username cannot be empty")
97 }
98
99 if len(username) < 3 {
100 return errors.New("username must be at least 3 characters")
101 }
102
103 if len(username) > 32 {
104 return errors.New("username cannot exceed 32 characters")
105 }
106
107 // Must start with lowercase letter
108 if username[0] < 'a' || username[0] > 'z' {
109 return errors.New("username must start with lowercase letter")
110 }
111
112 // Can contain lowercase, digits, underscore, hyphen
113 for _, ch := range username {
114 if !isUsernameChar(ch) {
115 return errors.New("username contains invalid characters")
116 }
117 }
118
119 return nil
120}
121
122func isUsernameChar(ch rune) bool {
123 switch {
124 case ch >= 'a' && ch <= 'z':
125 return true
126 case ch >= '0' && ch <= '9':
127 return true
128 case ch == '_' || ch == '-':
129 return true
130 default:
131 return false
132 }
133}
134
135// ValidateShell validates shell path
136func ValidateShell(shell string) error {
137 if shell == "" {
138 return errors.New("shell cannot be empty")
139 }
140
141 if !strings.HasPrefix(shell, "/") {
142 return errors.New("shell path must be absolute")
143 }
144
145 // Common valid shells
146 validShells := map[string]bool{
147 "/bin/bash": true,
148 "/bin/sh": true,
149 "/bin/zsh": true,
150 "/bin/fish": true,
151 "/usr/bin/zsh": true,
152 "/usr/bin/fish": true,
153 }
154
155 if validShells[shell] {
156 return nil
157 }
158
159 // Allow other shells if they look reasonable
160 if len(shell) > 256 {
161 return errors.New("shell path too long")
162 }
163
164 return nil
165}