def _get_token(self): ''' obtengo un token mediante el flujo client_credentials para poder llamar a la api de usuarios ''' grant = ClientCredentialsGrant(self.oidc_url, self.client_id, self.client_secret, verify=self.verify_ssl) token = grant.get_token(grant.access_token()) if not token: raise Exception() return token
def login(cls, usuario, clave): ''' obtengo un token mediante el flujo client_credentials para poder llamar a la api de usuarios ''' grant = ClientCredentialsGrant(cls.client_id, cls.client_secret) token = grant.get_token(grant.access_token()) if not token: raise LoginError() ''' se deben cheqeuar intentos de login, y disparar : SeguridadError en el caso de que se haya alcanzado el máximo de intentos ''' headers = { 'Authorization': 'Bearer {}'.format(token) } r = requests.post(cls.USERS_API_URL + '/auth', verify=cls.verify, headers=headers, json={'usuario':usuario, 'clave':clave}) if r.status_code == 200: clave_data = r.json() usuario_id = clave_data['usuario_id'] r = requests.get(cls.USERS_API_URL + '/usuarios/{}'.format(usuario_id), headers=headers, verify=cls.verify) if r.status_code == 200: return r.json() logging.debug(r) if r.status_code == 403: raise ClaveError() if r.status_code == 404: raise UsuarioNoEncontradoError() raise LoginError()
def __init__(self, oidc_url, client_id, client_secret, users_api_url, verify=True): self.cc = ClientCredentialsGrant(oidc_url, client_id, client_secret, verify) self.users_api_url = users_api_url self.verify = verify
class UsersApi: def __init__(self, oidc_url, client_id, client_secret, users_api_url, verify=True): self.cc = ClientCredentialsGrant(oidc_url, client_id, client_secret, verify) self.users_api_url = users_api_url self.verify = verify def get_token(self): r = self.cc.access_token() tk = self.cc.get_token(r) return tk def get_auth_headers(self, tk): headers = { 'Authorization': 'Bearer {}'.format(tk), 'Accept': 'application/json' } return headers def obtener_usuario_por_dni(self, headers, dni): url = f"{self.users_api_url}/usuario_por_dni/{dni}" r = requests.get(url, verify=self.verify, allow_redirects=False, headers=headers) return r.json() def obtener_usuarios(self, headers, uids=[]): puids = '+'.join(uids) url = f"{self.users_api_url}/usuarios/{puids}" r = requests.get(url, verify=self.verify, allow_redirects=False, headers=headers) return r.json() def buscar_usuarios(self, headers, search=None): if not search: return [] params = {'q': search} r = requests.get(self.users_api_url + f'/usuarios', verify=self.verify, allow_redirects=False, headers=headers, params=params) return r.json()
def __init__(self, oidc_url, api_url, client_id, client_secret, verify=True, realm=''): self.verify = verify self.realm = realm self.warden_url = api_url self.client_id = client_id self.client_secret = client_secret self.client_credentials = ClientCredentialsGrant( oidc_url, self.client_id, self.client_secret, self.verify)
class RecuperarClaveModel: verify = bool(int(os.environ.get('VERIFY_SSL', 0))) INTERNAL_DOMAINS = os.environ['INTERNAL_DOMAINS'].split(',') OIDC_URL = os.environ['OIDC_URL'] OIDC_ADMIN_URL = os.environ['OIDC_ADMIN_URL'] client_id = os.environ['OIDC_CLIENT_ID'] client_secret = os.environ['OIDC_CLIENT_SECRET'] REDIS_HOST = os.environ.get('REDIS_HOST', '127.0.0.1') REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379)) RESET_CLAVE_FROM = os.environ.get('RESET_CLAVE_FROM', '') USERS_API_URL = os.environ['USERS_API_URL'] grant = ClientCredentialsGrant(OIDC_URL, client_id, client_secret, verify=verify) redis = redis.StrictRedis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True) """ ////////////// MANEJO DE CACHE //////////////////////// """ @classmethod def _obtener_usuario_api(cls, url, token=None): if not token: token = cls._obtener_token() headers = {'Authorization': 'Bearer {}'.format(token)} r = requests.get(url, headers=headers, verify=cls.verify, allow_redirects=False) if not r.ok: logging.debug(r) raise Exception('error obteniendo usuario') usrs = r.json() if not usrs or len(usrs) <= 0: return None usr = None try: usr = usrs[0] except KeyError as e: usr = usrs assert 'id' in usr cls._setear_usuario_cache(usr) return usr @classmethod def _setear_usuario_cache(cls, usr): expire = 60 * 5 uid = usr['id'] kmails = 'r_usuario_mails_{}'.format(uid) cls.redis.delete(kmails) for m in usr['mails']: mid = m['id'] k = 'r_mail_{}'.format(mid) cls.redis.hmset(k, m) cls.redis.expire(k, expire * 2) cls.redis.sadd(kmails, mid) cls.redis.expire(kmails, expire * 2) k = 'r_usuario_uid_{}'.format(uid) cls.redis.hmset(k, usr) cls.redis.expire(k, expire) k = 'r_usuario_dni_{}'.format(usr['dni'].lower().replace(' ', '')) cls.redis.hset(k, 'uid', uid) cls.redis.expire(k, expire) @classmethod def _eliminar_usuario_de_cache(cls, uid): k = 'r_usuario_mails_{}'.format(uid) cls.redis.delete(k) k = 'r_usuario_uid_{}'.format(uid) cls.redis.delete(k) @classmethod def _format_mail_from_redis(cls, m): for k in m.keys(): if m[k] == 'None': m[k] = None return m @classmethod def _obtener_correo_cache(cls, mid): key = 'r_mail_{}'.format(mid) if not cls.redis.hexists(key, 'id'): return None mail = cls.redis.hgetall(key) fmail = cls._format_mail_from_redis(mail) return fmail @classmethod def _obtener_usuario_cache(cls, uid): assert uid is not None usr = cls.redis.hgetall('r_usuario_uid_{}'.format(uid)) if len(usr.keys()) > 0: try: uid = usr['id'] mailids = cls.redis.smembers('r_usuario_mails_{}'.format(uid)) mails = [ cls._obtener_correo_cache(mid) for mid in mailids if mid ] usr['mails'] = [m for m in mails if m] return usr except Exception as e: logging.exception(e) return None @classmethod def _obtener_usuario_por_uid(cls, uid, token=None): usr = cls._obtener_usuario_cache(uid) if usr: return usr url = '{}/usuarios/{}'.format(cls.USERS_API_URL, uid) usr = cls._obtener_usuario_api(url, token) return usr @classmethod def _obtener_usuario_por_dni(cls, dni, token=None): key = 'r_usuario_dni_{}'.format(dni.lower().replace(' ', '')) if cls.redis.hexists(key, 'uid'): uid = cls.redis.hget(key, 'uid') return cls._obtener_usuario_por_uid(uid, token) url = '{}/usuario_por_dni/{}'.format(cls.USERS_API_URL, dni) usr = cls._obtener_usuario_api(url, token) return usr @classmethod def _obtener_token(cls): token = cls.grant.get_token(cls.grant.access_token()) if not token: raise Exception('error obteniendo token de acceso') return token """ ////////////////////////// """ @classmethod def _obtener_correo_alternativo(cls, usr): correo = None correos_validos = [ m for m in usr['mails'] if not m['eliminado'] and m['confirmado'] ] correo = [ m for m in correos_validos if m['email'].split('@')[1] not in cls.INTERNAL_DOMAINS ] if len(correo) <= 0: return None else: return correo[0] @classmethod def verificar_dni(cls, dni): usr = cls._obtener_usuario_por_dni(dni) if not usr: return None c = cls._obtener_correo_alternativo(usr) if c: return {'tiene_correo': True, 'usuario': usr} else: return {'tiene_correo': False, 'usuario': usr} @classmethod def obtener_correo(cls, uid): usr = cls._obtener_usuario_por_uid(uid) if not usr: raise Exception('No existe ese usuario') correo = cls._obtener_correo_alternativo(usr) if not correo: raise Exception('No tiene correo alternativo') """ genero el ofuscamiento de la direccion """ mail = correo['email'] cs = mail.split('@') ayuda = None if len(cs[0]) <= 3: ayuda = mail else: l = int(len(cs[0]) / 2) ayuda = cs[0][:-l] + ('*' * l) + '@' + cs[1] r = {'correo': {'id': correo['id'], 'ayuda': ayuda}, 'usuario': usr} return r @classmethod def _generar_codigo(cls): return str(uuid.uuid4())[:8] @classmethod def _enviar_codigo_template(cls, usuario, codigo, correo): templ = MailsModel.obtener_template('codigo.tmpl') text = templ.render(usuario=usuario, codigo=codigo) r = MailsModel.enviar_correo(cls.RESET_CLAVE_FROM, correo, 'Reseteo de Clave FCE', text) if r.ok: logging.debug('correo enviado correctamente') else: logging.debug('error enviando correo') @classmethod def enviar_codigo(cls, session, eid, correo): correo = correo.lower().strip() mail = cls._obtener_correo_cache(eid) if not mail: return None if correo not in mail['email'].lower().strip(): return None uid = mail['usuario_id'] usuario = cls._obtener_usuario_por_uid(uid) if not usuario: return None codigo = None intentos = session.query(ResetClave).filter( ResetClave.correo == correo, ResetClave.confirmado == None).all() for rc in intentos: codigo = rc.codigo break else: codigo = cls._generar_codigo() rid = str(uuid.uuid4()) rc = ResetClave() rc.codigo = codigo rc.correo = correo rc.usuario_id = uid rc.id = rid session.add(rc) cls._enviar_codigo_template(usuario, codigo, correo) return rid @classmethod def detalle_recuperar_clave(cls, session): rc = session.query(ResetClave).all() return rc @classmethod def _generar_clave(cls): return str(uuid.uuid4())[:8] @classmethod def _enviar_clave_template(cls, usuario, clave, correo): templ = MailsModel.obtener_template('clave_temporal.tmpl') text = templ.render(usuario=usuario, clave=clave) r = MailsModel.enviar_correo(cls.RESET_CLAVE_FROM, correo, 'Reseteo de Clave FCE', text) if r.ok: logging.debug('correo enviado correctamente') else: logging.debug('error enviando correo') @classmethod def _cambiar_clave(cls, session, usuario, clave, es_temporal=True): uid = usuario['id'] dni = usuario['dni'] cs = session.query(UsuarioClave).filter( UsuarioClave.usuario_id == uid).all() for c in cs: c.eliminada = datetime.datetime.now() uc = UsuarioClave() uc.usuario_id = uid uc.usuario = dni uc.clave = clave uc.dirty = True uc.debe_cambiarla = es_temporal if es_temporal: uc.expiracion = datetime.datetime.now() + datetime.timedelta( days=5) else: ''' a google solo se sincronizan las claves que no son temporales ''' uc.google = True session.add(uc) session.commit() #try: # GoogleModel.sincronizar_dirty(session) #except Exception as e: # logging.exception(e) @classmethod def verificar_codigo(cls, session, iid, codigo): assert iid is not None assert codigo is not None rc = session.query(ResetClave).filter( ResetClave.id == iid, ResetClave.codigo == codigo).one_or_none() if not rc: return None uid = rc.usuario_id correo = rc.correo usuario = cls._obtener_usuario_por_uid(uid) if not usuario: raise Exception('no se pudo obtener el usuario') confirmado = datetime.datetime.now() actualizado = datetime.datetime.now() clave = cls._generar_clave() cls._cambiar_clave(session, usuario, clave, es_temporal=True) rcs = session.query(ResetClave).filter( ResetClave.codigo == codigo, ResetClave.correo == correo).all() for rc in rcs: rc.actualizado = actualizado rc.confirmado = confirmado rc.clave = clave session.commit() try: cls._enviar_clave_template(usuario, clave, correo) except Exception as e: logging.exception(e) try: cls._eliminar_usuario_de_cache(uid) except Exception as e: logging.exception(e) return clave @classmethod def cambiar_clave(cls, session, uid, clave, es_temporal=False): usuario = cls._obtener_usuario_por_uid(uid) if not usuario: raise Exception('no se pudo obtener el usuario') cls._cambiar_clave(session, usuario, clave, es_temporal) @classmethod def recuperar_cambiar_clave(cls, session, cid, clave): """ Para permitir varios tipos de flujos, el verificar código genera una clave temporal y este método permite cambiar la clave temporal por una fija, si es que como cid se pasa la temporal. """ uc = session.query(UsuarioClave).filter( UsuarioClave.clave == cid).one() uid = uc.usuario_id cls.cambiar_clave(session, uid, clave, es_temporal=False) @classmethod def precondiciones(cls, session, uid): ''' por ahora solo chequeo que no tenga clave temporal ''' ahora = datetime.datetime.now() q = session.query(UsuarioClave).filter(UsuarioClave.usuario_id == uid, UsuarioClave.eliminada == None) #existe un problema con la zona horaria de la fecha por lo que siempre da expirada. asi que por ahora lo comento #q.filter(or_(UsuarioClave.debe_cambiarla == True, UsuarioClave.expiracion <= ahora)) q = q.filter(UsuarioClave.debe_cambiarla == True) claves_temporales = q.count() return {'clave': claves_temporales <= 0}
finally: con.close() """ comienza la parte de ejecución sobre los sistemas actuales """ import requests oidc_url = os.environ['OIDC_URL'] client_id = os.environ['OIDC_CLIENT_ID'] client_secret = os.environ['OIDC_CLIENT_SECRET'] verify = False users_api_url = os.environ['USERS_API_URL'] from oidc.oidc import ClientCredentialsGrant cc = ClientCredentialsGrant(oidc_url, client_id, client_secret, verify) def _get_token(cc): r = cc.access_token() tk = cc.get_token(r) return tk def _get_auth_headers(tk): headers = { 'Authorization': 'Bearer {}'.format(tk), 'Accept': 'application/json' } return headers
class LoginModel: verify = VERIFY_SSL OIDC_URL = OIDC_URL OIDC_ADMIN_URL = OIDC_ADMIN_URL client_id = CLIENT_ID client_secret = CLIENT_SECRET api = API(url=OIDC_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, verify_ssl=VERIFY_SSL) hydra = HydraModel(OIDC_ADMIN_URL, verify) grant = ClientCredentialsGrant(OIDC_URL, client_id, client_secret, verify=verify) @classmethod def _obtener_token(cls): token = cls.grant.get_token(cls.grant.access_token()) if not token: raise Exception('error obteniendo token de acceso') return token @classmethod def _obtener_token_sesion(cls, sid): d = str(datetime.datetime.now()) h = '{}-{}'.format(sid,d).encode('utf-8') return hashlib.sha1(h).hexdigest() """ ----------------- El flujo de login ----------------- """ @classmethod def init_login_flow(cls, challenge): lc = cls.hydra.obtener_login_challenge(challenge) r = { 'redirect_to': None } if lc['skip']: r = cls.aceptar_login_challenge(challenge,lc) return r @classmethod def login(cls, session, usuario, clave, challenge): ahora = datetime.datetime.now() q = session.query(UsuarioClave).filter(UsuarioClave.usuario == usuario, UsuarioClave.clave == clave, UsuarioClave.eliminada == None) q = q.filter(or_(UsuarioClave.expiracion == None, UsuarioClave.expiracion > ahora)) c = q.one_or_none() if not c: r = cls.denegar_login_challenge(challenge, error='unknown_user', descripcion='Usuario o clave incorrectos') ''' logueo el intento ''' ll = LoginLog() ll.id = str(uuid.uuid4()) ll.created = datetime.datetime.utcnow() ll.usuario = usuario ll.clave = clave ll.status = False ll.challenge = challenge session.add(ll) else: """ #TODO: hace falta chequear el debe cambiarla para loguearlo y redirigirlo a usuarios. """ r = cls.aceptar_login_challenge(challenge, uid=c.usuario_id) ''' logueo el intento ''' ll = LoginLog() ll.id = str(uuid.uuid4()) ll.created = datetime.datetime.utcnow() ll.usuario = usuario ll.status = True ll.challenge = challenge session.add(ll) return r @classmethod def aceptar_login_challenge(cls, challenge, lc=None, uid=None, recordar=True, timeout=3600): if not lc: lc = cls.hydra.obtener_login_challenge(challenge) data = { 'subject': lc['subject'] if lc['skip'] else uid, 'remember': False if lc['skip'] else recordar, 'remember_for': timeout, 'acr':'' } return cls.hydra.aceptar_login_challenge(challenge, data) @classmethod def denegar_login_challenge(cls, challenge, error='', descripcion=''): data = { 'error': error, 'error_description': descripcion } return cls.hydra.denegar_login_challenge(challenge, data) """ --------------------- El flujo de consent --------------------- """ @classmethod def init_consent_flow(cls, challenge): cc = cls.hydra.obtener_consent_challenge(challenge) r = { 'redirect_to': None } if cc['skip']: r = cls.aceptar_consent_challenge(challenge,cc) else: """ En nuestro caso siempre aceptamos los consent ya que son apps internas Para cumplir con la especificación habría que mostrar una pantalla con el consent de los scopes """ r = cls.aceptar_consent_challenge(challenge,cc) return r """ https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims """ @classmethod def aceptar_consent_challenge(cls, challenge, cc=None, recordar=True, timeout=3600): if not cc: cc = cls.hydra.obtener_consent_challenge(challenge) """ ///////////////// TODO: cambiar este hack horrible a la api de usuarios!!! lo mejor a hacer es usar eventsourcing. """ uid = cc['subject'] tk = cls.api._get_token() def _get_user_uuid(api, uuid, token=None): query = '{}/usuarios/{}'.format(USERS_API_URL, uuid) r = api.get(query, token=token) if not r.ok: return None usr = r.json() if len(usr) > 0: return usr[0] return None def _get_primary_email(usr): mails = sorted([m for m in usr['mails'] if m['confirmado'] and not m['eliminado']], key=lambda m: m['email']) if len(mails) > 0: return mails[0] return None usr = _get_user_uuid(cls.api, uid, tk) if not usr: raise Exception('error obteniendo datos de usuario') """ ////////////////////////// """ atk = {} if 'profile' in cc['requested_scope']: atk['name'] = usr['nombre'] atk['family_name'] = usr['apellido'] atk['given_name'] = usr['nombre'] atk['middle_name'] = usr['nombre'].split(' ')[1] if len(usr['nombre'].split(' ')) > 1 else '' atk['nickname'] = '' atk['preferred_username'] = usr['dni'] atk['profile'] = '' atk['picture'] = '' atk['website'] = '' atk['gender'] = usr['genero'] atk['birthdate'] = usr['nacimiento'] atk['zoneinfo'] = 'America/Argentina/Buenos_Aires' atk['locale'] = 'es-ES' updated_at = None try: updated_at = parse(usr['actualizado']).timestamp() except Exception as ex: try: updated_at = parse(usr['creado']).timestamp() except Exception as exx: pass atk['updated_at'] = updated_at if 'email' in cc['requested_scope']: em = _get_primary_email(usr) if em: atk['email'] = em['email'] atk['email_verified'] = True if em['confirmado'] else False if 'address' in cc['requested_scope']: atk['address'] = { 'street_address':usr['direccion'], 'locality':usr['ciudad'], 'country':usr['pais'] } """ if 'phone' in cc['requested_scope']: atk['phone_number'] = '' atk['phone_number_verified'] = '' """ data = { 'grant_scope': cc['requested_scope'], 'remember': False if cc['skip'] else recordar, 'remember_for': timeout, 'session':{ 'access_token':atk, 'id_token':atk } } return cls.hydra.aceptar_consent_challenge(challenge, data) """ métodos utilitarios para la administración """ @classmethod def logout_hydra(cls, client_id, uid): logging.debug('deslogueando a {} en {}'.format(uid, client_id)) c = cls.hydra.obtener_cliente(client_id) logging.debug(c) ''' https://www.ory.sh/docs/api/hydra/?version=latest ''' url = c['client_uri'] r = { 'redirect_to': url } cls.hydra.eliminar_sesion_login_usuario(uid) logging.debug(r) return r @classmethod def obtener_sesiones_usuario(cls, uid): return cls.hydra.obtener_consent_sesiones(uid) @classmethod def eliminar_sesiones_usuario(cls, uid): cls.hydra.eliminar_sesion_login_usuario(uid) cls.hydra.eliminar_sesiones_usuario(uid) @classmethod def eliminar_sesiones_usuario_cliente(cls, uid, cid): cls.hydra.eliminar_sesiones_cliente_usuario(cid, uid)
class Warden: def __init__(self, oidc_url, api_url, client_id, client_secret, verify=True, realm=''): self.verify = verify self.realm = realm self.warden_url = api_url self.client_id = client_id self.client_secret = client_secret self.client_credentials = ClientCredentialsGrant( oidc_url, self.client_id, self.client_secret, self.verify) def _get_auth_headers(self): r = self.client_credentials.access_token(scopes=['warden']) tk = self.client_credentials.get_token(r) headers = { 'Authorization': 'Bearer {}'.format(tk), 'Accept': 'application/json' } return headers def check_access(self, token, resource, action): headers = self._get_auth_headers() data = {'token': token, 'action': action, 'resource': resource} r = requests.post(self.warden_url + '/allowed', verify=self.verify, allow_redirects=False, headers=headers, data=data) if r.ok: js = r.json() if js['allowed'] == True: return js return None def _has_profiles(self, token, profiles, op): headers = self._get_auth_headers() data = {'token': token, 'profiles': profiles, 'op': op} r = requests.post(self.warden_url + '/profile', verify=self.verify, allow_redirects=False, headers=headers, json=data) if r.ok: js = r.json() return js return None def has_one_profile(self, token, profiles=[]): return self._has_profiles(token, profiles, op='OR') def has_all_profiles(self, token, profiles=[]): return self._has_profiles(token, profiles, op='AND') """ esquema nuevo de permisos el token de consulta a warden se realiza usando el mismo token de usuario """ def has_permissions(self, token, permisos=[]): assert token is not None headers = { 'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json' } request = {'permissions': permisos} r = requests.post(self.warden_url + '/has_permissions', verify=self.verify, allow_redirects=False, headers=headers, json=request) if r.ok: js = r.json() return js['result'] return None def register_system_perms(self, token, system, permisos=[]): assert token is not None headers = { 'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json' } request = {'system': system, 'permissions': permisos} r = requests.post(self.warden_url + '/registrar_permisos', verify=self.verify, allow_redirects=False, headers=headers, json=request) if r.ok: return r.json() return None def _get_request_token(self): return self._bearer_token(flask.request.headers) """ introspección del token """ def _verify_valid_token(self, token): ''' Recupera y chequea el token por validez ''' if not token: return None headers = self._get_auth_headers() data = {'token': token} r = requests.post(self.warden_url + '/introspect', verify=self.verify, allow_redirects=False, headers=headers, json=data) if r.ok: js = r.json() if js['token']: return js['token'] return None def _require_valid_token(self): original_token = self._get_request_token() tk = self._verify_valid_token(original_token) if not tk: return original_token, None return original_token, tk def require_valid_token(self, f): @wraps(f) def decorated_function(*args, **kwargs): original_token, tk = self._require_valid_token() if not tk: return self._invalid_request() kwargs['token'] = tk #kwargs['original_token'] = original_token return f(*args, **kwargs) return decorated_function def require_valid_token2(self, f): @wraps(f) def decorated_function(*args, **kwargs): original_token, tk = self._require_valid_token() if not tk: return self._invalid_request() kwargs['token'] = tk kwargs['original_token'] = original_token return f(*args, **kwargs) return decorated_function def _bearer_token(self, headers): if 'Authorization' in headers: auth = headers['Authorization'].split(' ') if auth[0].lower() == 'bearer': return auth[1] return None def _invalid_request(self): return self._require_auth(text='Bad Request', error='invalid_request', status=400) def _invalid_token(self): return self._require_auth(text='Unauthorized', error='invalid_token', status=401) def _insufficient_scope(self): return self._require_auth(text='Forbidden', error='insufficient_scope', status=403) def _require_auth(self, text='Unauthorized', error=None, status=401, error_description=''): headers = None if error: headers = { 'WWW-Authenticate': 'Basic realm=\"{}\", error=\"{}\", error_description:\"{}\"'. format(self.realm, error, error_description) } else: headers = { 'WWW-Authenticate': 'Basic realm=\"{}\"'.format(self.realm) } return (text, status, headers)