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.shadow;
12 import std.array : split;
13 import std.conv;
14 import std.format;
15 import dunex.auth.fdb;
16 
17 /**
18     Password DB
19 */
20 class ShadowDB : UnixFDB!(ShadowEntry, ShadowDB, "/etc/shadow", "/etc/.shdw.lock") {
21 package(dunex.auth):
22     this(string data, bool readonly) { super(data, readonly); }
23 
24 public:
25     /**
26         Find entry with name
27     */
28     ref ShadowEntry find(string name) {
29         foreach(i, entry; entries) {
30             if (entry.username == name) return entries[i];
31         }
32         throw new Exception("could not find %s".format(name));
33     }
34 
35     /**
36         Gets wether a entry with specified name exists
37     */
38     bool has(string name) {
39         foreach(i, entry; entries) {
40             if (entry.username == name) return true;
41         }
42         return false;
43     }
44 }
45 
46 /**
47     Get shadow entry by name
48 */
49 ShadowEntry getspnam(string name) {
50     auto db = ShadowDB.openro();
51     return db.find(name);
52 }
53 
54 /**
55     An entry in /etc/passwd
56 */
57 struct ShadowEntry {
58     /**
59         The user's username
60     */
61     string username;
62 
63     /**
64         The user's password
65     */
66     string password;
67 
68     /**
69         Days since UNIX epoch that the password was last changed
70     */
71     uint lastChanged;
72 
73     /**
74         Minumum number of days required between password changes
75     */
76     uint minimum;
77 
78     /**
79         Maximum number of days required between password changes
80     */
81     uint maximum;
82 
83     /**
84         Number of days before password is expiring that the user is warned
85     */
86     uint warn;
87 
88     /**
89         The number of days that the account will be disabled after expiration
90     */
91     uint inactive;
92 
93     /**
94         The user's expiration date
95     */
96     uint expires;
97 
98     /**
99         Reserved spot for future use
100     */
101     string reserved;
102 
103     /**
104         Converts this entry back to a /etc/shadow formatted entry
105     */
106     string toString() const {
107         return "%s:%s:%s:%s:%s:%s:%s:%s:%s".format(
108             username,
109             password,
110             lastChanged == 0 ? "" : lastChanged.text,
111             minimum == 0 ? "" : minimum.text,
112             maximum == 0 ? "" : maximum.text,
113             warn == 0 ? "" : warn.text,
114             inactive == 0 ? "" : inactive.text,
115             expires == 0 ? "" : expires.text,
116             reserved
117         );
118     }
119 
120     /**
121         Parses an entry in the shadow file
122     */
123     static ShadowEntry parseEntry(string entry) {
124         string[] entities = entry.split(":");
125         if (entities.length != 9)
126             throw new Exception("Malformed shadow entry!");
127         return ShadowEntry(
128             entities[0],                                            // Username
129             entities[1],                                            // Password
130             entities[2].length == 0 ? 0 : entities[2].to!uint,      // Last Changed
131             entities[3].length == 0 ? 0 : entities[3].to!uint,      // Minumum
132             entities[4].length == 0 ? 0 : entities[4].to!uint,      // Maximum
133             entities[5].length == 0 ? 0 : entities[5].to!uint,      // Warning
134             entities[6].length == 0 ? 0 : entities[6].to!uint,      // Inactive
135             entities[7].length == 0 ? 0 : entities[7].to!uint,      // Expires
136             entities[8]                                             // Reserved
137         );
138     }
139 }
140 
141 unittest {
142     import std.stdio : writeln;
143     ShadowDB db = ShadowDB.openro();
144     
145     assert(db.has("root"), "Expected root to be present in file");
146     auto root = db.find("root");
147     destroy(db);
148 
149     root = getspnam("root");
150 }