1 /*
2     Copyright (c) 2019, DUNEX Contributors
3     Use, modification and distribution are subject to the
4     Boost Software License, Version 1.0.  (See accompanying file
5     COPYING or copy at http://www.boost.org/LICENSE_1_0.txt)
6 
7     dunex-auth: D toolkit to manage UNIX authentication
8     
9     Author: Clipsey
10 */
11 module dunex.auth.passwd;
12 import std.array : split;
13 import std.conv;
14 import std.format;
15 import dunex.auth.fdb;
16 public import dunex.auth.fdb : LockException, DoesNotExistException;
17 public import std.stdio : StdioException;
18 
19 /**
20     Password DB
21 */
22 class PasswdDB : UnixFDB!(PasswdEntry, PasswdDB, "/etc/passwd", "/etc/.pwd.lock") {
23 package(dunex.auth):
24     this(string data, bool readonly) { super(data, readonly); }
25 
26 public:
27     /**
28         Find entry with name
29     */
30     ref PasswdEntry find(string name) {
31         foreach(i, entry; entries) {
32             if (entry.username == name) return entries[i];
33         }
34         throw new DoesNotExistException("user", name);
35     }
36 
37     /**
38         Find entry with uid
39     */
40     ref PasswdEntry find(ushort uid) {
41         foreach(i, entry; entries) {
42             if (entry.userId == uid) return entries[i];
43         }
44         throw new DoesNotExistException("user", uid.text);
45     }
46 
47     /**
48         Gets wether a entry with specified name exists
49     */
50     bool has(string name) {
51         foreach(i, entry; entries) {
52             if (entry.username == name) return true;
53         }
54         return false;
55     }
56 }
57 
58 /**
59     Returns a field in the passwd database by name
60 */
61 PasswdEntry getpwnam(string name) {
62     auto db = PasswdDB.openro();
63     return db.find(name);
64 }
65 
66 /**
67     Returns a field in the passwd database by user id
68 */
69 PasswdEntry getpwuid(ushort uid) {
70     auto db = PasswdDB.openro();
71     return db.find(uid);
72 }
73 
74 /**
75     An entry in /etc/passwd
76 */
77 struct PasswdEntry {
78     /**
79         The user's username
80     */
81     string username;
82 
83     /**
84         The user's password
85     */
86     string password;
87 
88     /**
89         The user id
90     */
91     ushort userId;
92 
93     /**
94         The group id
95     */
96     ushort groupId;
97 
98     /**
99         Comment about entry
100     */
101     string comment;
102 
103     /**
104         Path to home directory
105     */
106     string homePath;
107 
108     /**
109         The user's preferred shell
110     */
111     string shell;
112 
113     /**
114         Gets wether the password is stored in /etc/shadow
115     */
116     bool isInShadow() {
117         return password == "x";
118     }
119 
120     /**
121         Converts this entry back to a /etc/passwd formatted entry
122     */
123     string toString() const {
124         return "%s:%s:%s:%s:%s:%s:%s".format(
125             username,
126             password,
127             userId.to!string,
128             groupId.to!string,
129             comment,
130             homePath,
131             shell
132         );
133     }
134 
135     /**
136         Parses an entry in the passwd file
137     */
138     static PasswdEntry parseEntry(string entry) {
139         string[] entities = entry.split(":");
140         if (entities.length != 7)
141             throw new Exception("Malformed passwd entry!");
142         return PasswdEntry(
143             entities[0],            // Username
144             entities[1],            // Password
145             entities[2].to!ushort,  // User ID
146             entities[3].to!ushort,  // Group ID
147             entities[4],            // Comment
148             entities[5],            // Home Path
149             entities[6]             // Shell
150         );
151     }
152 }
153 
154 unittest {
155     import std.stdio : writeln;
156     PasswdDB db = PasswdDB.openro();
157     
158     assert(db.has("root"), "Expected root to be present in file");
159     auto root = db.find("root");
160     assert(root.isInShadow(), "Root was expected to require a shadow file");
161     assert(root.username == "root", "root's name was expected to be root");
162     assert(root.userId == 0, "root's user id was expected to be 0");
163     assert(root.groupId == 0, "root's group id was expected to be 0");
164     assert(root.shell == "/bin/bash", "root's shell was expected to be /bin/bash");
165     destroy(db);
166 
167     // Try alternate ways to get root user
168     root = getpwnam("root");
169     root = getpwuid(0);
170 }