Skip to content

Commit

Permalink
Merge branch 'master' into update_ldap_docs
Browse files Browse the repository at this point in the history
  • Loading branch information
dpgaspar authored Feb 23, 2023
2 parents 967d66e + c5e453e commit d7f5c42
Show file tree
Hide file tree
Showing 15 changed files with 111 additions and 33 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Flask-AppBuilder ChangeLog
==========================

Improvements and Bug fixes on 4.2.1
-----------------------------------

- ci: fix pyodbc install failure (#1992) [Daniel Vaz Gaspar]
- fix: Remove unused parameter from QuerySelectMultipleField instantiation (#1991) [Dosenpfand]
- fix: Make sure user input is not treated as safe in the oauth view (#1978) [Glenn Schuurman]
- fix: don't use root logger on safe decorator (#1990) [Igor Khrol]
- chore: upgrade Font Awesome to version 6 (#1979) [Daniel Vaz Gaspar]

Improvements and Bug fixes on 4.2.0
-----------------------------------

Expand Down
2 changes: 1 addition & 1 deletion flask_appbuilder/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__author__ = "Daniel Vaz Gaspar"
__version__ = "4.2.0"
__version__ = "4.2.1"

from .actions import action # noqa: F401
from .api import ModelRestApi # noqa: F401
Expand Down
2 changes: 1 addition & 1 deletion flask_appbuilder/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def wraps(self: "BaseApi", *args: Any, **kwargs: Any) -> Response:
except BadRequest as e:
return self.response_400(message=str(e))
except Exception as e:
logging.exception(e)
log.exception(e)
return self.response_500(message=get_error_msg())

return functools.update_wrapper(wraps, f)
Expand Down
5 changes: 5 additions & 0 deletions flask_appbuilder/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,11 @@ def security_converge(self, dry: bool = False) -> Dict[str, Any]:
return {}
return self.sm.security_converge(self.baseviews, self.menu.menu, dry)

def get_url_for_login_with(self, next_url: str = None) -> str:
if self.sm.auth_view is None:
return ""
return url_for("%s.%s" % (self.sm.auth_view.endpoint, "login"), next=next_url)

@property
def get_url_for_login(self) -> str:
if self.sm.auth_view is None:
Expand Down
40 changes: 22 additions & 18 deletions flask_appbuilder/fieldwidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from wtforms.widgets import html_params


class DatePickerWidget(object):
class DatePickerWidget:
"""
Date Time picker from Eonasdan GitHub
Expand All @@ -30,7 +30,7 @@ def __call__(self, field, **kwargs):
)


class DateTimePickerWidget(object):
class DateTimePickerWidget:
"""
Date Time picker from Eonasdan GitHub
Expand Down Expand Up @@ -58,7 +58,7 @@ def __call__(self, field, **kwargs):

class BS3TextFieldWidget(widgets.TextInput):
def __call__(self, field, **kwargs):
kwargs["class"] = u"form-control"
kwargs["class"] = "form-control"
if field.label:
kwargs["placeholder"] = field.label.text
if "name_" in kwargs:
Expand All @@ -68,7 +68,7 @@ def __call__(self, field, **kwargs):

class BS3TextAreaFieldWidget(widgets.TextArea):
def __call__(self, field, **kwargs):
kwargs["class"] = u"form-control"
kwargs["class"] = "form-control"
kwargs["rows"] = 3
if field.label:
kwargs["placeholder"] = field.label.text
Expand All @@ -77,25 +77,26 @@ def __call__(self, field, **kwargs):

class BS3PasswordFieldWidget(widgets.PasswordInput):
def __call__(self, field, **kwargs):
kwargs["class"] = u"form-control"
kwargs["class"] = "form-control"
if field.label:
kwargs["placeholder"] = field.label.text
return super(BS3PasswordFieldWidget, self).__call__(field, **kwargs)


class Select2AJAXWidget(object):
class Select2AJAXWidget:
data_template = "<input %(text)s />"

def __init__(self, endpoint, extra_classes=None, style=None):
self.endpoint = endpoint
self.extra_classes = extra_classes
self.style = style or u"width:250px"
self.style = style or ""

def __call__(self, field, **kwargs):
kwargs.setdefault("id", field.id)
kwargs.setdefault("name", field.name)
kwargs.setdefault("endpoint", self.endpoint)
kwargs.setdefault("style", self.style)
if self.style:
kwargs.setdefault("style", self.style)
input_classes = "input-group my_select2_ajax"
if self.extra_classes:
input_classes = input_classes + " " + self.extra_classes
Expand All @@ -109,21 +110,22 @@ def __call__(self, field, **kwargs):
)


class Select2SlaveAJAXWidget(object):
class Select2SlaveAJAXWidget:
data_template = '<input class="input-group my_select2_ajax_slave" %(text)s />'

def __init__(self, master_id, endpoint, extra_classes=None, style=None):
self.endpoint = endpoint
self.master_id = master_id
self.extra_classes = extra_classes
self.style = style or u"width:250px"
self.style = style or ""

def __call__(self, field, **kwargs):
kwargs.setdefault("id", field.id)
kwargs.setdefault("name", field.name)
kwargs.setdefault("endpoint", self.endpoint)
kwargs.setdefault("master_id", self.master_id)
kwargs.setdefault("style", self.style)
if self.style:
kwargs.setdefault("style", self.style)
input_classes = "input-group my_select2_ajax"
if self.extra_classes:
input_classes = input_classes + " " + self.extra_classes
Expand All @@ -143,14 +145,15 @@ class Select2Widget(widgets.Select):

def __init__(self, extra_classes=None, style=None):
self.extra_classes = extra_classes
self.style = style or u"width:250px"
self.style = style
super(Select2Widget, self).__init__()

def __call__(self, field, **kwargs):
kwargs["class"] = u"my_select2 form-control"
kwargs["class"] = "my_select2 form-control"
if self.extra_classes:
kwargs["class"] = kwargs["class"] + " " + self.extra_classes
kwargs["style"] = self.style
if self.style:
kwargs["style"] = self.style
kwargs["data-placeholder"] = _("Select Value")
if "name_" in kwargs:
field.name = kwargs["name_"]
Expand All @@ -162,16 +165,17 @@ class Select2ManyWidget(widgets.Select):

def __init__(self, extra_classes=None, style=None):
self.extra_classes = extra_classes
self.style = style or u"width:250px"
self.style = style
super(Select2ManyWidget, self).__init__()

def __call__(self, field, **kwargs):
kwargs["class"] = u"my_select2 form-control"
kwargs["class"] = "my_select2 form-control"
if self.extra_classes:
kwargs["class"] = kwargs["class"] + " " + self.extra_classes
kwargs["style"] = self.style
if self.style:
kwargs["style"] = self.style
kwargs["data-placeholder"] = _("Select Value")
kwargs["multiple"] = u"true"
kwargs["multiple"] = "true"
if "name_" in kwargs:
field.name = kwargs["name_"]
return super(Select2ManyWidget, self).__call__(field, **kwargs)
2 changes: 0 additions & 2 deletions flask_appbuilder/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,11 @@ def _convert_many_to_many(
):
query_func = self._get_related_query_func(col_name, filter_rel_fields)
get_pk_func = self._get_related_pk_func(col_name)
allow_blank = True
form_props[col_name] = QuerySelectMultipleField(
label,
description=description,
query_func=query_func,
get_pk_func=get_pk_func,
allow_blank=allow_blank,
validators=lst_validators,
widget=Select2ManyWidget(),
)
Expand Down
12 changes: 6 additions & 6 deletions flask_appbuilder/security/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,15 +514,15 @@ def login(self):
return redirect(self.appbuilder.get_url_for_index)
form = LoginForm_db()
if form.validate_on_submit():
next_url = get_safe_redirect(request.args.get("next", ""))
user = self.appbuilder.sm.auth_user_db(
form.username.data, form.password.data
)
if not user:
flash(as_unicode(self.invalid_login_message), "warning")
return redirect(self.appbuilder.get_url_for_login)
return redirect(self.appbuilder.get_url_for_login_with(next_url))
login_user(user, remember=False)
next_url = request.args.get("next", "")
return redirect(get_safe_redirect(next_url))
return redirect(next_url)
return self.render_template(
self.login_template, title=self.title, form=form, appbuilder=self.appbuilder
)
Expand All @@ -537,15 +537,15 @@ def login(self):
return redirect(self.appbuilder.get_url_for_index)
form = LoginForm_db()
if form.validate_on_submit():
next_url = get_safe_redirect(request.args.get("next", ""))
user = self.appbuilder.sm.auth_user_ldap(
form.username.data, form.password.data
)
if not user:
flash(as_unicode(self.invalid_login_message), "warning")
return redirect(self.appbuilder.get_url_for_login)
return redirect(self.appbuilder.get_url_for_login_with(next_url))
login_user(user, remember=False)
next_url = request.args.get("next", "")
return redirect(get_safe_redirect(next_url))
return redirect(next_url)
return self.render_template(
self.login_template, title=self.title, form=form, appbuilder=self.appbuilder
)
Expand Down
4 changes: 4 additions & 0 deletions flask_appbuilder/static/appbuilder/css/ab.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ th.action_checkboxes {
width: 1%;
}

select {
width: 100%;
}

.cursor-hand {
cursor: pointer;
cursor: hand;
Expand Down
4 changes: 3 additions & 1 deletion flask_appbuilder/static/appbuilder/js/ab.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ $(function() {
$('.appbuilder_datetime').datetimepicker();
$('.appbuilder_date').datetimepicker({
pickTime: false });
$(".my_select2").select2({placeholder: "Select a State", allowClear: true});
$(".my_select2").select2(
{placeholder: "Select a State", allowClear: true, theme: "bootstrap"}
);
$(".my_select2.readonly").select2("readonly", true);
loadSelectData();
loadSelectDataSlave();
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script type="text/javascript">
var baseLoginUrl = "{{appbuilder.get_url_for_login}}";
var baseRegisterUrl = "{{appbuilder.get_url_for_login}}";
var next = "?next=" + encodeURIComponent("{{request.args.get('next', '') | safe}}");
var next = "?next=" + encodeURIComponent("{{request.args.get('next', '')}}");

function signin(provider) {
window.location.href = baseLoginUrl + provider + next;
Expand Down
1 change: 1 addition & 0 deletions flask_appbuilder/templates/appbuilder/init.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
{% endif %}
<link href="{{url_for('appbuilder.static',filename='datepicker/bootstrap-datepicker.css')}}" rel="stylesheet">
<link href="{{url_for('appbuilder.static',filename='select2/select2.css')}}" rel="stylesheet">
<link href="{{url_for('appbuilder.static',filename='select2/select2-bootstrap-theme.css')}}" rel="stylesheet">
<link href="{{url_for('appbuilder.static',filename='css/flags/flags16.css')}}" rel="stylesheet">
<link href="{{url_for('appbuilder.static',filename='css/ab.css')}}" rel="stylesheet">
{% endblock %}
Expand Down
30 changes: 29 additions & 1 deletion flask_appbuilder/tests/security/test_auth_ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import ldap
from mockldap import MockLdap


from ..const import USERNAME_ADMIN, USERNAME_READONLY

logging.basicConfig(format="%(asctime)s:%(levelname)s:%(name)s:%(message)s")
Expand Down Expand Up @@ -1187,3 +1186,32 @@ def test__indirect_bind__registered__multi_role__with_role_sync(self):
self.call_bind_alice,
],
)

def test_login_failed_keep_next_url(self):
"""
LDAP: Keeping next url after failed login attempt
"""

self.app.config["AUTH_LDAP_SEARCH"] = "ou=users,o=test"
self.app.config["AUTH_LDAP_USERNAME_FORMAT"] = "uid=%s,ou=users,o=test"
self.app.config["AUTH_USER_REGISTRATION"] = True
self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
self.app.config["WTF_CSRF_ENABLED"] = False
self.app.config["SECRET_KEY"] = "thisismyscretkey"

self.appbuilder = AppBuilder(self.app, self.db.session)
client = self.app.test_client()
client.get("/logout/")

response = client.post(
"/login/?next=/users/userinfo/",
data=dict(username="natalie", password="wrong_natalie_password"),
follow_redirects=False,
)
response = client.post(
response.location,
data=dict(username="natalie", password="natalie_password"),
follow_redirects=False,
)

assert response.location == "http://localhost/users/userinfo/"
22 changes: 21 additions & 1 deletion flask_appbuilder/tests/security/test_mvc_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,31 @@ def test_db_login_invalid_control_characters_next_url(self):
self.client,
USERNAME_ADMIN,
PASSWORD_ADMIN,
next_url=u"\u0001" + "sample.com",
next_url="\u0001" + "sample.com",
follow_redirects=False,
)
assert response.location == "http://localhost/"

def test_db_login_failed_keep_next_url(self):
"""
Test Security Keeping next url after failed login attempt
"""
self.browser_logout(self.client)
response = self.browser_login(
self.client,
USERNAME_ADMIN,
f"wrong_{PASSWORD_ADMIN}",
next_url="/users/list/",
follow_redirects=False,
)
response = self.client.post(
response.location,
data=dict(username=USERNAME_ADMIN, password=PASSWORD_ADMIN),
follow_redirects=False,
)

assert response.location == "http://localhost/users/list/"

def test_auth_builtin_roles(self):
"""
Test Security builtin roles readonly
Expand Down
2 changes: 1 addition & 1 deletion requirements-extra.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Pillow~=9.1
cython==0.29.17
mysqlclient==2.0.1
psycopg2-binary==2.8.6
pyodbc==4.0.30
pyodbc==4.0.35
requests==2.26.0
Authlib==0.15.4
python-ldap==3.3.1
Expand Down

0 comments on commit d7f5c42

Please sign in to comment.