mirror of
https://github.com/apple/container.git
synced 2026-09-08 00:35:43 +00:00
This change adds documentation to quite a few existing public types that didn't have a blurb before. Additionally, this fixes a couple things that I think either didn't make sense when going to document them: - Rename ConnectionStream to VsockConnectionStream. This type only functions for vsock connections. - Deletes NsLock+Closure. This was not used anywhere. - Rename ContainerizationOCI/Config.swift to ImageConfig.swift. Signed-off-by: Danny Canter <danny_canter@apple.com>
223 lines
8.7 KiB
Swift
223 lines
8.7 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the containerization project authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import ContainerizationError
|
|
import Foundation
|
|
|
|
/// `User` provides utilities to ensure that a given username exists in
|
|
/// /etc/passwd (and /etc/group).
|
|
public enum User {
|
|
private static let passwdFile = "/etc/passwd"
|
|
private static let groupFile = "/etc/group"
|
|
|
|
public struct ExecUser: Sendable {
|
|
public var uid: UInt32
|
|
public var gid: UInt32
|
|
public var sgids: [UInt32]
|
|
public var home: String
|
|
}
|
|
|
|
private struct User {
|
|
let name: String
|
|
let password: String
|
|
let uid: UInt32
|
|
let gid: UInt32
|
|
let gecos: String
|
|
let home: String
|
|
let 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)'")
|
|
}
|
|
guard let uid = UInt32(args[2]) else {
|
|
throw ContainerizationError.init(.invalidArgument, message: "Cannot parse uid from '\(args[2])'")
|
|
}
|
|
guard let gid = UInt32(args[3]) else {
|
|
throw ContainerizationError.init(.invalidArgument, message: "Cannot parse gid from '\(args[3])'")
|
|
}
|
|
self.name = String(args[0])
|
|
self.password = String(args[1])
|
|
self.uid = uid
|
|
self.gid = gid
|
|
self.gecos = String(args[4])
|
|
self.home = String(args[5])
|
|
self.shell = String(args[6])
|
|
}
|
|
}
|
|
|
|
private struct Group {
|
|
let name: String
|
|
let password: String
|
|
let gid: UInt32
|
|
let 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)'")
|
|
}
|
|
guard let gid = UInt32(args[2]) else {
|
|
throw ContainerizationError.init(.invalidArgument, message: "Cannot parse gid from '\(args[2])'")
|
|
}
|
|
self.name = String(args[0])
|
|
self.password = String(args[1])
|
|
self.gid = gid
|
|
self.users = args[3].split(separator: ",").map { String($0) }
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
let content = try String(contentsOf: file, encoding: .ascii)
|
|
let lines = content.components(separatedBy: .newlines)
|
|
for line in lines {
|
|
guard !line.isEmpty else {
|
|
continue
|
|
}
|
|
try handler(line.trimmingCharacters(in: .whitespaces))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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)
|
|
|
|
let userArg = String(parts[0])
|
|
let userIdArg = Int(userArg)
|
|
|
|
guard FileManager.default.fileExists(atPath: passwdPath.absolutePath()) else {
|
|
guard let userIdArg else {
|
|
throw ContainerizationError(.internalError, message: "Cannot parse username \(userArg)")
|
|
}
|
|
let uid = UInt32(userIdArg)
|
|
guard parts.count > 1 else {
|
|
return ExecUser(uid: uid, gid: uid, sgids: [], home: "/")
|
|
}
|
|
guard let gid = UInt32(String(parts[1])) else {
|
|
throw ContainerizationError(.internalError, message: "Cannot parse user group from \(userString)")
|
|
}
|
|
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] = {
|
|
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 []
|
|
}
|
|
}()
|
|
guard registeredGroups.count > 0 else {
|
|
return user
|
|
}
|
|
let matchingGroups = registeredGroups.filter { registeredGroup in
|
|
if !groupArg.isEmpty {
|
|
return registeredGroup.gid == (groupIdArg ?? -1) || registeredGroup.name == groupArg
|
|
}
|
|
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
|
|
})
|
|
}
|
|
return user
|
|
}
|
|
}
|