require "csv" require "file_utils" # FIXME: Should we work on arrays and convert to CSV at the last second when adding rows? # FIXME: Use split, not CSV. # FIXME: Prevent using ':' in fields. class Passwd end require "./user.cr" require "./group.cr" class Passwd @passwd : String @group : String # FIXME: Missing operations: # - Reading groups. # - Adding and removing groups (ok, maybe admins can do that?) # - Adding users to group. # - Removing users from group. def initialize(@passwd, @group) end private def passwd_as_array CSV.parse File.read(@passwd), separator: ':' end private def group_as_array CSV.parse File.read(@group), separator: ':' end private def set_user_groups(user : Passwd::User) group_as_array.each do |line| group = Passwd::Group.new line if group.users.any? { |name| name == user.login } user.groups << group.name end end end def each_user(&block) passwd_as_array.each do |line| yield Passwd::User.new line end end def user_exists?(login : String) : Bool each_user do |user| return true if user.login == login end false end def get_user(&block) : Passwd::User? each_user do |user| if yield user set_user_groups user return user end end end def get_user(uid : Int32) get_user &.uid.==(uid) end def get_user(login : String) get_user &.login.==(login) end ## # Will fail if the user is found but the password is invalid. # Use this method if you somehow need to check the password… # Be careful though, your system probably does not use SHA256 hashes as # passwords. You should use this only on your own passwd and group files. def get_user(login : String, password : String) hash = Passwd.hash_password password get_user do |user| user.login == login && user.password_hash == hash end end def each_group(&block) group_as_array.each do |line| yield Passwd::Group.new line end end def get_all_users users = Array(Passwd::User).new passwd_as_array.each do |line| users << Passwd::User.new line end users end def get_group(&block) : Passwd::Group? each_group do |group| if yield group return group end end end def get_group(gid : Int32) get_group &.gid.==(gid) end def get_group(name : String) get_group &.name.==(name) end def get_all_groups groups = Array(Passwd::Group).new group_as_array.each do |line| groups << Passwd::Group.new line end groups end def to_passwd get_all_users.map(&.to_csv).join("\n") + "\n" end def to_group get_all_groups.map(&.to_csv).join("\n") + "\n" end def get_free_uid(starting_uid = 1000) uid = starting_uid users = get_all_users while users.any? &.uid.== uid uid += 1 end uid end def get_free_gid(starting_gid = 1000) gid = starting_gid users = get_all_users while users.any? &.gid.== gid gid += 1 end gid end def self.hash_password(password) digest = OpenSSL::Digest.new("sha256") digest << password digest.hexdigest end def add_user(login, password = nil, uid = nil, gid = nil, home = "/", shell = "/bin/nologin", full_name = nil, location = nil, office_phone_number = nil, home_phone_number = nil, other_contact = nil) # FIXME: If user already exists, exception? Replacement? uid = get_free_uid if uid.nil? gid = get_free_gid if gid.nil? password_hash = if password Passwd.hash_password password else "x" end user = Passwd::User.new(login, password_hash, uid, gid, home, shell, full_name: full_name, location: location, office_phone_number: office_phone_number, home_phone_number: home_phone_number, other_contact: other_contact) File.write(@passwd, user.to_csv + "\n", mode: "a") add_group login, gid: gid, users: [user.login] set_user_groups user user end def add_group(name, password_hash = "x", gid = nil, users = Array(String).new) gid = get_free_gid if gid.nil? group = Passwd::Group.new name, password_hash, gid, users File.write(@group, group.to_csv + "\n", mode: "a") end # FIXME: Edit other important fields. def mod_user(uid, password_hash : String? = nil) new_passwd = passwd_as_array.map do |line| user = Passwd::User.new line if uid == user.uid password_hash.try do |hash| user.password_hash = hash end user.to_csv else line.join(':') end end File.write @passwd, new_passwd.join("\n") + "\n" end def mod_group(group : ::Passwd::Group) new_group = group_as_array.map do |line| _group = Passwd::Group.new line if _group.gid == group.gid group.to_csv else line.join(':') end end File.write @group, new_group.join('\n') + '\n' end private def safe_rewrite(path : String, body : String) tempfile = File.tempfile(File.basename path) tempfile << body tempfile.close FileUtils.cp tempfile.path, path end def remove_user_from_groups(user) new_group = group_as_array.map do |line| group = Group.new line group.users.select! &.!=(user.login) group.users.select! &.!=(user.uid.to_s) group.to_csv end safe_rewrite @group, new_group.join('\n') + '\n' end def remove_user(&block) new_passwd = passwd_as_array.compact_map do |line| user = User.new line if yield user remove_user_from_groups user nil else line.join ':' end end safe_rewrite @passwd, new_passwd.join("\n") + "\n" end def remove_user(uid : Int32) remove_user &.uid.==(uid) end def remove_user(login : String) remove_user &.login.==(login) end def remove_group(&block) new_group = group_as_array.compact_map do |line| group = Group.new line if yield group nil else line.join ':' end end safe_rewrite @group, new_group.join('\n') + '\n' end def remove_group(gid : Int32) remove_group &.gid.==(gid) end def remove_group(name : String) remove_group &.name.==(name) end end