@@ -18,39 +18,52 @@ import ContainerizationError
1818import Foundation
1919
2020/// `User` provides utilities to ensure that a given username exists in
21- /// /etc/passwd (and /etc/group).
21+ /// /etc/passwd (and /etc/group). Largely inspired by runc (and moby's)
22+ /// `user` packages.
2223public enum User {
23- private static let passwdFile = " /etc/passwd "
24- private static let groupFile = " /etc/group "
24+ public static let passwdFilePath = URL ( filePath: " /etc/passwd " )
25+ public static let groupFilePath = URL ( filePath: " /etc/group " )
26+
27+ private static let minID : UInt32 = 0
28+ private static let maxID : UInt32 = 2_147_483_647
2529
2630 public struct ExecUser : Sendable {
2731 public var uid : UInt32
2832 public var gid : UInt32
2933 public var sgids : [ UInt32 ]
3034 public var home : String
35+ public var shell : String
36+
37+ public init ( uid: UInt32 , gid: UInt32 , sgids: [ UInt32 ] , home: String , shell: String ) {
38+ self . uid = uid
39+ self . gid = gid
40+ self . sgids = sgids
41+ self . home = home
42+ self . shell = shell
43+ }
3144 }
3245
33- private struct User {
34- let name : String
35- let password : String
36- let uid : UInt32
37- let gid : UInt32
38- let gecos : String
39- let home : String
40- let shell : String
46+ public struct User {
47+ public var name : String
48+ public var password : String
49+ public var uid : UInt32
50+ public var gid : UInt32
51+ public var gecos : String
52+ public var home : String
53+ public var shell : String
4154
4255 /// The argument `rawString` must follow the below format.
4356 /// Name:Password:Uid:Gid:Gecos:Home:Shell
4457 init ( rawString: String ) throws {
4558 let args = rawString. split ( separator: " : " , omittingEmptySubsequences: false )
4659 guard args. count == 7 else {
47- throw ContainerizationError . init ( . invalidArgument , message : " Cannot parse User from ' \( rawString) ' " )
60+ throw Error . parseError ( " Cannot parse User from ' \( rawString) ' " )
4861 }
4962 guard let uid = UInt32 ( args [ 2 ] ) else {
50- throw ContainerizationError . init ( . invalidArgument , message : " Cannot parse uid from ' \( args [ 2 ] ) ' " )
63+ throw Error . parseError ( " Cannot parse uid from ' \( args [ 2 ] ) ' " )
5164 }
5265 guard let gid = UInt32 ( args [ 3 ] ) else {
53- throw ContainerizationError . init ( . invalidArgument , message : " Cannot parse gid from ' \( args [ 3 ] ) ' " )
66+ throw Error . parseError ( " Cannot parse gid from ' \( args [ 3 ] ) ' " )
5467 }
5568 self . name = String ( args [ 0 ] )
5669 self . password = String ( args [ 1 ] )
@@ -62,21 +75,21 @@ public enum User {
6275 }
6376 }
6477
65- private struct Group {
66- let name : String
67- let password : String
68- let gid : UInt32
69- let users : [ String ]
78+ struct Group {
79+ var name : String
80+ var password : String
81+ var gid : UInt32
82+ var users : [ String ]
7083
7184 /// The argument `rawString` must follow the below format.
7285 /// Name:Password:Gid:user1,user2
7386 init ( rawString: String ) throws {
7487 let args = rawString. split ( separator: " : " , omittingEmptySubsequences: false )
7588 guard args. count == 4 else {
76- throw ContainerizationError . init ( . invalidArgument , message : " Cannot parse Group from ' \( rawString) ' " )
89+ throw Error . parseError ( " Cannot parse Group from ' \( rawString) ' " )
7790 }
7891 guard let gid = UInt32 ( args [ 2 ] ) else {
79- throw ContainerizationError . init ( . invalidArgument , message : " Cannot parse gid from ' \( args [ 2 ] ) ' " )
92+ throw Error . parseError ( " Cannot parse gid from ' \( args [ 2 ] ) ' " )
8093 }
8194 self . name = String ( args [ 0 ] )
8295 self . password = String ( args [ 1 ] )
@@ -89,134 +102,174 @@ public enum User {
89102// MARK: Private methods
90103
91104extension User {
92- /// Parse the contents of the passwd file
93- private static func parsePasswd( passwdFile: URL ) throws -> [ User ] {
105+ private static func parse( file: URL , handler: ( _ line: String ) throws -> Void ) throws {
106+ let fm = FileManager . default
107+ guard fm. fileExists ( atPath: file. absolutePath ( ) ) else {
108+ throw Error . missingFile ( file. absolutePath ( ) )
109+ }
110+ let content = try String ( contentsOf: file, encoding: . ascii)
111+ let lines = content. components ( separatedBy: . newlines)
112+ for line in lines {
113+ guard !line. isEmpty else {
114+ continue
115+ }
116+ try handler ( line. trimmingCharacters ( in: . whitespaces) )
117+ }
118+ }
119+
120+ /// Parse the contents of the passwd file with a provided filter function.
121+ static func parsePasswd( passwdFile: URL , filter: ( ( User ) -> Bool ) ? = nil ) throws -> [ User ] {
94122 var users : [ User ] = [ ]
95123 try self . parse ( file: passwdFile) { line in
96124 let user = try User ( rawString: line)
125+ if let filter {
126+ guard filter ( user) else {
127+ return
128+ }
129+ }
97130 users. append ( user)
98131 }
99132 return users
100133 }
101134
102- /// Parse the contents of the group file
103- private static func parseGroup( groupFile: URL ) throws -> [ Group ] {
135+ /// Parse the contents of the group file with a provided filter function.
136+ static func parseGroup( groupFile: URL , filter : ( ( Group ) -> Bool ) ? = nil ) throws -> [ Group ] {
104137 var groups : [ Group ] = [ ]
105138 try self . parse ( file: groupFile) { line in
106139 let group = try Group ( rawString: line)
140+ if let filter {
141+ guard filter ( group) else {
142+ return
143+ }
144+ }
107145 groups. append ( group)
108146 }
109147 return groups
110148 }
111-
112- private static func parse( file: URL , handler: ( _ line: String ) throws -> Void ) throws {
113- let fm = FileManager . default
114- guard fm. fileExists ( atPath: file. absolutePath ( ) ) else {
115- throw ContainerizationError ( . notFound, message: " File \( file. absolutePath ( ) ) does not exist " )
116- }
117- let content = try String ( contentsOf: file, encoding: . ascii)
118- let lines = content. components ( separatedBy: . newlines)
119- for line in lines {
120- guard !line. isEmpty else {
121- continue
122- }
123- try handler ( line. trimmingCharacters ( in: . whitespaces) )
124- }
125- }
126149}
127150
128151// MARK: Public methods
129152
130153extension User {
131- public static func parseUser( root: String , userString: String ) throws -> ExecUser {
132- let defaultUser = ExecUser ( uid: 0 , gid: 0 , sgids: [ ] , home: " / " )
133- guard !userString. isEmpty else {
134- return defaultUser
154+ /// Looks up uid in the password file specified by `passwdPath`.
155+ public static func lookupUid( passwdPath: URL = Self . passwdFilePath, uid: UInt32 ) throws -> User {
156+ let users = try parsePasswd (
157+ passwdFile: passwdPath,
158+ filter: { u in
159+ u. uid == uid
160+ } )
161+ if users. count == 0 {
162+ throw Error . noPasswdEntries
135163 }
164+ return users [ 0 ]
165+ }
136166
137- let passwdPath = URL ( filePath: root) . appending ( path: Self . passwdFile)
138- let groupPath = URL ( filePath: root) . appending ( path: Self . groupFile)
139- let parts = userString. split ( separator: " : " , maxSplits: 1 , omittingEmptySubsequences: false )
167+ /// Parses a user string in any of the following formats:
168+ /// "user, uid, user:group, uid:gid, uid:group, user:gid"
169+ /// and returns an ExecUser type from the information.
170+ public static func getExecUser(
171+ userString: String ,
172+ defaults: ExecUser ? = nil ,
173+ passwdPath: URL = Self . passwdFilePath,
174+ groupPath: URL = Self . groupFilePath
175+ ) throws -> ExecUser {
176+ let defaults = defaults ?? ExecUser ( uid: 0 , gid: 0 , sgids: [ ] , home: " / " , shell: " " )
140177
141- let userArg = String ( parts [ 0 ] )
142- let userIdArg = Int ( userArg)
178+ var user = ExecUser (
179+ uid: defaults. uid,
180+ gid: defaults. gid,
181+ sgids: defaults. sgids,
182+ home: defaults. home,
183+ shell: defaults. shell
184+ )
143185
144- guard FileManager . default. fileExists ( atPath: passwdPath. absolutePath ( ) ) else {
145- guard let userIdArg else {
146- throw ContainerizationError ( . internalError, message: " Cannot parse username \( userArg) " )
147- }
148- let uid = UInt32 ( userIdArg)
149- guard parts. count > 1 else {
150- return ExecUser ( uid: uid, gid: uid, sgids: [ ] , home: " / " )
151- }
152- guard let gid = UInt32 ( String ( parts [ 1 ] ) ) else {
153- throw ContainerizationError ( . internalError, message: " Cannot parse user group from \( userString) " )
154- }
155- return ExecUser ( uid: uid, gid: gid, sgids: [ ] , home: " / " )
156- }
186+ let parts = userString. split (
187+ separator: " : " ,
188+ maxSplits: 1 ,
189+ omittingEmptySubsequences: false
190+ )
191+ let userArg = parts. isEmpty ? " " : String ( parts [ 0 ] )
192+ let groupArg = parts. count > 1 ? String ( parts [ 1 ] ) : " "
157193
158- let registeredUsers = try parsePasswd ( passwdFile: passwdPath)
159- guard registeredUsers. count > 0 else {
160- throw ContainerizationError ( . internalError, message: " No users configured in passwd file. " )
161- }
162- let matches = registeredUsers. filter { registeredUser in
163- // Check for a match (either uid/name) against the configured users from the passwd file.
164- // We have to check both the uid and the name cause we dont know the type of `userString`
165- registeredUser. name == userArg || registeredUser. uid == ( userIdArg ?? - 1 )
166- }
167- guard let match = matches. first else {
168- // We did not find a matching uid/username in the passwd file
169- throw ContainerizationError ( . internalError, message: " Cannot find User ' \( userArg) ' in passwd file. " )
194+ let uidArg = UInt32 ( userArg)
195+ let notUID = uidArg == nil
196+ let gidArg = UInt32 ( groupArg)
197+ let notGID = gidArg == nil
198+
199+ let users : [ User ]
200+ do {
201+ users = try parsePasswd ( passwdFile: passwdPath) { u in
202+ if userArg. isEmpty {
203+ return u. uid == user. uid
204+ }
205+ if !notUID {
206+ return uidArg! == u. uid
207+ }
208+ return u. name == userArg
209+ }
210+ } catch Error . missingFile {
211+ users = [ ]
170212 }
171213
172- var user = ExecUser ( uid: match. uid, gid: match. gid, sgids: [ match. gid] , home: match. home)
214+ var matchedUserName = " "
215+ if !users. isEmpty {
216+ let matchedUser = users [ 0 ]
217+ matchedUserName = matchedUser. name
218+ user. uid = matchedUser. uid
219+ user. gid = matchedUser. gid
220+ user. home = matchedUser. home
221+ user. shell = matchedUser. shell
222+ } else if !userArg. isEmpty {
223+ if notUID {
224+ throw Error . noPasswdEntries
225+ }
173226
174- guard !match. name. isEmpty else {
175- return user
176- }
177- let matchedUser = match. name
178- var groupArg = " "
179- var groupIdArg : Int ? = nil
180- if parts. count > 1 {
181- groupArg = String ( parts [ 1 ] )
182- groupIdArg = Int ( groupArg)
227+ user. uid = uidArg!
228+ if user. uid < minID || user. uid > maxID {
229+ throw Error . range
230+ }
183231 }
184232
185- let registeredGroups : [ Group ] = {
233+ if !groupArg. isEmpty || !matchedUserName. isEmpty {
234+ let groups : [ Group ]
186235 do {
187- // Parse the <root>/etc/group file for a list of registered groups.
188- // If the file is missing / malformed, we bail out
189- return try parseGroup ( groupFile: groupPath)
190- } catch {
191- return [ ]
236+ groups = try parseGroup ( groupFile: groupPath) { g in
237+ if groupArg. isEmpty {
238+ return g. users. contains ( matchedUserName)
239+ }
240+ if !notGID {
241+ return gidArg! == g. gid
242+ }
243+ return g. name == groupArg
244+ }
245+ } catch Error . missingFile {
246+ groups = [ ]
192247 }
193- } ( )
194- guard registeredGroups. count > 0 else {
195- return user
196- }
197- let matchingGroups = registeredGroups. filter { registeredGroup in
248+
198249 if !groupArg. isEmpty {
199- return registeredGroup. gid == ( groupIdArg ?? - 1 ) || registeredGroup. name == groupArg
200- }
201- return registeredGroup. users. contains ( matchedUser) || registeredGroup. gid == match. gid
202- }
203- guard matchingGroups. count > 0 else {
204- throw ContainerizationError ( . internalError, message: " Cannot find Group ' \( groupArg) ' in groups file. " )
205- }
206- // We have found a list of groups that match the group specified in the argument `userString`.
207- // Set the matched groups as the supplement groups for the user
208- if !groupArg. isEmpty {
209- // Reassign the user's group only we were explicitly asked for a group
210- user. gid = matchingGroups. first!. gid
211- user. sgids = matchingGroups. map { group in
212- group. gid
250+ if !groups. isEmpty {
251+ user. gid = groups [ 0 ] . gid
252+ } else {
253+ if notGID {
254+ throw Error . noGroupEntries
255+ }
256+
257+ user. gid = gidArg!
258+ if user. gid < minID || user. gid > maxID {
259+ throw Error . range
260+ }
261+ }
213262 }
214- } else {
215- user. sgids. append (
216- contentsOf: matchingGroups. map { group in
217- group. gid
218- } )
263+ user. sgids = groups. map { $0. gid }
219264 }
220265 return user
221266 }
267+
268+ public enum Error : Swift . Error {
269+ case missingFile( String )
270+ case range
271+ case noPasswdEntries
272+ case noGroupEntries
273+ case parseError( String )
274+ }
222275}
0 commit comments