const std = @import("std"); const os = std.os; const fmt = std.fmt; const log = std.log; const mem = std.mem; const process = std.process; const once = std.once; const Mutex = std.Thread.Mutex; const DB = @import("DB.zig"); const File = @import("File.zig"); const ErrCtx = @import("ErrCtx.zig"); const CGroup = @import("Group.zig").CGroup; const PackedGroup = @import("PackedGroup.zig"); const CUser = @import("User.zig").CUser; const PackedUser = @import("PackedUser.zig"); const ShellReader = @import("shell.zig").ShellReader; const c = @cImport({ @cInclude("nss.h"); }); const ENV_DB = "TURBONSS_DB"; const ENV_LOGLEVEL = "TURBONSS_LOGLEVEL"; const ENV_OMIT_MEMBERS = "TURBONSS_OMIT_MEMBERS"; export var turbonss_db_path: [:0]const u8 = "/etc/turbonss/db.turbo"; pub var log_level: std.log.Level = .err; // State is a type of the global variable holding the process state: // the DB handle and all the iterators. const State = struct { file: File, omit_members: bool, getpwent_iterator_mu: Mutex = Mutex{}, getpwent_iterator: ?PackedUser.Iterator = null, getgrent_iterator_mu: Mutex = Mutex{}, getgrent_iterator: ?PackedGroup.Iterator = null, }; // global_state is initialized on first call to an nss function var global_state: ?State = null; var global_init = once(init); // assigns State from environment variables et al fn init() void { if (os.getenvZ(ENV_LOGLEVEL)) |env| { const got = mem.sliceTo(env, 0); if (mem.eql(u8, got, "0")) { log_level = .err; } else if (mem.eql(u8, got, "1")) { log_level = .warn; } else if (mem.eql(u8, got, "2")) { log_level = .info; } else if (mem.eql(u8, got, "3")) { log_level = .debug; } else { std.debug.print( "warning: unrecognized {s}={s}. Expected between 0 and 3\n", .{ ENV_LOGLEVEL, got }, ); } } const omit_members = blk: { if (os.getenvZ(ENV_OMIT_MEMBERS)) |env| { const got = mem.sliceTo(env, 0); if (mem.eql(u8, got, "1")) { break :blk true; } else if (mem.eql(u8, got, "0")) { break :blk false; } else if (mem.eql(u8, got, "auto")) { // not set, do autodiscover } else { std.debug.print( "warning: unrecognized {s}={s}. Expected 0, 1 or auto\n", .{ ENV_OMIT_MEMBERS, got }, ); } } if (os.argv.len == 0) break :blk false; break :blk mem.eql(u8, mem.sliceTo(os.argv[0], 0), "id"); }; log.debug("omitting members from getgr* calls: {any}\n", .{omit_members}); const fname = os.getenvZ(ENV_DB) orelse turbonss_db_path; log.debug("opening '{s}'", .{fname}); const file = File.open(fname) catch |err| { log.warn("open '{s}': {s}", .{ fname, @errorName(err) }); return; }; log.debug("turbonss database opened", .{}); global_state = State{ .file = file, .omit_members = omit_members, }; } export fn _nss_turbo_getpwuid_r( uid: c_uint, passwd: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = getDBErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; var cuser = db.getpwuid(uid, buffer[0..buflen]) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; const got_cuser = cuser orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; passwd.* = got_cuser; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_getpwnam_r( name: [*:0]const u8, passwd: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = getDBErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; const nameSlice = mem.sliceTo(name, 0); var buf = buffer[0..buflen]; const cuser = db.getpwnam(nameSlice, buf) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; const got_cuser = cuser orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; passwd.* = got_cuser; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_getgrgid_r( gid: c_uint, gr: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; const db = state.file.db; const omit_members = state.omit_members; var buf = buffer[0..buflen]; var cgroup = db.getgrgid(gid, buf, omit_members) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; const got_cgroup = cgroup orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; gr.* = got_cgroup; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_getgrnam_r( name: [*:0]const u8, group: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; const db = state.file.db; const omit_members = state.omit_members; const nameSlice = mem.sliceTo(name, 0); var buf = buffer[0..buflen]; var cgroup = db.getgrnam(nameSlice, buf, omit_members) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; const got_cgroup = cgroup orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; group.* = got_cgroup; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_setpwent(_: c_int) void { global_init.call(); var state = global_state orelse return; state.getpwent_iterator_mu.lock(); defer state.getpwent_iterator_mu.unlock(); state.getpwent_iterator = PackedUser.iterator( state.file.db.users, ShellReader{ .index = state.file.db.shell_index, .blob = state.file.db.shell_blob, }, ); } export fn _nss_turbo_endpwent() void { global_init.call(); var state = global_state orelse return; state.getpwent_iterator_mu.lock(); state.getpwent_iterator = null; state.getpwent_iterator_mu.unlock(); } export fn _nss_turbo_setgrent(_: c_int) void { var state = getState() orelse return; state.getgrent_iterator_mu.lock(); defer state.getgrent_iterator_mu.unlock(); state.getgrent_iterator = PackedGroup.iterator( state.file.db.groups, ); } export fn _nss_turbo_endgrent() void { var state = getState() orelse return; state.getgrent_iterator_mu.lock(); state.getgrent_iterator = null; state.getgrent_iterator_mu.unlock(); } export fn _nss_turbo_getgrent_r( result: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { var state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; state.getgrent_iterator_mu.lock(); defer state.getgrent_iterator_mu.unlock(); var it = state.getgrent_iterator orelse { // logic from _nss_systemd_getgrent_r errnop.* = @enumToInt(os.E.HOSTDOWN); return c.NSS_STATUS_UNAVAIL; }; const group = it.next() orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; const cgroup1 = if (state.omit_members) DB.packCGroupNoMembers(group, buffer[0..buflen]) else state.file.db.packCGroup(group, buffer[0..buflen]); if (cgroup1) |cgroup| { result.* = cgroup; return c.NSS_STATUS_SUCCESS; } else |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, } } export fn _nss_turbo_getpwent_r( result: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { var state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; state.getpwent_iterator_mu.lock(); defer state.getpwent_iterator_mu.unlock(); var it = state.getpwent_iterator orelse { // logic from _nss_systemd_getgrent_r errnop.* = @enumToInt(os.E.HOSTDOWN); return c.NSS_STATUS_UNAVAIL; }; const user = it.next() orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; const buf = buffer[0..buflen]; const cuser = state.file.db.writeUser(user, buf) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; result.* = cuser; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_initgroups_dyn( user_name: [*:0]const u8, _: u32, start: *c_long, size: *c_long, groups: [*]u32, limit: c_long, errnop: *c_int, ) c.enum_nss_status { const db = getDBErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; const user = db.getUser(mem.sliceTo(user_name, 0)) orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; _ = user; _ = start; _ = size; _ = groups; _ = limit; return c.NSS_STATUS_SUCCESS; } fn getState() ?State { global_init.call(); return global_state; } fn getStateErrno(errnop: *c_int) ?State { global_init.call(); if (global_state) |state| { return state; } else { errnop.* = @enumToInt(os.E.AGAIN); return null; } } fn getDBErrno(errnop: *c_int) ?DB { if (getStateErrno(errnop)) |state| { return state.file.db; } else return null; } const testing = std.testing; test "getpwuid_r and getpwnam_r" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); const turbonss_db_path_old = turbonss_db_path; turbonss_db_path = tf.path; defer { turbonss_db_path = turbonss_db_path_old; } var buffer: [1024]u8 = undefined; var errno: c_int = 0; var passwd: CUser = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getpwuid_r(128, &passwd, &buffer, buffer.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantas(passwd); passwd = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getpwnam_r("vidmantas", &passwd, &buffer, buffer.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantas(passwd); passwd = undefined; try testing.expectEqual( c.NSS_STATUS_NOTFOUND, _nss_turbo_getpwnam_r("does not exist", &passwd, &buffer, buffer.len, &errno), ); try testing.expectEqual(@enumToInt(os.E.NOENT), @intCast(u16, errno)); passwd = undefined; var small_buffer: [1]u8 = undefined; try testing.expectEqual( c.NSS_STATUS_TRYAGAIN, _nss_turbo_getpwuid_r(0, &passwd, &small_buffer, small_buffer.len, &errno), ); try testing.expectEqual(@enumToInt(os.E.RANGE), @intCast(u16, errno)); } fn testVidmantas(u: CUser) !void { try testing.expectEqualStrings("vidmantas", mem.sliceTo(u.pw_name, 0)); try testing.expectEqual(@as(u32, 128), u.pw_uid); try testing.expectEqual(@as(u32, 128), u.pw_gid); try testing.expectEqualStrings("Vidmantas Kaminskas", mem.sliceTo(u.pw_gecos, 0)); try testing.expectEqualStrings("/bin/bash", mem.sliceTo(u.pw_shell, 0)); } test "getgrgid_r and getgrnam_r" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); const turbonss_db_path_old = turbonss_db_path; turbonss_db_path = tf.path; defer { turbonss_db_path = turbonss_db_path_old; } var buffer: [1024]u8 = undefined; var errno: c_int = 0; var group: CGroup = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getgrgid_r(128, &group, &buffer, buffer.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantasGroup(group); group = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getgrnam_r("vidmantas", &group, &buffer, buffer.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantasGroup(group); group = undefined; try testing.expectEqual( c.NSS_STATUS_NOTFOUND, _nss_turbo_getgrnam_r("does not exist", &group, &buffer, buffer.len, &errno), ); try testing.expectEqual(@enumToInt(os.E.NOENT), @intCast(u16, errno)); } fn testVidmantasGroup(g: CGroup) !void { try testing.expectEqual(@as(u32, 128), g.gid); try testing.expectEqualStrings("vidmantas", mem.sliceTo(g.name, 0)); const members = g.members; try testing.expect(members[0] != null); try testing.expectEqualStrings("vidmantas", mem.sliceTo(members[0].?, 0)); }