diff --git a/shard.yml b/shard.yml index ed510ae..6ea6461 100644 --- a/shard.yml +++ b/shard.yml @@ -11,8 +11,6 @@ description: | targets: authd: main: src/main.cr - authd-adduser: - main: src/adduser.cr crystal: 0.27 @@ -23,9 +21,5 @@ dependencies: jwt: github: crystal-community/jwt branch: master - pg: - github: will/crystal-pg - crecto: - github: fridgerator/crecto license: EUPL diff --git a/src/adduser.cr b/src/adduser.cr deleted file mode 100644 index 17dc19d..0000000 --- a/src/adduser.cr +++ /dev/null @@ -1,89 +0,0 @@ - -require "option_parser" - -require "./user.cr" - -user_name : String? = nil -user_password : String? = nil -user_perms = Array(String).new - -database_url = "postgres:localhost" -database_name = "authd" -database_user = "nico" -database_password = "nico-nico-nii" - -OptionParser.parse! do |parser| - parser.banner = "usage: #{ARGV[0]} [arguments]" - - parser.on "-a url", "--database url", "URL of authd’s database." do |url| - database_url = url - end - parser.on "-U name", "--database-user name", "Name of the database’s user." do |name| - database_user = name - end - parser.on "-P password", "--database-pasword password", "Password of the database’s user." do |password| - database_password = password - end - parser.on "-N name", "--database-name name", "Name of the database." do |name| - database_name = name - end - - parser.on "-u name", "--username name", "Name of the user to add." do |name| - user_name = name - end - parser.on "-p password", "--password password", "Password of the user." do |password| - user_password = password - end - parser.on "-x perm", "--permission", "Add a permission token to the user." do |token| - user_perms << token - end - - parser.on("-h", "--help", "Show this help") do - puts parser - exit 0 - end - - parser.invalid_option do |flag| - puts "error: #{flag} is not a valid option." - puts parser - - exit 1 - end -end - -module DataBase - extend Crecto::Repo -end - -DataBase.config do |conf| - conf.adapter = Crecto::Adapters::Postgres - conf.hostname = database_url - conf.username = database_user - conf.password = database_password - conf.database = database_name -end - -if user_name.nil? - STDERR.puts "No username specified." - exit 1 -end -if user_password.nil? - STDERR.puts "No user password specified." - exit 1 -end - -user = AuthD::User.new -user.username = user_name -user.password = user_password -user.perms = user_perms -changeset = DataBase.insert user - -if changeset.valid? - puts "User added successfully." -else - changeset.errors.each do |e| - p e - end -end - - diff --git a/src/authd.cr b/src/authd.cr index f94c718..c36f65d 100644 --- a/src/authd.cr +++ b/src/authd.cr @@ -4,6 +4,7 @@ require "jwt" require "ipc" require "./user.cr" +require "./group.cr" module AuthD enum RequestTypes @@ -18,6 +19,7 @@ module AuthD class GetTokenRequest JSON.mapping({ + # FIXME: Rename to "login" for consistency. username: String, password: String }) diff --git a/src/group.cr b/src/group.cr new file mode 100644 index 0000000..8a64189 --- /dev/null +++ b/src/group.cr @@ -0,0 +1,11 @@ + +class AuthD::Group + getter name : String + getter password_hash : String + getter gid : Int32 + getter users = Array(String).new + + def initialize(@name, @password_hash, @gid, @users) + end +end + diff --git a/src/main.cr b/src/main.cr index 6ab8a6a..381e676 100644 --- a/src/main.cr +++ b/src/main.cr @@ -9,30 +9,21 @@ require "crecto" require "ipc" require "./authd.cr" +require "./passwd.cr" extend AuthD -authd_db_name = "authd" -authd_db_hostname = "localhost" -authd_db_user = "user" -authd_db_password = "nico-nico-nii" +authd_passwd_file = "passwd" +authd_group_file = "group" authd_jwt_key = "nico-nico-nii" OptionParser.parse! do |parser| - parser.on "-d name", "--database-name name", "Database name." do |name| - authd_db_name = name + parser.on "-u file", "--passwd-file file", "passwd file." do |name| + authd_passwd_file = name end - parser.on "-u name", "--database-username user", "Database user." do |name| - authd_db_user = name - end - - parser.on "-a host", "--hostname host", "Database host name." do |host| - authd_db_hostname = host - end - - parser.on "-P file", "--password-file file", "Password file." do |file_name| - authd_db_password = File.read(file_name).chomp + parser.on "-g file", "--group-file file", "group file." do |name| + authd_group_file = name end parser.on "-K file", "--key-file file", "JWT key file" do |file_name| @@ -46,26 +37,7 @@ OptionParser.parse! do |parser| end end -module DataBase - extend Crecto::Repo -end - -DataBase.config do |conf| - conf.adapter = Crecto::Adapters::Postgres - conf.hostname = authd_db_hostname - conf.database = authd_db_name - conf.username = authd_db_user - conf.password = authd_db_password -end - -# Dummy query to check DB connection is possible. -begin - DataBase.all User, Crecto::Repo::Query.new -rescue e - puts "Database connection failed: #{e.message}" - - exit 1 -end +passwd = Passwd.new authd_passwd_file, authd_group_file ## # Provides a JWT-based authentication scheme for service-specific users. @@ -87,9 +59,7 @@ IPC::Service.new "auth" do |event| next end - user = DataBase.get_by User, - username: request.username, - password: request.password + user = passwd.get_user request.username, request.password if user.nil? client.send ResponseTypes::INVALID_CREDENTIALS.value.to_u8, "" diff --git a/src/passwd.cr b/src/passwd.cr new file mode 100644 index 0000000..42c241c --- /dev/null +++ b/src/passwd.cr @@ -0,0 +1,215 @@ +require "csv" +require "uuid" + +require "./user.cr" +require "./group.cr" + +# FIXME: Should we work on arrays and convert to CSV at the last second when adding rows? + +class Passwd + @passwd : String + @group : String + + # FIXME: Missing operations: + # - Removing users. + # - Reading groups. + # - Adding and removing groups (ok, maybe admins can do that?) + # - Adding users to group. + # - Removing users from group. + + # FIXME: Safety. Ensure passwd and group cannot be in a half-written state. (backups will probably work well enough in the mean-time) + + 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 : AuthD::User) + group_as_array.each do |line| + group = AuthD::Group.new line + + if group.users.any? { |name| name == user.login } + user.groups << group.name + end + + pp group + end + end + + def each_user(&block) + passwd_as_array.each do |line| + yield AuthD::User.new line + end + end + + def get_user(uid : Int32) : AuthD::User? + each_user do |user| + if user.uid == uid + # FIXME: Check user groups and register them here. + set_user_groups user + + return user + end + end + end + + ## + # Will fail if the user is found but the password is invalid. + def get_user(login : String, password : String) : AuthD::User? + digest = OpenSSL::Digest.new("sha256") + digest << password + hash = digest.hexdigest + + each_user do |user| + if user.login == login + # FIXME: XXX: HASH!!!!! + if user.password_hash == hash + return user + end + + next + end + end + end + + def get_all_users + users = Array(AuthD::User).new + + passwd_as_array.each do |line| + users << AuthD::User.new line + end + + users + end + + def get_all_groups + groups = Array(AuthD::Group).new + + group_as_array.each do |line| + groups << AuthD::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 + + private def get_free_uid + uid = 1000 + + users = get_all_users + + while users.any? &.uid.== uid + uid += 1 + end + + uid + end + + private def get_free_gid + gid = 1000 + + users = get_all_users + + while users.any? &.gid.== gid + gid += 1 + end + + gid + end + + def add_user(login, password = nil, uid = nil, gid = nil, home = "/", shell = "/bin/nologin") + # 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 + digest = OpenSSL::Digest.new("sha256") + digest << password + digest.hexdigest + else + "x" + end + + user = AuthD::User.new login, password_hash, uid, gid, home, shell + + File.write(@passwd, user.to_csv + "\n", mode: "a") + + add_group login, gid: gid, users: [user.login] + end + + def add_group(name, password_hash = "x", gid = nil, users = Array(String).new) + gid = get_free_gid if gid.nil? + + group = AuthD::Group.new name, password_hash, gid, users + + File.write(@group, group.to_csv + "\n", mode: "a") + end +end + +class AuthD::Group + def initialize(line : Array(String)) + @name = line[0] + @password_hash = line[1] + @gid = line[2].to_i + + @users = line[3].split "," + end + + def to_csv + [@name, @password_hash, @gid, @users.join ","].join ":" + end +end + +class AuthD::User + def initialize(line : Array(String)) + @login = line[0] + @password_hash = line[1] + + @uid = line[2].to_i + @gid = line[3].to_i + + CSV.parse(line[4], separator: ',')[0]?.try do |gecos| + @full_name = gecos[0]? + @location = gecos[1]? + @office_phone_number = gecos[2]? + @home_phone_number = gecos[3]? + @other_contact = gecos[4]? + end + + # FIXME: What about those two fields? Keep them, remove them? + @home = line[5] + @shell = line[6] + end + + def to_csv + [@login, @password_hash, @uid, @gid, gecos, @home, @shell].join ":" + end + + def gecos + unless @location || @office_phone_number || @home_phone_number || @other_contact + if @full_name + return @full_name + else + return "" + end + end + + [@full_name || "", @location || "", @office_phone_number || "", @home_phone_number || "", @other_contact || ""].join "," + end +end + diff --git a/src/user.cr b/src/user.cr index df50ac2..6d2e95d 100644 --- a/src/user.cr +++ b/src/user.cr @@ -1,24 +1,51 @@ -require "pg" -require "crecto" +class AuthD::User + getter login : String + getter password_hash : String + getter uid : Int32 + getter gid : Int32 + getter home : String = "/" + getter shell : String = "/bin/nologin" + getter groups = Array(String).new + getter full_name : String? = nil + getter avatar : String? = nil + getter location : String? = nil + getter office_phone_number : String? = nil + getter home_phone_number : String? = nil + getter other_contact : String? = nil -class AuthD::User < Crecto::Model - schema "users" do # table name - field :username, String - field :realname, String - field :avatar, String - field :password, String - field :perms, Array(String) + JSON.mapping({ + login: String, + password_hash: String, + uid: Int32, + gid: Int32, + home: String, + shell: String, + groups: Array(String), + full_name: String?, + avatar: String?, + office_phone_number: String?, + home_phone_number: String?, + other_contact: String? + }) + + def initialize(@login, @password_hash, @uid, @gid, @home, @shell) end - validate_required [:username, :password, :perms] - def to_h { - :username => @username, - :realname => @realname, - :perms => @perms, - :avatar => @avatar + :login => @login, + :password_hash => "x", # Not real hash in JWT. + :uid => @uid, + :gid => @gid, + :home => @home, + :shell => @shell, + :groups => @groups, + :full_name => @full_name, + :avatar => @avatar, + :office_phone_number => @office_phone_number, + :home_phone_number => @home_phone_number, + :other_contact => @other_contact } end end