From 6a008daf873fcb7a6a1499e3ce82dfada7c0c908 Mon Sep 17 00:00:00 2001 From: Luka Vandervelden Date: Mon, 17 Dec 2018 08:56:03 +0900 Subject: [PATCH 1/4] Crecto replacement. Replaced by a `passwd` and `group` files reader that both follow UNIX format. Only difference with a live system will be the password hash field, which is stored in `passwd` instead of a dedicated `shadow` file (and which is likely encoded differently). --- shard.yml | 6 -- src/adduser.cr | 89 -------------------- src/authd.cr | 2 + src/group.cr | 11 +++ src/main.cr | 48 +++-------- src/passwd.cr | 215 +++++++++++++++++++++++++++++++++++++++++++++++++ src/user.cr | 57 +++++++++---- 7 files changed, 279 insertions(+), 149 deletions(-) delete mode 100644 src/adduser.cr create mode 100644 src/group.cr create mode 100644 src/passwd.cr 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 From 313536f996cbdd31eb4e62621c141ae81a4b82e7 Mon Sep 17 00:00:00 2001 From: Luka Vandervelden Date: Mon, 17 Dec 2018 12:39:01 +0900 Subject: [PATCH 2/4] Variable naming. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some breaking changes in the communications with authd (some request attributes renamed), but projects using AuthD::Client shouldn’t see anything. --- src/authd.cr | 6 +++--- src/main.cr | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/authd.cr b/src/authd.cr index c36f65d..7d2575d 100644 --- a/src/authd.cr +++ b/src/authd.cr @@ -20,7 +20,7 @@ module AuthD class GetTokenRequest JSON.mapping({ # FIXME: Rename to "login" for consistency. - username: String, + login: String, password: String }) end @@ -34,9 +34,9 @@ module AuthD initialize "auth" end - def get_token?(username : String, password : String) + def get_token?(login : String, password : String) send RequestTypes::GET_TOKEN.value.to_u8, { - :username => username, + :login => login, :password => password }.to_json diff --git a/src/main.cr b/src/main.cr index 381e676..b49d068 100644 --- a/src/main.cr +++ b/src/main.cr @@ -59,7 +59,7 @@ IPC::Service.new "auth" do |event| next end - user = passwd.get_user request.username, request.password + user = passwd.get_user request.login, request.password if user.nil? client.send ResponseTypes::INVALID_CREDENTIALS.value.to_u8, "" From a7a1c54161642f64a4fe0e041243159729e5fd90 Mon Sep 17 00:00:00 2001 From: Luka Vandervelden Date: Wed, 19 Dec 2018 21:54:19 +0900 Subject: [PATCH 3/4] WIP registration. --- src/authd.cr | 35 +++++++++++++++++++++++++++++++++++ src/main.cr | 24 ++++++++++++++++++++++++ src/passwd.cr | 12 ++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/authd.cr b/src/authd.cr index 7d2575d..f59c2ec 100644 --- a/src/authd.cr +++ b/src/authd.cr @@ -9,12 +9,14 @@ require "./group.cr" module AuthD enum RequestTypes GET_TOKEN + ADD_USER end enum ResponseTypes OK MALFORMED_REQUEST INVALID_CREDENTIALS + INVALID_USER end class GetTokenRequest @@ -25,6 +27,17 @@ module AuthD }) end + class AddUserRequest + JSON.mapping({ + login: String, + password: String, + uid: Int32?, + gid: Int32?, + home: String?, + shell: String? + }) + end + class Client < IPC::Client property key : String @@ -49,6 +62,10 @@ module AuthD end end + def send(type : RequestTypes, payload) + send type.value.to_u8, payload + end + def decode_token(token) user, meta = JWT.decode token, @key, "HS256" @@ -56,6 +73,24 @@ module AuthD {user, meta} end + + # FIXME: Extra options may be useful to implement here. + def add_user(login : String, password : String) : AuthD::User | Exception + send RequestTypes::ADD_USER, { + :login => login, + :password => password + }.to_json + + response = read + + pp! response.type + case ResponseTypes.new response.type.to_i + when ResponseTypes::OK + AuthD::User.from_json response.payload + else + Exception.new response.payload + end + end end end diff --git a/src/main.cr b/src/main.cr index b49d068..c968cde 100644 --- a/src/main.cr +++ b/src/main.cr @@ -13,6 +13,12 @@ require "./passwd.cr" extend AuthD +class IPC::RemoteClient + def send(type : ResponseTypes, payload : String) + send type.value.to_u8, payload + end +end + authd_passwd_file = "passwd" authd_group_file = "group" authd_jwt_key = "nico-nico-nii" @@ -69,6 +75,24 @@ IPC::Service.new "auth" do |event| client.send ResponseTypes::OK.value.to_u8, JWT.encode user.to_h, authd_jwt_key, "HS256" + when RequestTypes::ADD_USER + begin + request = AddUserRequest.from_json payload + rescue e + client.send ResponseTypes::MALFORMED_REQUEST.value.to_u8, e.message || "" + + next + end + + if passwd.user_exists? request.login + client.send ResponseTypes::INVALID_USER, "Another user with the same login already exists." + + next + end + + user = passwd.add_user request.login, request.password + + client.send ResponseTypes::OK, user.to_json end end end diff --git a/src/passwd.cr b/src/passwd.cr index 42c241c..6811d60 100644 --- a/src/passwd.cr +++ b/src/passwd.cr @@ -48,6 +48,14 @@ class Passwd end end + def user_exists?(login : String) : Bool + each_user do |user| + return true if user.login == login + end + + false + end + def get_user(uid : Int32) : AuthD::User? each_user do |user| if user.uid == uid @@ -150,6 +158,10 @@ class Passwd 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) From ddb8edacbb24246bb670254da9b607711ce15bbb Mon Sep 17 00:00:00 2001 From: Luka Vandervelden Date: Wed, 19 Dec 2018 21:57:48 +0900 Subject: [PATCH 4/4] Coding style. --- src/authd.cr | 20 ++++++++++---------- src/main.cr | 16 ++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/authd.cr b/src/authd.cr index f59c2ec..132d0ee 100644 --- a/src/authd.cr +++ b/src/authd.cr @@ -8,15 +8,15 @@ require "./group.cr" module AuthD enum RequestTypes - GET_TOKEN - ADD_USER + GetToken + AddUser end enum ResponseTypes - OK - MALFORMED_REQUEST - INVALID_CREDENTIALS - INVALID_USER + Ok + MalformedRequest + InvalidCredentials + InvalidUser end class GetTokenRequest @@ -48,14 +48,14 @@ module AuthD end def get_token?(login : String, password : String) - send RequestTypes::GET_TOKEN.value.to_u8, { + send RequestTypes::GetToken.value.to_u8, { :login => login, :password => password }.to_json response = read - if response.type == ResponseTypes::OK.value.to_u8 + if response.type == ResponseTypes::Ok.value.to_u8 response.payload else nil @@ -76,7 +76,7 @@ module AuthD # FIXME: Extra options may be useful to implement here. def add_user(login : String, password : String) : AuthD::User | Exception - send RequestTypes::ADD_USER, { + send RequestTypes::AddUser, { :login => login, :password => password }.to_json @@ -85,7 +85,7 @@ module AuthD pp! response.type case ResponseTypes.new response.type.to_i - when ResponseTypes::OK + when ResponseTypes::Ok AuthD::User.from_json response.payload else Exception.new response.payload diff --git a/src/main.cr b/src/main.cr index c968cde..3bca688 100644 --- a/src/main.cr +++ b/src/main.cr @@ -56,11 +56,11 @@ IPC::Service.new "auth" do |event| payload = message.payload case RequestTypes.new message.type.to_i - when RequestTypes::GET_TOKEN + when RequestTypes::GetToken begin request = GetTokenRequest.from_json payload rescue e - client.send ResponseTypes::MALFORMED_REQUEST.value.to_u8, e.message || "" + client.send ResponseTypes::MalformedRequest.value.to_u8, e.message || "" next end @@ -68,31 +68,31 @@ IPC::Service.new "auth" do |event| user = passwd.get_user request.login, request.password if user.nil? - client.send ResponseTypes::INVALID_CREDENTIALS.value.to_u8, "" + client.send ResponseTypes::InvalidCredentials.value.to_u8, "" next end - client.send ResponseTypes::OK.value.to_u8, + client.send ResponseTypes::Ok.value.to_u8, JWT.encode user.to_h, authd_jwt_key, "HS256" - when RequestTypes::ADD_USER + when RequestTypes::AddUser begin request = AddUserRequest.from_json payload rescue e - client.send ResponseTypes::MALFORMED_REQUEST.value.to_u8, e.message || "" + client.send ResponseTypes::MalformedRequest.value.to_u8, e.message || "" next end if passwd.user_exists? request.login - client.send ResponseTypes::INVALID_USER, "Another user with the same login already exists." + client.send ResponseTypes::InvalidUser, "Another user with the same login already exists." next end user = passwd.add_user request.login, request.password - client.send ResponseTypes::OK, user.to_json + client.send ResponseTypes::Ok, user.to_json end end end