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 }