1// Copyright 2010 Jonas mg
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7package user
8
9import (
10	"fmt"
11	"os"
12	"reflect"
13	"strings"
14)
15
16type gshadowField int
17
18// Field names for shadowed group database.
19const (
20	GS_NAME gshadowField = 1 << iota
21	GS_PASSWD
22	GS_ADMIN
23	GS_MEMBER
24
25	GS_ALL
26)
27
28func (f gshadowField) String() string {
29	switch f {
30	case GS_NAME:
31		return "Name"
32	case GS_PASSWD:
33		return "Passwd"
34	case GS_ADMIN:
35		return "Admin"
36	case GS_MEMBER:
37		return "Member"
38	}
39	return "ALL"
40}
41
42// A GShadow represents the format of the shadowed information for a group account.
43type GShadow struct {
44	// Group name. (Unique)
45	//
46	// It must be a valid group name, which exist on the system.
47	Name string
48
49	// Hashed password
50	//
51	// If the password field contains some string that is not a valid result of
52	// crypt, for instance "!" or "*", users will not be able to use a unix
53	// password to access the group (but group members do not need the password).
54	//
55	// The password is used when an user who is not a member of the group wants
56	// to gain the permissions of this group (see "newgrp(1)").
57	//
58	// This field may be empty, in which case only the group members can gain
59	// the group permissions.
60	//
61	// A password field which starts with a exclamation mark means that the
62	// password is locked. The remaining characters on the line represent the
63	// password field before the password was locked.
64	//
65	// This password supersedes any password specified in '/etc/group'.
66	password string
67
68	// Group administrator list
69	//
70	// It must be a comma-separated list of user names.
71	//
72	// Administrators can change the password or the members of the group.
73	// Administrators also have the same permissions as the members (see below).
74	AdminList []string
75
76	// Group member list
77	//
78	// It must be a comma-separated list of user names.
79	//
80	// Members can access the group without being prompted for a password.
81	// You should use the same list of users as in /etc/group.
82	UserList []string
83}
84
85// NewGShadow returns a new GShadow.
86func NewGShadow(username string, members ...string) *GShadow {
87	return &GShadow{
88		Name:     username,
89		UserList: members,
90	}
91}
92
93func (gs *GShadow) filename() string { return _GSHADOW_FILE }
94
95func (gs *GShadow) String() string {
96	return fmt.Sprintf("%s:%s:%s:%s\n",
97		gs.Name, gs.password, strings.Join(gs.AdminList, ","), strings.Join(gs.UserList, ","))
98}
99
100// parseGShadow parses the row of a group shadow.
101func parseGShadow(row string) (*GShadow, error) {
102	fields := strings.Split(row, ":")
103	if len(fields) != 4 {
104		return nil, rowError{_GSHADOW_FILE, row}
105	}
106
107	return &GShadow{
108		fields[0],
109		fields[1],
110		strings.Split(fields[2], ","),
111		strings.Split(fields[3], ","),
112	}, nil
113}
114
115// == Lookup
116//
117
118// lookUp parses the shadowed group line searching a value into the field.
119// Returns nil if it isn't found.
120func (*GShadow) lookUp(line string, f field, value interface{}) interface{} {
121	_field := f.(gshadowField)
122	_value := value.(string)
123	allField := strings.Split(line, ":")
124	arrayField := make(map[int][]string)
125
126	arrayField[2] = strings.Split(allField[2], ",")
127	arrayField[3] = strings.Split(allField[3], ",")
128
129	// Check fields
130	var isField bool
131	if GS_NAME&_field != 0 && allField[0] == _value {
132		isField = true
133	} else if GS_PASSWD&_field != 0 && allField[1] == _value {
134		isField = true
135	} else if GS_ADMIN&_field != 0 && checkGroup(arrayField[2], _value) {
136		isField = true
137	} else if GS_MEMBER&_field != 0 && checkGroup(arrayField[3], _value) {
138		isField = true
139	} else if GS_ALL&_field != 0 {
140		isField = true
141	}
142
143	if isField {
144		return &GShadow{
145			allField[0],
146			allField[1],
147			arrayField[2],
148			arrayField[3],
149		}
150	}
151	return nil
152}
153
154// LookupGShadow looks up a shadowed group by name.
155func LookupGShadow(name string) (*GShadow, error) {
156	entries, err := LookupInGShadow(GS_NAME, name, 1)
157	if err != nil {
158		return nil, err
159	}
160
161	return entries[0], err
162}
163
164// LookupInGShadow looks up a shadowed group by the given values.
165//
166// The count determines the number of fields to return:
167//   n > 0: at most n fields
168//   n == 0: the result is nil (zero fields)
169//   n < 0: all fields
170func LookupInGShadow(field gshadowField, value string, n int) ([]*GShadow, error) {
171	checkRoot()
172
173	iEntries, err := lookUp(&GShadow{}, field, value, n)
174	if err != nil {
175		return nil, err
176	}
177
178	// == Convert to type GShadow
179	valueSlice := reflect.ValueOf(iEntries)
180	entries := make([]*GShadow, valueSlice.Len())
181
182	for i := 0; i < valueSlice.Len(); i++ {
183		entries[i] = valueSlice.Index(i).Interface().(*GShadow)
184	}
185
186	return entries, err
187}
188
189// == Editing
190//
191
192// Add adds a new shadowed group.
193// If the key is not nil, generates a hashed password.
194//
195// It is created a backup before of modify the original file.
196func (gs *GShadow) Add(key []byte) (err error) {
197	loadConfig()
198
199	gshadow, err := LookupGShadow(gs.Name)
200	if err != nil {
201		if _, ok := err.(NoFoundError); !ok {
202			return
203		}
204	}
205	if gshadow != nil {
206		return ErrGroupExist
207	}
208
209	if gs.Name == "" {
210		return RequiredError("Name")
211	}
212
213	// Backup
214	if err = backup(_GSHADOW_FILE); err != nil {
215		return
216	}
217
218	db, err := openDBFile(_GSHADOW_FILE, os.O_WRONLY|os.O_APPEND)
219	if err != nil {
220		return
221	}
222	defer func() {
223		e := db.close()
224		if e != nil && err == nil {
225			err = e
226		}
227	}()
228
229	if key != nil {
230		gs.password, _ = config.crypter.Generate(key, nil)
231	} else {
232		gs.password = "*" // Password disabled.
233	}
234
235	_, err = db.file.WriteString(gs.String())
236	return
237}
238