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