From 436e875efbe024eba66ce53afbb6db39780ad25d Mon Sep 17 00:00:00 2001 From: Andy Foston Date: Mon, 22 Nov 2021 16:01:09 +0000 Subject: [PATCH 1/3] Support user passwords being stored in the DB --- db/dummy_data.sql | 10 +-- db/schema-update.v0-1637581007.sql | 6 ++ db/schema.v0.sql | 1 + setup.py | 3 +- src/oncall/auth/modules/db_auth.py | 46 +++++++++++ src/oncall/bin/reset_password.py | 93 +++++++++++++++++++++++ src/oncall/bin/user_password_create.py | 101 +++++++++++++++++++++++++ 7 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 db/schema-update.v0-1637581007.sql create mode 100644 src/oncall/auth/modules/db_auth.py create mode 100755 src/oncall/bin/reset_password.py create mode 100755 src/oncall/bin/user_password_create.py diff --git a/db/dummy_data.sql b/db/dummy_data.sql index fdd9f53c..1c322727 100644 --- a/db/dummy_data.sql +++ b/db/dummy_data.sql @@ -2,11 +2,11 @@ LOCK TABLES `user` WRITE; /*!40000 ALTER TABLE `user` DISABLE KEYS */; -INSERT INTO `user` VALUES - (1,'root',1,'God User',NULL,NULL,1), - (2,'manager',1,'Team Admin',NULL,NULL,0), - (3,'jdoe',1,'Juan Doş',NULL,NULL,0), - (4,'asmith',1,'Alice Smith',NULL,NULL,0); +INSERT INTO `user` (`id`,`name`,`hashed_password`,`active`,`full_name`,`time_zone`,`photo_url`,`god`) VALUES + (1,'root',NULL,1,'God User',NULL,NULL,1), + (2,'manager',NULL,1,'Team Admin',NULL,NULL,0), + (3,'jdoe',NULL,1,'Juan Doş',NULL,NULL,0), + (4,'asmith',NULL,1,'Alice Smith',NULL,NULL,0); /*!40000 ALTER TABLE `user` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/schema-update.v0-1637581007.sql b/db/schema-update.v0-1637581007.sql new file mode 100644 index 00000000..ef73bf4b --- /dev/null +++ b/db/schema-update.v0-1637581007.sql @@ -0,0 +1,6 @@ +-- ----------------------------------------------------- +-- Update to Table `user` +-- ----------------------------------------------------- + +ALTER TABLE `user` + ADD COLUMN IF NOT EXISTS hashed_password VARCHAR(255); diff --git a/db/schema.v0.sql b/db/schema.v0.sql index 34614c36..f4dc3d6d 100644 --- a/db/schema.v0.sql +++ b/db/schema.v0.sql @@ -40,6 +40,7 @@ CREATE TABLE IF NOT EXISTS `deleted_team` ( CREATE TABLE IF NOT EXISTS `user` ( `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(45) NOT NULL, + `hashed_password` VARCHAR(255), `active` BOOL DEFAULT 1 NOT NULL, `full_name` VARCHAR(255), `time_zone` VARCHAR(64), diff --git a/setup.py b/setup.py index 86512b59..14c9534a 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,8 @@ 'slackclient==1.3.1', 'icalendar', 'pymsteams', - 'idna==2.10' + 'idna==2.10', + 'py-bcrypt==0.4' ], extras_require={ 'ldap': ['python-ldap'], diff --git a/src/oncall/auth/modules/db_auth.py b/src/oncall/auth/modules/db_auth.py new file mode 100644 index 00000000..82d071b4 --- /dev/null +++ b/src/oncall/auth/modules/db_auth.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import bcrypt +import logging +import re + +from oncall import db + +logger = logging.getLogger() + + +class Authenticator(object): + def __init__(self, config): + self.config = config + + def check_password_strength(self, password): + if not password: + logger.error("A password must be provided") + return False + + rules = self.config["auth"].get('password_rules', []) + for rule in rules: + if not re.match(rule["rule"], password): + logging.error(rule["message"]) + return False + return True + + def authenticate(self, username, password): + connection = db.connect() + cursor = connection.cursor(db.DictCursor) + + cursor.execute("SELECT `hashed_password` FROM `user` WHERE `name` = %s", username) + if cursor.rowcount != 1: + cursor.close() + connection.close() + return False + + hashed_password = cursor.fetchone()["hashed_password"] + if not hashed_password: + # Ignore users without a password set + cursor.close() + connection.close() + return False + + cursor.close() + connection.close() + return bcrypt.checkpw(password, hashed_password) diff --git a/src/oncall/bin/reset_password.py b/src/oncall/bin/reset_password.py new file mode 100755 index 00000000..c7474e91 --- /dev/null +++ b/src/oncall/bin/reset_password.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from gevent import monkey, spawn +monkey.patch_all() # NOQA + +import argparse +import bcrypt +import getpass +import logging +import logging.handlers +import os +import sys +import importlib +from oncall import utils, db +from oncall.auth.modules.db_auth import Authenticator + +logger = logging.getLogger() + + +def setup_logger(): + logging.getLogger('requests').setLevel(logging.WARNING) + formatter = logging.Formatter( + '%(asctime)s %(levelname)s %(name)s %(message)s') + + log_file = os.environ.get('USER_UPDATE_LOG_FILE') + if log_file: + ch = logging.handlers.RotatingFileHandler( + log_file, mode='a', maxBytes=10485760, backupCount=10) + else: + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + ch.setFormatter(formatter) + logger.setLevel(logging.INFO) + logger.addHandler(ch) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Update an existing user.") + parser.add_argument('config', metavar='C', help="The config file used by the app.") + parser.add_argument('--name', required=True, help="The username to create.") + parser.add_argument('--password-stdin', action='store_true', help="Read the password from stdin.") + return parser.parse_args() + + +def get_password(authenticator): + password1 = "" + while True: + print("Please enter a password:") + password1 = getpass.getpass() + print("Please re-enter the password:") + password2 = getpass.getpass() + if password1 != password2: + logging.error("The two passwords don't match") + elif not password1 or authenticator.check_password_strength(password1): + break + + return password1 + + +def main(): + setup_logger() + args = parse_args() + config = utils.read_config(args.config) + authenticator = Authenticator(config) + if args.password_stdin: + password = sys.stdin.readline().rstrip() + if not authenticator.check_password_strength(password): + sys.exit(1) + else: + password = get_password(authenticator) + + if password: + salt = bcrypt.gensalt() + hashed_password = bcrypt.hashpw(password, salt) + else: + hashed_password = None + + db.init(config['db']) + connection = db.connect() + cursor = connection.cursor() + + cursor.execute("UPDATE `user` SET hashed_password = %s WHERE name = %s", ( + hashed_password, + args.name, + )) + connection.commit() + cursor.close() + connection.close() + + +if __name__ == '__main__': + main() diff --git a/src/oncall/bin/user_password_create.py b/src/oncall/bin/user_password_create.py new file mode 100755 index 00000000..eaf92f32 --- /dev/null +++ b/src/oncall/bin/user_password_create.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# -*- coding:utf-8 -*- + +from gevent import monkey, spawn +monkey.patch_all() # NOQA + +import argparse +import bcrypt +import getpass +import logging +import logging.handlers +import os +import sys +import importlib +from oncall import utils, db +from oncall.auth.modules.db_auth import Authenticator + +logger = logging.getLogger() + + +def setup_logger(): + logging.getLogger('requests').setLevel(logging.WARNING) + formatter = logging.Formatter( + '%(asctime)s %(levelname)s %(name)s %(message)s') + + log_file = os.environ.get('USER_PASSWORD_CREATE_LOG_FILE') + if log_file: + ch = logging.handlers.RotatingFileHandler( + log_file, mode='a', maxBytes=10485760, backupCount=10) + else: + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + ch.setFormatter(formatter) + logger.setLevel(logging.INFO) + logger.addHandler(ch) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Create a new user with local password.") + parser.add_argument('config', metavar='C', help="The config file used by the app.") + parser.add_argument('--name', required=True, help="The username to create.") + parser.add_argument('--password-stdin', action='store_true', help="Read the password from stdin.") + parser.add_argument('--inactive', action='store_true', help="Sets the user to 'inactive'.") + parser.add_argument('--full-name', help="The full name of the user.") + parser.add_argument('--time-zone', help="The time zone the user belongs to.") + parser.add_argument('--photo-url', help="The URL where the user's photo can be found.") + parser.add_argument('--is-god', action='store_true', help="Gives the user 'god' permissions.") + return parser.parse_args() + + +def get_password(authenticator): + password1 = "" + while True: + print("Please enter a password:") + password1 = getpass.getpass() + print("Please re-enter the password:") + password2 = getpass.getpass() + if password1 != password2: + logging.error("The two passwords don't match") + elif authenticator.check_password_strength(password1): + break + + return password1 + + +def main(): + setup_logger() + args = parse_args() + config = utils.read_config(args.config) + authenticator = Authenticator(config) + if args.password_stdin: + password = sys.stdin.readline().rstrip() + if not authenticator.check_password_strength(password): + sys.exit(1) + else: + password = get_password(authenticator) + + salt = bcrypt.gensalt() + hashed_password = bcrypt.hashpw(password, salt) + + db.init(config['db']) + connection = db.connect() + cursor = connection.cursor() + cursor.execute("INSERT into `user` (`name`, `hashed_password`, `active`, " + "`full_name`, `time_zone`, `photo_url`, `god`) VALUES " + "(%s, %s, %s, %s, %s, %s, %s)", ( + args.name, + hashed_password, + int(not args.inactive), + args.full_name, + args.time_zone, + args.photo_url, + int(args.is_god), + )) + connection.commit() + cursor.close() + connection.close() + + +if __name__ == '__main__': + main() From 0d04abc13ac56d35954a4f236141d71f79c8c220 Mon Sep 17 00:00:00 2001 From: Andy Foston Date: Mon, 22 Nov 2021 16:40:17 +0000 Subject: [PATCH 2/3] Fix static analysis --- src/oncall/bin/reset_password.py | 4 ---- src/oncall/bin/user_password_create.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/oncall/bin/reset_password.py b/src/oncall/bin/reset_password.py index c7474e91..045d32f0 100755 --- a/src/oncall/bin/reset_password.py +++ b/src/oncall/bin/reset_password.py @@ -1,9 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from gevent import monkey, spawn -monkey.patch_all() # NOQA - import argparse import bcrypt import getpass @@ -11,7 +8,6 @@ import logging.handlers import os import sys -import importlib from oncall import utils, db from oncall.auth.modules.db_auth import Authenticator diff --git a/src/oncall/bin/user_password_create.py b/src/oncall/bin/user_password_create.py index eaf92f32..de7c59f9 100755 --- a/src/oncall/bin/user_password_create.py +++ b/src/oncall/bin/user_password_create.py @@ -1,9 +1,6 @@ #!/usr/bin/env python # -*- coding:utf-8 -*- -from gevent import monkey, spawn -monkey.patch_all() # NOQA - import argparse import bcrypt import getpass @@ -11,7 +8,6 @@ import logging.handlers import os import sys -import importlib from oncall import utils, db from oncall.auth.modules.db_auth import Authenticator From d43e89b981eb2e2f5dea47ff86104c06d60dff94 Mon Sep 17 00:00:00 2001 From: Andy Foston Date: Mon, 22 Nov 2021 16:48:47 +0000 Subject: [PATCH 3/3] Fix another linting issue --- src/oncall/bin/reset_password.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/oncall/bin/reset_password.py b/src/oncall/bin/reset_password.py index 045d32f0..5f8f0b81 100755 --- a/src/oncall/bin/reset_password.py +++ b/src/oncall/bin/reset_password.py @@ -77,9 +77,9 @@ def main(): cursor = connection.cursor() cursor.execute("UPDATE `user` SET hashed_password = %s WHERE name = %s", ( - hashed_password, - args.name, - )) + hashed_password, + args.name, + )) connection.commit() cursor.close() connection.close()