b70582effbfce48c225547b3bf0aa47fe99f162a
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / provider.py
1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
2
3 from ipsilon.providers.common import ProviderException
4 from ipsilon.util import config as pconfig
5 from ipsilon.util.config import ConfigHelper
6 from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP, NSMAP
7 from ipsilon.util.log import Log
8 from lxml import etree
9 import lasso
10 import re
11
12
13 VALID_IN_NAME = r'[^\ a-zA-Z0-9]'
14
15
16 class InvalidProviderId(ProviderException):
17
18     def __init__(self, code):
19         message = 'Invalid Provider ID: %s' % code
20         super(InvalidProviderId, self).__init__(message)
21         self.debug(message)
22
23
24 class NameIdNotAllowed(Exception):
25
26     def __init__(self, nid):
27         message = 'Name ID [%s] is not allowed' % nid
28         super(NameIdNotAllowed, self).__init__(message)
29         self.message = message
30
31     def __str__(self):
32         return repr(self.message)
33
34
35 class ServiceProviderConfig(ConfigHelper):
36     def __init__(self):
37         super(ServiceProviderConfig, self).__init__()
38
39
40 class ServiceProvider(ServiceProviderConfig):
41
42     def __init__(self, config, provider_id):
43         super(ServiceProvider, self).__init__()
44         self.cfg = config
45         data = self.cfg.get_data(name='id', value=provider_id)
46         if len(data) != 1:
47             raise InvalidProviderId('multiple matches')
48         idval = data.keys()[0]
49         data = self.cfg.get_data(idval=idval)
50         self._properties = data[idval]
51         self._staging = dict()
52         self.load_config()
53         self.logout_mechs = []
54         xmldoc = etree.XML(str(data[idval]['metadata']))
55         logout = xmldoc.xpath('//md:EntityDescriptor'
56                               '/md:SPSSODescriptor'
57                               '/md:SingleLogoutService',
58                               namespaces=NSMAP)
59         for service in logout:
60             self.logout_mechs.append(service.values()[0])
61
62     def load_config(self):
63         self.new_config(
64             self.provider_id,
65             pconfig.String(
66                 'Name',
67                 'A nickname used to easily identify the Service Provider.'
68                 ' Only alphanumeric characters [A-Z,a-z,0-9] and spaces are'
69                 '  accepted.',
70                 self.name),
71             pconfig.Pick(
72                 'Default NameID',
73                 'Default NameID used by Service Providers.',
74                 SAML2_NAMEID_MAP.keys(),
75                 self.default_nameid),
76             pconfig.Choice(
77                 'Allowed NameIDs',
78                 'Allowed NameIDs for this Service Provider.',
79                 SAML2_NAMEID_MAP.keys(),
80                 self.allowed_nameids),
81             pconfig.String(
82                 'User Owner',
83                 'The user that owns this Service Provider',
84                 self.owner),
85             pconfig.MappingList(
86                 'Attribute Mapping',
87                 'Defines how to map attributes before returning them to'
88                 ' the SP. Setting this overrides the global values.',
89                 self.attribute_mappings),
90             pconfig.ComplexList(
91                 'Allowed Attributes',
92                 'Defines a list of allowed attributes, applied after mapping.'
93                 ' Setting this overrides the global values.',
94                 self.allowed_attributes),
95         )
96
97     @property
98     def provider_id(self):
99         return self._properties['id']
100
101     @property
102     def name(self):
103         return self._properties['name']
104
105     @name.setter
106     def name(self, value):
107         self._staging['name'] = value
108
109     @property
110     def owner(self):
111         if 'owner' in self._properties:
112             return self._properties['owner']
113         else:
114             return ''
115
116     @owner.setter
117     def owner(self, value):
118         self._staging['owner'] = value
119
120     @property
121     def allowed_nameids(self):
122         if 'allowed nameids' in self._properties:
123             allowed = self._properties['allowed nameids']
124             return [x.strip() for x in allowed.split(',')]
125         else:
126             return self.cfg.default_allowed_nameids
127
128     @allowed_nameids.setter
129     def allowed_nameids(self, value):
130         if not isinstance(value, list):
131             raise ValueError("Must be a list")
132         self._staging['allowed nameids'] = ','.join(value)
133
134     @property
135     def default_nameid(self):
136         if 'default nameid' in self._properties:
137             return self._properties['default nameid']
138         else:
139             return self.cfg.default_nameid
140
141     @default_nameid.setter
142     def default_nameid(self, value):
143         self._staging['default nameid'] = value
144
145     @property
146     def attribute_mappings(self):
147         if 'attribute mappings' in self._properties:
148             attr_map = pconfig.MappingList('temp', 'temp', None)
149             attr_map.import_value(str(self._properties['attribute mappings']))
150             return attr_map.get_value()
151         else:
152             return None
153
154     @attribute_mappings.setter
155     def attribute_mappings(self, attr_map):
156         if isinstance(attr_map, pconfig.MappingList):
157             value = attr_map.export_value()
158         else:
159             temp = pconfig.MappingList('temp', 'temp', None)
160             temp.set_value(attr_map)
161             value = temp.export_value()
162         self._staging['attribute mappings'] = value
163
164     @property
165     def allowed_attributes(self):
166         if 'allowed_attributes' in self._properties:
167             attr_map = pconfig.ComplexList('temp', 'temp', None)
168             attr_map.import_value(str(self._properties['allowed_attributes']))
169             return attr_map.get_value()
170         else:
171             return None
172
173     @allowed_attributes.setter
174     def allowed_attributes(self, attr_map):
175         if isinstance(attr_map, pconfig.ComplexList):
176             value = attr_map.export_value()
177         else:
178             temp = pconfig.ComplexList('temp', 'temp', None)
179             temp.set_value(attr_map)
180             value = temp.export_value()
181         self._staging['allowed_attributes'] = value
182
183     def save_properties(self):
184         data = self.cfg.get_data(name='id', value=self.provider_id)
185         if len(data) != 1:
186             raise InvalidProviderId('Could not find SP data')
187         idval = data.keys()[0]
188         data = dict()
189         data[idval] = self._staging
190         self.cfg.save_data(data)
191         data = self.cfg.get_data(idval=idval)
192         self._properties = data[idval]
193         self._staging = dict()
194
195     def refresh_config(self):
196         """
197         Create a new config object for displaying in the UI based on
198         the current set of properties.
199         """
200         del self._config
201         self.load_config()
202
203     def get_valid_nameid(self, nip):
204         self.debug('Requested NameId [%s]' % (nip.format,))
205         if nip.format is None:
206             return SAML2_NAMEID_MAP[self.default_nameid]
207         else:
208             allowed = self.allowed_nameids
209             self.debug('Allowed NameIds %s' % (repr(allowed)))
210             for nameid in allowed:
211                 if nip.format == SAML2_NAMEID_MAP[nameid]:
212                     return nip.format
213         raise NameIdNotAllowed(nip.format)
214
215     def permanently_delete(self):
216         data = self.cfg.get_data(name='id', value=self.provider_id)
217         if len(data) != 1:
218             raise InvalidProviderId('Could not find SP data')
219         idval = data.keys()[0]
220         self.cfg.del_datum(idval)
221
222     def normalize_username(self, username):
223         if 'strip domain' in self._properties:
224             return username.split('@', 1)[0]
225         return username
226
227     def is_valid_name(self, value):
228         if re.search(VALID_IN_NAME, value):
229             return False
230         return True
231
232     def is_valid_nameid(self, value):
233         if value in SAML2_NAMEID_MAP:
234             return True
235         return False
236
237     def valid_nameids(self):
238         return SAML2_NAMEID_MAP.keys()
239
240
241 class ServiceProviderCreator(object):
242
243     def __init__(self, config):
244         self.cfg = config
245
246     def create_from_buffer(self, name, metabuf):
247         '''Test and add data'''
248
249         if re.search(VALID_IN_NAME, name):
250             raise InvalidProviderId("Name must contain only "
251                                     "numbers and letters")
252
253         test = lasso.Server()
254         test.addProviderFromBuffer(lasso.PROVIDER_ROLE_SP, metabuf)
255         newsps = test.get_providers()
256         if len(newsps) != 1:
257             raise InvalidProviderId("Metadata must contain one Provider")
258
259         spid = newsps.keys()[0]
260         data = self.cfg.get_data(name='id', value=spid)
261         if len(data) != 0:
262             raise InvalidProviderId("Provider Already Exists")
263         datum = {'id': spid, 'name': name, 'type': 'SP', 'metadata': metabuf}
264         self.cfg.new_datum(datum)
265
266         data = self.cfg.get_data(name='id', value=spid)
267         if len(data) != 1:
268             raise InvalidProviderId("Internal Error")
269         idval = data.keys()[0]
270         data = self.cfg.get_data(idval=idval)
271         sp = data[idval]
272         self.cfg.idp.add_provider(sp)
273
274         return ServiceProvider(self.cfg, spid)
275
276
277 class IdentityProvider(Log):
278     def __init__(self, config, sessionfactory):
279         self.server = lasso.Server(config.idp_metadata_file,
280                                    config.idp_key_file,
281                                    None,
282                                    config.idp_certificate_file)
283         self.server.role = lasso.PROVIDER_ROLE_IDP
284         self.sessionfactory = sessionfactory
285
286     def add_provider(self, sp):
287         self.server.addProviderFromBuffer(lasso.PROVIDER_ROLE_SP,
288                                           sp['metadata'])
289         self.debug('Added SP %s' % sp['name'])
290
291     def get_login_handler(self, dump=None):
292         if dump:
293             return lasso.Login.newFromDump(self.server, dump)
294         else:
295             return lasso.Login(self.server)
296
297     def get_providers(self):
298         return self.server.get_providers()
299
300     def get_logout_handler(self, dump=None):
301         if dump:
302             return lasso.Logout.newFromDump(self.server, dump)
303         else:
304             return lasso.Logout(self.server)