18 |
18 |
import logging
|
19 |
19 |
|
20 |
20 |
from django.contrib import messages
|
21 |
|
from django.core.exceptions import MultipleObjectsReturned
|
22 |
21 |
from django.db.transaction import atomic
|
23 |
22 |
from django.utils.translation import ugettext as _
|
24 |
23 |
from mellon.adapters import DefaultAdapter, UserCreationError
|
25 |
|
from mellon.utils import get_setting
|
26 |
24 |
|
27 |
|
from authentic2.a2_rbac.models import OrganizationalUnit as OU
|
28 |
|
from authentic2.a2_rbac.models import Role
|
29 |
25 |
from authentic2.a2_rbac.utils import get_default_ou
|
30 |
26 |
from authentic2.backends import get_user_queryset
|
31 |
27 |
from authentic2.models import Lock
|
32 |
28 |
from authentic2.utils import misc as utils_misc
|
33 |
|
from authentic2.utils.evaluate import evaluate_condition
|
34 |
29 |
|
35 |
30 |
from .models import SAMLAuthenticator
|
36 |
31 |
|
... | ... | |
114 |
109 |
|
115 |
110 |
@atomic
|
116 |
111 |
def provision_a2_attributes(self, user, idp, saml_attributes):
|
117 |
|
"""Copy incoming SAML attributes to user attributes, A2_ATTRIBUTE_MAPPING must be a list of
|
118 |
|
dictionaries like:
|
119 |
|
|
120 |
|
{
|
121 |
|
'attribute': 'email',
|
122 |
|
'saml_attribute': 'email',
|
123 |
|
# optional:
|
124 |
|
'mandatory': False,
|
125 |
|
}
|
126 |
|
|
127 |
|
If an attribute is not mandatory any error is just logged, if the attribute is
|
128 |
|
mandatory, login will fail.
|
129 |
|
"""
|
130 |
112 |
saml_attributes = saml_attributes.copy()
|
131 |
|
attribute_mapping = get_setting(idp, 'A2_ATTRIBUTE_MAPPING', [])
|
132 |
|
|
133 |
|
if not isinstance(attribute_mapping, list):
|
134 |
|
raise MappingError(_('invalid A2_ATTRIBUTE_MAPPING'))
|
135 |
113 |
|
136 |
|
self.apply_attribute_mapping(user, idp, saml_attributes, attribute_mapping)
|
137 |
|
|
138 |
|
def apply_attribute_mapping(self, user, idp, saml_attributes, attribute_mapping):
|
139 |
114 |
self.rename_attributes(idp, saml_attributes)
|
140 |
115 |
self.set_attributes(user, idp, saml_attributes)
|
141 |
|
|
142 |
|
for mapping in attribute_mapping:
|
143 |
|
if not isinstance(mapping, dict):
|
144 |
|
raise MappingError(_('invalid mapping action "%(mapping)s"'), mapping=mapping)
|
145 |
|
action = mapping.get('action', 'set-attribute')
|
146 |
|
mandatory = mapping.get('mandatory', False) is True
|
147 |
|
method = None
|
148 |
|
if isinstance(action, str):
|
149 |
|
try:
|
150 |
|
method = getattr(self, 'action_' + action.replace('-', '_'))
|
151 |
|
except AttributeError:
|
152 |
|
pass
|
153 |
|
try:
|
154 |
|
if not method:
|
155 |
|
raise MappingError(_('invalid action'))
|
156 |
|
logger.debug('auth_saml: applying provisionning mapping %s', mapping)
|
157 |
|
method(user, idp, saml_attributes, mapping)
|
158 |
|
except MappingError as e:
|
159 |
|
if mandatory:
|
160 |
|
# it's mandatory, provisionning should fail completely
|
161 |
|
raise e
|
162 |
|
logger.warning('auth_saml: mapping action failed: %s', e)
|
|
116 |
self.action_add_role(user, idp, saml_attributes)
|
163 |
117 |
|
164 |
118 |
def rename_attributes(self, idp, saml_attributes):
|
165 |
119 |
for action in idp['authenticator'].rename_attribute_actions.all():
|
... | ... | |
208 |
162 |
return True
|
209 |
163 |
return False
|
210 |
164 |
|
211 |
|
def get_ou(self, role_desc):
|
212 |
|
ou_desc = role_desc.get('ou')
|
213 |
|
if ou_desc is None:
|
214 |
|
return None
|
215 |
|
if not isinstance(ou_desc, dict):
|
216 |
|
raise MappingError(_('invalid ou description'))
|
217 |
|
slug = ou_desc.get('slug')
|
218 |
|
name = ou_desc.get('name')
|
219 |
|
if slug:
|
220 |
|
if not isinstance(slug, str):
|
221 |
|
raise MappingError(_('invalid ou.slug in ou description'))
|
222 |
|
try:
|
223 |
|
return OU.objects.get(slug=slug)
|
224 |
|
except OU.DoesNotExist:
|
225 |
|
raise MappingError(_('unknown ou: "%(slug)s"'), slug=slug)
|
226 |
|
elif name:
|
227 |
|
if not isinstance(name, str):
|
228 |
|
raise MappingError(_('invalid ou.slug in ou description'))
|
229 |
|
try:
|
230 |
|
return OU.objects.get(name=name)
|
231 |
|
except OU.DoesNotExist:
|
232 |
|
raise MappingError(_('unknown ou: "%(name)s"'), name=name)
|
233 |
|
else:
|
234 |
|
raise MappingError(_('invalid ou description'))
|
235 |
|
|
236 |
|
def get_role(self, mapping):
|
237 |
|
role_desc = mapping.get('role')
|
238 |
|
if not role_desc or not isinstance(role_desc, dict):
|
239 |
|
raise MappingError(_('missing or invalid role description'))
|
240 |
|
slug = role_desc.get('slug')
|
241 |
|
name = role_desc.get('name')
|
242 |
|
ou = self.get_ou(role_desc)
|
243 |
|
|
244 |
|
kwargs = {}
|
245 |
|
if ou:
|
246 |
|
kwargs['ou'] = ou
|
247 |
|
|
248 |
|
if slug:
|
249 |
|
if not isinstance(slug, str):
|
250 |
|
raise MappingError(_('invalid role slug: "%(slug)s"'), slug=slug)
|
251 |
|
kwargs['slug'] = slug
|
252 |
|
elif name:
|
253 |
|
if not isinstance(name, str):
|
254 |
|
raise MappingError(_('invalid role name: "%(name)s"'), name=name)
|
255 |
|
kwargs['name'] = name
|
256 |
|
else:
|
257 |
|
raise MappingError(_('invalid role description'))
|
258 |
|
|
259 |
|
try:
|
260 |
|
return Role.objects.get(**kwargs)
|
261 |
|
except Role.DoesNotExist:
|
262 |
|
raise MappingError(_('unknown role: %(kwargs)s'), kwargs=kwargs)
|
263 |
|
except MultipleObjectsReturned:
|
264 |
|
raise MappingError(_('ambiugous role description: %(kwargs)s'), kwargs=kwargs)
|
265 |
|
|
266 |
|
def evaluate_condition(self, user, saml_attributes, mapping):
|
267 |
|
condition = mapping.get('condition')
|
268 |
|
if condition is None:
|
269 |
|
return True
|
270 |
|
if not isinstance(condition, str):
|
271 |
|
raise MappingError(_('invalid condition'))
|
272 |
|
try:
|
273 |
|
# use a proxy to simplify condition expressions as subscript is forbidden
|
274 |
|
# you can write "email == 'a@b.com'" but also "'a@b.com' in email__list"
|
275 |
|
value = evaluate_condition(condition, SamlConditionContextProxy(saml_attributes))
|
276 |
|
logger.debug('auth_saml: condition %r is %s', condition, value, extra={'user': user})
|
277 |
|
return value
|
278 |
|
except Exception as e:
|
279 |
|
raise MappingError(_('condition evaluation failed: %(message)s'), message=e)
|
280 |
|
|
281 |
|
def action_add_role(self, user, idp, saml_attributes, mapping):
|
282 |
|
role = self.get_role(mapping)
|
283 |
|
if self.evaluate_condition(user, saml_attributes, mapping):
|
284 |
|
if role not in user.roles.all():
|
285 |
|
logger.info('auth_saml: adding role "%s"', role, extra={'user': user})
|
286 |
|
user.roles.add(role)
|
287 |
|
else:
|
288 |
|
if role in user.roles.all():
|
289 |
|
logger.info('auth_saml: removing role "%s"', role, extra={'user': user})
|
290 |
|
user.roles.remove(role)
|
291 |
|
|
292 |
|
def action_toggle_role(self, *args, **kwargs):
|
293 |
|
return self.action_add_role(*args, **kwargs)
|
|
165 |
def action_add_role(self, user, idp, saml_attributes):
|
|
166 |
for action in idp['authenticator'].add_role_actions.all():
|
|
167 |
if action.role not in user.roles.all():
|
|
168 |
logger.info('auth_saml: adding role "%s"', action.role, extra={'user': user})
|
|
169 |
user.roles.add(action.role)
|
294 |
170 |
|
295 |
171 |
def auth_login(self, request, user):
|
296 |
172 |
utils_misc.login(request, user, 'saml')
|