Skip to content

Commit

Permalink
Merge branch 'master' into saml2-support
Browse files Browse the repository at this point in the history
  • Loading branch information
zacqed authored Dec 11, 2024
2 parents 5a1643b + fab9013 commit c590626
Show file tree
Hide file tree
Showing 17 changed files with 146 additions and 24 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.8]
python-version: ["3.10"]
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12"]
env:
SQLALCHEMY_DATABASE_URI:
postgresql+psycopg2://pguser:[email protected]:15432/app
Expand Down Expand Up @@ -91,7 +91,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.8]
python-version: ["3.10"]
env:
SQLALCHEMY_DATABASE_URI: |
mysql+mysqldb://mysqluser:[email protected]:13306/app?charset=utf8mb4&binary_prefix=true
Expand Down Expand Up @@ -142,7 +142,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.8]
python-version: ["3.10"]
env:
SQLALCHEMY_DATABASE_URI: |
mssql+pyodbc://sa:Password_123@localhost:11433/master?driver=FreeTDS
Expand Down Expand Up @@ -192,7 +192,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.8]
python-version: ["3.10"]
services:
mongo:
image: mongo:4.4.1-bionic
Expand Down Expand Up @@ -224,7 +224,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.8]
python-version: ["3.10"]
steps:
- uses: actions/checkout@v4
- name: Setup Python
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
Flask-AppBuilder ChangeLog
==========================

Improvements and Bug fixes on 4.5.2
-----------------------------------

