Skip to content

Commit

Permalink
implement role binding from: AUTH_LDAP, AUTH_OAUTH
Browse files Browse the repository at this point in the history
  • Loading branch information
thesuperzapper committed May 20, 2020
1 parent 0e7f624 commit 99d5cda
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 31 deletions.
29 changes: 29 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ Use config.py to configure the following parameters. By default it will use SQLL
| | exist. Mandatory when using user | |
| | registration | |
+----------------------------------------+--------------------------------------------+-----------+
| AUTH_ROLES_SYNC_AT_LOGIN | Sets if user's roles are replaced each | No |
| | login with those received from LDAP/OAUTH | |
| | Default: False | |
+----------------------------------------+--------------------------------------------+-----------+
| AUTH_ROLES_MAPPING | A mapping from LDAP/OAUTH group names | No |
| | to FAB roles | |
| | | |
+----------------------------------------+--------------------------------------------+-----------+
| AUTH_LDAP_SERVER | define your ldap server when AUTH_TYPE=2 | Cond. |
| | example: | |
| | | |
Expand Down Expand Up @@ -96,6 +104,27 @@ Use config.py to configure the following parameters. By default it will use SQLL
| | | |
| | AUTH_LDAP_UID_FIELD = "uid" | |
+----------------------------------------+--------------------------------------------+-----------+
| AUTH_LDAP_GROUP_FIELD | sets the field in the ldap directory that | No |
| | stores the user's group uids. This field | |
| | is used in combination with | |
| | AUTH_ROLES_MAPPING to propagate the users | |
| | groups into the User database. | |
| | Default is "memberOf". | |
| | example: | |
| | | |
| | AUTH_TYPE = 2 | |
| | | |
| | AUTH_LDAP_SERVER = "ldap://ldapserver.new" | |
| | | |
| | AUTH_LDAP_SEARCH = "ou=people,dc=example" | |
| | | |
| | AUTH_LDAP_GROUP_FIELD = "memberOf" | |
| | | |
| | AUTH_ROLES_MAPPING = { | |
| | "cn=User,ou=groups,dc=example,dc=com": | |
| | "User" | |
| | } | |
+----------------------------------------+--------------------------------------------+-----------+
| AUTH_LDAP_FIRSTNAME_FIELD | sets the field in the ldap directory that | No |
| | stores the user's first name. This field | |
| | is used to propagate user's first name | |
Expand Down
115 changes: 104 additions & 11 deletions docs/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -466,16 +466,22 @@ key is just the configuration for flask-oauthlib::
AUTH_TYPE = AUTH_OAUTH
OAUTH_PROVIDERS = [
{'name':'twitter', 'icon':'fa-twitter',
'remote_app': {
'consumer_key':'TWITTER KEY',
'consumer_secret':'TWITTER SECRET',
'base_url':'https://api.twitter.com/1.1/',
'request_token_url':'https://api.twitter.com/oauth/request_token',
'access_token_url':'https://api.twitter.com/oauth/access_token',
'authorize_url':'https://api.twitter.com/oauth/authenticate'}
{
'name':'twitter',
'icon':'fa-twitter',
'remote_app': {
'consumer_key':'TWITTER KEY',
'consumer_secret':'TWITTER SECRET',
'base_url':'https://api.twitter.com/1.1/',
'request_token_url':'https://api.twitter.com/oauth/request_token',
'access_token_url':'https://api.twitter.com/oauth/access_token',
'authorize_url':'https://api.twitter.com/oauth/authenticate'
}
},
{'name':'google', 'icon':'fa-google', 'token_key':'access_token',
{
'name':'google',
'icon':'fa-google',
'token_key':'access_token',
'remote_app': {
'consumer_key':'GOOGLE KEY',
'consumer_secret':'GOOGLE SECRET',
Expand All @@ -486,14 +492,29 @@ key is just the configuration for flask-oauthlib::
'request_token_url':None,
'access_token_url':'https://accounts.google.com/o/oauth2/token',
'authorize_url':'https://accounts.google.com/o/oauth2/auth'}
}
},
{
'name': 'okta',
'icon': 'fa-circle-o',
'token_key': 'access_token',
'remote_app': {
'consumer_key': 'OKTA_KEY',
'consumer_secret': 'OKTA_SECRET',
'base_url': 'https://OKTA_DOMAIN.okta.com/oauth2/v1/',
'request_token_params': {
'scope': 'openid profile email groups'
},
'access_token_url': 'https://OKTA_DOMAIN.okta.com/oauth2/v1/token',
'authorize_url': 'https://OKTA_DOMAIN.okta.com/oauth2/v1/authorize',
},
},
]

This needs a small explanation, you basically have five special keys:

:name: The name of the provider, you can choose whatever you want. But the framework as some
builtin logic to retrieve information about a user that you can make use of if you choose:
'twitter', 'google', 'github','linkedin'.
'twitter', 'google', 'github', 'linkedin', 'okta'.

:icon: The font-awesome icon for this provider.
:token_key: The token key name that this provider uses, google and github uses *'access_token'*,
Expand Down Expand Up @@ -523,6 +544,78 @@ this provider with, **response** is the response.

Take a look at the `example <https://github.com/dpgaspar/Flask-AppBuilder/tree/master/examples/oauth>`_

External Role Mapping
--------------------

:note: currently we only support mapping external groups into FAB roles with: AUTH_LDAP, AUTH_OAUTH (Okta)

If you have an external source of truth for groups, you might want to have FAB sync user's roles from that system
as they login.

Here is an example config for LDAP, (Note this is for Okta LDAP, but can be extended to any LDAP provider)::

# Force users to re-auth after 15min of inactivity
# NOTE: this is important to keep roles in sync
PERMANENT_SESSION_LIFETIME = 900

AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Viewer"

AUTH_ROLES_SYNC_AT_LOGIN = True
AUTH_ROLES_MAPPING = {
"cn=User,ou=groups,dc=OKTA_DOMAIN,dc=com": "User",
"cn=Admin,ou=groups,dc=OKTA_DOMAIN,dc=com": "Admin",
}

AUTH_TYPE = AUTH_LDAP
AUTH_LDAP_SERVER = "ldaps://OKTA_DOMAIN.ldap.okta.com:636"
AUTH_LDAP_USE_TLS = False

AUTH_LDAP_BIND_USER = "uid=bind-admin,dc=OKTA_DOMAIN,dc=okta,dc=com"
AUTH_LDAP_BIND_PASSWORD = "xxxxxxxxxxxx"

AUTH_LDAP_SEARCH = "ou=users,dc=OKTA_DOMAIN,dc=okta,dc=com"
AUTH_LDAP_SEARCH_FILTER = "(objectclass=inetOrgPerson)"
AUTH_LDAP_APPEND_DOMAIN = "OKTA_DOMAIN.com"

AUTH_LDAP_UID_FIELD = "uid"
AUTH_LDAP_GROUP_FIELD = "memberOf"
AUTH_LDAP_FIRSTNAME_FIELD = "givenName"
AUTH_LDAP_LASTNAME_FIELD = "sn"
AUTH_LDAP_EMAIL_FIELD = "email"

Here is an example config for OAUTH, (Note this is for Okta OAUTH)::

# Force users to re-auth after 15min of inactivity
# NOTE: this is important to keep roles in sync
PERMANENT_SESSION_LIFETIME = 900

AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Viewer"

AUTH_ROLES_SYNC_AT_LOGIN = True
AUTH_ROLES_MAPPING = {
"USER_GROUP_NAME": "User",
"ADMIN_GROUP_NAME": "Admin",
}

OAUTH_PROVIDERS = [
{
"name": "okta",
"icon": "fa-circle-o",
"token_key": "access_token",
"remote_app": {
"consumer_key": "OKTA_KEY",
"consumer_secret": "OKTA_SECRET",
"base_url": "https://OKTA_DOMAIN.okta.com/oauth2/v1/",
"request_token_params": {
"scope": "openid profile email groups"
},
"access_token_url": "https://OKTA_DOMAIN.okta.com/oauth2/v1/token",
"authorize_url": "https://OKTA_DOMAIN.okta.com/oauth2/v1/authorize",
}
]

Your Custom Security
--------------------

Expand Down
29 changes: 29 additions & 0 deletions examples/oauth/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@
"authorize_url": "https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2/authorize",
},
},
{
"name": "okta",
"icon": "fa-circle-o",
"token_key": "access_token",
"remote_app": {
"consumer_key": os.environ.get("OKTA_KEY"),
"consumer_secret": os.environ.get("OKTA_SECRET"),
"base_url": "https://{}.okta.com/oauth2/v1/".format(os.environ.get("OKTA_DOMAIN")),
"request_token_params": {
"scope": "openid profile email groups"
},
"access_token_url": "https://{}.okta.com/oauth2/v1/token".format(os.environ.get("OKTA_DOMAIN")),
"authorize_url": "https://{}.okta.com/oauth2/v1/authorize".format(os.environ.get("OKTA_DOMAIN")),
},
},
]

