ims.sso 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. ims/sso/__init__.py +16 -0
  2. ims/sso/browser/__init__.py +1 -0
  3. ims/sso/browser/configure.zcml +142 -0
  4. ims/sso/browser/cron.py +23 -0
  5. ims/sso/browser/login.py +257 -0
  6. ims/sso/browser/password.py +9 -0
  7. ims/sso/browser/personalpreferences.py +1 -0
  8. ims/sso/browser/register.py +161 -0
  9. ims/sso/browser/templates/add.pt +46 -0
  10. ims/sso/browser/templates/change_password.pt +47 -0
  11. ims/sso/browser/templates/linkaccount_anonymous.pt +24 -0
  12. ims/sso/browser/templates/linkaccount_disallowed.pt +22 -0
  13. ims/sso/browser/templates/linkaccount_finish.pt +21 -0
  14. ims/sso/browser/templates/linkaccount_invalid.pt +25 -0
  15. ims/sso/browser/templates/login.pt +23 -0
  16. ims/sso/browser/templates/logout_warning_viewlet.pt +26 -0
  17. ims/sso/browser/templates/newuser_form.pt +69 -0
  18. ims/sso/browser/templates/reactivation.pt +26 -0
  19. ims/sso/browser/templates/relog.pt +30 -0
  20. ims/sso/browser/templates/unauthorized_shib_authenticated.pt +58 -0
  21. ims/sso/browser/templates/unauthorized_shib_unauthenticated.pt +57 -0
  22. ims/sso/browser/templates/usergroups_usersoverview.pt +568 -0
  23. ims/sso/browser/templates/warnings.pt +44 -0
  24. ims/sso/browser/unauthorized.py +7 -0
  25. ims/sso/browser/usergroups_usersoverview.py +322 -0
  26. ims/sso/browser/viewlets.py +55 -0
  27. ims/sso/configs.py +18 -0
  28. ims/sso/configure.zcml +30 -0
  29. ims/sso/controlpanels/__init__.py +0 -0
  30. ims/sso/controlpanels/configure.zcml +13 -0
  31. ims/sso/controlpanels/settings.pt +24 -0
  32. ims/sso/controlpanels/sso.py +22 -0
  33. ims/sso/errors.py +6 -0
  34. ims/sso/events.py +31 -0
  35. ims/sso/events.zcml +12 -0
  36. ims/sso/indexes.py +9 -0
  37. ims/sso/interfaces.py +171 -0
  38. ims/sso/mailing/__init__.py +1 -0
  39. ims/sso/mailing/configure.zcml +48 -0
  40. ims/sso/mailing/mailers.py +13 -0
  41. ims/sso/mailing/mass_relink.pt +36 -0
  42. ims/sso/mailing/notify.py +101 -0
  43. ims/sso/mailing/relink.py +103 -0
  44. ims/sso/permissions.zcml +40 -0
  45. ims/sso/plugin.py +195 -0
  46. ims/sso/profiles/default/actions.xml +59 -0
  47. ims/sso/profiles/default/browserlayer.xml +6 -0
  48. ims/sso/profiles/default/controlpanel.xml +43 -0
  49. ims/sso/profiles/default/metadata.xml +4 -0
  50. ims/sso/profiles/default/registry/bundle.xml +12 -0
  51. ims/sso/profiles/default/registry/settings.xml +4 -0
  52. ims/sso/profiles/default/rolemap.xml +43 -0
  53. ims/sso/profiles/uninstall/actions.xml +7 -0
  54. ims/sso/profiles/uninstall/browserlayer.xml +6 -0
  55. ims/sso/profiles/uninstall/controlpanel.xml +10 -0
  56. ims/sso/profiles/uninstall/registry.xml +6 -0
  57. ims/sso/profiles.zcml +23 -0
  58. ims/sso/setuphandlers.py +139 -0
  59. ims/sso/static/bundle/chunks/881.f292144b2330c5cc4d06.min.js +2 -0
  60. ims/sso/static/bundle/chunks/881.f292144b2330c5cc4d06.min.js.LICENSE.txt +1 -0
  61. ims/sso/static/bundle/chunks/975.1473fe663da179434339.min.js +2 -0
  62. ims/sso/static/bundle/chunks/975.1473fe663da179434339.min.js.LICENSE.txt +1 -0
  63. ims/sso/static/bundle/ims.sso.min.js +2 -0
  64. ims/sso/static/bundle/ims.sso.min.js.LICENSE.txt +1 -0
  65. ims/sso/static/main.css +0 -0
  66. ims/sso/static/main.less +0 -0
  67. ims/sso/static/multiplugin.gif +0 -0
  68. ims/sso/testing.py +55 -0
  69. ims/sso/upgrades.py +3 -0
  70. ims/sso/upgrades.zcml +8 -0
  71. ims/sso/utilities.zcml +21 -0
  72. ims/sso/utility.py +468 -0
  73. ims/sso/vocabulary.py +22 -0
  74. ims/sso/vocabulary.zcml +15 -0
  75. ims_sso-1.0.0.dist-info/METADATA +17 -0
  76. ims_sso-1.0.0.dist-info/RECORD +78 -0
  77. ims_sso-1.0.0.dist-info/WHEEL +4 -0
  78. ims_sso-1.0.0.dist-info/entry_points.txt +2 -0
