From 6187faf20384b0c5a4966343b2d4ca47f8b11e45 Mon Sep 17 00:00:00 2001 From: Philip Paquette Date: Wed, 26 Sep 2018 07:48:55 -0400 Subject: Release v1.0.0 - Diplomacy Game Engine - AGPL v3+ License --- diplomacy/server/users.py | 234 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 diplomacy/server/users.py (limited to 'diplomacy/server/users.py') diff --git a/diplomacy/server/users.py b/diplomacy/server/users.py new file mode 100644 index 0000000..d1c8ca0 --- /dev/null +++ b/diplomacy/server/users.py @@ -0,0 +1,234 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette, Steven Bocco +# +# 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 . +# ============================================================================== +""" Helper class to manage user accounts and connections on server side. + + A user is associated to 0 or more connected tokens, + and each connected token is associated to at most 1 connection handler. + + When a connection handler is closed or invalidated, + related tokens are kept and may be further associated to new connection handlers. + + Tokens are effectively deleted when they expire after TOKEN_LIFETIME_SECONDS seconds since last token usage. +""" +import logging + +from diplomacy.server.user import User +from diplomacy.utils import common, parsing, strings +from diplomacy.utils.common import generate_token +from diplomacy.utils.jsonable import Jsonable + +LOGGER = logging.getLogger(__name__) + +# Token lifetime in seconds: default 24hours. +TOKEN_LIFETIME_SECONDS = 24 * 60 * 60 + +class Users(Jsonable): + """ Users class. Properties: + - users: dictionary mapping usernames to User object.s + - administrators: set of administrator usernames. + - token_timestamp: dictionary mapping each token to its creation/last confirmation timestamp. + - token_to_username: dictionary mapping each token to its username. + - username_to_tokens: dictionary mapping each username to a set of its tokens. + - token_to_connection_handler: (memory only) dictionary mapping each token to a connection handler + - connection_handler_to_tokens (memory only) dictionary mapping a connection handler to a set of its tokens + """ + __slots__ = ['users', 'administrators', 'token_timestamp', 'token_to_username', 'username_to_tokens', + 'token_to_connection_handler', 'connection_handler_to_tokens'] + model = { + strings.USERS: parsing.DefaultValueType(parsing.DictType(str, parsing.JsonableClassType(User)), {}), + # {username => User} + strings.ADMINISTRATORS: parsing.DefaultValueType(parsing.SequenceType(str, sequence_builder=set), ()), + # {usernames} + strings.TOKEN_TIMESTAMP: parsing.DefaultValueType(parsing.DictType(str, int), {}), + strings.TOKEN_TO_USERNAME: parsing.DefaultValueType(parsing.DictType(str, str), {}), + strings.USERNAME_TO_TOKENS: parsing.DefaultValueType(parsing.DictType(str, parsing.SequenceType(str, set)), {}), + } + + def __init__(self, **kwargs): + self.users = {} + self.administrators = set() + self.token_timestamp = {} + self.token_to_username = {} + self.username_to_tokens = {} + self.token_to_connection_handler = {} + self.connection_handler_to_tokens = {} + super(Users, self).__init__(**kwargs) + + def has_username(self, username): + """ Return True if users have given username. """ + return username in self.users + + def has_user(self, username, password): + """ Return True if users have given username with given password. """ + return username in self.users and self.users[username].is_valid_password(password) + + def has_admin(self, username): + """ Return True if given username is an administrator. """ + return username in self.administrators + + def has_token(self, token): + """ Return True if users have given token. """ + return token in self.token_to_username + + def token_is_alive(self, token): + """ Return True if given token is known and still alive. + A token is alive if elapsed time since last token usage does not exceed token lifetime + (TOKEN_LIFETIME_SECONDS). + """ + if self.has_token(token): + current_time = common.timestamp_microseconds() + elapsed_time_seconds = (current_time - self.token_timestamp[token]) / 1000000 + return elapsed_time_seconds <= TOKEN_LIFETIME_SECONDS + return False + + def relaunch_token(self, token): + """ Update timestamp of given token with current timestamp. """ + if self.has_token(token): + self.token_timestamp[token] = common.timestamp_microseconds() + + def token_is_admin(self, token): + """ Return True if given token is associated to an administrator. """ + return self.has_token(token) and self.has_admin(self.get_name(token)) + + def count_connections(self): + """ Return number of registered connection handlers. """ + return len(self.connection_handler_to_tokens) + + def get_tokens(self, username): + """ Return a sequence of tokens associated to given username. """ + return self.username_to_tokens[username].copy() + + def get_name(self, token): + """ Return username of given token. """ + return self.token_to_username[token] + + def get_connection_handler(self, token): + """ Return connection handler associated to given token, or None if no handler currently associated. """ + return self.token_to_connection_handler.get(token, None) + + def add_admin(self, username): + """ Set given username as administrator. Related user must exists in this Users object. """ + assert username in self.users + self.administrators.add(username) + + def remove_admin(self, username): + """ Remove given username from administrators. """ + if username in self.administrators: + self.administrators.remove(username) + + def create_token(self): + """ Return a new token guaranteed to not exist in this Users object. """ + token = generate_token() + while self.has_token(token): + token = generate_token() + return token + + def add_user(self, username, password_hash): + """ Add a new user with given username and hashed password. + See diplomacy.utils.common.hash_password() for hashing purposes. + """ + user = User(username=username, password_hash=password_hash) + self.users[username] = user + return user + + def remove_user(self, username): + """ Remove user related to given username. """ + user = self.users.pop(username) + self.remove_admin(username) + for token in self.username_to_tokens.pop(user.username): + self.token_timestamp.pop(token) + self.token_to_username.pop(token) + connection_handler = self.token_to_connection_handler.pop(token, None) + if connection_handler: + self.connection_handler_to_tokens[connection_handler].remove(token) + if not self.connection_handler_to_tokens[connection_handler]: + self.connection_handler_to_tokens.pop(connection_handler) + + def remove_connection(self, connection_handler, remove_tokens=True): + """ Remove given connection handler. + Return tokens associated to this connection handler, + or None if connection handler is unknown. + :param connection_handler: connection handler to remove. + :param remove_tokens: if True, tokens related to connection handler are deleted. + :return: either None or a set of tokens. + """ + if connection_handler in self.connection_handler_to_tokens: + tokens = self.connection_handler_to_tokens.pop(connection_handler) + for token in tokens: + self.token_to_connection_handler.pop(token) + if remove_tokens: + self.token_timestamp.pop(token) + user = self.users[self.token_to_username.pop(token)] + self.username_to_tokens[user.username].remove(token) + if not self.username_to_tokens[user.username]: + self.username_to_tokens.pop(user.username) + return tokens + return None + + def connect_user(self, username, connection_handler): + """ Connect given username to given connection handler with a new generated token, and return + token generated. + :param username: username to connect + :param connection_handler: connection handler to link to user + :return: a new token generated for connexion + """ + token = self.create_token() + user = self.users[username] + if connection_handler not in self.connection_handler_to_tokens: + self.connection_handler_to_tokens[connection_handler] = set() + if user.username not in self.username_to_tokens: + self.username_to_tokens[user.username] = set() + self.token_to_username[token] = user.username + self.token_to_connection_handler[token] = connection_handler + self.username_to_tokens[user.username].add(token) + self.connection_handler_to_tokens[connection_handler].add(token) + self.token_timestamp[token] = common.timestamp_microseconds() + return token + + def attach_connection_handler(self, token, connection_handler): + """ Associate given token with given connection handler if token is known. + If there is a previous connection handler associated to given token, it should be the same + as given connection handler, otherwise an error is raised (meaning previous connection handler + was not correctly disconnected from given token. It should be a programming error). + :param token: token + :param connection_handler: connection handler + """ + if self.has_token(token): + previous_connection = self.get_connection_handler(token) + if previous_connection: + assert previous_connection == connection_handler, \ + "A new connection handler cannot be attached to a token always connected to another handler." + else: + LOGGER.warning('Attaching a new connection handler to a token.') + if connection_handler not in self.connection_handler_to_tokens: + self.connection_handler_to_tokens[connection_handler] = set() + self.token_to_connection_handler[token] = connection_handler + self.connection_handler_to_tokens[connection_handler].add(token) + self.token_timestamp[token] = common.timestamp_microseconds() + + def disconnect_token(self, token): + """ Remove given token. """ + self.token_timestamp.pop(token) + user = self.users[self.token_to_username.pop(token)] + self.username_to_tokens[user.username].remove(token) + if not self.username_to_tokens[user.username]: + self.username_to_tokens.pop(user.username) + connection_handler = self.token_to_connection_handler.pop(token, None) + if connection_handler: + self.connection_handler_to_tokens[connection_handler].remove(token) + if not self.connection_handler_to_tokens[connection_handler]: + self.connection_handler_to_tokens.pop(connection_handler) -- cgit v1.2.3