Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support user passwords being stored in the DB #361

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions db/dummy_data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 6 additions & 0 deletions db/schema-update.v0-1637581007.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- -----------------------------------------------------
-- Update to Table `user`
-- -----------------------------------------------------

ALTER TABLE `user`
ADD COLUMN IF NOT EXISTS hashed_password VARCHAR(255);
1 change: 1 addition & 0 deletions db/schema.v0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
46 changes: 46 additions & 0 deletions src/oncall/auth/modules/db_auth.py
Original file line number Diff line number Diff line change
@@ -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)
89 changes: 89 additions & 0 deletions src/oncall/bin/reset_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
import bcrypt
import getpass
import logging
import logging.handlers
import os
import sys
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()
97 changes: 97 additions & 0 deletions src/oncall/bin/user_password_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import argparse
import bcrypt
import getpass
import logging
import logging.handlers
import os
import sys
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()