1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
|
"""
Code for checking against our user db.
Todo: evaluate using twisted.cred for this; but then I guess all this needs
a thorough shakeup looking towards OAuth2 anyway.
We store the passwords hashed with scrypt with 16 bytes of salt.
Of course, since we only support http basic auth at this point, this
level of security really only makes sense if credential transmission
is restricted to https; and with current DaCHS, this means disabling
http altogether.
"""
#c Copyright 2008-2020, the GAVO project
#c
#c This program is free software, covered by the GNU GPL. See the
#c COPYING file in the source distribution.
import base64
import functools
import hashlib
import os
from gavo import base
from gavo import svcs
from gavo.utils import AllEncompassingSet
# we're limiting the password length to thwart DoS with endless passwords.
# (and because we don't think overlong passwords make any sense at all)
MAX_PASSWORD_LENGTH = 64
# this should only be changed for unit tests
adminProfile = "admin"
# We fix the scrypt parameters to what looks reasonable in 2020.
# If the default arguments are ever changed, use a different hash prefix.
scryptN4 = functools.partial(hashlib.scrypt, n=4, r=8, p=1)
def hashPassword(pw):
"""returns pw hashed and encoded with the salt.
Our storage format is: "scrypt:"+b64encode(<16 bytes of salt><hash>
"""
if len(pw)>MAX_PASSWORD_LENGTH:
raise base.ReportableError("Passwords in DaCHS must be shorter than"
" %d characters")
salt = os.urandom(16)
payload = pw.encode("utf-8")
hash = scryptN4(payload, salt=salt)
return "scrypt:"+base64.b64encode(salt+hash).decode("ascii")
def hashMatches(pwIn, storedHash):
"""returns true if pwIn matches the encoded hash value computed with
hashPassword.
"""
if len(pwIn)>MAX_PASSWORD_LENGTH:
raise svcs.ForbiddenURI("You passed in an overlong password."
" The server will not even look at it.")
if not storedHash.startswith("scrypt:"):
raise ValueError("Bad hash serialisation: '%s'"%storedHash)
saltAndHash = base64.b64decode(storedHash[7:])
salt, hash = saltAndHash[:16], saltAndHash[16:]
return hash==scryptN4(pwIn.encode("utf-8"), salt=salt)
def getGroupsForUser(username, password):
"""returns a set of all groups user username belongs to.
If username and password don't match, you'll get an empty set.
"""
# see below on this sore
if isinstance(username, bytes):
username = username.decode("utf-8")
if isinstance(password, bytes):
password = password.decode("utf-8")
if username is None:
return set()
if username=='gavoadmin' and (
password and password==base.getConfig("web", "adminpasswd")):
return AllEncompassingSet()
query = ("SELECT groupname, password"
" FROM dc.groups"
" NATURAL JOIN dc.users"
" WHERE username=%(username)s")
res = set()
storedHash = None
with base.getAdminConn() as conn:
for row in conn.query(query, locals()):
storedHash = row[1]
res.add(row[0])
# we only need to check the password once because user is primary in
# dc.users.
if storedHash and hashMatches(password, storedHash):
return res
else:
return set()
def hasCredentials(user, password, reqGroup):
"""returns true if user and password match the db entry and the user
is in the reqGroup.
If reqGroup is None, true will be returned if the user/password pair
is in the user table.
"""
# sometimes my request.getUser returns an empty string (it should be
# bytes, I guess). I won't hunt this down and just work around it
if isinstance(user, bytes):
user = user.decode("utf-8")
if isinstance(password, bytes):
password = password.decode("utf-8")
if user=="gavoadmin" and base.getConfig("web", "adminpasswd"
) and password==base.getConfig("web", "adminpasswd"):
return True
with base.getAdminConn() as conn:
dbRes = list(conn.query("select password from dc.users where"
" username=%(user)s", {"user": user}))
if not dbRes or not dbRes[0]:
return False
storedForm = dbRes[0][0]
if not hashMatches(password, storedForm):
return False
if reqGroup:
dbRes = list(conn.query("select groupname from dc.groups where"
" username=%(user)s and groupname=%(group)s",
{"user": user, "group": reqGroup,}))
return not not dbRes
else:
return True
def addUser(conn, username, password, remarks):
"""Adds a user to the users table.
This will always also create a like-named group. It will raise an
IntegrityError if the user already exists.
This will commit conn in order to catch integrity problems early.
"""
storedForm = hashPassword(password)
conn.execute("INSERT INTO dc.users (username, password, remarks)"
" VALUES (%(username)s, %(storedForm)s, %(remarks)s)", locals())
conn.commit()
conn.execute("INSERT INTO dc.groups (username, groupname)"
" VALUES (%(username)s, %(username)s)", locals())
conn.commit()
def addToGroup(conn, username, groupname):
"""Adds a user to a group.
A group would come into being by this operation if it didn't exist before.
Adding a non-existent user will raise an IntegrityError.
This will commit conn in order to catch integrity problems early.
"""
conn.execute("INSERT INTO dc.groups (username, groupname)"
" VALUES (%(username)s, %(groupname)s)", locals())
conn.commit()
def removeFromGroup(conn, username, groupname):
"""Removes a user from a group.
It is not an error to remove a user from a group they are not in.
This returns the number of rows removed in the operation (which should
be 1 when the user has been a member of the group).
"""
c = conn.cursor()
c.execute("DELETE FROM dc.groups WHERE groupname=%(groupname)s"
" and username=%(username)s", locals())
return c.rowcount
conn.execute("INSERT INTO dc.groups (username, groupname)"
" VALUES (%(username)s, %(group)s)", locals())
def delUser(conn, username):
"""Removes a user and their associated group memberships from the
users and groups tables.
This returns then number of database rows affected; if this is 0, nothing
was removed.
"""
cursor = conn.cursor()
cursor.execute("DELETE FROM dc.users WHERE username=%(username)s",
locals())
rowsAffected = cursor.rowcount
cursor.execute("DELETE FROM dc.groups WHERE username=%(username)s",
locals())
rowsAffected += cursor.rowcount
cursor.close()
return rowsAffected
|