Fix file permissions and remove shebang's
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / auth.py
1 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
2 #
3 # see file 'COPYING' for use and warranty information
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 from ipsilon.providers.common import ProviderPageBase, ProviderException
19 from ipsilon.providers.common import AuthenticationError, InvalidRequest
20 from ipsilon.providers.saml2.provider import ServiceProvider
21 from ipsilon.providers.saml2.provider import InvalidProviderId
22 from ipsilon.providers.saml2.provider import NameIdNotAllowed
23 from ipsilon.util.user import UserSession
24 from ipsilon.util.trans import Transaction
25 import cherrypy
26 import datetime
27 import lasso
28
29
30 class UnknownProvider(ProviderException):
31
32     def __init__(self, message):
33         super(UnknownProvider, self).__init__(message)
34         self._debug(message)
35
36
37 class AuthenticateRequest(ProviderPageBase):
38
39     def __init__(self, *args, **kwargs):
40         super(AuthenticateRequest, self).__init__(*args, **kwargs)
41         self.stage = 'init'
42         self.trans = None
43
44     def _preop(self, *args, **kwargs):
45         try:
46             # generate a new id or get current one
47             self.trans = Transaction('saml2', **kwargs)
48             if self.trans.cookie.value != self.trans.provider:
49                 self.debug('Invalid transaction, %s != %s' % (
50                            self.trans.cookie.value, self.trans.provider))
51         except Exception, e:  # pylint: disable=broad-except
52             self.debug('Transaction initialization failed: %s' % repr(e))
53             raise cherrypy.HTTPError(400, 'Invalid transaction id')
54
55     def pre_GET(self, *args, **kwargs):
56         self._preop(*args, **kwargs)
57
58     def pre_POST(self, *args, **kwargs):
59         self._preop(*args, **kwargs)
60
61     def auth(self, login):
62         try:
63             self.saml2checks(login)
64         except AuthenticationError, e:
65             self.saml2error(login, e.code, e.message)
66         return self.reply(login)
67
68     def _parse_request(self, message):
69
70         login = self.cfg.idp.get_login_handler()
71
72         try:
73             login.processAuthnRequestMsg(message)
74         except (lasso.ProfileInvalidMsgError,
75                 lasso.ProfileMissingIssuerError), e:
76
77             msg = 'Malformed Request %r [%r]' % (e, message)
78             raise InvalidRequest(msg)
79
80         except (lasso.ProfileInvalidProtocolprofileError,
81                 lasso.DsError), e:
82
83             msg = 'Invalid SAML Request: %r (%r [%r])' % (login.request,
84                                                           e, message)
85             raise InvalidRequest(msg)
86
87         except (lasso.ServerProviderNotFoundError,
88                 lasso.ProfileUnknownProviderError), e:
89
90             msg = 'Invalid SP [%s] (%r [%r])' % (login.remoteProviderId,
91                                                  e, message)
92             raise UnknownProvider(msg)
93
94         self._debug('SP %s requested authentication' % login.remoteProviderId)
95
96         return login
97
98     def saml2login(self, request):
99
100         if not request:
101             raise cherrypy.HTTPError(400,
102                                      'SAML request token missing or empty')
103
104         try:
105             login = self._parse_request(request)
106         except InvalidRequest, e:
107             self._debug(str(e))
108             raise cherrypy.HTTPError(400, 'Invalid SAML request token')
109         except UnknownProvider, e:
110             self._debug(str(e))
111             raise cherrypy.HTTPError(400, 'Unknown Service Provider')
112         except Exception, e:  # pylint: disable=broad-except
113             self._debug(str(e))
114             raise cherrypy.HTTPError(500)
115
116         return login
117
118     def saml2checks(self, login):
119
120         us = UserSession()
121         user = us.get_user()
122         if user.is_anonymous:
123             if self.stage == 'init':
124                 returl = '%s/saml2/SSO/Continue?%s' % (
125                     self.basepath, self.trans.get_GET_arg())
126                 data = {'saml2_stage': 'auth',
127                         'saml2_request': login.dump(),
128                         'login_return': returl,
129                         'login_target': login.remoteProviderId}
130                 self.trans.store(data)
131                 redirect = '%s/login?%s' % (self.basepath,
132                                             self.trans.get_GET_arg())
133                 raise cherrypy.HTTPRedirect(redirect)
134             else:
135                 raise AuthenticationError(
136                     "Unknown user", lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
137
138         self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))
139
140         # We can wipe the transaction now, as this is the last step
141         self.trans.wipe()
142
143         # TODO: check if this is the first time this user access this SP
144         # If required by user prefs, ask user for consent once and then
145         # record it
146         consent = True
147
148         # TODO: check destination
149
150         try:
151             provider = ServiceProvider(self.cfg, login.remoteProviderId)
152             nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
153         except NameIdNotAllowed, e:
154             raise AuthenticationError(
155                 str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
156         except InvalidProviderId, e:
157             raise AuthenticationError(
158                 str(e), lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
159
160         # TODO: check login.request.forceAuthn
161
162         login.validateRequestMsg(not user.is_anonymous, consent)
163
164         authtime = datetime.datetime.utcnow()
165         skew = datetime.timedelta(0, 60)
166         authtime_notbefore = authtime - skew
167         authtime_notafter = authtime + skew
168
169         # TODO: get authentication type fnd name format from session
170         # need to save which login manager authenticated and map it to a
171         # saml2 authentication context
172         authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
173
174         timeformat = '%Y-%m-%dT%H:%M:%SZ'
175         login.buildAssertion(authn_context,
176                              authtime.strftime(timeformat),
177                              None,
178                              authtime_notbefore.strftime(timeformat),
179                              authtime_notafter.strftime(timeformat))
180
181         nameid = None
182         if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
183             # TODO map to something else ?
184             nameid = provider.normalize_username(user.name)
185         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
186             # TODO map to something else ?
187             nameid = provider.normalize_username(user.name)
188         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
189             nameid = us.get_data('user', 'krb_principal_name')
190         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
191             nameid = us.get_user().email
192             if not nameid:
193                 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
194
195         if nameid:
196             login.assertion.subject.nameId.format = nameidfmt
197             login.assertion.subject.nameId.content = nameid
198         else:
199             self.trans.wipe()
200             raise AuthenticationError("Unavailable Name ID type",
201                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
202
203         # TODO: filter user attributes as policy requires from 'usersession'
204         if not login.assertion.attributeStatement:
205             attrstat = lasso.Saml2AttributeStatement()
206             login.assertion.attributeStatement = [attrstat]
207         else:
208             attrstat = login.assertion.attributeStatement[0]
209         if not attrstat.attribute:
210             attrstat.attribute = ()
211
212         attributes = dict()
213         userattrs = us.get_user_attrs()
214         for key, value in userattrs.get('userdata', {}).iteritems():
215             if type(value) is str:
216                 attributes[key] = value
217         if 'groups' in userattrs:
218             attributes['group'] = userattrs['groups']
219         for _, info in userattrs.get('extras', {}).iteritems():
220             for key, value in info.items():
221                 attributes[key] = value
222
223         for key in attributes:
224             values = attributes[key]
225             if type(values) is not list:
226                 values = [values]
227             for value in values:
228                 attr = lasso.Saml2Attribute()
229                 attr.name = key
230                 attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
231                 value = str(value).encode('utf-8')
232                 self.debug('value %s' % value)
233                 node = lasso.MiscTextNode.newWithString(value)
234                 node.textChild = True
235                 attrvalue = lasso.Saml2AttributeValue()
236                 attrvalue.any = [node]
237                 attr.attributeValue = [attrvalue]
238                 attrstat.attribute = attrstat.attribute + (attr,)
239
240         self.debug('Assertion: %s' % login.assertion.dump())
241
242     def saml2error(self, login, code, message):
243         status = lasso.Samlp2Status()
244         status.statusCode = lasso.Samlp2StatusCode()
245         status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
246         status.statusCode.statusCode = lasso.Samlp2StatusCode()
247         status.statusCode.statusCode.value = code
248         login.response.status = status
249
250     def reply(self, login):
251         if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
252             # TODO
253             raise cherrypy.HTTPError(501)
254         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
255             login.buildAuthnResponseMsg()
256             self._debug('POSTing back to SP [%s]' % (login.msgUrl))
257             context = {
258                 "title": 'Redirecting back to the web application',
259                 "action": login.msgUrl,
260                 "fields": [
261                     [lasso.SAML2_FIELD_RESPONSE, login.msgBody],
262                     [lasso.SAML2_FIELD_RELAYSTATE, login.msgRelayState],
263                 ],
264                 "submit": 'Return to application',
265             }
266             # pylint: disable=star-args
267             return self._template('saml2/post_response.html', **context)
268
269         else:
270             raise cherrypy.HTTPError(500)