#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Isomer - The distributed application framework
# ==============================================
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <riot@c-base.org> and others.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Module: Auth
============
Authentication (and later Authorization) system
"""
from hmac import compare_digest
from uuid import uuid4
from circuits import Event, Timer
from isomer.component import handler, ConfigurableComponent
from isomer.events.client import authentication, send
from isomer.database import objectmodels
from isomer.logger import error, warn, debug
from isomer.misc.std import std_salt, std_hash, std_now, std_uuid, std_human_uid
minimum_password_length = 5
minimum_username_length = 1
[docs]class AuthenticationError(Exception):
"""Something unspecified went wrong during authentication"""
pass
[docs]class add_auth_hook(Event):
"""Allows for adding event hooks to the authentication process"""
[docs] def __init__(self, authenticator_name, event, *args, **kwargs):
super(add_auth_hook, self).__init__(*args, **kwargs)
self.authenticator_name = authenticator_name
self.event = event
[docs]class Authenticator(ConfigurableComponent):
"""
Authenticates users against the database.
"""
channel = "isomer-web"
configprops = {}
[docs] def __init__(self, *args):
super(Authenticator, self).__init__("AUTH", *args)
# self.log(objectmodels['systemconfig'], lvl=error)
self.failing_clients = {}
systemconfig = objectmodels["systemconfig"].find_one({"active": True})
# TODO: Decouple systemconfig creation from authenticator
try:
salt = systemconfig.salt.encode("ascii")
self.log("Using active systemconfig salt")
except (KeyError, AttributeError):
self.log("No active system configuration found!", lvl=error)
salt = std_salt().encode("ascii")
self.salt = salt
self.systemconfig = systemconfig
self.auth_hooks = {}
[docs] @handler("add_auth_hook")
def add_auth_hook(self, event):
"""Register event hook on reception of add_auth_hook-event"""
self.log("Adding authentication hook for", event.authenticator_name)
self.auth_hooks[event.authenticator_name] = event.event
def _fail(self, event, message="Invalid credentials"):
"""Sends a failure message to the requesting client"""
notification = {"component": "auth", "action": "fail", "data": message}
ip = event.sock.getpeername()[0]
self.failing_clients[ip] = event
Timer(
3, Event.create("notify_fail", event.clientuuid, notification, ip)
).register(self)
[docs] def notify_fail(self, uuid, notification, ip):
self.log("Transmitting delayed fail notification", lvl=debug)
self.fireEvent(send(uuid, notification, sendtype="client"))
del self.failing_clients[ip]
def _login(self, event, user_account, user_profile, client_config):
"""Send login notification to client"""
user_account.lastlogin = std_now()
user_account.save()
user_account.passhash = ""
self.fireEvent(
authentication(
user_account.name,
(user_account, user_profile, client_config),
event.clientuuid,
user_account.uuid,
event.sock,
),
"auth",
)
[docs] @handler("authenticationrequest", channel="auth")
def authenticationrequest(self, event):
"""Handles authentication requests from clients
:param event: AuthenticationRequest with user's credentials
"""
if event.sock.getpeername()[0] in self.failing_clients:
self.log("Client failed a login and has to wait", lvl=debug)
return
if event.auto:
self._handle_autologin(event)
else:
self._handle_login(event)
def _handle_autologin(self, event):
"""Automatic logins for client configurations that allow it"""
self.log("Verifying automatic login request")
# TODO: Check for a common secret
# noinspection PyBroadException
try:
client_config = objectmodels["client"].find_one(
{"uuid": event.requestedclientuuid}
)
except Exception:
client_config = None
if client_config is None or client_config.autologin is False:
self.log("Autologin failed:", event.requestedclientuuid, lvl=error)
self._fail(event)
return
try:
user_account = objectmodels["user"].find_one({"uuid": client_config.owner})
if user_account is None:
raise AuthenticationError
self.log("Autologin for", user_account.name, lvl=debug)
except Exception as e:
self.log("No user object due to error: ", e, type(e), lvl=error)
self._fail(event)
return
if user_account.active is False:
self.log("Account deactivated.")
self._fail(event, "Account deactivated.")
return
user_profile = self._get_profile(user_account)
self._login(event, user_account, user_profile, client_config)
self.log("Autologin successful!", lvl=warn)
def _handle_login(self, event):
"""Manual password based login"""
# TODO: Refactor to simplify
self.log("Auth request for ", event.username, "client:", event.clientuuid)
# TODO: Define the requirements for secure passwords etc.
# They're also required in the Enrol module..!
if (len(event.username) < minimum_username_length) or (
len(event.password) < minimum_password_length
):
self.log("Illegal username or password received, login cancelled", lvl=warn)
self._fail(event, "Password or username too short")
return
client_config = None
try:
user_account = objectmodels["user"].find_one({"name": event.username})
# self.log("Account: %s" % user_account._fields, lvl=debug)
if user_account is None:
raise AuthenticationError
except Exception as e:
self.log("No userobject due to error: ", e, type(e), lvl=error)
self._fail(event)
return
self.log("User found.", lvl=debug)
if user_account.active is False:
self.log("Account deactivated.")
self._fail(event, "Account deactivated.")
return
if compare_digest(std_hash(event.password, self.salt), user_account.passhash) \
is False:
self.log("Password was wrong!", lvl=warn)
self._fail(event)
return
self.log("Passhash matches, checking client and profile.", lvl=debug)
requested_client_uuid = event.requestedclientuuid
if requested_client_uuid is not None:
client_config = objectmodels["client"].find_one(
{"uuid": requested_client_uuid}
)
if client_config:
self.log("Checking client configuration permissions", lvl=debug)
# TODO: Shareable client configurations?
if client_config.owner != user_account.uuid:
client_config = None
self.log("Unauthorized client configuration " "requested", lvl=warn)
else:
self.log(
"Unknown client configuration requested: ",
requested_client_uuid,
event.__dict__,
lvl=warn,
)
if not client_config:
self.log("Creating new default client configuration")
# Either no configuration was found or not requested
# -> Create a new client configuration
uuid = event.clientuuid if event.clientuuid is not None else str(uuid4())
client_config = objectmodels["client"]({"uuid": uuid})
client_config.name = std_human_uid(kind="place")
client_config.description = (
"New client configuration from " + user_account.name
)
client_config.owner = user_account.uuid
# TODO: Get client configuration storage done right, this one is too simple
client_config.save()
user_profile = self._get_profile(user_account)
self._login(event, user_account, user_profile, client_config)
self.log("Done with Login request", lvl=debug)
def _get_profile(self, user_account):
"""Retrieves a user's profile"""
try:
# TODO: Load active profile, not just any
user_profile = objectmodels["profile"].find_one(
{"owner": str(user_account.uuid)}
)
self.log("Profile: ", user_profile, user_account.uuid, lvl=debug)
except Exception as e:
self.log("No profile due to error: ", e, type(e), lvl=error)
user_profile = None
if not user_profile:
default = {
"uuid": std_uuid(),
"owner": user_account.uuid,
"userdata": {"notes": "Default profile of " + user_account.name},
}
user_profile = objectmodels["profile"](default)
user_profile.save()
return user_profile