views.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. import datetime
  2. import logging
  3. import re
  4. from typing import Any, List, Optional
  5. from flask import abort, current_app, flash, g, redirect, request, session, url_for
  6. from flask_appbuilder._compat import as_unicode
  7. from flask_appbuilder.actions import action
  8. from flask_appbuilder.baseviews import BaseView
  9. from flask_appbuilder.charts.views import DirectByChartView
  10. from flask_appbuilder.fieldwidgets import BS3PasswordFieldWidget
  11. from flask_appbuilder.security.decorators import has_access, no_cache
  12. from flask_appbuilder.security.forms import (
  13. DynamicForm,
  14. LoginForm_db,
  15. LoginForm_oid,
  16. ResetPasswordForm,
  17. SelectDataRequired,
  18. UserInfoEdit,
  19. )
  20. from flask_appbuilder.security.utils import generate_random_string
  21. from flask_appbuilder.utils.base import get_safe_redirect, lazy_formatter_gettext
  22. from flask_appbuilder.validators import PasswordComplexityValidator
  23. from flask_appbuilder.views import expose, ModelView, SimpleFormView
  24. from flask_appbuilder.widgets import ListWidget, ShowWidget
  25. from flask_babel import lazy_gettext
  26. from flask_login import login_user, logout_user
  27. import jwt
  28. from werkzeug.security import generate_password_hash
  29. from werkzeug.wrappers import Response as WerkzeugResponse
  30. from wtforms import PasswordField, validators
  31. from wtforms.validators import EqualTo
  32. log = logging.getLogger(__name__)
  33. class PermissionModelView(ModelView):
  34. route_base = "/permissions"
  35. base_permissions = ["can_list"]
  36. list_title = lazy_gettext("List Base Permissions")
  37. show_title = lazy_gettext("Show Base Permission")
  38. add_title = lazy_gettext("Add Base Permission")
  39. edit_title = lazy_gettext("Edit Base Permission")
  40. label_columns = {"name": lazy_gettext("Name")}
  41. class ViewMenuModelView(ModelView):
  42. route_base = "/viewmenus"
  43. base_permissions = ["can_list"]
  44. list_title = lazy_gettext("List View Menus")
  45. show_title = lazy_gettext("Show View Menu")
  46. add_title = lazy_gettext("Add View Menu")
  47. edit_title = lazy_gettext("Edit View Menu")
  48. label_columns = {"name": lazy_gettext("Name")}
  49. class PermissionViewModelView(ModelView):
  50. route_base = "/permissionviews"
  51. base_permissions = ["can_list"]
  52. list_title = lazy_gettext("List Permissions on Views/Menus")
  53. show_title = lazy_gettext("Show Permission on Views/Menus")
  54. add_title = lazy_gettext("Add Permission on Views/Menus")
  55. edit_title = lazy_gettext("Edit Permission on Views/Menus")
  56. label_columns = {
  57. "permission": lazy_gettext("Permission"),
  58. "view_menu": lazy_gettext("View/Menu"),
  59. }
  60. list_columns = ["permission", "view_menu"]
  61. class ResetMyPasswordView(SimpleFormView):
  62. """
  63. View for resetting own user password
  64. """
  65. route_base = "/resetmypassword"
  66. form = ResetPasswordForm
  67. form_title = lazy_gettext("Reset Password Form")
  68. redirect_url = "/"
  69. message = lazy_gettext("Password Changed")
  70. def form_post(self, form: DynamicForm) -> None:
  71. self.appbuilder.sm.reset_password(g.user.id, form.password.data)
  72. flash(as_unicode(self.message), "info")
  73. class ResetPasswordView(SimpleFormView):
  74. """
  75. View for reseting all users password
  76. """
  77. route_base = "/resetpassword"
  78. form = ResetPasswordForm
  79. form_title = lazy_gettext("Reset Password Form")
  80. redirect_url = "/"
  81. message = lazy_gettext("Password Changed")
  82. def form_post(self, form: DynamicForm) -> None:
  83. pk = request.args.get("pk")
  84. self.appbuilder.sm.reset_password(pk, form.password.data)
  85. flash(as_unicode(self.message), "info")
  86. class UserInfoEditView(SimpleFormView):
  87. form = UserInfoEdit
  88. form_title = lazy_gettext("Edit User Information")
  89. redirect_url = "/"
  90. message = lazy_gettext("User information changed")
  91. def form_get(self, form: DynamicForm) -> None:
  92. item = self.appbuilder.sm.get_user_by_id(g.user.id)
  93. # fills the form generic solution
  94. for key, value in form.data.items():
  95. if key == "csrf_token":
  96. continue
  97. form_field = getattr(form, key)
  98. form_field.data = getattr(item, key)
  99. def form_post(self, form: DynamicForm) -> None:
  100. form = self.form.refresh(request.form)
  101. item = self.appbuilder.sm.get_user_by_id(g.user.id)
  102. form.populate_obj(item)
  103. self.appbuilder.sm.update_user(item)
  104. flash(as_unicode(self.message), "info")
  105. def _roles_custom_formatter(string: str) -> str:
  106. if current_app.config.get("AUTH_ROLES_SYNC_AT_LOGIN", False):
  107. string += (
  108. ". <div class='alert alert-warning' role='alert'>"
  109. "AUTH_ROLES_SYNC_AT_LOGIN is enabled, changes to this field will "
  110. "not persist between user logins."
  111. "</div>"
  112. )
  113. return string
  114. class UserModelView(ModelView):
  115. route_base = "/users"
  116. list_title = lazy_gettext("List Users")
  117. show_title = lazy_gettext("Show User")
  118. add_title = lazy_gettext("Add User")
  119. edit_title = lazy_gettext("Edit User")
  120. label_columns = {
  121. "get_full_name": lazy_gettext("Full Name"),
  122. "first_name": lazy_gettext("First Name"),
  123. "last_name": lazy_gettext("Last Name"),
  124. "username": lazy_gettext("User Name"),
  125. "password": lazy_gettext("Password"),
  126. "active": lazy_gettext("Is Active?"),
  127. "email": lazy_gettext("Email"),
  128. "roles": lazy_gettext("Role"),
  129. "last_login": lazy_gettext("Last login"),
  130. "login_count": lazy_gettext("Login count"),
  131. "fail_login_count": lazy_gettext("Failed login count"),
  132. "created_on": lazy_gettext("Created on"),
  133. "created_by": lazy_gettext("Created by"),
  134. "changed_on": lazy_gettext("Changed on"),
  135. "changed_by": lazy_gettext("Changed by"),
  136. }
  137. description_columns = {
  138. "first_name": lazy_gettext("Write the user first name or names"),
  139. "last_name": lazy_gettext("Write the user last name"),
  140. "username": lazy_gettext(
  141. "Username valid for authentication on DB or LDAP, unused for OID auth"
  142. ),
  143. "password": lazy_gettext("The user's password for authentication"),
  144. "active": lazy_gettext(
  145. "It's not a good policy to remove a user, just make it inactive"
  146. ),
  147. "email": lazy_gettext("The user's email, this will also be used for OID auth"),
  148. "roles": lazy_formatter_gettext(
  149. "The user role on the application,"
  150. " this will associate with a list of permissions",
  151. _roles_custom_formatter,
  152. ),
  153. "conf_password": lazy_gettext("Please rewrite the user's password to confirm"),
  154. }
  155. list_columns = ["first_name", "last_name", "username", "email", "active", "roles"]
  156. show_fieldsets = [
  157. (
  158. lazy_gettext("User info"),
  159. {"fields": ["username", "active", "roles", "login_count"]},
  160. ),
  161. (
  162. lazy_gettext("Personal Info"),
  163. {"fields": ["first_name", "last_name", "email"], "expanded": True},
  164. ),
  165. (
  166. lazy_gettext("Audit Info"),
  167. {
  168. "fields": [
  169. "last_login",
  170. "fail_login_count",
  171. "created_on",
  172. "created_by",
  173. "changed_on",
  174. "changed_by",
  175. ],
  176. "expanded": False,
  177. },
  178. ),
  179. ]
  180. user_show_fieldsets = [
  181. (
  182. lazy_gettext("User info"),
  183. {"fields": ["username", "active", "roles", "login_count"]},
  184. ),
  185. (
  186. lazy_gettext("Personal Info"),
  187. {"fields": ["first_name", "last_name", "email"], "expanded": True},
  188. ),
  189. ]
  190. search_columns = [
  191. "first_name",
  192. "last_name",
  193. "username",
  194. "email",
  195. "active",
  196. "roles",
  197. "created_on",
  198. "changed_on",
  199. "last_login",
  200. "login_count",
  201. "fail_login_count",
  202. ]
  203. add_columns = ["first_name", "last_name", "username", "active", "email", "roles"]
  204. edit_columns = ["first_name", "last_name", "username", "active", "email", "roles"]
  205. user_info_title = lazy_gettext("Your user information")
  206. @expose("/userinfo/")
  207. @has_access
  208. def userinfo(self) -> WerkzeugResponse:
  209. item = self.datamodel.get(g.user.id, self._base_filters)
  210. widgets = self._get_show_widget(
  211. g.user.id, item, show_fieldsets=self.user_show_fieldsets
  212. )
  213. self.update_redirect()
  214. return self.render_template(
  215. self.show_template,
  216. title=self.user_info_title,
  217. widgets=widgets,
  218. appbuilder=self.appbuilder,
  219. )
  220. @action("userinfoedit", lazy_gettext("Edit User"), "", "fa-edit", multiple=False)
  221. def userinfoedit(self, item: Any) -> WerkzeugResponse:
  222. return redirect(
  223. url_for(self.appbuilder.sm.userinfoeditview.__name__ + ".this_form_get")
  224. )
  225. class UserOIDModelView(UserModelView):
  226. """
  227. View that add OID specifics to User view.
  228. Override to implement your own custom view.
  229. Then override useroidmodelview property on SecurityManager
  230. """
  231. pass
  232. class UserLDAPModelView(UserModelView):
  233. """
  234. View that add LDAP specifics to User view.
  235. Override to implement your own custom view.
  236. Then override userldapmodelview property on SecurityManager
  237. """
  238. pass
  239. class UserOAuthModelView(UserModelView):
  240. """
  241. View that add OAUTH specifics to User view.
  242. Override to implement your own custom view.
  243. Then override userldapmodelview property on SecurityManager
  244. """
  245. pass
  246. class UserRemoteUserModelView(UserModelView):
  247. """
  248. View that add REMOTE_USER specifics to User view.
  249. Override to implement your own custom view.
  250. Then override userldapmodelview property on SecurityManager
  251. """
  252. pass
  253. class UserDBModelView(UserModelView):
  254. """
  255. View that add DB specifics to User view.
  256. Override to implement your own custom view.
  257. Then override userdbmodelview property on SecurityManager
  258. """
  259. add_form_extra_fields = {
  260. "password": PasswordField(
  261. lazy_gettext("Password"),
  262. description=lazy_gettext("The user's password for authentication"),
  263. validators=[validators.DataRequired(), PasswordComplexityValidator()],
  264. widget=BS3PasswordFieldWidget(),
  265. ),
  266. "conf_password": PasswordField(
  267. lazy_gettext("Confirm Password"),
  268. description=lazy_gettext("Please rewrite the user's password to confirm"),
  269. validators=[
  270. validators.DataRequired(),
  271. EqualTo("password", message=lazy_gettext("Passwords must match")),
  272. ],
  273. widget=BS3PasswordFieldWidget(),
  274. ),
  275. }
  276. add_columns = [
  277. "first_name",
  278. "last_name",
  279. "username",
  280. "active",
  281. "email",
  282. "roles",
  283. "password",
  284. "conf_password",
  285. ]
  286. validators_columns = {"roles": [SelectDataRequired()]}
  287. @expose("/show/<pk>", methods=["GET"])
  288. @has_access
  289. def show(self, pk: Any) -> WerkzeugResponse:
  290. actions = dict()
  291. actions["resetpasswords"] = self.actions.get("resetpasswords")
  292. item = self.datamodel.get(pk, self._base_filters)
  293. if not item:
  294. abort(404)
  295. widgets = self._get_show_widget(pk, item, actions=actions)
  296. self.update_redirect()
  297. return self.render_template(
  298. self.show_template,
  299. pk=pk,
  300. title=self.show_title,
  301. widgets=widgets,
  302. appbuilder=self.appbuilder,
  303. related_views=self._related_views,
  304. )
  305. @expose("/userinfo/")
  306. @has_access
  307. def userinfo(self) -> WerkzeugResponse:
  308. actions = dict()
  309. actions["resetmypassword"] = self.actions.get("resetmypassword")
  310. actions["userinfoedit"] = self.actions.get("userinfoedit")
  311. item = self.datamodel.get(g.user.id, self._base_filters)
  312. widgets = self._get_show_widget(
  313. g.user.id, item, actions=actions, show_fieldsets=self.user_show_fieldsets
  314. )
  315. self.update_redirect()
  316. return self.render_template(
  317. self.show_template,
  318. title=self.user_info_title,
  319. widgets=widgets,
  320. appbuilder=self.appbuilder,
  321. )
  322. @action(
  323. "resetmypassword",
  324. lazy_gettext("Reset my password"),
  325. "",
  326. "fa-lock",
  327. multiple=False,
  328. )
  329. def resetmypassword(self, item: Any):
  330. return redirect(
  331. url_for(self.appbuilder.sm.resetmypasswordview.__name__ + ".this_form_get")
  332. )
  333. @action(
  334. "resetpasswords", lazy_gettext("Reset Password"), "", "fa-lock", multiple=False
  335. )
  336. def resetpasswords(self, item: Any) -> WerkzeugResponse:
  337. return redirect(
  338. url_for(
  339. self.appbuilder.sm.resetpasswordview.__name__ + ".this_form_get",
  340. pk=item.id,
  341. )
  342. )
  343. def pre_update(self, item: Any) -> None:
  344. item.changed_on = datetime.datetime.now()
  345. item.changed_by_fk = g.user.id
  346. def pre_add(self, item: Any) -> None:
  347. item.password = generate_password_hash(item.password)
  348. class UserStatsChartView(DirectByChartView):
  349. chart_title = lazy_gettext("User Statistics")
  350. label_columns = {
  351. "username": lazy_gettext("User Name"),
  352. "login_count": lazy_gettext("Login count"),
  353. "fail_login_count": lazy_gettext("Failed login count"),
  354. }
  355. search_columns = UserModelView.search_columns
  356. definitions = [
  357. {"label": "Login Count", "group": "username", "series": ["login_count"]},
  358. {
  359. "label": "Failed Login Count",
  360. "group": "username",
  361. "series": ["fail_login_count"],
  362. },
  363. ]
  364. class RoleListWidget(ListWidget):
  365. template = "appbuilder/general/widgets/roles/list.html"
  366. def __init__(self, **kwargs):
  367. kwargs["appbuilder"] = current_app.appbuilder
  368. super().__init__(**kwargs)
  369. class RoleShowWidget(ShowWidget):
  370. template = "appbuilder/general/widgets/roles/show.html"
  371. def __init__(self, **kwargs):
  372. kwargs["appbuilder"] = current_app.appbuilder
  373. super().__init__(**kwargs)
  374. class RoleModelView(ModelView):
  375. route_base = "/roles"
  376. list_title = lazy_gettext("List Roles")
  377. show_title = lazy_gettext("Show Role")
  378. add_title = lazy_gettext("Add Role")
  379. edit_title = lazy_gettext("Edit Role")
  380. list_widget = RoleListWidget
  381. show_widget = RoleShowWidget
  382. label_columns = {
  383. "name": lazy_gettext("Name"),
  384. "permissions": lazy_gettext("Permissions"),
  385. }
  386. list_columns = ["name", "permissions"]
  387. show_columns = ["name", "permissions"]
  388. edit_columns = ["name", "permissions"]
  389. add_columns = edit_columns
  390. order_columns = ["name"]
  391. @action(
  392. "copyrole",
  393. lazy_gettext("Copy Role"),
  394. lazy_gettext("Copy the selected roles?"),
  395. icon="fa-copy",
  396. single=False,
  397. )
  398. def copy_role(self, items):
  399. self.update_redirect()
  400. for item in items:
  401. new_role = item.__class__()
  402. new_role.name = item.name
  403. new_role.permissions = item.permissions
  404. new_role.name = new_role.name + " copy"
  405. self.datamodel.add(new_role)
  406. return redirect(self.get_redirect())
  407. class RegisterUserModelView(ModelView):
  408. route_base = "/registeruser"
  409. base_permissions = ["can_list", "can_show", "can_delete"]
  410. list_title = lazy_gettext("List of Registration Requests")
  411. show_title = lazy_gettext("Show Registration")
  412. list_columns = ["username", "registration_date", "email"]
  413. show_exclude_columns = ["password"]
  414. search_exclude_columns = ["password"]
  415. class AuthView(BaseView):
  416. route_base = ""
  417. login_template = ""
  418. invalid_login_message = lazy_gettext("Invalid login. Please try again.")
  419. title = lazy_gettext("Sign In")
  420. @expose("/login/", methods=["GET", "POST"])
  421. def login(self):
  422. pass
  423. @expose("/logout/")
  424. def logout(self):
  425. logout_user()
  426. return redirect(
  427. self.appbuilder.app.config.get(
  428. "LOGOUT_REDIRECT_URL", self.appbuilder.get_url_for_index
  429. )
  430. )
  431. class AuthDBView(AuthView):
  432. login_template = "appbuilder/general/security/login_db.html"
  433. @expose("/login/", methods=["GET", "POST"])
  434. @no_cache
  435. def login(self):
  436. if g.user is not None and g.user.is_authenticated:
  437. return redirect(self.appbuilder.get_url_for_index)
  438. form = LoginForm_db()
  439. if form.validate_on_submit():
  440. next_url = get_safe_redirect(request.args.get("next", ""))
  441. user = self.appbuilder.sm.auth_user_db(
  442. form.username.data, form.password.data
  443. )
  444. if not user:
  445. flash(as_unicode(self.invalid_login_message), "warning")
  446. return redirect(self.appbuilder.get_url_for_login_with(next_url))
  447. login_user(user, remember=False)
  448. return redirect(next_url)
  449. return self.render_template(
  450. self.login_template, title=self.title, form=form, appbuilder=self.appbuilder
  451. )
  452. class AuthLDAPView(AuthView):
  453. login_template = "appbuilder/general/security/login_ldap.html"
  454. @expose("/login/", methods=["GET", "POST"])
  455. @no_cache
  456. def login(self):
  457. if g.user is not None and g.user.is_authenticated:
  458. return redirect(self.appbuilder.get_url_for_index)
  459. form = LoginForm_db()
  460. if form.validate_on_submit():
  461. next_url = get_safe_redirect(request.args.get("next", ""))
  462. user = self.appbuilder.sm.auth_user_ldap(
  463. form.username.data, form.password.data
  464. )
  465. if not user:
  466. flash(as_unicode(self.invalid_login_message), "warning")
  467. return redirect(self.appbuilder.get_url_for_login_with(next_url))
  468. login_user(user, remember=False)
  469. return redirect(next_url)
  470. return self.render_template(
  471. self.login_template, title=self.title, form=form, appbuilder=self.appbuilder
  472. )
  473. class AuthOIDView(AuthView):
  474. login_template = "appbuilder/general/security/login_oid.html"
  475. oid_ask_for = ["email"]
  476. oid_ask_for_optional: List[str] = []
  477. @expose("/login/", methods=["GET", "POST"])
  478. @no_cache
  479. def login(self, flag=True) -> WerkzeugResponse:
  480. @self.appbuilder.sm.oid.loginhandler
  481. def login_handler(self):
  482. if g.user is not None and g.user.is_authenticated:
  483. return redirect(self.appbuilder.get_url_for_index)
  484. form = LoginForm_oid()
  485. if form.validate_on_submit():
  486. session["remember_me"] = form.remember_me.data
  487. identity_url = self.appbuilder.sm.get_oid_identity_url(form.openid.data)
  488. if identity_url is None:
  489. flash(as_unicode(self.invalid_login_message), "warning")
  490. return redirect(self.appbuilder.get_url_for_login)
  491. return self.appbuilder.sm.oid.try_login(
  492. identity_url,
  493. ask_for=self.oid_ask_for,
  494. ask_for_optional=self.oid_ask_for_optional,
  495. )
  496. return self.render_template(
  497. self.login_template,
  498. title=self.title,
  499. form=form,
  500. providers=self.appbuilder.sm.openid_providers,
  501. appbuilder=self.appbuilder,
  502. )
  503. @self.appbuilder.sm.oid.after_login
  504. def after_login(resp):
  505. if resp.email is None or resp.email == "":
  506. flash(as_unicode(self.invalid_login_message), "warning")
  507. return redirect(self.appbuilder.get_url_for_login)
  508. user = self.appbuilder.sm.auth_user_oid(resp.email)
  509. if user is None:
  510. flash(as_unicode(self.invalid_login_message), "warning")
  511. return redirect(self.appbuilder.get_url_for_login)
  512. remember_me = False
  513. if "remember_me" in session:
  514. remember_me = session["remember_me"]
  515. session.pop("remember_me", None)
  516. log.warning(
  517. "AUTH_OID is deprecated and will be removed in version 5. "
  518. "Migrate to other authentication methods."
  519. )
  520. login_user(user, remember=remember_me)
  521. next_url = request.args.get("next", "")
  522. return redirect(get_safe_redirect(next_url))
  523. return login_handler(self)
  524. class AuthOAuthView(AuthView):
  525. login_template = "appbuilder/general/security/login_oauth.html"
  526. @expose("/login/")
  527. @expose("/login/<provider>")
  528. def login(self, provider: Optional[str] = None) -> WerkzeugResponse:
  529. log.debug("Provider: %s", provider)
  530. if g.user is not None and g.user.is_authenticated:
  531. log.debug("Already authenticated %s", g.user)
  532. return redirect(self.appbuilder.get_url_for_index)
  533. if provider is None:
  534. return self.render_template(
  535. self.login_template,
  536. providers=self.appbuilder.sm.oauth_providers,
  537. title=self.title,
  538. appbuilder=self.appbuilder,
  539. )
  540. log.debug("Going to call authorize for: %s", provider)
  541. random_state = generate_random_string()
  542. state = jwt.encode(
  543. request.args.to_dict(flat=False), random_state, algorithm="HS256"
  544. )
  545. session["oauth_state"] = random_state
  546. try:
  547. if provider == "twitter":
  548. return self.appbuilder.sm.oauth_remotes[provider].authorize_redirect(
  549. redirect_uri=url_for(
  550. ".oauth_authorized",
  551. provider=provider,
  552. _external=True,
  553. state=state,
  554. )
  555. )
  556. else:
  557. return self.appbuilder.sm.oauth_remotes[provider].authorize_redirect(
  558. redirect_uri=url_for(
  559. ".oauth_authorized", provider=provider, _external=True
  560. ),
  561. state=state.decode("ascii") if isinstance(state, bytes) else state,
  562. )
  563. except Exception as e:
  564. log.error("Error on OAuth authorize: %s", e)
  565. flash(as_unicode(self.invalid_login_message), "warning")
  566. return redirect(self.appbuilder.get_url_for_index)
  567. @expose("/oauth-authorized/<provider>")
  568. def oauth_authorized(self, provider: str) -> WerkzeugResponse:
  569. log.debug("Authorized init")
  570. if provider not in self.appbuilder.sm.oauth_remotes:
  571. flash("Provider not supported.", "warning")
  572. log.warning("OAuth authorized got an unknown provider %s", provider)
  573. return redirect(self.appbuilder.get_url_for_login)
  574. try:
  575. resp = self.appbuilder.sm.oauth_remotes[provider].authorize_access_token()
  576. except Exception as e:
  577. log.error("Error authorizing OAuth access token: %s", e)
  578. flash("The request to sign in was denied.", "error")
  579. return redirect(self.appbuilder.get_url_for_login)
  580. if resp is None:
  581. flash("You denied the request to sign in.", "warning")
  582. return redirect(self.appbuilder.get_url_for_login)
  583. log.debug("OAUTH Authorized resp: %s", resp)
  584. # Retrieves specific user info from the provider
  585. try:
  586. self.appbuilder.sm.set_oauth_session(provider, resp)
  587. userinfo = self.appbuilder.sm.oauth_user_info(provider, resp)
  588. except Exception as e:
  589. log.error("Error returning OAuth user info: %s", e)
  590. user = None
  591. else:
  592. log.debug("User info retrieved from %s: %s", provider, userinfo)
  593. # User email is not whitelisted
  594. if provider in self.appbuilder.sm.oauth_whitelists:
  595. whitelist = self.appbuilder.sm.oauth_whitelists[provider]
  596. allow = False
  597. for email in whitelist:
  598. if "email" in userinfo and re.search(email, userinfo["email"]):
  599. allow = True
  600. break
  601. if not allow:
  602. flash("You are not authorized.", "warning")
  603. return redirect(self.appbuilder.get_url_for_login)
  604. else:
  605. log.debug("No whitelist for OAuth provider")
  606. user = self.appbuilder.sm.auth_user_oauth(userinfo)
  607. if user is None:
  608. flash(as_unicode(self.invalid_login_message), "warning")
  609. return redirect(self.appbuilder.get_url_for_login)
  610. else:
  611. try:
  612. state = jwt.decode(
  613. request.args["state"], session["oauth_state"], algorithms=["HS256"]
  614. )
  615. except (jwt.InvalidTokenError, KeyError):
  616. flash(as_unicode("Invalid state signature"), "warning")
  617. return redirect(self.appbuilder.get_url_for_login)
  618. login_user(user)
  619. next_url = self.appbuilder.get_url_for_index
  620. # Check if there is a next url on state
  621. if "next" in state and len(state["next"]) > 0:
  622. next_url = get_safe_redirect(state["next"][0])
  623. return redirect(next_url)
  624. class AuthRemoteUserView(AuthView):
  625. login_template = ""
  626. @expose("/login/")
  627. def login(self) -> WerkzeugResponse:
  628. username = request.environ.get(self.appbuilder.sm.auth_remote_user_env_var)
  629. if g.user is not None and g.user.is_authenticated:
  630. next_url = request.args.get("next", "")
  631. return redirect(get_safe_redirect(next_url))
  632. if username:
  633. user = self.appbuilder.sm.auth_user_remote_user(username)
  634. if user is None:
  635. flash(as_unicode(self.invalid_login_message), "warning")
  636. else:
  637. login_user(user)
  638. else:
  639. flash(as_unicode(self.invalid_login_message), "warning")
  640. next_url = request.args.get("next", "")
  641. return redirect(get_safe_redirect(next_url))