mirror of
https://github.com/xcat2/confluent.git
synced 2024-11-26 19:40:12 +00:00
Add support for Operator role
Support a reduced privilege user that can still perform most operations, but cannot modify, delete, or add users/groups to confluent or to BMCs.
This commit is contained in:
parent
93e9a54e86
commit
4d5bfb13bf
@ -23,6 +23,7 @@ import confluent.config.configmanager as configmanager
|
||||
import eventlet
|
||||
import eventlet.tpool
|
||||
import Cryptodome.Protocol.KDF as KDF
|
||||
from fnmatch import fnmatch
|
||||
import hashlib
|
||||
import hmac
|
||||
import multiprocessing
|
||||
@ -40,7 +41,43 @@ _passchecking = {}
|
||||
authworkers = None
|
||||
authcleaner = None
|
||||
|
||||
_allowedbyrole = {
|
||||
'Operator': {
|
||||
'retrieve': ['*'],
|
||||
'create': [
|
||||
'/node*/media/uploads/',
|
||||
'/node*/inventory/firmware/updates/*',
|
||||
'/node*/suppport/servicedata*',
|
||||
],
|
||||
'update': [
|
||||
'/discovery/*',
|
||||
'/networking/macs/rescan',
|
||||
'/node*/power/state',
|
||||
'/node*/power/reseat',
|
||||
'/node*/attributes/*',
|
||||
'/node*/media/*tach',
|
||||
'/node*/boot/nextdevice',
|
||||
'/node*/identify',
|
||||
'/node*/configuration/*',
|
||||
],
|
||||
'start': [
|
||||
'/nodes/*/console/session*',
|
||||
'/nodes/*/shell/sessions*',
|
||||
],
|
||||
'delete': [
|
||||
'/node*/*/events/hardware/log',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
_deniedbyrole = {
|
||||
# This supersedes the above and is only consulted after the allowed has happened
|
||||
'Operator': {
|
||||
'update': [
|
||||
'/node*/configuration/management_controller/users/*',
|
||||
]
|
||||
}
|
||||
}
|
||||
class Credentials(object):
|
||||
def __init__(self, username, passphrase):
|
||||
self.username = username
|
||||
@ -113,14 +150,15 @@ def authorize(name, element, tenant=False, operation='create',
|
||||
and the relevant ConfigManager object for the context of the
|
||||
request.
|
||||
"""
|
||||
if operation not in ('create', 'start', 'update', 'retrieve', 'delete'):
|
||||
return None
|
||||
# skipuserobj is a leftover from the now abandoned plan to use pam session
|
||||
# to do authorization and authentication. Now confluent always does authorization
|
||||
# even if pam does authentication.
|
||||
if operation not in ('create', 'start', 'update', 'retrieve', 'delete', None):
|
||||
return False
|
||||
user, tenant = _get_usertenant(name, tenant)
|
||||
if tenant is not None and not configmanager.is_tenant(tenant):
|
||||
return None
|
||||
return False
|
||||
manager = configmanager.ConfigManager(tenant, username=user)
|
||||
if skipuserobj:
|
||||
return None, manager, user, tenant, skipuserobj
|
||||
userobj = manager.get_user(user)
|
||||
if not userobj:
|
||||
for group in userutil.grouplist(user):
|
||||
@ -128,11 +166,21 @@ def authorize(name, element, tenant=False, operation='create',
|
||||
if userobj:
|
||||
break
|
||||
if userobj: # returning
|
||||
role = userobj.get('role', 'Administrator')
|
||||
if element and role != 'Administrator':
|
||||
for rule in _allowedbyrole.get(role, {}).get(operation, []):
|
||||
if fnmatch(element, rule):
|
||||
break
|
||||
else:
|
||||
return False
|
||||
for rule in _deniedbyrole.get(role, {}).get(operation, []):
|
||||
if fnmatch(element, rule):
|
||||
return False
|
||||
return userobj, manager, user, tenant, skipuserobj
|
||||
return None
|
||||
return False
|
||||
|
||||
|
||||
def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
def check_user_passphrase(name, passphrase, operation=None, element=None, tenant=False):
|
||||
"""Check a a login name and passphrase for authenticity and authorization
|
||||
|
||||
The function combines authentication and authorization into one function.
|
||||
@ -179,7 +227,7 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
return None
|
||||
if (user, tenant) in _passcache:
|
||||
if hashlib.sha256(passphrase).digest() == _passcache[(user, tenant)]:
|
||||
return authorize(user, element, tenant)
|
||||
return authorize(user, element, tenant, operation=operation)
|
||||
else:
|
||||
# In case of someone trying to guess,
|
||||
# while someone is legitimately logged in
|
||||
@ -214,7 +262,7 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
# delay as well
|
||||
if crypt == crypted:
|
||||
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
|
||||
return authorize(user, element, tenant)
|
||||
return authorize(user, element, tenant, operation)
|
||||
try:
|
||||
pammy = PAM.pam()
|
||||
pammy.start(_pamservice, user, credobj.pam_conv)
|
||||
@ -222,7 +270,7 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
pammy.acct_mgmt()
|
||||
del pammy
|
||||
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
|
||||
return authorize(user, element, tenant, skipuserobj=False)
|
||||
return authorize(user, element, tenant, operation, skipuserobj=False)
|
||||
except NameError:
|
||||
pass
|
||||
except PAM.error:
|
||||
|
@ -99,6 +99,7 @@ _attraliases = {
|
||||
'bmcpass': 'secret.hardwaremanagementpassword',
|
||||
'switchpass': 'secret.hardwaremanagementpassword',
|
||||
}
|
||||
_validroles = ('Administrator', 'Operator')
|
||||
|
||||
def _mkpath(pathname):
|
||||
try:
|
||||
@ -1263,7 +1264,16 @@ class ConfigManager(object):
|
||||
|
||||
def _true_set_usergroup(self, groupname, attributemap):
|
||||
for attribute in attributemap:
|
||||
self._cfgstore['usergroups'][attribute] = attributemap[attribute]
|
||||
if attribute == 'role':
|
||||
therole = None
|
||||
for candrole in _validroles:
|
||||
if candrole.lower().startswith(attributemap[attribute].lower()):
|
||||
therole = candrole
|
||||
if therole not in _validroles:
|
||||
raise ValueError(
|
||||
'Unrecognized role "{0}" (valid roles: {1})'.format(attributemap[attribute], ','.join(_validroles)))
|
||||
attributemap[attribute] = therole
|
||||
self._cfgstore['usergroups'][groupname][attribute] = attributemap[attribute]
|
||||
_mark_dirtykey('usergroups', groupname, self.tenant)
|
||||
self._bg_sync_to_file()
|
||||
|
||||
@ -1322,6 +1332,15 @@ class ConfigManager(object):
|
||||
def _true_set_user(self, name, attributemap):
|
||||
user = self._cfgstore['users'][name]
|
||||
for attribute in attributemap:
|
||||
if attribute == 'role':
|
||||
therole = None
|
||||
for candrole in _validroles:
|
||||
if candrole.lower().startswith(attributemap[attribute].lower()):
|
||||
therole = candrole
|
||||
if therole not in _validroles:
|
||||
raise ValueError(
|
||||
'Unrecognized role "{0}" (valid roles: {1})'.format(attributemap[attribute], ','.join(_validroles)))
|
||||
attributemap[attribute] = therole
|
||||
if attribute == 'password':
|
||||
salt = os.urandom(8)
|
||||
#TODO: WORKERPOOL, offload password set to a worker
|
||||
@ -1598,7 +1617,7 @@ class ConfigManager(object):
|
||||
del attribmap[group][attr]
|
||||
if 'noderange' in attribmap[group]:
|
||||
if len(attribmap[group]) > 1:
|
||||
raise ValueErorr('noderange attribute must be set by itself')
|
||||
raise ValueError('noderange attribute must be set by itself')
|
||||
for attr in attribmap[group]:
|
||||
if attr in _attraliases:
|
||||
newattr = _attraliases[attr]
|
||||
|
@ -407,19 +407,21 @@ def create_usergroup(inputdata, configmanager):
|
||||
|
||||
def update_usergroup(groupname, attribmap, configmanager):
|
||||
try:
|
||||
configmanager.set_usergroup(name, attribmap)
|
||||
except ValueError:
|
||||
raise exc.InvalidArgumentException()
|
||||
configmanager.set_usergroup(groupname, attribmap)
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
|
||||
def update_user(name, attribmap, configmanager):
|
||||
try:
|
||||
configmanager.set_user(name, attribmap)
|
||||
except ValueError:
|
||||
raise exc.InvalidArgumentException()
|
||||
except ValueError as e:
|
||||
raise exc.InvalidArgumentException(str(e))
|
||||
|
||||
|
||||
def show_usergroup(groupname, configmanager):
|
||||
return []
|
||||
groupinfo = configmanager.get_usergroup(groupname)
|
||||
for attr in groupinfo:
|
||||
yield msg.Attributes(kv={attr: groupinfo[attr]})
|
||||
|
||||
def show_user(name, configmanager):
|
||||
userobj = configmanager.get_user(name)
|
||||
@ -437,6 +439,10 @@ def show_user(name, configmanager):
|
||||
rv[attr] = userobj[attr]
|
||||
yield msg.Attributes(kv={attr: rv[attr]},
|
||||
desc=attrscheme.user[attr]['description'])
|
||||
if 'role' in userobj:
|
||||
yield msg.Attributes(kv={'role': userobj['role']})
|
||||
|
||||
|
||||
|
||||
|
||||
def stripnode(iterablersp, node):
|
||||
|
@ -269,6 +269,9 @@ def _authorize_request(env, operation):
|
||||
name = ''
|
||||
sessionid = None
|
||||
cookie = Cookie.SimpleCookie()
|
||||
element = env['PATH_INFO']
|
||||
if element.startswith('/sessions/current/'):
|
||||
element = None
|
||||
if 'HTTP_COOKIE' in env:
|
||||
#attempt to use the cookie. If it matches
|
||||
cc = RobustCookie()
|
||||
@ -290,7 +293,7 @@ def _authorize_request(env, operation):
|
||||
httpsessions[sessionid]['expiry'] = time.time() + 90
|
||||
name = httpsessions[sessionid]['name']
|
||||
authdata = auth.authorize(
|
||||
name, element=None,
|
||||
name, element=element, operation=operation,
|
||||
skipuserobj=httpsessions[sessionid]['skipuserobject'])
|
||||
if (not authdata) and 'HTTP_AUTHORIZATION' in env:
|
||||
if env['PATH_INFO'] == '/sessions/current/logout':
|
||||
@ -303,8 +306,10 @@ def _authorize_request(env, operation):
|
||||
return ('logout',)
|
||||
name, passphrase = base64.b64decode(
|
||||
env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(':', 1)
|
||||
authdata = auth.check_user_passphrase(name, passphrase, element=None)
|
||||
if not authdata:
|
||||
authdata = auth.check_user_passphrase(name, passphrase, operation=operation, element=element)
|
||||
if authdata is False:
|
||||
return {'code': 403}
|
||||
elif not authdata:
|
||||
return {'code': 401}
|
||||
sessid = util.randomstring(32)
|
||||
while sessid in httpsessions:
|
||||
@ -341,15 +346,10 @@ def _authorize_request(env, operation):
|
||||
if 'csrftoken' in httpsessions[sessid]:
|
||||
authinfo['authtoken'] = httpsessions[sessid]['csrftoken']
|
||||
return authinfo
|
||||
else:
|
||||
elif authdata is None:
|
||||
return {'code': 401}
|
||||
# TODO(jbjohnso): actually evaluate the request for authorization
|
||||
# In theory, the x509 or http auth stuff will get translated and then
|
||||
# passed on to the core authorization function in an appropriate form
|
||||
# expresses return in the form of http code
|
||||
# 401 if there is no known identity
|
||||
# 403 if valid identity, but no access
|
||||
# going to run 200 just to get going for now
|
||||
else:
|
||||
return {'code': 403}
|
||||
|
||||
|
||||
def _pick_mimetype(env):
|
||||
@ -429,7 +429,7 @@ def resourcehandler_backend(env, start_response):
|
||||
return
|
||||
if authorized['code'] == 403:
|
||||
start_response('403 Forbidden', badauth)
|
||||
yield 'authorization failed'
|
||||
yield 'Forbidden'
|
||||
return
|
||||
if authorized['code'] != 200:
|
||||
raise Exception("Unrecognized code from auth engine")
|
||||
|
@ -120,7 +120,7 @@ def sessionhdl(connection, authname, skipauth=False, cert=None):
|
||||
cfm = configmanager.ConfigManager(tenant=None, username=authname)
|
||||
elif authname:
|
||||
authdata = auth.authorize(authname, element=None)
|
||||
if authdata is not None:
|
||||
if authdata:
|
||||
cfm = authdata[1]
|
||||
authenticated = True
|
||||
send_data(connection, "Confluent -- v0 --")
|
||||
@ -145,7 +145,7 @@ def sessionhdl(connection, authname, skipauth=False, cert=None):
|
||||
# element path, that authorization will need to be called
|
||||
# per request the user makes
|
||||
authdata = auth.check_user_passphrase(authname, passphrase)
|
||||
if authdata is None:
|
||||
if not authdata:
|
||||
auditlog.log(
|
||||
{'operation': 'connect', 'user': authname, 'allowed': False})
|
||||
else:
|
||||
@ -212,7 +212,7 @@ def process_request(connection, request, cfm, authdata, authname, skipauth):
|
||||
}
|
||||
if not skipauth:
|
||||
authdata = auth.authorize(authdata[2], path, authdata[3], operation)
|
||||
if authdata is None:
|
||||
if not authdata:
|
||||
auditmsg['allowed'] = False
|
||||
auditlog.log(auditmsg)
|
||||
raise exc.ForbiddenRequest()
|
||||
|
Loading…
Reference in New Issue
Block a user