Merge pull request #8 from Lukc/master

Crecto replacement.
ipc07
Luka Vandervelden 2018-12-30 01:09:31 +09:00 committed by GitHub
commit 58e8bd28a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 363 additions and 162 deletions

View File

@ -11,8 +11,6 @@ description: |
targets: targets:
authd: authd:
main: src/main.cr main: src/main.cr
authd-adduser:
main: src/adduser.cr
crystal: 0.27 crystal: 0.27
@ -23,9 +21,5 @@ dependencies:
jwt: jwt:
github: crystal-community/jwt github: crystal-community/jwt
branch: master branch: master
pg:
github: will/crystal-pg
crecto:
github: fridgerator/crecto
license: EUPL license: EUPL

View File

@ -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 authds database." do |url|
database_url = url
end
parser.on "-U name", "--database-user name", "Name of the databases user." do |name|
database_user = name
end
parser.on "-P password", "--database-pasword password", "Password of the databases 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

View File

@ -4,25 +4,40 @@ require "jwt"
require "ipc" require "ipc"
require "./user.cr" require "./user.cr"
require "./group.cr"
module AuthD module AuthD
enum RequestTypes enum RequestTypes
GET_TOKEN GetToken
AddUser
end end
enum ResponseTypes enum ResponseTypes
OK Ok
MALFORMED_REQUEST MalformedRequest
INVALID_CREDENTIALS InvalidCredentials
InvalidUser
end end
class GetTokenRequest class GetTokenRequest
JSON.mapping({ JSON.mapping({
username: String, # FIXME: Rename to "login" for consistency.
login: String,
password: String password: String
}) })
end end
class AddUserRequest
JSON.mapping({
login: String,
password: String,
uid: Int32?,
gid: Int32?,
home: String?,
shell: String?
})
end
class Client < IPC::Client class Client < IPC::Client
property key : String property key : String
@ -32,21 +47,25 @@ module AuthD
initialize "auth" initialize "auth"
end end
def get_token?(username : String, password : String) def get_token?(login : String, password : String)
send RequestTypes::GET_TOKEN.value.to_u8, { send RequestTypes::GetToken.value.to_u8, {
:username => username, :login => login,
:password => password :password => password
}.to_json }.to_json
response = read response = read
if response.type == ResponseTypes::OK.value.to_u8 if response.type == ResponseTypes::Ok.value.to_u8
response.payload response.payload
else else
nil nil
end end
end end
def send(type : RequestTypes, payload)
send type.value.to_u8, payload
end
def decode_token(token) def decode_token(token)
user, meta = JWT.decode token, @key, "HS256" user, meta = JWT.decode token, @key, "HS256"
@ -54,6 +73,24 @@ module AuthD
{user, meta} {user, meta}
end end
# FIXME: Extra options may be useful to implement here.
def add_user(login : String, password : String) : AuthD::User | Exception
send RequestTypes::AddUser, {
: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
end end

11
src/group.cr Normal file
View File

@ -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

View File

@ -9,30 +9,27 @@ require "crecto"
require "ipc" require "ipc"
require "./authd.cr" require "./authd.cr"
require "./passwd.cr"
extend AuthD extend AuthD
authd_db_name = "authd" class IPC::RemoteClient
authd_db_hostname = "localhost" def send(type : ResponseTypes, payload : String)
authd_db_user = "user" send type.value.to_u8, payload
authd_db_password = "nico-nico-nii" end
end
authd_passwd_file = "passwd"
authd_group_file = "group"
authd_jwt_key = "nico-nico-nii" authd_jwt_key = "nico-nico-nii"
OptionParser.parse! do |parser| OptionParser.parse! do |parser|
parser.on "-d name", "--database-name name", "Database name." do |name| parser.on "-u file", "--passwd-file file", "passwd file." do |name|
authd_db_name = name authd_passwd_file = name
end end
parser.on "-u name", "--database-username user", "Database user." do |name| parser.on "-g file", "--group-file file", "group file." do |name|
authd_db_user = name authd_group_file = 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
end end
parser.on "-K file", "--key-file file", "JWT key file" do |file_name| parser.on "-K file", "--key-file file", "JWT key file" do |file_name|
@ -46,26 +43,7 @@ OptionParser.parse! do |parser|
end end
end end
module DataBase passwd = Passwd.new authd_passwd_file, authd_group_file
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
## ##
# Provides a JWT-based authentication scheme for service-specific users. # Provides a JWT-based authentication scheme for service-specific users.
@ -78,27 +56,43 @@ IPC::Service.new "auth" do |event|
payload = message.payload payload = message.payload
case RequestTypes.new message.type.to_i case RequestTypes.new message.type.to_i
when RequestTypes::GET_TOKEN when RequestTypes::GetToken
begin begin
request = GetTokenRequest.from_json payload request = GetTokenRequest.from_json payload
rescue e rescue e
client.send ResponseTypes::MALFORMED_REQUEST.value.to_u8, e.message || "" client.send ResponseTypes::MalformedRequest.value.to_u8, e.message || ""
next next
end end
user = DataBase.get_by User, user = passwd.get_user request.login, request.password
username: request.username,
password: request.password
if user.nil? if user.nil?
client.send ResponseTypes::INVALID_CREDENTIALS.value.to_u8, "" client.send ResponseTypes::InvalidCredentials.value.to_u8, ""
next next
end end
client.send ResponseTypes::OK.value.to_u8, client.send ResponseTypes::Ok.value.to_u8,
JWT.encode user.to_h, authd_jwt_key, "HS256" JWT.encode user.to_h, authd_jwt_key, "HS256"
when RequestTypes::AddUser
begin
request = AddUserRequest.from_json payload
rescue e
client.send ResponseTypes::MalformedRequest.value.to_u8, e.message || ""
next
end
if passwd.user_exists? request.login
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
end end
end end
end end

227
src/passwd.cr Normal file
View File

@ -0,0 +1,227 @@
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 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
# 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]
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 = 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

View File

@ -1,24 +1,51 @@
require "pg" class AuthD::User
require "crecto" 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 JSON.mapping({
schema "users" do # table name login: String,
field :username, String password_hash: String,
field :realname, String uid: Int32,
field :avatar, String gid: Int32,
field :password, String home: String,
field :perms, Array(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 end
validate_required [:username, :password, :perms]
def to_h def to_h
{ {
:username => @username, :login => @login,
:realname => @realname, :password_hash => "x", # Not real hash in JWT.
:perms => @perms, :uid => @uid,
:avatar => @avatar :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
end end