- fix: wtforms 3.2 breaking field_flags (#2279) [Daniel Vaz Gaspar]
- fix: hardcoded year removed from copyright (#2277) [Emad Rad]

Improvements and Bug fixes on 4.5.1
-----------------------------------

- feat: add no cache directive to login forms (#2266) [Daniel Vaz Gaspar]
- chore: bump cryptography to 42.0.4 (#2238) [Daniel Vaz Gaspar]
- docs: Fixing broken link (#2252) [Chase Jones]
- fix: rate limiter key function (#2254) [Daniel Vaz Gaspar]
- chore: bump dnspython to fix vulnerability (#2255) [Daniel Vaz Gaspar]

Improvements and Bug fixes on 4.5.0
-----------------------------------

- feat: REST API new select columns query param (#2242) [Daniel Vaz Gaspar]
- chore: bump werkzeug to 3.0.3 (#2237) [Daniel Vaz Gaspar]
- fix: Keycloak OAuth2, get groups as role_keys per default (#2235) [Andreas 'count' Kotes]
- fix: Check if Oauth login with OKTA is correct (#1926) [Hojjat Ali Mohammadi]
- docs: Update quickcharts.rst for typo gold to goal (#2217) [Abhinav Pareek]

Improvements and Bug fixes on 4.4.1
-----------------------------------

Expand Down
4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import sys
import os
import sphinx_rtd_theme
from datetime import datetime

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
Expand Down Expand Up @@ -55,7 +56,8 @@

# General information about the project.
project = u'Flask-AppBuilder'
copyright = u'2013, Daniel Vaz Gaspar'
current_year = datetime.now().year
copyright = u'{}, Daniel Vaz Gaspar'.format(current_year)

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
Expand Down
2 changes: 1 addition & 1 deletion docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ F.A.B comes with bootswatch themes ready to use, to change bootstrap default the

app.config['APP_THEME'] = "spacelab.css"
You can choose from the following `themes <https://github.com/dpgaspar/Flask-AppBuilder-Skeleton/blob/master/config.py>`_
You can choose from the following `themes <https://github.com/dpgaspar/Flask-AppBuilder-Skeleton/blob/master/config.py.tpl>`_


Changing the index
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.4.1"
__version__ = "4.5.2"

from .actions import action # noqa: F401
from .api import ModelRestApi # noqa: F401
Expand Down
22 changes: 19 additions & 3 deletions flask_appbuilder/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
API_PERMISSIONS_RIS_KEY,
API_RESULT_RES_KEY,
API_SELECT_COLUMNS_RIS_KEY,
API_SELECT_SEL_COLUMNS_RIS_KEY,
API_SHOW_COLUMNS_RES_KEY,
API_SHOW_COLUMNS_RIS_KEY,
API_SHOW_TITLE_RES_KEY,
Expand Down Expand Up @@ -1572,8 +1573,23 @@ def get_list_headless(self, **kwargs: Any) -> Response:
response = dict()
args = kwargs.get("rison", {})
# handle select columns
select_cols = args.get(API_SELECT_COLUMNS_RIS_KEY, [])
pruned_select_cols = [col for col in select_cols if col in self.list_columns]
output_select_cols = args.get(API_SELECT_COLUMNS_RIS_KEY, [])
select_cols = args.get(API_SELECT_SEL_COLUMNS_RIS_KEY, [])
if select_cols and output_select_cols:
return self.response_400(message="Cannot use both select and sel columns")
list_select_columns = self.list_select_columns
pruned_select_cols = []
if output_select_cols:
pruned_select_cols = [
col for col in output_select_cols if col in self.list_columns
]
if select_cols:
pruned_select_cols = [
col for col in select_cols if col in self.list_columns
]
list_select_columns = [
col for col in select_cols if col in self.list_select_columns
]
# map decorated metadata
self.set_response_key_mappings(
response,
Expand Down Expand Up @@ -1606,7 +1622,7 @@ def get_list_headless(self, **kwargs: Any) -> Response:
order_direction,
page=page_index,
page_size=page_size,
select_columns=self.list_select_columns,
select_columns=list_select_columns,
outer_default_load=self.list_outer_default_load,
)
pks = self.datamodel.get_keys(lst)
Expand Down
2 changes: 2 additions & 0 deletions flask_appbuilder/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
API_PERMISSIONS_RIS_KEY,
API_SELECT_COLUMNS_RIS_KEY,
API_SELECT_KEYS_RIS_KEY,
API_SELECT_SEL_COLUMNS_RIS_KEY,
API_SHOW_COLUMNS_RIS_KEY,
API_SHOW_TITLE_RIS_KEY,
)
Expand Down Expand Up @@ -70,6 +71,7 @@ def load(self, data, *, instance=None, **kwargs):
},
},
API_SELECT_COLUMNS_RIS_KEY: {"type": "array", "items": {"type": "string"}},
API_SELECT_SEL_COLUMNS_RIS_KEY: {"type": "array", "items": {"type": "string"}},
API_ORDER_COLUMN_RIS_KEY: {"type": "string"},
API_ORDER_DIRECTION_RIS_KEY: {"type": "string", "enum": ["asc", "desc"]},
API_PAGE_INDEX_RIS_KEY: {"type": "integer"},
Expand Down
1 change: 1 addition & 0 deletions flask_appbuilder/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
API_FILTERS_RIS_KEY = "filters"
API_PERMISSIONS_RIS_KEY = "permissions"
API_SELECT_COLUMNS_RIS_KEY = "columns"
API_SELECT_SEL_COLUMNS_RIS_KEY = "select_columns"
API_SELECT_KEYS_RIS_KEY = "keys"
API_ORDER_COLUMN_RIS_KEY = "order_column"
API_ORDER_DIRECTION_RIS_KEY = "order_direction"
Expand Down
14 changes: 14 additions & 0 deletions flask_appbuilder/security/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@
P = ParamSpec("P")


def no_cache(view: Callable[..., Response]) -> Callable[..., Response]:
@functools.wraps(view)
def wrapped_view(*args, **kwargs) -> Response:
response = make_response(view(*args, **kwargs))
response.headers[
"Cache-Control"
] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response

return wrapped_view


def response_unauthorized_mvc(status_code: int) -> Response:
response = make_response(
jsonify({"message": str(FLAMSG_ERR_SEC_ACCESS_DENIED), "severity": "danger"}),
Expand Down
2 changes: 1 addition & 1 deletion flask_appbuilder/security/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class SelectDataRequired(DataRequired):
select fields
"""

field_flags = ()
field_flags = {}


class LoginForm_oid(DynamicForm):
Expand Down
24 changes: 21 additions & 3 deletions flask_appbuilder/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,9 @@ def __init__(self, appbuilder):
self.limiter = self.create_limiter(app)

def create_limiter(self, app: Flask) -> Limiter:
limiter = Limiter(key_func=get_remote_address)
limiter = Limiter(
key_func=app.config.get("RATELIMIT_KEY_FUNC", get_remote_address)
)
limiter.init_app(app)
return limiter

Expand Down Expand Up @@ -731,11 +733,26 @@ def get_oauth_user_info(self, provider, resp):
me = self.appbuilder.sm.oauth_remotes[provider].get("userinfo")
data = me.json()
log.debug("User info from Okta: %s", data)
if "error" not in data:
return {
"username": f"{provider}_{data['sub']}",
"first_name": data.get("given_name", ""),
"last_name": data.get("family_name", ""),
"email": data["email"],
"role_keys": data.get("groups", []),
}
else:
log.error(data.get("error_description"))
return {}
# for Auth0
if provider == "auth0":
data = self.appbuilder.sm.oauth_remotes[provider].userinfo()
log.debug("User info from Auth0: %s", data)
return {
"username": "okta_" + data.get("sub", ""),
"username": f"{provider}_{data['sub']}",
"first_name": data.get("given_name", ""),
"last_name": data.get("family_name", ""),
"email": data.get("email", ""),
"email": data["email"],
"role_keys": data.get("groups", []),
}
# for Keycloak
Expand All @@ -751,6 +768,7 @@ def get_oauth_user_info(self, provider, resp):
"first_name": data.get("given_name", ""),
"last_name": data.get("family_name", ""),
"email": data.get("email", ""),
"role_keys": data.get("groups", []),
}
else:
return {}
Expand Down
5 changes: 4 additions & 1 deletion flask_appbuilder/security/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from flask_appbuilder.baseviews import BaseView
from flask_appbuilder.charts.views import DirectByChartView
from flask_appbuilder.fieldwidgets import BS3PasswordFieldWidget
from flask_appbuilder.security.decorators import has_access
from flask_appbuilder.security.decorators import has_access, no_cache
from flask_appbuilder.security.forms import (
DynamicForm,
LoginForm_db,
Expand Down Expand Up @@ -529,6 +529,7 @@ class AuthDBView(AuthView):
login_template = "appbuilder/general/security/login_db.html"

@expose("/login/", methods=["GET", "POST"])
@no_cache
def login(self):
if g.user is not None and g.user.is_authenticated:
return redirect(self.appbuilder.get_url_for_index)
Expand All @@ -552,6 +553,7 @@ class AuthLDAPView(AuthView):
login_template = "appbuilder/general/security/login_ldap.html"

@expose("/login/", methods=["GET", "POST"])
@no_cache
def login(self):
if g.user is not None and g.user.is_authenticated:
return redirect(self.appbuilder.get_url_for_index)
Expand All @@ -577,6 +579,7 @@ class AuthOIDView(AuthView):
oid_ask_for_optional: List[str] = []

@expose("/login/", methods=["GET", "POST"])
@no_cache
def login(self, flag=True) -> WerkzeugResponse:
@self.appbuilder.sm.oid.loginhandler
def login_handler(self):
Expand Down
2 changes: 1 addition & 1 deletion flask_appbuilder/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Unique:
a specified table field.
"""

field_flags = ("unique",)
field_flags = {"unique": True}

def __init__(
self, datamodel: BaseInterface, col_name: str, message: Optional[str] = None
Expand Down
6 changes: 3 additions & 3 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ colorama==0.4.6
# via flask-appbuilder
deprecated==1.2.14
# via limits
dnspython==2.4.2
dnspython==2.6.1
# via email-validator
email-validator==1.3.1
# via flask-appbuilder
Expand Down Expand Up @@ -127,15 +127,15 @@ typing-extensions==4.8.0
# via
# flask-limiter
# limits
werkzeug==3.0.1
werkzeug==3.0.3
# via
# flask
# flask-appbuilder
# flask-jwt-extended
# flask-login
wrapt==1.15.0
# via deprecated
wtforms==3.1.0
wtforms==3.2.1
# via
# flask-appbuilder
# flask-wtf
2 changes: 1 addition & 1 deletion requirements/extra.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ cffi==1.16.0
# via cryptography
charset-normalizer==2.0.12
# via requests
cryptography==42.0.2
cryptography==42.0.4
# via authlib
cython==0.29.17
# via -r requirements/extra.in
Expand Down
13 changes: 13 additions & 0 deletions tests/security/test_mvc_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ class Model1View(ModelView):

self.appbuilder.add_view(Model1View, "Model1", category="Model1")

def test_sec_login_no_cache(self):
"""
Test Security Login, no cache directives
"""
rv = self.client.get("/login/")
assert rv.status_code == 200
assert (
rv.headers.get("Cache-Control")
== "no-store, no-cache, must-revalidate, max-age=0"
)
assert rv.headers["Pragma"] == "no-cache"
assert rv.headers["Expires"] == "0"

def test_sec_login(self):
"""
Test Security Login, Logout, invalid login, invalid access
Expand Down
Loading

0 comments on commit c590626

Please sign in to comment.