|
1 # |
|
2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net> |
|
3 # All Rights Reserved. |
|
4 # |
|
5 # This software is subject to the provisions of the Zope Public License, |
|
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
|
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
|
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
|
10 # FOR A PARTICULAR PURPOSE. |
|
11 # |
|
12 |
|
13 __docformat__ = 'restructuredtext' |
|
14 |
|
15 |
|
16 # import standard library |
|
17 import re |
|
18 |
|
19 # import interfaces |
|
20 from pyams_skin.interfaces import IContentSearch |
|
21 from zope.annotation.interfaces import IAttributeAnnotatable |
|
22 from zope.container.interfaces import IContainer |
|
23 from zope.location.interfaces import IContained |
|
24 |
|
25 # import packages |
|
26 from pyams_utils.schema import EncodedPassword |
|
27 from zope.configuration.fields import GlobalObject |
|
28 from zope.container.constraints import contains, containers |
|
29 from zope.interface import implementer, Interface, Attribute, invariant, Invalid |
|
30 from zope.schema import TextLine, Text, Int, Bool, List, Tuple, Set, Dict, Choice, Datetime |
|
31 |
|
32 from pyams_security import _ |
|
33 |
|
34 |
|
35 class IPermission(Interface): |
|
36 """Permission utility class""" |
|
37 |
|
38 id = TextLine(title="Unique ID", |
|
39 required=True) |
|
40 |
|
41 title = TextLine(title="Title", |
|
42 required=True) |
|
43 |
|
44 description = Text(title="Description", |
|
45 required=False) |
|
46 |
|
47 |
|
48 class IRole(Interface): |
|
49 """Role utility class""" |
|
50 |
|
51 id = TextLine(title="Unique ID", |
|
52 required=True) |
|
53 |
|
54 title = TextLine(title="Title", |
|
55 required=True) |
|
56 |
|
57 description = Text(title="Description", |
|
58 required=False) |
|
59 |
|
60 permissions = Set(title="Permissions", |
|
61 description="ID of role's permissions", |
|
62 value_type=TextLine(), |
|
63 required=False) |
|
64 |
|
65 |
|
66 class IPrincipalInfo(Interface): |
|
67 """Principal info class |
|
68 |
|
69 This is the generic interface of objects defined in request 'principal' attribute |
|
70 """ |
|
71 |
|
72 id = TextLine(title="Globally unique ID", |
|
73 required=True) |
|
74 |
|
75 title = TextLine(title="Principal name", |
|
76 required=True) |
|
77 |
|
78 groups = Set(title="Principal groups", |
|
79 description="IDs of principals to which this principal directly belongs", |
|
80 value_type=TextLine()) |
|
81 |
|
82 |
|
83 class ICredentials(Interface): |
|
84 """Credentials interface""" |
|
85 |
|
86 prefix = TextLine(title="Credentials plug-in prefix", |
|
87 description="Prefix of plug-in which extracted credentials") |
|
88 |
|
89 id = TextLine(title="Credentials ID") |
|
90 |
|
91 attributes = Dict(title="Credentials attributes", |
|
92 description="Attributes dictionary defined by each credentials plug-in", |
|
93 required=False, |
|
94 default={}) |
|
95 |
|
96 |
|
97 # |
|
98 # Credentials, authentication and directory plug-ins |
|
99 # |
|
100 |
|
101 class IPluginEvent(Interface): |
|
102 """Plug-in event interface""" |
|
103 |
|
104 plugin = Attribute("Event plug-in name") |
|
105 |
|
106 |
|
107 class IAuthenticatedPrincipalEvent(IPluginEvent): |
|
108 """Authenticated principal event interface""" |
|
109 |
|
110 principal_id = Attribute("Authenticated principal ID") |
|
111 |
|
112 infos = Attribute("Event custom infos") |
|
113 |
|
114 |
|
115 @implementer(IAuthenticatedPrincipalEvent) |
|
116 class AuthenticatedPrincipalEvent(object): |
|
117 """Authenticated principal event""" |
|
118 |
|
119 def __init__(self, plugin, principal_id, **infos): |
|
120 self.plugin = plugin |
|
121 self.principal_id = principal_id |
|
122 self.infos = infos |
|
123 |
|
124 |
|
125 class IPlugin(IContained, IAttributeAnnotatable): |
|
126 """Basic authentication plug-in interface""" |
|
127 |
|
128 containers('pyams_security.interfaces.IAuthentication') |
|
129 |
|
130 prefix = TextLine(title=_("Plug-in prefix"), |
|
131 description=_("This prefix is mainly used by authentication plug-ins to mark principals")) |
|
132 |
|
133 title = TextLine(title=_("Plug-in title"), |
|
134 required=False) |
|
135 |
|
136 enabled = Bool(title=_("Enabled plug-in?"), |
|
137 description=_("You can choose to disable any plug-in..."), |
|
138 required=True, |
|
139 default=True) |
|
140 |
|
141 |
|
142 class ICredentialsInfo(Interface): |
|
143 """Credentials extraction plug-in base interface""" |
|
144 |
|
145 def extract_credentials(self, request): |
|
146 """Extract user credentials from given request |
|
147 |
|
148 Result of 'extract_credentials' call should be an ICredentials object for which |
|
149 id is the 'raw' principal ID (without prefix); only authentication plug-ins should |
|
150 add a prefix to principal IDs to distinguish principals |
|
151 """ |
|
152 |
|
153 |
|
154 class ICredentialsPlugin(ICredentialsInfo, IPlugin): |
|
155 """Credentials extraction plug-in interface""" |
|
156 |
|
157 |
|
158 class IAuthenticationInfo(Interface): |
|
159 """Principal authentication plug-in base interface""" |
|
160 |
|
161 def authenticate(self, credentials, request): |
|
162 """Authenticate given credentials and returns a principal ID or None""" |
|
163 |
|
164 |
|
165 class IAuthenticationPlugin(IAuthenticationInfo, IPlugin): |
|
166 """Principal authentication plug-in interface""" |
|
167 |
|
168 |
|
169 class IAdminAuthenticationPlugin(IAuthenticationPlugin): |
|
170 """Admin authentication plug-in base interface""" |
|
171 |
|
172 login = TextLine(title=_("Admin. login")) |
|
173 |
|
174 password = EncodedPassword(title=_("Admin. password")) |
|
175 |
|
176 |
|
177 class IDirectoryInfo(Interface): |
|
178 """Principal directory plug-in interface""" |
|
179 |
|
180 def get_principal(self, principal_id): |
|
181 """Returns real principal matching given ID, or None""" |
|
182 |
|
183 def get_all_principals(self, principal_id): |
|
184 """Returns all principals matching given principal ID""" |
|
185 |
|
186 def find_principals(self, query): |
|
187 """Find principals matching given query""" |
|
188 |
|
189 |
|
190 class IDirectoryPlugin(IDirectoryInfo, IPlugin): |
|
191 """Principal directory plug-in info""" |
|
192 |
|
193 |
|
194 class IDirectorySearchPlugin(IDirectoryPlugin, IContentSearch): |
|
195 """Principal directory plug-in supporting search""" |
|
196 |
|
197 |
|
198 class IUsersFolderPlugin(IAuthenticationPlugin, IDirectorySearchPlugin): |
|
199 """Local users folder interface""" |
|
200 |
|
201 def check_login(self, login): |
|
202 """Check for existence of given login""" |
|
203 |
|
204 |
|
205 # |
|
206 # User registration |
|
207 # |
|
208 |
|
209 def check_password(password): |
|
210 """Check validity of a given password""" |
|
211 nbmaj = 0 |
|
212 nbmin = 0 |
|
213 nbn = 0 |
|
214 nbo = 0 |
|
215 for car in password: |
|
216 if ord(car) in range(ord('A'), ord('Z') + 1): |
|
217 nbmaj += 1 |
|
218 elif ord(car) in range(ord('a'), ord('z') + 1): |
|
219 nbmin += 1 |
|
220 elif ord(car) in range(ord('0'), ord('9') + 1): |
|
221 nbn += 1 |
|
222 else: |
|
223 nbo += 1 |
|
224 if [nbmin, nbmaj, nbn, nbo].count(0) > 1: |
|
225 raise Invalid(_("Your password must contain at least three of these kinds of characters: " |
|
226 "lowercase letters, uppercase letters, numbers and special characters")) |
|
227 |
|
228 |
|
229 EMAIL_REGEX = re.compile("[^@]+@[^@]+\.[^@]+") |
|
230 |
|
231 |
|
232 class IUserRegistrationInfo(Interface): |
|
233 """User registration info""" |
|
234 |
|
235 login = TextLine(title=_("User login"), |
|
236 description=_("If you don't provide a custom login, your login will be your email address..."), |
|
237 required=False) |
|
238 |
|
239 @invariant |
|
240 def check_login(self): |
|
241 if not self.login: |
|
242 self.login = self.email |
|
243 |
|
244 email = TextLine(title=_("E-mail address"), |
|
245 description=_("An email will be sent to this address to validate account activation; " |
|
246 "it will be used as your future user login"), |
|
247 required=True) |
|
248 |
|
249 @invariant |
|
250 def check_email(self): |
|
251 if not EMAIL_REGEX.match(self.email): |
|
252 raise Invalid(_("Your email address is not valid!")) |
|
253 |
|
254 firstname = TextLine(title=_("First name"), |
|
255 required=True) |
|
256 |
|
257 lastname = TextLine(title=_("Last name"), |
|
258 required=True) |
|
259 |
|
260 company_name = TextLine(title=_("Company name"), |
|
261 required=False) |
|
262 |
|
263 password = EncodedPassword(title=_("Password"), |
|
264 description=_("Password must be at least 8 characters long, and contain at least " |
|
265 "three kins of characters between lowercase letters, uppercase " |
|
266 "letters, numbers and special characters"), |
|
267 min_length=8, |
|
268 required=True) |
|
269 |
|
270 confirmed_password = EncodedPassword(title=_("Confirmed password"), |
|
271 required=True) |
|
272 |
|
273 @invariant |
|
274 def check_password(self): |
|
275 if self.password != self.confirmed_password: |
|
276 raise Invalid(_("You didn't confirmed your password correctly!")) |
|
277 check_password(self.password) |
|
278 |
|
279 |
|
280 class IUserRegistrationConfirmationInfo(Interface): |
|
281 """User registration confirmation info""" |
|
282 |
|
283 activation_hash = TextLine(title=_("Activation hash"), |
|
284 required=True) |
|
285 |
|
286 login = TextLine(title=_("User login"), |
|
287 required=True) |
|
288 |
|
289 password = EncodedPassword(title=_("Password"), |
|
290 min_length=8, |
|
291 required=True) |
|
292 |
|
293 confirmed_password = EncodedPassword(title=_("Confirmed password"), |
|
294 required=True) |
|
295 |
|
296 @invariant |
|
297 def check_password(self): |
|
298 if self.password != self.confirmed_password: |
|
299 raise Invalid(_("You didn't confirmed your password correctly!")) |
|
300 check_password(self.password) |
|
301 |
|
302 |
|
303 class ILocalUser(IAttributeAnnotatable): |
|
304 """Local user interface""" |
|
305 |
|
306 login = TextLine(title=_("User login"), |
|
307 required=True, |
|
308 readonly=True) |
|
309 |
|
310 @invariant |
|
311 def check_login(self): |
|
312 if not self.login: |
|
313 self.login = self.email |
|
314 |
|
315 email = TextLine(title=_("User email address"), |
|
316 required=True) |
|
317 |
|
318 @invariant |
|
319 def check_email(self): |
|
320 if not EMAIL_REGEX.match(self.email): |
|
321 raise Invalid(_("Given email address is not valid!")) |
|
322 |
|
323 firstname = TextLine(title=_("First name"), |
|
324 required=True) |
|
325 |
|
326 lastname = TextLine(title=_("Last name"), |
|
327 required=True) |
|
328 |
|
329 title = Attribute("User full name") |
|
330 |
|
331 company_name = TextLine(title=_("Company name"), |
|
332 required=False) |
|
333 |
|
334 password_manager = Choice(title=_("Password manager name"), |
|
335 required=True, |
|
336 vocabulary='PyAMS password managers', |
|
337 default='SSHA') |
|
338 |
|
339 password = EncodedPassword(title=_("Password"), |
|
340 min_length=8, |
|
341 required=False) |
|
342 |
|
343 wait_confirmation = Bool(title=_("Wait confirmation?"), |
|
344 description=_("If 'no', user will be activated immediately without waiting email " |
|
345 "confirmation"), |
|
346 required=True, |
|
347 default=True) |
|
348 |
|
349 self_registered = Bool(title=_("Self-registered profile?"), |
|
350 required=True, |
|
351 default=True, |
|
352 readonly=True) |
|
353 |
|
354 activation_secret = TextLine(title=_("Activation secret key"), |
|
355 description=_("This private secret is used to create and check activation hash"), |
|
356 readonly=True) |
|
357 |
|
358 activation_hash = TextLine(title=_("Activation hash"), |
|
359 description=_("This hash is provided into activation message URL. Activation hash " |
|
360 "is missing for local users which were registered without waiting " |
|
361 "their confirmation."), |
|
362 readonly=True) |
|
363 |
|
364 activation_date = Datetime(title=_("Activation date"), |
|
365 required=False) |
|
366 |
|
367 activated = Bool(title=_("Activation date"), |
|
368 required=True, |
|
369 default=False) |
|
370 |
|
371 def check_password(self, password): |
|
372 """Check user password against provided one""" |
|
373 |
|
374 def generate_secret(self, login, password): |
|
375 """Generate secret key of this profile""" |
|
376 |
|
377 def check_activation(self, hash, login, password): |
|
378 """Check activation for given settings""" |
|
379 |
|
380 |
|
381 # |
|
382 # Security manager |
|
383 # |
|
384 |
|
385 class ISecurityManager(IContainer, IDirectoryInfo): |
|
386 """Authentication and principals management utility""" |
|
387 |
|
388 contains(IPlugin) |
|
389 |
|
390 enable_social_login = Bool(title=_("Enable social login?"), |
|
391 description=_("Enable login via social OAuth plug-ins"), |
|
392 required=True, |
|
393 default=False) |
|
394 |
|
395 authomatic_secret = TextLine(title=_("Authomatic secret"), |
|
396 description=_("This secret phrase is used to encrypt Authomatic cookie"), |
|
397 default='this is not a secret', |
|
398 required=True) |
|
399 |
|
400 social_login_use_popup = Bool(title=_("Use social popup?"), |
|
401 required=True, |
|
402 default=False) |
|
403 |
|
404 open_registration = Bool(title=_("Open registration?"), |
|
405 description=_("If 'Yes', any use will be able to create a new user account"), |
|
406 required=True, |
|
407 default=False) |
|
408 |
|
409 users_folder = Choice(title=_("Users folder"), |
|
410 description=_("Name of users folder used to store registered principals"), |
|
411 required=False, |
|
412 vocabulary='PyAMS users folders') |
|
413 |
|
414 @invariant |
|
415 def check_users_folder(self): |
|
416 if self.open_registration and not self.users_folder: |
|
417 raise Invalid(_("You can't activate open registration without selecting a users folder")) |
|
418 |
|
419 credentials_plugins_names = Tuple(title=_("Credentials plug-ins"), |
|
420 description=_("These plug-ins can be used to extract request credentials"), |
|
421 value_type=TextLine(), |
|
422 readonly=True, |
|
423 default=()) |
|
424 |
|
425 authentication_plugins_names = Tuple(title=_("Authentication plug-ins"), |
|
426 description=_("The plug-ins can be used to check extracted credentials " |
|
427 "against a local or remote users database"), |
|
428 value_type=TextLine(), |
|
429 default=()) |
|
430 |
|
431 directory_plugins_names = Tuple(title=_("Directory plug-ins"), |
|
432 description=_("The plug-in can be used to extract principals information"), |
|
433 value_type=TextLine(), |
|
434 default=()) |
|
435 |
|
436 def get_plugin(self, name): |
|
437 """Get plug-in matching given name""" |
|
438 |
|
439 def get_credentials_plugins(self): |
|
440 """Extract list of credentials plug-ins""" |
|
441 |
|
442 def order_credentials_plugins(self, names): |
|
443 """Define credentials plug-ins order""" |
|
444 |
|
445 def get_authentication_plugins(self): |
|
446 """Extract list of authentication plug-ins""" |
|
447 |
|
448 def order_authentication_plugins(self, names): |
|
449 """Define authentication plug-ins order""" |
|
450 |
|
451 def get_directory_plugins(self): |
|
452 """Extract list of directory plug-ins""" |
|
453 |
|
454 def order_directory_plugins(self, names): |
|
455 """Define directory plug-ins order""" |
|
456 |
|
457 |
|
458 LOGIN_REFERER_KEY = 'pyams_security.login.referer' |
|
459 |
|
460 |
|
461 class ILoginView(Interface): |
|
462 """Login view marker interface""" |
|
463 |
|
464 |
|
465 # |
|
466 # Social login configuration |
|
467 # |
|
468 |
|
469 class ISocialLoginConfiguration(Interface): |
|
470 """Social login configuration interface""" |
|
471 |
|
472 configuration = Dict(title=_("Social login configuration"), |
|
473 key_type=TextLine(title=_("Provider name")), |
|
474 value_type=Dict(title=_("Provider configuration"))) |
|
475 |
|
476 |
|
477 class ISocialLoginProviderInfo(Interface): |
|
478 """Social login provider info""" |
|
479 |
|
480 provider_name = TextLine(title=_("Provider name"), |
|
481 required=True) |
|
482 |
|
483 provider_id = Int(title=_("Provider ID"), |
|
484 description=_("This value should be unique between all providers"), |
|
485 required=True, |
|
486 min=0) |
|
487 |
|
488 klass = GlobalObject(title=_("Provider class"), |
|
489 required=True) |
|
490 |
|
491 consumer_key = TextLine(title=_("Provider consumer key"), |
|
492 required=True) |
|
493 |
|
494 consumer_secret = TextLine(title=_("Provider secret"), |
|
495 required=True) |
|
496 |
|
497 scope = List(title=_("Provider scope"), |
|
498 required=True, |
|
499 value_type=TextLine(), |
|
500 default=['email']) |