# Uncomment to setup Full admin role name
Expand All @@ -97,6 +112,20 @@
# The default user self registration role
AUTH_USER_REGISTRATION_ROLE = "Admin"

# Replace users database roles each login with those received from OAUTH/LDAP
AUTH_ROLES_SYNC_AT_LOGIN = True

# A mapping from LDAP/OAUTH group names to FAB roles
AUTH_ROLES_MAPPING = {
# For OAUTH
"USER_GROUP_NAME": "User",
"ADMIN_GROUP_NAME": "Admin",

# For LDAP
# "cn=User,ou=groups,dc=example,dc=com": "User",
# "cn=Admin,ou=groups,dc=example,dc=com": "Admin",
}

# When using LDAP Auth, setup the ldap server
# AUTH_LDAP_SERVER = "ldap://ldapserver.new"
# AUTH_LDAP_USE_TLS = False
Expand Down
37 changes: 18 additions & 19 deletions flask_appbuilder/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
LOGMSG_ERR_SEC_AUTH_LDAP_TLS,
LOGMSG_WAR_SEC_LOGIN_FAILED,
LOGMSG_WAR_SEC_NO_USER,
LOGMSG_WAR_SEC_NOLDAP_OBJ,
PERMISSION_PREFIX,
)

Expand Down Expand Up @@ -684,7 +683,7 @@ def register_views(self):
category="Security",
)
if self.appbuilder.app.config.get(
"FAB_ADD_SECURITY_PERMISSION_VIEWS_VIEW", True
"FAB_ADD_SECURITY_PERMISSION_VIEWS_VIEW", True
):
self.appbuilder.add_view(
self.permissionviewmodelview,
Expand Down Expand Up @@ -1052,15 +1051,15 @@ def is_item_public(self, permission_name, view_name):
if permissions:
for i in permissions:
if (view_name == i.view_menu.name) and (
permission_name == i.permission.name
permission_name == i.permission.name
):
return True
return False
else:
return False

def _has_access_builtin_roles(
self, role, permission_name: str, view_name: str
self, role, permission_name: str, view_name: str
) -> bool:
"""
Checks permission on builtin role
Expand All @@ -1070,13 +1069,13 @@ def _has_access_builtin_roles(
_view_name = pvm[0]
_permission_name = pvm[1]
if re.match(_view_name, view_name) and re.match(
_permission_name, permission_name
_permission_name, permission_name
):
return True
return False

def _has_view_access(
self, user: object, permission_name: str, view_name: str
self, user: object, permission_name: str, view_name: str
) -> bool:
roles = user.roles
db_role_ids = list()
Expand All @@ -1093,7 +1092,7 @@ def _has_view_access(
return self.exist_permission_on_roles(view_name, permission_name, db_role_ids)

def _get_user_permission_view_menus(
self, user: object, permission_name: str, view_menus_name: List[str]
self, user: object, permission_name: str, view_menus_name: List[str]
) -> Set[str]:
"""
Return a set of view menu names with a certain permission name
Expand All @@ -1113,7 +1112,7 @@ def _get_user_permission_view_menus(
if role.name in self.builtin_roles:
for view_menu_name in view_menus_name:
if self._has_access_builtin_roles(
role, permission_name, view_menu_name
role, permission_name, view_menu_name
):
result.add(view_menu_name)
else:
Expand Down Expand Up @@ -1195,8 +1194,8 @@ def add_permissions_view(self, base_permissions, view_menu):
self.del_permission_role(role, perm)
self.del_permission_view_menu(perm_view.permission.name, view_menu)
elif (
self.auth_role_admin not in self.builtin_roles
and perm_view not in role_admin.permissions
self.auth_role_admin not in self.builtin_roles
and perm_view not in role_admin.permissions
):
# Role Admin must have all permissions
self.add_permission_role(role_admin, perm_view)
Expand Down Expand Up @@ -1253,7 +1252,7 @@ def _get_new_old_permissions(baseview) -> Dict:
)
# Actions do not get prefix when normally defined
if hasattr(baseview, "actions") and baseview.actions.get(
old_permission_name
old_permission_name
):
permission_prefix = ""
else:
Expand All @@ -1271,11 +1270,11 @@ def _get_new_old_permissions(baseview) -> Dict:

@staticmethod
def _add_state_transition(
state_transition: Dict,
old_view_name: str,
old_perm_name: str,
view_name: str,
perm_name: str,
state_transition: Dict,
old_view_name: str,
old_perm_name: str,
view_name: str,
perm_name: str,
) -> None:
old_pvm = state_transition["add"].get((old_view_name, old_perm_name))
if old_pvm:
Expand Down Expand Up @@ -1427,7 +1426,7 @@ def find_register_user(self, registration_hash):
raise NotImplementedError

def add_register_user(
self, username, first_name, last_name, email, password="", hashed_password=""
self, username, first_name, last_name, email, password="", hashed_password=""
):
"""
Generic function to add user registration
Expand Down Expand Up @@ -1521,12 +1520,12 @@ def find_permission(self, name):
raise NotImplementedError

def find_roles_permission_view_menus(
self, permission_name: str, role_ids: List[int]
self, permission_name: str, role_ids: List[int]
):
raise NotImplementedError

def exist_permission_on_roles(
self, view_name: str, permission_name: str, role_ids: List[int]
self, view_name: str, permission_name: str, role_ids: List[int]
) -> bool:
"""
Finds and returns permission views for a group of roles
Expand Down
2 changes: 1 addition & 1 deletion flask_appbuilder/security/sqla/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def add_user(
user.username = username
user.email = email
user.active = True
user.roles.append(role)
user.roles = role if isinstance(role, list) else [role]
if hashed_password:
user.password = hashed_password
else:
Expand Down

0 comments on commit 99d5cda

Please sign in to comment.