mirror of
https://github.com/apple/container.git
synced 2026-09-10 09:45:39 +00:00
ContainerizationOS: Rework User type (#279)
This commit is contained in:
@@ -18,39 +18,52 @@ import ContainerizationError
|
||||
import Foundation
|
||||
|
||||
/// `User` provides utilities to ensure that a given username exists in
|
||||
/// /etc/passwd (and /etc/group).
|
||||
/// /etc/passwd (and /etc/group). Largely inspired by runc (and moby's)
|
||||
/// `user` packages.
|
||||
public enum User {
|
||||
private static let passwdFile = "/etc/passwd"
|
||||
private static let groupFile = "/etc/group"
|
||||
public static let passwdFilePath = URL(filePath: "/etc/passwd")
|
||||
public static let groupFilePath = URL(filePath: "/etc/group")
|
||||
|
||||
private static let minID: UInt32 = 0
|
||||
private static let maxID: UInt32 = 2_147_483_647
|
||||
|
||||
public struct ExecUser: Sendable {
|
||||
public var uid: UInt32
|
||||
public var gid: UInt32
|
||||
public var sgids: [UInt32]
|
||||
public var home: String
|
||||
public var shell: String
|
||||
|
||||
public init(uid: UInt32, gid: UInt32, sgids: [UInt32], home: String, shell: String) {
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
self.sgids = sgids
|
||||
self.home = home
|
||||
self.shell = shell
|
||||
}
|
||||
}
|
||||
|
||||
private struct User {
|
||||
let name: String
|
||||
let password: String
|
||||
let uid: UInt32
|
||||
let gid: UInt32
|
||||
let gecos: String
|
||||
let home: String
|
||||
let shell: String
|
||||
public struct User {
|
||||
public var name: String
|
||||
public var password: String
|
||||
public var uid: UInt32
|
||||
public var gid: UInt32
|
||||
public var gecos: String
|
||||
public var home: String
|
||||
public var shell: String
|
||||
|
||||
/// The argument `rawString` must follow the below format.
|
||||
/// Name:Password:Uid:Gid:Gecos:Home:Shell
|
||||
init(rawString: String) throws {
|
||||
let args = rawString.split(separator: ":", omittingEmptySubsequences: false)
|
||||
guard args.count == 7 else {
|
||||
throw ContainerizationError.init(.invalidArgument, message: "Cannot parse User from '\(rawString)'")
|
||||
throw Error.parseError("Cannot parse User from '\(rawString)'")
|
||||
}
|
||||
guard let uid = UInt32(args[2]) else {
|
||||
throw ContainerizationError.init(.invalidArgument, message: "Cannot parse uid from '\(args[2])'")
|
||||
throw Error.parseError("Cannot parse uid from '\(args[2])'")
|
||||
}
|
||||
guard let gid = UInt32(args[3]) else {
|
||||
throw ContainerizationError.init(.invalidArgument, message: "Cannot parse gid from '\(args[3])'")
|
||||
throw Error.parseError("Cannot parse gid from '\(args[3])'")
|
||||
}
|
||||
self.name = String(args[0])
|
||||
self.password = String(args[1])
|
||||
@@ -62,21 +75,21 @@ public enum User {
|
||||
}
|
||||
}
|
||||
|
||||
private struct Group {
|
||||
let name: String
|
||||
let password: String
|
||||
let gid: UInt32
|
||||
let users: [String]
|
||||
struct Group {
|
||||
var name: String
|
||||
var password: String
|
||||
var gid: UInt32
|
||||
var users: [String]
|
||||
|
||||
/// The argument `rawString` must follow the below format.
|
||||
/// Name:Password:Gid:user1,user2
|
||||
init(rawString: String) throws {
|
||||
let args = rawString.split(separator: ":", omittingEmptySubsequences: false)
|
||||
guard args.count == 4 else {
|
||||
throw ContainerizationError.init(.invalidArgument, message: "Cannot parse Group from '\(rawString)'")
|
||||
throw Error.parseError("Cannot parse Group from '\(rawString)'")
|
||||
}
|
||||
guard let gid = UInt32(args[2]) else {
|
||||
throw ContainerizationError.init(.invalidArgument, message: "Cannot parse gid from '\(args[2])'")
|
||||
throw Error.parseError("Cannot parse gid from '\(args[2])'")
|
||||
}
|
||||
self.name = String(args[0])
|
||||
self.password = String(args[1])
|
||||
@@ -89,30 +102,10 @@ public enum User {
|
||||
// MARK: Private methods
|
||||
|
||||
extension User {
|
||||
/// Parse the contents of the passwd file
|
||||
private static func parsePasswd(passwdFile: URL) throws -> [User] {
|
||||
var users: [User] = []
|
||||
try self.parse(file: passwdFile) { line in
|
||||
let user = try User(rawString: line)
|
||||
users.append(user)
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
/// Parse the contents of the group file
|
||||
private static func parseGroup(groupFile: URL) throws -> [Group] {
|
||||
var groups: [Group] = []
|
||||
try self.parse(file: groupFile) { line in
|
||||
let group = try Group(rawString: line)
|
||||
groups.append(group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
private static func parse(file: URL, handler: (_ line: String) throws -> Void) throws {
|
||||
let fm = FileManager.default
|
||||
guard fm.fileExists(atPath: file.absolutePath()) else {
|
||||
throw ContainerizationError(.notFound, message: "File \(file.absolutePath()) does not exist")
|
||||
throw Error.missingFile(file.absolutePath())
|
||||
}
|
||||
let content = try String(contentsOf: file, encoding: .ascii)
|
||||
let lines = content.components(separatedBy: .newlines)
|
||||
@@ -123,100 +116,160 @@ extension User {
|
||||
try handler(line.trimmingCharacters(in: .whitespaces))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the contents of the passwd file with a provided filter function.
|
||||
static func parsePasswd(passwdFile: URL, filter: ((User) -> Bool)? = nil) throws -> [User] {
|
||||
var users: [User] = []
|
||||
try self.parse(file: passwdFile) { line in
|
||||
let user = try User(rawString: line)
|
||||
if let filter {
|
||||
guard filter(user) else {
|
||||
return
|
||||
}
|
||||
}
|
||||
users.append(user)
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
/// Parse the contents of the group file with a provided filter function.
|
||||
static func parseGroup(groupFile: URL, filter: ((Group) -> Bool)? = nil) throws -> [Group] {
|
||||
var groups: [Group] = []
|
||||
try self.parse(file: groupFile) { line in
|
||||
let group = try Group(rawString: line)
|
||||
if let filter {
|
||||
guard filter(group) else {
|
||||
return
|
||||
}
|
||||
}
|
||||
groups.append(group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public methods
|
||||
|
||||
extension User {
|
||||
public static func parseUser(root: String, userString: String) throws -> ExecUser {
|
||||
let defaultUser = ExecUser(uid: 0, gid: 0, sgids: [], home: "/")
|
||||
guard !userString.isEmpty else {
|
||||
return defaultUser
|
||||
/// Looks up uid in the password file specified by `passwdPath`.
|
||||
public static func lookupUid(passwdPath: URL = Self.passwdFilePath, uid: UInt32) throws -> User {
|
||||
let users = try parsePasswd(
|
||||
passwdFile: passwdPath,
|
||||
filter: { u in
|
||||
u.uid == uid
|
||||
})
|
||||
if users.count == 0 {
|
||||
throw Error.noPasswdEntries
|
||||
}
|
||||
return users[0]
|
||||
}
|
||||
|
||||
let passwdPath = URL(filePath: root).appending(path: Self.passwdFile)
|
||||
let groupPath = URL(filePath: root).appending(path: Self.groupFile)
|
||||
let parts = userString.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
|
||||
/// Parses a user string in any of the following formats:
|
||||
/// "user, uid, user:group, uid:gid, uid:group, user:gid"
|
||||
/// and returns an ExecUser type from the information.
|
||||
public static func getExecUser(
|
||||
userString: String,
|
||||
defaults: ExecUser? = nil,
|
||||
passwdPath: URL = Self.passwdFilePath,
|
||||
groupPath: URL = Self.groupFilePath
|
||||
) throws -> ExecUser {
|
||||
let defaults = defaults ?? ExecUser(uid: 0, gid: 0, sgids: [], home: "/", shell: "")
|
||||
|
||||
let userArg = String(parts[0])
|
||||
let userIdArg = Int(userArg)
|
||||
var user = ExecUser(
|
||||
uid: defaults.uid,
|
||||
gid: defaults.gid,
|
||||
sgids: defaults.sgids,
|
||||
home: defaults.home,
|
||||
shell: defaults.shell
|
||||
)
|
||||
|
||||
guard FileManager.default.fileExists(atPath: passwdPath.absolutePath()) else {
|
||||
guard let userIdArg else {
|
||||
throw ContainerizationError(.internalError, message: "Cannot parse username \(userArg)")
|
||||
let parts = userString.split(
|
||||
separator: ":",
|
||||
maxSplits: 1,
|
||||
omittingEmptySubsequences: false
|
||||
)
|
||||
let userArg = parts.isEmpty ? "" : String(parts[0])
|
||||
let groupArg = parts.count > 1 ? String(parts[1]) : ""
|
||||
|
||||
let uidArg = UInt32(userArg)
|
||||
let notUID = uidArg == nil
|
||||
let gidArg = UInt32(groupArg)
|
||||
let notGID = gidArg == nil
|
||||
|
||||
let users: [User]
|
||||
do {
|
||||
users = try parsePasswd(passwdFile: passwdPath) { u in
|
||||
if userArg.isEmpty {
|
||||
return u.uid == user.uid
|
||||
}
|
||||
if !notUID {
|
||||
return uidArg! == u.uid
|
||||
}
|
||||
return u.name == userArg
|
||||
}
|
||||
let uid = UInt32(userIdArg)
|
||||
guard parts.count > 1 else {
|
||||
return ExecUser(uid: uid, gid: uid, sgids: [], home: "/")
|
||||
} catch Error.missingFile {
|
||||
users = []
|
||||
}
|
||||
|
||||
var matchedUserName = ""
|
||||
if !users.isEmpty {
|
||||
let matchedUser = users[0]
|
||||
matchedUserName = matchedUser.name
|
||||
user.uid = matchedUser.uid
|
||||
user.gid = matchedUser.gid
|
||||
user.home = matchedUser.home
|
||||
user.shell = matchedUser.shell
|
||||
} else if !userArg.isEmpty {
|
||||
if notUID {
|
||||
throw Error.noPasswdEntries
|
||||
}
|
||||
guard let gid = UInt32(String(parts[1])) else {
|
||||
throw ContainerizationError(.internalError, message: "Cannot parse user group from \(userString)")
|
||||
|
||||
user.uid = uidArg!
|
||||
if user.uid < minID || user.uid > maxID {
|
||||
throw Error.range
|
||||
}
|
||||
return ExecUser(uid: uid, gid: gid, sgids: [], home: "/")
|
||||
}
|
||||
|
||||
let registeredUsers = try parsePasswd(passwdFile: passwdPath)
|
||||
guard registeredUsers.count > 0 else {
|
||||
throw ContainerizationError(.internalError, message: "No users configured in passwd file.")
|
||||
}
|
||||
let matches = registeredUsers.filter { registeredUser in
|
||||
// Check for a match (either uid/name) against the configured users from the passwd file.
|
||||
// We have to check both the uid and the name cause we dont know the type of `userString`
|
||||
registeredUser.name == userArg || registeredUser.uid == (userIdArg ?? -1)
|
||||
}
|
||||
guard let match = matches.first else {
|
||||
// We did not find a matching uid/username in the passwd file
|
||||
throw ContainerizationError(.internalError, message: "Cannot find User '\(userArg)' in passwd file.")
|
||||
}
|
||||
|
||||
var user = ExecUser(uid: match.uid, gid: match.gid, sgids: [match.gid], home: match.home)
|
||||
|
||||
guard !match.name.isEmpty else {
|
||||
return user
|
||||
}
|
||||
let matchedUser = match.name
|
||||
var groupArg = ""
|
||||
var groupIdArg: Int? = nil
|
||||
if parts.count > 1 {
|
||||
groupArg = String(parts[1])
|
||||
groupIdArg = Int(groupArg)
|
||||
}
|
||||
|
||||
let registeredGroups: [Group] = {
|
||||
if !groupArg.isEmpty || !matchedUserName.isEmpty {
|
||||
let groups: [Group]
|
||||
do {
|
||||
// Parse the <root>/etc/group file for a list of registered groups.
|
||||
// If the file is missing / malformed, we bail out
|
||||
return try parseGroup(groupFile: groupPath)
|
||||
} catch {
|
||||
return []
|
||||
groups = try parseGroup(groupFile: groupPath) { g in
|
||||
if groupArg.isEmpty {
|
||||
return g.users.contains(matchedUserName)
|
||||
}
|
||||
if !notGID {
|
||||
return gidArg! == g.gid
|
||||
}
|
||||
return g.name == groupArg
|
||||
}
|
||||
} catch Error.missingFile {
|
||||
groups = []
|
||||
}
|
||||
}()
|
||||
guard registeredGroups.count > 0 else {
|
||||
return user
|
||||
}
|
||||
let matchingGroups = registeredGroups.filter { registeredGroup in
|
||||
|
||||
if !groupArg.isEmpty {
|
||||
return registeredGroup.gid == (groupIdArg ?? -1) || registeredGroup.name == groupArg
|
||||
if !groups.isEmpty {
|
||||
user.gid = groups[0].gid
|
||||
} else {
|
||||
if notGID {
|
||||
throw Error.noGroupEntries
|
||||
}
|
||||
|
||||
user.gid = gidArg!
|
||||
if user.gid < minID || user.gid > maxID {
|
||||
throw Error.range
|
||||
}
|
||||
}
|
||||
}
|
||||
return registeredGroup.users.contains(matchedUser) || registeredGroup.gid == match.gid
|
||||
}
|
||||
guard matchingGroups.count > 0 else {
|
||||
throw ContainerizationError(.internalError, message: "Cannot find Group '\(groupArg)' in groups file.")
|
||||
}
|
||||
// We have found a list of groups that match the group specified in the argument `userString`.
|
||||
// Set the matched groups as the supplement groups for the user
|
||||
if !groupArg.isEmpty {
|
||||
// Reassign the user's group only we were explicitly asked for a group
|
||||
user.gid = matchingGroups.first!.gid
|
||||
user.sgids = matchingGroups.map { group in
|
||||
group.gid
|
||||
}
|
||||
} else {
|
||||
user.sgids.append(
|
||||
contentsOf: matchingGroups.map { group in
|
||||
group.gid
|
||||
})
|
||||
user.sgids = groups.map { $0.gid }
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
public enum Error: Swift.Error {
|
||||
case missingFile(String)
|
||||
case range
|
||||
case noPasswdEntries
|
||||
case noGroupEntries
|
||||
case parseError(String)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,8 +233,8 @@ extension IntegrationSuite {
|
||||
let id = "test-process-user"
|
||||
|
||||
let bs = try await bootstrap()
|
||||
let buffer = BufferWriter()
|
||||
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
|
||||
var buffer = BufferWriter()
|
||||
var container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
|
||||
config.process.arguments = ["/usr/bin/id"]
|
||||
config.process.user = .init(uid: 1, gid: 1, additionalGids: [1])
|
||||
config.process.stdout = buffer
|
||||
@@ -243,18 +243,82 @@ extension IntegrationSuite {
|
||||
try await container.create()
|
||||
try await container.start()
|
||||
|
||||
let status = try await container.wait()
|
||||
var status = try await container.wait()
|
||||
try await container.stop()
|
||||
|
||||
guard status == 0 else {
|
||||
throw IntegrationError.assert(msg: "process status \(status) != 0")
|
||||
}
|
||||
let expected = "uid=1(bin) gid=1(bin) groups=1(bin)"
|
||||
|
||||
var expected = "uid=1(bin) gid=1(bin) groups=1(bin)"
|
||||
guard String(data: buffer.data, encoding: .utf8) == "\(expected)\n" else {
|
||||
throw IntegrationError.assert(
|
||||
msg: "process should have returned on stdout '\(expected)' != '\(String(data: buffer.data, encoding: .utf8)!)'")
|
||||
}
|
||||
|
||||
buffer = BufferWriter()
|
||||
container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
|
||||
config.process.arguments = ["/usr/bin/id"]
|
||||
// Try some uid that doesn't exist. This is supported.
|
||||
config.process.user = .init(uid: 40000, gid: 40000)
|
||||
config.process.stdout = buffer
|
||||
}
|
||||
|
||||
try await container.create()
|
||||
try await container.start()
|
||||
|
||||
status = try await container.wait()
|
||||
try await container.stop()
|
||||
|
||||
guard status == 0 else {
|
||||
throw IntegrationError.assert(msg: "process status \(status) != 0")
|
||||
}
|
||||
|
||||
expected = "uid=40000 gid=40000 groups=40000"
|
||||
guard String(data: buffer.data, encoding: .utf8) == "\(expected)\n" else {
|
||||
throw IntegrationError.assert(
|
||||
msg: "process should have returned on stdout '\(expected)' != '\(String(data: buffer.data, encoding: .utf8)!)'")
|
||||
}
|
||||
|
||||
buffer = BufferWriter()
|
||||
container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
|
||||
config.process.arguments = ["/usr/bin/id"]
|
||||
// Try some uid that doesn't exist. This is supported.
|
||||
config.process.user = .init(username: "40000:40000")
|
||||
config.process.stdout = buffer
|
||||
}
|
||||
|
||||
try await container.create()
|
||||
try await container.start()
|
||||
|
||||
status = try await container.wait()
|
||||
try await container.stop()
|
||||
|
||||
guard status == 0 else {
|
||||
throw IntegrationError.assert(msg: "process status \(status) != 0")
|
||||
}
|
||||
|
||||
expected = "uid=40000 gid=40000 groups=40000"
|
||||
guard String(data: buffer.data, encoding: .utf8) == "\(expected)\n" else {
|
||||
throw IntegrationError.assert(
|
||||
msg: "process should have returned on stdout '\(expected)' != '\(String(data: buffer.data, encoding: .utf8)!)'")
|
||||
}
|
||||
|
||||
buffer = BufferWriter()
|
||||
container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
|
||||
config.process.arguments = ["/usr/bin/id"]
|
||||
// Now for our final trick, try and run a username that doesn't exist.
|
||||
config.process.user = .init(username: "thisdoesntexist")
|
||||
config.process.stdout = buffer
|
||||
}
|
||||
|
||||
try await container.create()
|
||||
do {
|
||||
try await container.start()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
throw IntegrationError.assert(msg: "container start should have failed")
|
||||
}
|
||||
|
||||
// Ensure if we ask for a terminal we set TERM.
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
import Testing
|
||||
@@ -43,7 +42,7 @@ final class UsersTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func testOnlyPasswd() throws {
|
||||
func testExecUserOnlyPasswd() throws {
|
||||
let passwordContent = """
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||
@@ -60,55 +59,44 @@ final class UsersTests {
|
||||
try Self.createFile(path: passwdPath, content: passwordContent.data(using: .ascii)!)
|
||||
|
||||
let testCases: [TestCase] = [
|
||||
.init("root", .init(uid: 0, gid: 0, sgids: [0], home: "/root"), false),
|
||||
.init("0:0", .init(uid: 0, gid: 0, sgids: [0], home: "/root"), false),
|
||||
.init("platform", .init(uid: 1000, gid: 1000, sgids: [1000], home: "/home/platform"), false),
|
||||
.init("65534", .init(uid: 65534, gid: 65534, sgids: [65534], home: "/nonexistent"), false),
|
||||
.init("should_fail", .init(uid: 456, gid: 123, sgids: [9999], home: "/undefined"), true),
|
||||
.init(":nouser", .init(uid: 456, gid: 123, sgids: [9999], home: "/undefined"), true),
|
||||
.init("root", .init(uid: 0, gid: 0, sgids: [], home: "/root", shell: "/bin/bash"), false),
|
||||
.init("0:0", .init(uid: 0, gid: 0, sgids: [], home: "/root", shell: "/bin/bash"), false),
|
||||
.init("platform", .init(uid: 1000, gid: 1000, sgids: [], home: "/home/platform", shell: "/bin/sh"), false),
|
||||
.init("65534", .init(uid: 65534, gid: 65534, sgids: [], home: "/nonexistent", shell: "/usr/sbin/nologin"), false),
|
||||
.init("should_fail", .init(uid: 456, gid: 123, sgids: [], home: "/undefined", shell: ""), true),
|
||||
.init(":nouser", .init(uid: 456, gid: 123, sgids: [], home: "/undefined", shell: ""), true),
|
||||
]
|
||||
|
||||
let groupPath = tempDir.appending(path: "etc/group")
|
||||
for testCase in testCases {
|
||||
if testCase.shouldThrow {
|
||||
#expect(throws: ContainerizationError.self) {
|
||||
try User.parseUser(root: tempDir.absolutePath(), userString: testCase.userString)
|
||||
#expect(throws: User.Error.self) {
|
||||
try User.getExecUser(userString: testCase.userString, passwdPath: passwdPath, groupPath: groupPath)
|
||||
}
|
||||
continue
|
||||
}
|
||||
let user = try User.parseUser(root: tempDir.absolutePath(), userString: testCase.userString)
|
||||
let user = try User.getExecUser(userString: testCase.userString, passwdPath: passwdPath, groupPath: groupPath)
|
||||
#expect(testCase.expect.uid == user.uid)
|
||||
#expect(testCase.expect.gid == user.gid)
|
||||
#expect(testCase.expect.home == user.home)
|
||||
#expect(testCase.expect.sgids == user.sgids)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
TestCase("foobar", .init(uid: 0, gid: 0, sgids: [0], home: "/root"), true),
|
||||
TestCase("101:101", .init(uid: 101, gid: 101, sgids: [], home: "/"), false),
|
||||
TestCase("1025:must-fail", .init(uid: 0, gid: 0, sgids: [], home: "/"), true),
|
||||
])
|
||||
func testNoPasswd(testCase: TestCase) throws {
|
||||
let fileManager = FileManager.default
|
||||
let tempDir = fileManager.uniqueTemporaryDirectory()
|
||||
defer {
|
||||
try? fileManager.removeItem(at: tempDir)
|
||||
}
|
||||
if testCase.shouldThrow {
|
||||
#expect(throws: ContainerizationError.self) {
|
||||
try User.parseUser(root: tempDir.absolutePath(), userString: testCase.userString)
|
||||
}
|
||||
} else {
|
||||
let parsed = try User.parseUser(root: tempDir.absolutePath(), userString: testCase.userString)
|
||||
#expect(testCase.expect.uid == parsed.uid)
|
||||
#expect(testCase.expect.gid == parsed.gid)
|
||||
#expect(testCase.expect.home == parsed.home)
|
||||
#expect(testCase.expect.sgids == parsed.sgids)
|
||||
#expect(testCase.expect.shell == user.shell)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testPasswdGroup() throws {
|
||||
func testExecUserNoPasswdFile() throws {
|
||||
#expect(throws: User.Error.self) {
|
||||
try User.getExecUser(
|
||||
userString: "root:root",
|
||||
passwdPath: URL(filePath: "/foobar-passwd"),
|
||||
groupPath: URL(filePath: "/foobar-group")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testExecUserPasswdGroup() throws {
|
||||
let passwordContent = """
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||
@@ -141,24 +129,25 @@ final class UsersTests {
|
||||
try Self.createFile(path: groupPath, content: groupContent.data(using: .ascii)!)
|
||||
|
||||
let testCases: [TestCase] = [
|
||||
.init("root:bin", .init(uid: 0, gid: 2, sgids: [2], home: "/root"), false),
|
||||
.init("daemon:platform", .init(uid: 1, gid: 1000, sgids: [1000], home: "/usr/sbin"), false),
|
||||
.init("platform", .init(uid: 1000, gid: 1000, sgids: [4, 27, 29, 44, 1000], home: "/home/platform"), false),
|
||||
.init("nobody", .init(uid: 65534, gid: 65534, sgids: [65534], home: "/nonexistent"), false),
|
||||
.init("2:1000", .init(uid: 2, gid: 1000, sgids: [1000], home: "/bin"), false),
|
||||
.init("root:bin", .init(uid: 0, gid: 2, sgids: [2], home: "/root", shell: "/bin/bash"), false),
|
||||
.init("daemon:platform", .init(uid: 1, gid: 1000, sgids: [1000], home: "/usr/sbin", shell: "/usr/sbin/nologin"), false),
|
||||
.init("platform", .init(uid: 1000, gid: 1000, sgids: [4, 27, 29, 44], home: "/home/platform", shell: "/bin/bash"), false),
|
||||
.init("nobody", .init(uid: 65534, gid: 65534, sgids: [], home: "/nonexistent", shell: "/usr/sbin/nologin"), false),
|
||||
.init("2:1000", .init(uid: 2, gid: 1000, sgids: [1000], home: "/bin", shell: "/usr/sbin/nologin"), false),
|
||||
]
|
||||
|
||||
for testCase in testCases {
|
||||
if testCase.shouldThrow {
|
||||
#expect(throws: ContainerizationError.self) {
|
||||
try User.parseUser(root: tempDir.absolutePath(), userString: testCase.userString)
|
||||
#expect(throws: User.Error.self) {
|
||||
try User.getExecUser(userString: testCase.userString, passwdPath: passwdPath, groupPath: groupPath)
|
||||
}
|
||||
}
|
||||
let user = try User.parseUser(root: tempDir.absolutePath(), userString: testCase.userString)
|
||||
let user = try User.getExecUser(userString: testCase.userString, passwdPath: passwdPath, groupPath: groupPath)
|
||||
#expect(testCase.expect.uid == user.uid)
|
||||
#expect(testCase.expect.gid == user.gid)
|
||||
#expect(testCase.expect.home == user.home)
|
||||
#expect(Set(testCase.expect.sgids) == Set(user.sgids))
|
||||
#expect(testCase.expect.shell == user.shell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,13 +962,25 @@ extension Initd {
|
||||
process.cwd = "/"
|
||||
}
|
||||
|
||||
// Username is truthfully a Windows field, but we use this as away to pass through
|
||||
// the exact string representation of a username a client may have given us.
|
||||
// NOTE: The OCI runtime specs Username field is truthfully Windows exclusive, but we use this as a way
|
||||
// to pass through the exact string representation of a username (or username:group, uid:group etc.) a client
|
||||
// may have given us.
|
||||
let username = process.user.username.isEmpty ? "\(process.user.uid):\(process.user.gid)" : process.user.username
|
||||
let parsedUser = try User.parseUser(root: root.path, userString: username)
|
||||
let parsedUser = try User.getExecUser(
|
||||
userString: username,
|
||||
passwdPath: URL(filePath: root.path).appending(path: "etc/passwd"),
|
||||
groupPath: URL(filePath: root.path).appending(path: "etc/group")
|
||||
)
|
||||
process.user.uid = parsedUser.uid
|
||||
process.user.gid = parsedUser.gid
|
||||
process.user.additionalGids = parsedUser.sgids
|
||||
process.user.additionalGids.append(contentsOf: parsedUser.sgids)
|
||||
process.user.additionalGids.append(process.user.gid)
|
||||
|
||||
var seenSuppGids = Set<UInt32>()
|
||||
process.user.additionalGids = process.user.additionalGids.filter {
|
||||
seenSuppGids.insert($0).inserted
|
||||
}
|
||||
|
||||
if !process.env.contains(where: { $0.hasPrefix("HOME=") }) {
|
||||
process.env.append("HOME=\(parsedUser.home)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user