ims/sso/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ from Products.PluggableAuthService import registerMultiPlugin
2
+
3
+ from . import plugin
4
+
5
+ __version__ = "1.0.0"
6
+
7
+
8
+ def initialize(context):
9
+ registerMultiPlugin(plugin.ImsSsoPlugin.meta_type)
10
+ context.registerClass(
11
+ plugin.ImsSsoPlugin,
12
+ permission="Manage portal",
13
+ constructors=(plugin.manage_addImsSsoForm, plugin.manage_addImsSso),
14
+ visibility=None,
15
+ icon="static/multiplugin.gif",
16
+ )
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1,142 @@
1
+ <configure
2
+ xmlns="http://namespaces.zope.org/zope"
3
+ xmlns:browser="http://namespaces.zope.org/browser"
4
+ i18n_domain="ims.sso"
5
+ >
6
+
7
+ <!-- Plone override -->
8
+ <browser:page
9
+ name="usergroup-userprefs"
10
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
11
+ class=".usergroups_usersoverview.UsersOverviewControlPanel"
12
+ template="templates/usergroups_usersoverview.pt"
13
+ permission="plone.app.controlpanel.UsersAndGroups"
14
+ layer="..interfaces.IBrowserLayer"
15
+ />
16
+
17
+ <browser:page
18
+ name="sso-tasks"
19
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
20
+ class=".cron.PeriodicTasks"
21
+ permission="cmf.ManagePortal"
22
+ />
23
+
24
+ <!-- Plone override -->
25
+ <browser:page
26
+ name="new-user"
27
+ for="plone.app.layout.navigation.interfaces.INavigationRoot"
28
+ class=".register.AddUserForm"
29
+ permission="plone.app.controlpanel.UsersAndGroups"
30
+ layer="..interfaces.IBrowserLayer"
31
+ />
32
+
33
+ <!-- login/logout -->
34
+ <browser:page
35
+ name="sso-logout"
36
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
37
+ class=".login.SsoLogout"
38
+ permission="zope.Public"
39
+ />
40
+ <browser:page
41
+ name="linkaccount"
42
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
43
+ class=".login.SsoLinkaccount"
44
+ permission="zope.Public"
45
+ />
46
+ <browser:page
47
+ name="request_reactivation"
48
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
49
+ class=".login.RequestReactivation"
50
+ attribute="render"
51
+ permission="zope.Public"
52
+ />
53
+ <browser:page
54
+ name="reactivation"
55
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
56
+ template="templates/reactivation.pt"
57
+ permission="zope.Public"
58
+ />
59
+ <browser:page
60
+ name="reactivate_user"
61
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
62
+ class=".login.ReactivateUserAccount"
63
+ permission="zope.Public"
64
+ />
65
+ <browser:page
66
+ name="get_login_url"
67
+ for="zope.interface.Interface"
68
+ class=".login.LoginUrl"
69
+ permission="zope.Public"
70
+ />
71
+ <browser:page
72
+ name="unauthorized_shib_unauthenticated"
73
+ for="zope.interface.Interface"
74
+ class=".unauthorized.Unauthorized"
75
+ template="templates/unauthorized_shib_unauthenticated.pt"
76
+ permission="zope.Public"
77
+ />
78
+ <browser:page
79
+ name="unauthorized_shib_authenticated"
80
+ for="zope.interface.Interface"
81
+ class=".unauthorized.Unauthorized"
82
+ template="templates/unauthorized_shib_authenticated.pt"
83
+ permission="zope.Public"
84
+ />
85
+ <browser:page
86
+ name="login_condition"
87
+ for="zope.interface.Interface"
88
+ class=".login.LoginCondition"
89
+ permission="zope.Public"
90
+ />
91
+ <browser:page
92
+ name="logout_url"
93
+ for="zope.interface.Interface"
94
+ class=".login.LogoutUrl"
95
+ permission="zope.Public"
96
+ />
97
+ <!-- change password -->
98
+ <browser:page
99
+ name="change-password"
100
+ for="plone.app.layout.navigation.interfaces.INavigationRoot"
101
+ class=".password.ChangePassword"
102
+ permission="cmf.SetOwnPassword"
103
+ layer="..interfaces.IBrowserLayer"
104
+ />
105
+
106
+
107
+ <browser:page
108
+ name="require_login"
109
+ for="*"
110
+ class=".login.RequireLoginView"
111
+ permission="zope.Public"
112
+ layer="..interfaces.IBrowserLayer"
113
+ />
114
+
115
+
116
+ <browser:viewlet
117
+ name="sso_warnings"
118
+ manager="plone.app.layout.viewlets.interfaces.IAboveContent"
119
+ class=".viewlets.SsoWarningsViewlet"
120
+ permission="zope.Public"
121
+ layer="..interfaces.IBrowserLayer"
122
+ />
123
+
124
+
125
+ <browser:page
126
+ name="relog-success"
127
+ for="Products.CMFPlone.interfaces.IPloneSiteRoot"
128
+ template="templates/relog.pt"
129
+ permission="zope2.Public"
130
+ layer="..interfaces.IBrowserLayer"
131
+ />
132
+
133
+ <!-- this just prevents the default login form -->
134
+ <browser:page
135
+ name="login"
136
+ for="plone.base.interfaces.INavigationRoot"
137
+ class=".login.RedirectLogin"
138
+ permission="zope.Public"
139
+ layer="..interfaces.IBrowserLayer"
140
+ />
141
+
142
+ </configure>
@@ -0,0 +1,23 @@
1
+ from plone.protect.interfaces import IDisableCSRFProtection
2
+ from Products.Five import BrowserView
3
+ from zope.component import getUtility
4
+ from zope.interface.declarations import alsoProvides
5
+
6
+ from ..interfaces import IReactivationUtility, ISingleSignonUtility
7
+
8
+
9
+ class PeriodicTasks(BrowserView):
10
+ """This should be called by a cron job. It does three things
11
+ 1. Users that have not been linked since X days will be deleted
12
+ 2. Users who have not logged in in Y days will have their accounts disabled
13
+ 3. Remove expired annotations
14
+ """
15
+
16
+ def __call__(self, *args, **kwargs):
17
+ alsoProvides(self.request, IDisableCSRFProtection)
18
+ sso = getUtility(ISingleSignonUtility)
19
+ react = getUtility(IReactivationUtility)
20
+ sso.purge_unlinked()
21
+ sso.disable_user_accounts()
22
+ react.purge_activation_keys()
23
+ return "User accounts updated."
@@ -0,0 +1,257 @@
1
+ from plone import api
2
+ from plone.protect import CheckAuthenticator, PostOnly
3
+ from plone.protect.interfaces import IDisableCSRFProtection
4
+ from Products.CMFPlone.browser.login.login import LoginForm
5
+ from Products.CMFPlone.PasswordResetTool import InvalidRequestError
6
+ from Products.Five import BrowserView
7
+ from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
8
+ from zope.annotation.interfaces import IAnnotations
9
+ from zope.component import getMultiAdapter, getUtility
10
+ from zope.event import notify
11
+ from zope.interface.declarations import alsoProvides, implementer
12
+ from zope.publisher.interfaces import IPublishTraverse
13
+
14
+ from ..configs import AUTHENTICATED_KEY, NOT_LINKED, _
15
+ from ..events import UserIdpUpdated
16
+ from ..interfaces import IReactivationUtility, ISingleSignonUtility
17
+
18
+
19
+ class RedirectLogin(LoginForm):
20
+ """This is the Plone default login page. SSO sites do not use it."""
21
+
22
+ def __call__(self):
23
+ self.request.response.redirect(api.portal.get().absolute_url())
24
+ return
25
+
26
+
27
+ class SsoLogout(BrowserView):
28
+ @property
29
+ def sso(self):
30
+ return getUtility(ISingleSignonUtility)
31
+
32
+ def __call__(self):
33
+ self.context.restrictedTraverse("browser_id_manager").flushBrowserIdCookie()
34
+ for cookie in self.request.cookies:
35
+ self.request.response.setCookie(
36
+ cookie,
37
+ self.request.cookies[cookie],
38
+ path="/",
39
+ expires="Thu, 01 Jan 1970 00:00:00 GMT",
40
+ )
41
+
42
+ self.request.response.redirect(self.sso.get_url_logout(request=self.request))
43
+
44
+
45
+ @implementer(IPublishTraverse)
46
+ class SsoLinkaccount(BrowserView):
47
+ def __init__(self, context, request):
48
+ self.subpath = []
49
+ super().__init__(context, request)
50
+
51
+ def publishTraverse(self, request, name):
52
+ self.subpath.append(name)
53
+ return self
54
+
55
+ def __call__(self):
56
+ if not api.user.is_anonymous():
57
+ # most likely a re-click or a tester
58
+ return self.invalid_key()
59
+ alsoProvides(self.request, IDisableCSRFProtection)
60
+ sso = getUtility(ISingleSignonUtility)
61
+ try:
62
+ key = self.subpath[0]
63
+ user_id = self.subpath[1]
64
+ except IndexError:
65
+ return self.invalid_key()
66
+
67
+ usr = api.user.get(userid=user_id)
68
+ is_relink = usr and NOT_LINKED not in usr.getUserName()
69
+ login_name = sso.get_login_from_request(self.request)
70
+
71
+ email = self.request.get(sso.get_setting("shib_header_email"))
72
+
73
+ if login_name and email:
74
+ pw_tool = api.portal.get_tool("portal_password_reset")
75
+ try:
76
+ pw_tool.verifyKey(key)
77
+ password = sso.generate_password(user_id) # random, unknown password
78
+ pw_tool.resetPassword(user_id, key, password)
79
+ except InvalidRequestError:
80
+ return self.invalid_key()
81
+
82
+ try:
83
+ sso.set_login_name(user_id, login_name)
84
+ except ValueError: # likely duplicate
85
+ return self.disallowed()
86
+ else:
87
+ sso.send_portal_link(email)
88
+ usr = api.user.get(userid=user_id) # we must reinstantiate after change
89
+ if is_relink:
90
+ plone_view = getMultiAdapter((self.context, self.request), name="plone")
91
+ sso.notify_relinked(usr, email, plone_view)
92
+ notify(UserIdpUpdated(usr))
93
+ return self.success()
94
+ else:
95
+ return ViewPageTemplateFile("templates/linkaccount_anonymous.pt")(self)
96
+
97
+ def disallowed(self):
98
+ return ViewPageTemplateFile("templates/linkaccount_disallowed.pt")(self)
99
+
100
+ def success(self):
101
+ return ViewPageTemplateFile("templates/linkaccount_finish.pt")(self)
102
+
103
+ def invalid_key(self):
104
+ username = api.user.get_current().getUserName()
105
+ if username != "Anonymous User":
106
+ api.portal.show_message(
107
+ _(
108
+ "Invalid or expired key. No accounts were linked, but you are already logged in. Please update "
109
+ "your bookmark to this page."
110
+ ),
111
+ self.request,
112
+ type="warning",
113
+ )
114
+ return self.request.response.redirect(api.portal.get().absolute_url())
115
+ else:
116
+ return ViewPageTemplateFile("templates/linkaccount_invalid.pt")(self)
117
+
118
+
119
+ class LoginUrl(BrowserView):
120
+ def __call__(self):
121
+ acl = api.portal.get_tool("acl_users")
122
+ if "ims_sso_plugin" not in acl.objectIds():
123
+ return api.portal.get().absolute_url()
124
+ login_url = acl.ims_sso_plugin.login_url(self.request.ACTUAL_URL)
125
+ # stops loop if they try to login to an unauth page
126
+ # if "target=" in login_url and self.request.get("came_from"):
127
+ # return login_url.split("target=")[0] + "target=" + self.request["came_from"]
128
+ # else:
129
+ return login_url
130
+
131
+
132
+ class LoginCondition(BrowserView):
133
+ def __call__(self):
134
+ if not api.user.is_anonymous(): # sanity fallback
135
+ return False
136
+ sso = getUtility(ISingleSignonUtility)
137
+ return not sso.is_shibboleth_authenticated(self.request)
138
+
139
+
140
+ class LogoutUrl(BrowserView):
141
+ def __call__(self):
142
+ return f"{api.portal.get().absolute_url()}/sso-logout"
143
+
144
+
145
+ class RequireLoginView(BrowserView):
146
+ def __call__(self, *args, **kw):
147
+
148
+ utility = getUtility(ISingleSignonUtility)
149
+
150
+ if utility.is_plone_authenticated():
151
+ return api.content.get_view(
152
+ context=api.portal.get(),
153
+ request=self.request,
154
+ name="insufficient-privileges",
155
+ )()
156
+ else:
157
+ if utility.is_shibboleth_authenticated(self.request):
158
+ return api.content.get_view(
159
+ context=self.context,
160
+ request=self.request,
161
+ name="unauthorized_shib_authenticated",
162
+ )()
163
+ else:
164
+ return api.content.get_view(
165
+ context=self.context,
166
+ request=self.request,
167
+ name="unauthorized_shib_unauthenticated",
168
+ )()
169
+
170
+
171
+ class RequestReactivation(BrowserView):
172
+ def render(self):
173
+ PostOnly(self.request)
174
+ CheckAuthenticator(self.request)
175
+
176
+ util = getUtility(IReactivationUtility)
177
+ portal = api.portal.get()
178
+ portal_url = portal.absolute_url()
179
+
180
+ _key = util.request_reactivation()
181
+ if not _key:
182
+ api.portal.show_message(
183
+ message="An activation key could not be obtained, contact the site administrator",
184
+ type="error",
185
+ )
186
+ self.request.response.redirect(portal_url)
187
+ return
188
+ _key, expiry = _key
189
+ expiry = api.portal.get_tool("translation_service").ulocalized_time(expiry, long_format=1)
190
+
191
+ reg_link = f"{portal_url}/reactivate_user/{_key}"
192
+ portal_title = api.portal.get_registry_record("plone.site_title")
193
+ msg = (
194
+ f"You are receiving this because you requested to have your account on {portal_title} \
195
+ reactivated. To reactivate your account, visit {reg_link}.\n\n"
196
+ f"This link will expire in 1 day (by {expiry}). If the link expires, log into {portal_url} and "
197
+ f"reactivate your account again.\n\n"
198
+ f"If you did not initiate this action, you may ignore this message."
199
+ )
200
+ user_id = IAnnotations(self.request)[AUTHENTICATED_KEY]
201
+ api.portal.send_email(
202
+ recipient=api.user.get(user_id).getProperty("email"),
203
+ subject=f"Reactivate your account for {portal_title}",
204
+ body=msg,
205
+ immediate=False,
206
+ )
207
+ api.portal.show_message(
208
+ "An email with a link to reactivate your account has been sent to the address "
209
+ "associated with your account. If you do not receive the email, contact the "
210
+ f"<a href='{api.portal.get().absolute_url()}/contact-info'>site "
211
+ "administrator</a>."
212
+ )
213
+ self.request.response.redirect(portal_url + "/reactivation")
214
+ return
215
+
216
+
217
+ class ReactivateUserAccount(BrowserView):
218
+ def __init__(self, context, request):
219
+ self.subpath = []
220
+ super().__init__(context, request)
221
+
222
+ def publishTraverse(self, request, name):
223
+ self.subpath.append(name)
224
+ return self
225
+
226
+ def __call__(self):
227
+ util = getUtility(IReactivationUtility)
228
+ try:
229
+ key = self.subpath[0]
230
+ except IndexError:
231
+ api.portal.show_message(
232
+ "Sorry, this appears to be an invalid request. Please make sure you copied "
233
+ "the URL exactly as it appears in your email.",
234
+ type="warning",
235
+ )
236
+ self.request.response.redirect(api.portal.get().absolute_url() + "/reactivation")
237
+ return
238
+ status = util.reactivate_user(key)
239
+ if status:
240
+ api.portal.show_message("Your account has been successfully reactivated.")
241
+ self.request.response.redirect(api.portal.get().absolute_url())
242
+ else:
243
+ if not api.user.is_anonymous():
244
+ # the activation key is invalid but the user is authenticated. They are probably visiting using
245
+ # the reactivation email link, after already doing that step.
246
+ self.request.response.redirect(api.portal.get().absolute_url())
247
+ return
248
+
249
+ api.portal.show_message(
250
+ "Your account could not be activated. This may "
251
+ "be due to an invalid or expired reactivation link, or "
252
+ "your account may have been disabled after the reactivation link was sent. "
253
+ f'Contact the <a href="{api.portal.get().absolute_url()}/contact-info">'
254
+ "site administrator</a> for help with accessing the site.",
255
+ type="error",
256
+ )
257
+ self.request.response.redirect(api.portal.get().absolute_url() + "/reactivation")
@@ -0,0 +1,9 @@
1
+ from plone.app.users.browser.passwordpanel import PasswordPanel
2
+ from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
3
+
4
+
5
+ class ChangePassword(PasswordPanel):
6
+ cp_template = ViewPageTemplateFile("templates/change_password.pt")
7
+
8
+ def __call__(self):
9
+ return self.cp_template()
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1,161 @@
1
+ import datetime
2
+
3
+ import plone.api as api
4
+ from AccessControl import getSecurityManager
5
+ from plone.app.users.browser.register import AddUserForm as BaseAddUserForm
6
+ from Products.CMFCore.permissions import ManagePortal
7
+ from Products.CMFCore.utils import getToolByName
8
+ from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
9
+ from z3c.form import button
10
+ from zExceptions import Forbidden
11
+ from zope.component import getUtility
12
+
13
+ from ..configs import _
14
+ from ..interfaces import IMailTemplatesUtility, ISingleSignonUtility
15
+
16
+
17
+ class AddUserForm(BaseAddUserForm):
18
+ template = ViewPageTemplateFile("templates/newuser_form.pt")
19
+ enable_unload_protection = False # we have two forms
20
+ confirmation_required = False
21
+ confirmation_user = ""
22
+
23
+ @property
24
+ def sso(self):
25
+ return getUtility(ISingleSignonUtility)
26
+
27
+ def sso_configured(self):
28
+ return self.sso.get_setting("mail_format")
29
+
30
+ def updateFields(self):
31
+ """override - hide some fields we want to generate (eg fullname) or require (mail_me)"""
32
+ super().updateFields()
33
+ if not self.sso_configured():
34
+ api.portal.show_message(
35
+ _("SSO not properly configured. Consult administrator."),
36
+ self.request,
37
+ type="error",
38
+ )
39
+ return self.request.response.redirect(f"{api.portal.get().absolute_url()}/@@usergroup-userprefs")
40
+ del self.fields["fullname"]
41
+ del self.fields["password"]
42
+ del self.fields["password_ctl"]
43
+ if "mail_me" in self.fields:
44
+ del self.fields["mail_me"]
45
+ del self.fields["username"]
46
+
47
+ def updateWidgets(self):
48
+ super().updateWidgets()
49
+
50
+ # technically these are customizable fields, so we should check that they
51
+ # exist on the registration form
52
+ requested_fields = ("first_name", "last_name", "email")
53
+ for requested_field in requested_fields:
54
+ if requested_field in self.widgets and requested_field in self.request:
55
+ self.widgets[requested_field].value = self.request[requested_field]
56
+
57
+ def updateActions(self):
58
+ super().updateActions()
59
+ classes = self.actions["register"].klass
60
+ if "btn-secondary" in classes:
61
+ classes = classes.replace("btn-secondary", "") # already has the primary class
62
+ self.actions["register"].klass = classes
63
+
64
+ def generate_user_id(self, data):
65
+ """we hide fullname and usernames, generate them"""
66
+ data["fullname"] = data.get("last_name") or ""
67
+ if data.get("first_name"):
68
+ data["fullname"] += ", " + data.get("first_name")
69
+ super().generate_user_id(data)
70
+ data["username"] = data["user_id"]
71
+ return data["user_id"]
72
+
73
+ def handle_join_success(self, data):
74
+ """always email the new user
75
+ update login name
76
+ """
77
+
78
+ data["mail_me"] = True
79
+ base_method = super().handle_join_success
80
+
81
+ getUtility(IMailTemplatesUtility).get_mailer()
82
+ data["fullname"] = data["last_name"]
83
+ if data.get("first_name"):
84
+ data["fullname"] += ", " + data["first_name"]
85
+ base_method(data)
86
+ self.sso.initialize_login(data["user_id"])
87
+
88
+ member = api.user.get(userid=data["user_id"])
89
+ member.setMemberProperties({
90
+ "created_date": datetime.date.today(),
91
+ "activation_date": datetime.date.today(),
92
+ "active": "active",
93
+ })
94
+
95
+ def applyProperties(self, userid, data):
96
+ """we needed to add fullname but now we have to remove it or applyProperties fails
97
+ this is because applyProperties assumes every key in data is a key of self.fields
98
+ except for ['login_name', 'user_id'] and getFieldNames(IRegisterSchema) + getFieldNames(IAddUserSchema)
99
+ we'll set this manually afterwards
100
+ """
101
+ fullname = data["fullname"]
102
+ del data["fullname"]
103
+ super().applyProperties(userid, data)
104
+ member = api.user.get(userid=userid)
105
+ member.setMemberProperties({"fullname": fullname})
106
+
107
+ @button.buttonAndHandler(_("label_register", default="Register"), name="register")
108
+ def action_join(self, action):
109
+ # we overwrite the super action_join so it doesn't redirect with search string
110
+
111
+ data, _errors = self.extractData()
112
+ # extra password validation
113
+ self.validate_registration(action, data)
114
+
115
+ if action.form.widgets.errors:
116
+ self.status = self.formErrorsMessage
117
+ return
118
+
119
+ match = api.portal.get_tool("acl_users").searchUsers(email=data["email"])
120
+ if match and not self.request.get("form.widgets.allow_match"):
121
+ self.confirmation_required = True
122
+
123
+ def wrap_name(name, match):
124
+ if len(match) > 1 and match[-1]["title"] == name:
125
+ return f' and "{name}"'
126
+ return f'"{name}"'
127
+
128
+ joiner = ", " if len(match) > 2 else " "
129
+ self.confirmation_user = joiner.join([wrap_name(m["title"], match) for m in match])
130
+
131
+ return
132
+
133
+ self.handle_join_success(data)
134
+
135
+ if not self._finishedRegister:
136
+ return
137
+
138
+ portal_groups = getToolByName(self.context, "portal_groups")
139
+ user_id = data["user_id"]
140
+ is_zope_manager = getSecurityManager().checkPermission(
141
+ ManagePortal,
142
+ self.context,
143
+ )
144
+ try:
145
+ # Add user to the selected group(s)
146
+ if data.get("groups", None) is not None:
147
+ for groupname in data["groups"]:
148
+ group = portal_groups.getGroupById(groupname)
149
+ if "Manager" in group.getRoles() and not is_zope_manager:
150
+ raise Forbidden
151
+ portal_groups.addPrincipalToGroup(user_id, groupname, self.request)
152
+ except (AttributeError, ValueError) as err:
153
+ api.portal.show_message(err, type="error")
154
+ return
155
+ api.portal.show_message("User added.")
156
+ self.request.response.redirect(self.context.absolute_url() + "/@@usergroup-userprefs")
157
+
158
+ @button.buttonAndHandler("Cancel", name="cancel")
159
+ def handleCancel(self, action):
160
+ api.portal.show_message("User registration cancelled.")
161
+ self.request.response.redirect(self.context.absolute_url() + "/@@usergroup-userprefs")
@@ -0,0 +1,46 @@
1
+ <div tal:replace="structure context/manage_page_header"></div>
2
+ <div tal:replace="structure context/manage_tabs"></div>
3
+
4
+ <h2>Add ims.sso plugin</h2>
5
+
6
+ <p class="form-help">
7
+ Install ims.sso plugin
8
+ </p>
9
+
10
+ <form action="manage_addImsSso"
11
+ method="POST"
12
+ >
13
+ <table>
14
+ <tr>
15
+ <td class="form-label">
16
+ ID
17
+ </td>
18
+ <td>
19
+ <input name="id"
20
+ type="text"
21
+ />
22
+ </td>
23
+ </tr>
24
+ <tr>
25
+ <td class="form-label">
26
+ Title
27
+ </td>
28
+ <td>
29
+ <input name="title"
30
+ type="text"
31
+ />
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td colspan="2">
36
+ <div class="form-element">
37
+ <input type="submit"
38
+ value="Add"
39
+ />
40
+ </div>
41
+ </td>
42
+ </tr>
43
+ </table>
44
+ </form>
45
+
46
+ <div tal:replace="structure context/manage_page_footer"></div>