Sin descripción

access_controls.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. # IRIS Source Code
  2. # Copyright (C) 2024 - DFIR-IRIS
  3. # contact@dfir-iris.org
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU Lesser General Public
  7. # License as published by the Free Software Foundation; either
  8. # version 3 of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # Lesser General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with this program; if not, write to the Free Software Foundation,
  17. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. import json
  19. import logging as log
  20. import traceback
  21. import uuid
  22. from functools import wraps
  23. import jwt
  24. import requests
  25. from flask import Request
  26. from flask import url_for
  27. from flask import request
  28. from flask import render_template
  29. from flask import session
  30. from flask_login import current_user
  31. from flask_login import login_user
  32. from flask_wtf import FlaskForm
  33. from jwt import PyJWKClient
  34. from requests.auth import HTTPBasicAuth
  35. from werkzeug.utils import redirect
  36. from app import TEMPLATE_PATH
  37. from app import app
  38. from app import db
  39. from app.blueprints.responses import response_error
  40. from app.datamgmt.case.case_db import get_case
  41. from app.datamgmt.manage.manage_access_control_db import user_has_client_access
  42. from app.datamgmt.manage.manage_users_db import get_user
  43. from app.iris_engine.access_control.utils import ac_fast_check_user_has_case_access
  44. from app.iris_engine.access_control.utils import ac_get_effective_permissions_of_user
  45. from app.iris_engine.utils.tracker import track_activity
  46. from app.models.cases import Cases
  47. from app.models.authorization import Permissions
  48. from app.models.authorization import CaseAccessLevel
  49. def _user_has_at_least_a_required_permission(permissions: list[Permissions]):
  50. """
  51. Returns true as soon as the user has at least one permission in the list of permissions
  52. Returns true if the list of required permissions is empty
  53. """
  54. if not permissions:
  55. return True
  56. for permission in permissions:
  57. if session['permissions'] & permission.value:
  58. return True
  59. return False
  60. def _set_caseid_from_current_user():
  61. redir = False
  62. if current_user.ctx_case is None:
  63. redir = True
  64. current_user.ctx_case = 1
  65. caseid = current_user.ctx_case
  66. return redir, caseid
  67. def _log_exception_and_error(e):
  68. log.exception(e)
  69. log.error(traceback.print_exc())
  70. def _get_caseid_from_request_data(request_data, no_cid_required):
  71. caseid = request_data.args.get('cid', default=None, type=int)
  72. if caseid:
  73. return False, caseid, True
  74. if no_cid_required:
  75. return False, caseid, True
  76. js_d = None
  77. try:
  78. if request_data.content_type == 'application/json':
  79. js_d = request_data.get_json()
  80. if not js_d:
  81. redir, caseid = _set_caseid_from_current_user()
  82. return redir, caseid, True
  83. if 'cid' not in js_d:
  84. cookie_session = request_data.cookies.get('session')
  85. if not cookie_session:
  86. redir, caseid = _set_caseid_from_current_user()
  87. return redir, caseid, True
  88. caseid = js_d.get('cid')
  89. return False, caseid, True
  90. except Exception as e:
  91. cookie_session = request_data.cookies.get('session')
  92. if not cookie_session:
  93. redir, caseid = _set_caseid_from_current_user()
  94. return redir, caseid, True
  95. _log_exception_and_error(e)
  96. return True, 0, False
  97. def _handle_no_cid_required(no_cid_required):
  98. if no_cid_required:
  99. js_d = request.get_json(silent=True)
  100. try:
  101. if type(js_d) == str:
  102. js_d = json.loads(js_d)
  103. caseid = js_d.get('cid') if type(js_d) == dict else None
  104. if caseid and 'cid' in request.json:
  105. request.json.pop('cid')
  106. except Exception:
  107. return None, False
  108. return caseid, True
  109. return None, False
  110. def _update_denied_case(caseid):
  111. session['current_case'] = {
  112. 'case_name': "{} to #{}".format("Access denied", caseid),
  113. 'case_info': "",
  114. 'case_id': caseid,
  115. 'access': '<i class="ml-2 text-danger mt-1 fa-solid fa-ban"></i>'
  116. }
  117. def _update_current_case(caseid, restricted_access):
  118. if session['current_case']['case_id'] != caseid:
  119. case = get_case(caseid)
  120. if case:
  121. session['current_case'] = {
  122. 'case_name': "{}".format(case.name),
  123. 'case_info': "(#{} - {})".format(caseid, case.client.name),
  124. 'case_id': caseid,
  125. 'access': restricted_access
  126. }
  127. def _update_session(caseid, eaccess_level):
  128. restricted_access = ''
  129. if not eaccess_level:
  130. eaccess_level = [CaseAccessLevel.read_only, CaseAccessLevel.full_access]
  131. if CaseAccessLevel.read_only.value == eaccess_level:
  132. restricted_access = '<i class="ml-2 text-warning mt-1 fa-solid fa-lock" title="Read only access"></i>'
  133. _update_current_case(caseid, restricted_access)
  134. # TODO would be nice to remove parameter no_cid_required
  135. def _get_case_access(request_data, access_level, no_cid_required=False):
  136. redir, caseid, has_access = _get_caseid_from_request_data(request_data, no_cid_required)
  137. ctmp, has_access = _handle_no_cid_required(no_cid_required)
  138. redir = False
  139. if ctmp is not None:
  140. return redir, ctmp, has_access
  141. eaccess_level = ac_fast_check_user_has_case_access(current_user.id, caseid, access_level)
  142. if eaccess_level is None and access_level:
  143. _update_denied_case(caseid)
  144. return redir, caseid, False
  145. _update_session(caseid, eaccess_level)
  146. if caseid is not None and not get_case(caseid):
  147. log.warning('No case found. Using default case')
  148. return True, 1, True
  149. return redir, caseid, True
  150. def _is_csrf_token_valid():
  151. if request.method != 'POST':
  152. return True
  153. if request.headers.get('X-IRIS-AUTH') is not None:
  154. return True
  155. if request.headers.get('Authorization') is not None:
  156. return True
  157. cookie_session = request.cookies.get('session')
  158. # True in the absence of a session cookie, because no CSRF token is required for API calls
  159. if not cookie_session:
  160. return True
  161. form = FlaskForm()
  162. if not form.validate():
  163. return False
  164. # TODO not nice to have a side-effect within a 'is' method.
  165. if request.is_json:
  166. request.json.pop('csrf_token')
  167. return True
  168. def _ac_return_access_denied(caseid: int = None):
  169. error_uuid = uuid.uuid4()
  170. log.warning(f"Access denied to case #{caseid} for user ID {current_user.id}. Error {error_uuid}")
  171. return render_template('pages/error-403.html', user=current_user, caseid=caseid, error_uuid=error_uuid,
  172. template_folder=TEMPLATE_PATH), 403
  173. def ac_requires_case_identifier(*access_level):
  174. def decorate_with_requires_case_identifier(f):
  175. @wraps(f)
  176. def wrap(*args, **kwargs):
  177. try:
  178. redir, caseid, has_access = get_case_access_from_api(request, access_level)
  179. except Exception as e:
  180. log.exception(e)
  181. return response_error('Invalid data. Check server logs', status=500)
  182. if not caseid and not redir:
  183. return response_error('Invalid case ID', status=404)
  184. if not has_access:
  185. return ac_api_return_access_denied(caseid=caseid)
  186. kwargs.update({'caseid': caseid})
  187. return f(*args, **kwargs)
  188. return wrap
  189. return decorate_with_requires_case_identifier
  190. def get_case_access_from_api(request_data, access_level):
  191. redir, caseid, has_access = _get_caseid_from_request_data(request_data, False)
  192. redir = False
  193. if not hasattr(current_user, 'id'):
  194. # Anonymous request, deny access
  195. return False, 1, False
  196. eaccess_level = ac_fast_check_user_has_case_access(current_user.id, caseid, access_level)
  197. if eaccess_level is None and access_level:
  198. return redir, caseid, False
  199. if caseid is not None and not get_case(caseid):
  200. log.warning('No case found. Using default case')
  201. return True, 1, True
  202. return redir, caseid, True
  203. def not_authenticated_redirection_url(request_url: str):
  204. redirection_mapper = {
  205. "oidc_proxy": lambda: app.config.get("AUTHENTICATION_PROXY_LOGOUT_URL"),
  206. "local": lambda: url_for('login.login', next=request_url),
  207. "ldap": lambda: url_for('login.login', next=request_url),
  208. "oidc": lambda: url_for('login.login', next=request_url,)
  209. }
  210. return redirection_mapper.get(app.config.get("AUTHENTICATION_TYPE"))()
  211. def ac_case_requires(*access_level):
  212. def inner_wrap(f):
  213. @wraps(f)
  214. def wrap(*args, **kwargs):
  215. if not is_user_authenticated(request):
  216. return redirect(not_authenticated_redirection_url(request.full_path))
  217. redir, caseid, has_access = _get_case_access(request, access_level)
  218. if not has_access:
  219. return _ac_return_access_denied(caseid=caseid)
  220. kwargs.update({"caseid": caseid, "url_redir": redir})
  221. return f(*args, **kwargs)
  222. return wrap
  223. return inner_wrap
  224. # TODO try to remove option no_cid_required
  225. def ac_requires(*permissions, no_cid_required=False):
  226. def inner_wrap(f):
  227. @wraps(f)
  228. def wrap(*args, **kwargs):
  229. if not is_user_authenticated(request):
  230. return redirect(not_authenticated_redirection_url(request.full_path))
  231. redir, caseid, _ = _get_case_access(request, [], no_cid_required=no_cid_required)
  232. kwargs.update({'caseid': caseid, 'url_redir': redir})
  233. if not _user_has_at_least_a_required_permission(permissions):
  234. return _ac_return_access_denied()
  235. return f(*args, **kwargs)
  236. return wrap
  237. return inner_wrap
  238. def ac_api_requires(*permissions):
  239. def inner_wrap(f):
  240. @wraps(f)
  241. def wrap(*args, **kwargs):
  242. if not _is_csrf_token_valid():
  243. return response_error('Invalid CSRF token')
  244. if not is_user_authenticated(request):
  245. return response_error('Authentication required', status=401)
  246. if 'permissions' not in session:
  247. session['permissions'] = ac_get_effective_permissions_of_user(current_user)
  248. if not _user_has_at_least_a_required_permission(permissions):
  249. return response_error('Permission denied', status=403)
  250. return f(*args, **kwargs)
  251. return wrap
  252. return inner_wrap
  253. def ac_requires_client_access():
  254. def inner_wrap(f):
  255. @wraps(f)
  256. def wrap(*args, **kwargs):
  257. client_id = kwargs.get('client_id')
  258. if not user_has_client_access(current_user.id, client_id):
  259. return _ac_return_access_denied()
  260. return f(*args, **kwargs)
  261. return wrap
  262. return inner_wrap
  263. def ac_socket_requires(*access_level):
  264. def inner_wrap(f):
  265. @wraps(f)
  266. def wrap(*args, **kwargs):
  267. if not is_user_authenticated(request):
  268. return redirect(not_authenticated_redirection_url(request.full_path))
  269. else:
  270. chan_id = args[0].get('channel')
  271. if chan_id:
  272. case_id = int(chan_id.replace('case-', '').split('-')[0])
  273. else:
  274. return _ac_return_access_denied(caseid=0)
  275. access = ac_fast_check_user_has_case_access(current_user.id, case_id, access_level)
  276. if not access:
  277. return _ac_return_access_denied(caseid=case_id)
  278. return f(*args, **kwargs)
  279. return wrap
  280. return inner_wrap
  281. def ac_api_return_access_denied(caseid: int = None):
  282. user_id = current_user.id if hasattr(current_user, 'id') else 'Anonymous'
  283. error_uuid = uuid.uuid4()
  284. log.warning(f"EID {error_uuid} - Access denied with case #{caseid} for user ID {user_id} "
  285. f"accessing URI {request.full_path}")
  286. data = {
  287. 'user_id': user_id,
  288. 'case_id': caseid,
  289. 'error_uuid': error_uuid
  290. }
  291. return response_error('Permission denied', data=data, status=403)
  292. def ac_api_requires_client_access():
  293. def inner_wrap(f):
  294. @wraps(f)
  295. def wrap(*args, **kwargs):
  296. client_id = kwargs.get('client_id')
  297. if not user_has_client_access(current_user.id, client_id):
  298. return response_error("Permission denied", status=403)
  299. return f(*args, **kwargs)
  300. return wrap
  301. return inner_wrap
  302. def _authenticate_with_email(user_email):
  303. user = get_user(user_email, id_key="email")
  304. if not user:
  305. log.error(f'User with email {user_email} is not registered in the IRIS')
  306. return False
  307. login_user(user)
  308. track_activity(f"User '{user.id}' successfully logged-in", ctx_less=True)
  309. caseid = user.ctx_case
  310. session['permissions'] = ac_get_effective_permissions_of_user(user)
  311. if caseid is None:
  312. case = Cases.query.order_by(Cases.case_id).first()
  313. user.ctx_case = case.case_id
  314. user.ctx_human_case = case.name
  315. db.session.commit()
  316. session['current_case'] = {
  317. 'case_name': user.ctx_human_case,
  318. 'case_info': "",
  319. 'case_id': user.ctx_case
  320. }
  321. return True
  322. def _oidc_proxy_authentication_process(incoming_request: Request):
  323. # Get the OIDC JWT authentication token from the request header
  324. authentication_token = incoming_request.headers.get('X-Forwarded-Access-Token', '')
  325. if app.config.get("AUTHENTICATION_TOKEN_VERIFY_MODE") == 'lazy':
  326. user_email = incoming_request.headers.get('X-Email')
  327. if user_email:
  328. return _authenticate_with_email(user_email.split(',')[0])
  329. elif app.config.get("AUTHENTICATION_TOKEN_VERIFY_MODE") == 'introspection':
  330. # Use the authentication server's token introspection endpoint in order to determine if the request is valid /
  331. # authenticated. The TLS_ROOT_CA is used to validate the authentication server's certificate.
  332. # The other solution was to skip the certificate verification, BUT as the authentication server might be
  333. # located on another server, this check is necessary.
  334. introspection_body = {"token": authentication_token}
  335. introspection = requests.post(
  336. app.config.get("AUTHENTICATION_TOKEN_INTROSPECTION_URL"),
  337. auth=HTTPBasicAuth(app.config.get('AUTHENTICATION_CLIENT_ID'), app.config.get('AUTHENTICATION_CLIENT_SECRET')),
  338. data=introspection_body,
  339. verify=app.config.get("TLS_ROOT_CA")
  340. )
  341. if introspection.status_code == 200:
  342. response_json = introspection.json()
  343. if response_json.get("active", False) is True:
  344. user_email = response_json.get("sub")
  345. return _authenticate_with_email(user_email=user_email)
  346. else:
  347. log.info("USER IS NOT AUTHENTICATED")
  348. return False
  349. elif app.config.get("AUTHENTICATION_TOKEN_VERIFY_MODE") == 'signature':
  350. # Use the JWKS urls provided by the OIDC discovery to fetch the signing keys
  351. # and check the signature of the token
  352. try:
  353. jwks_client = PyJWKClient(app.config.get("AUTHENTICATION_JWKS_URL"))
  354. signing_key = jwks_client.get_signing_key_from_jwt(authentication_token)
  355. try:
  356. data = jwt.decode(
  357. authentication_token,
  358. signing_key.key,
  359. algorithms=["RS256"],
  360. audience=app.config.get("AUTHENTICATION_AUDIENCE"),
  361. options={"verify_exp": app.config.get("AUTHENTICATION_VERIFY_TOKEN_EXP")},
  362. )
  363. except jwt.ExpiredSignatureError:
  364. log.error("Provided token has expired")
  365. return False
  366. except Exception as e:
  367. log.error(f"Error decoding JWT. {e.__str__()}")
  368. return False
  369. # Extract the user email
  370. user_email = data.get("sub")
  371. return _authenticate_with_email(user_email)
  372. else:
  373. log.error("ERROR DURING TOKEN INTROSPECTION PROCESS")
  374. return False
  375. def _local_authentication_process(incoming_request: Request):
  376. return current_user.is_authenticated
  377. def is_user_authenticated(incoming_request: Request):
  378. authentication_mapper = {
  379. "oidc_proxy": _oidc_proxy_authentication_process,
  380. "local": _local_authentication_process,
  381. "ldap": _local_authentication_process,
  382. "oidc": _local_authentication_process,
  383. }
  384. return authentication_mapper.get(app.config.get("AUTHENTICATION_TYPE"))(incoming_request)
  385. def is_authentication_oidc():
  386. return app.config.get('AUTHENTICATION_TYPE') == "oidc"
  387. def is_authentication_ldap():
  388. return app.config.get('AUTHENTICATION_TYPE') == "ldap"