diff --git a/docs/config.rst b/docs/config.rst
index bad0a30fa4..35e64711c9 100755
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -34,6 +34,13 @@ Use config.py to configure the following parameters. By default it will use SQLL
| | exist. Mandatory when using user | |
| | registration | |
+----------------------------------------+--------------------------------------------+-----------+
+| AUTH_USER_REGISTRATION_ROLE_JMESPATH | The `JMESPath `_ | No |
+| | expression used to evaluate user role on | |
+| | registration. If set, takes precedence | |
+| | over ``AUTH_USER_REGISTRATION_ROLE``. | |
+| | Requires ``jmespath`` to be installed. | |
+| | See :ref:`jmespath-examples` for examples | |
++----------------------------------------+--------------------------------------------+-----------+
| AUTH_LDAP_SERVER | define your ldap server when AUTH_TYPE=2 | Cond. |
| | example: | |
| | | |
@@ -261,3 +268,25 @@ Next you only have to import them to the Flask app object, like this
app.config.from_object('config')
Take a look at the skeleton `config.py `_
+
+
+.. _jmespath-examples:
+
+Using JMESPath to map user registration role
+--------------------------------------------
+
+If user self registration is enabled and ``AUTH_USER_REGISTRATION_ROLE_JMESPATH`` is set, it is
+used as a `JMESPath `_ expression to evalate user registration role. The input
+values is ``userinfo`` dict, returned by ``get_oauth_user_info`` function of Security Manager.
+Usage of JMESPath expressions requires `jmespath `_ package
+to be installed.
+
+In case of Google OAuth, userinfo contains user's email that can be used to map some users as admins
+and rest of the domain users as read only users. For example, this expression:
+``contains(['user1@domain.com', 'user2@domain.com'], email) && 'Admin' || 'Viewer'``
+causes users 1 and 2 to be registered with role ``Admin`` and rest with the role ``Viewer``.
+
+JMESPath expression allow more groups to be evaluated:
+``email == 'user1@domain.com' && 'Admin' || (email == 'user2@domain.com' && 'Op' || 'Viewer')``
+
+For more example, see `specification `_.
diff --git a/examples/oauth/config.py b/examples/oauth/config.py
index 1ddad40393..d929ea9ace 100644
--- a/examples/oauth/config.py
+++ b/examples/oauth/config.py
@@ -94,9 +94,12 @@
# Will allow user self registration
AUTH_USER_REGISTRATION = True
-# The default user self registration role
+# The default user self registration role for all users
AUTH_USER_REGISTRATION_ROLE = "Admin"
+# Self registration role based on user info
+AUTH_USER_REGISTRATION_ROLE_JMESPATH = "contains(['alice@example.com', 'celine@example.com'], email) && 'Admin' || 'Public'"
+
# When using LDAP Auth, setup the ldap server
# AUTH_LDAP_SERVER = "ldap://ldapserver.new"
# AUTH_LDAP_USE_TLS = False
diff --git a/flask_appbuilder/security/manager.py b/flask_appbuilder/security/manager.py
index 078a4bf870..5ebf885f45 100644
--- a/flask_appbuilder/security/manager.py
+++ b/flask_appbuilder/security/manager.py
@@ -215,6 +215,7 @@ def __init__(self, appbuilder):
# Self Registration
app.config.setdefault("AUTH_USER_REGISTRATION", False)
app.config.setdefault("AUTH_USER_REGISTRATION_ROLE", self.auth_role_public)
+ app.config.setdefault("AUTH_USER_REGISTRATION_ROLE_JMESPATH", None)
# LDAP Config
if self.auth_type == AUTH_LDAP:
@@ -347,6 +348,10 @@ def auth_user_registration(self):
def auth_user_registration_role(self):
return self.appbuilder.get_app.config["AUTH_USER_REGISTRATION_ROLE"]
+ @property
+ def auth_user_registration_role_jmespath(self) -> str:
+ return self.appbuilder.get_app.config["AUTH_USER_REGISTRATION_ROLE_JMESPATH"]
+
@property
def auth_ldap_search(self):
return self.appbuilder.get_app.config["AUTH_LDAP_SEARCH"]
@@ -1020,12 +1025,19 @@ def auth_user_oauth(self, userinfo):
return None
# User does not exist, create one if self registration.
if not user:
+ role_name = self.auth_user_registration_role
+ if self.auth_user_registration_role_jmespath:
+ import jmespath
+
+ role_name = jmespath.search(
+ self.auth_user_registration_role_jmespath, userinfo
+ )
user = self.add_user(
username=userinfo["username"],
first_name=userinfo.get("first_name", ""),
last_name=userinfo.get("last_name", ""),
email=userinfo.get("email", ""),
- role=self.find_role(self.auth_user_registration_role),
+ role=self.find_role(role_name),
)
if not user:
log.error("Error creating a new OAuth user %s" % userinfo["username"])
diff --git a/flask_appbuilder/tests/_test_oauth_registration_role.py b/flask_appbuilder/tests/_test_oauth_registration_role.py
new file mode 100644
index 0000000000..cc6b4cffc1
--- /dev/null
+++ b/flask_appbuilder/tests/_test_oauth_registration_role.py
@@ -0,0 +1,59 @@
+import logging
+import unittest
+
+from flask import Flask
+from flask_appbuilder import AppBuilder, SQLA
+
+
+logging.basicConfig(format="%(asctime)s:%(levelname)s:%(name)s:%(message)s")
+logging.getLogger().setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+
+class OAuthRegistrationRoleTestCase(unittest.TestCase):
+ def setUp(self):
+ self.app = Flask(__name__)
+ self.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
+ self.db = SQLA(self.app)
+
+ def tearDown(self):
+ self.appbuilder = None
+ self.app = None
+ self.db = None
+
+ def test_self_registration_not_enabled(self):
+ self.app.config["AUTH_USER_REGISTRATION"] = False
+ self.appbuilder = AppBuilder(self.app, self.db.session)
+
+ result = self.appbuilder.sm.auth_user_oauth(userinfo={"username": "testuser"})
+
+ self.assertIsNone(result)
+ self.assertEqual(len(self.appbuilder.sm.get_all_users()), 0)
+
+ def test_register_and_attach_static_role(self):
+ self.app.config["AUTH_USER_REGISTRATION"] = True
+ self.app.config["AUTH_USER_REGISTRATION_ROLE"] = "Public"
+ self.appbuilder = AppBuilder(self.app, self.db.session)
+
+ user = self.appbuilder.sm.auth_user_oauth(userinfo={"username": "testuser"})
+
+ self.assertEqual(user.roles[0].name, "Public")
+
+ def test_register_and_attach_dynamic_role(self):
+ self.app.config["AUTH_USER_REGISTRATION"] = True
+ self.app.config[
+ "AUTH_USER_REGISTRATION_ROLE_JMESPATH"
+ ] = "contains(['alice', 'celine'], username) && 'Admin' || 'Public'"
+ self.appbuilder = AppBuilder(self.app, self.db.session)
+
+ # Role for admin
+ user = self.appbuilder.sm.auth_user_oauth(
+ userinfo={"username": "alice", "email": "alice@example.com"}
+ )
+ self.assertEqual(user.roles[0].name, "Admin")
+
+ # Role for non-admin
+ user = self.appbuilder.sm.auth_user_oauth(
+ userinfo={"username": "bob", "email": "bob@example.com"}
+ )
+ self.assertEqual(user.roles[0].name, "Public")
diff --git a/requirements-dev.txt b/requirements-dev.txt
index f58074adb2..533014d69c 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -14,3 +14,4 @@ mysqlclient>=1.4.2, < 2.0.0
cython==0.29.17
pymssql==2.1.4
black==19.3b0
+jmespath==0.9.5
diff --git a/setup.py b/setup.py
index 4e5c289ae7..0c56bc67e2 100644
--- a/setup.py
+++ b/setup.py
@@ -65,6 +65,7 @@ def desc():
"PyJWT>=1.7.1",
"sqlalchemy-utils>=0.32.21, <1",
],
+ extras_require={"jmespath": ["jmespath>=0.9.5"]},
tests_require=["nose>=1.0", "mockldap>=0.3.0"],
classifiers=[
"Development Status :: 5 - Production/Stable",