ims.sso 1.0.0a1__tar.gz

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.

Potentially problematic release.


This version of ims.sso might be problematic. Click here for more details.

Files changed (77) hide show
  1. ims_sso-1.0.0a1/.gitignore +59 -0
  2. ims_sso-1.0.0a1/PKG-INFO +17 -0
  3. ims_sso-1.0.0a1/ims/sso/__init__.py +16 -0
  4. ims_sso-1.0.0a1/ims/sso/browser/__init__.py +1 -0
  5. ims_sso-1.0.0a1/ims/sso/browser/configure.zcml +142 -0
  6. ims_sso-1.0.0a1/ims/sso/browser/cron.py +23 -0
  7. ims_sso-1.0.0a1/ims/sso/browser/login.py +257 -0
  8. ims_sso-1.0.0a1/ims/sso/browser/password.py +9 -0
  9. ims_sso-1.0.0a1/ims/sso/browser/personalpreferences.py +1 -0
  10. ims_sso-1.0.0a1/ims/sso/browser/register.py +161 -0
  11. ims_sso-1.0.0a1/ims/sso/browser/templates/add.pt +46 -0
  12. ims_sso-1.0.0a1/ims/sso/browser/templates/change_password.pt +47 -0
  13. ims_sso-1.0.0a1/ims/sso/browser/templates/linkaccount_anonymous.pt +24 -0
  14. ims_sso-1.0.0a1/ims/sso/browser/templates/linkaccount_disallowed.pt +22 -0
  15. ims_sso-1.0.0a1/ims/sso/browser/templates/linkaccount_finish.pt +21 -0
  16. ims_sso-1.0.0a1/ims/sso/browser/templates/linkaccount_invalid.pt +25 -0
  17. ims_sso-1.0.0a1/ims/sso/browser/templates/login.pt +23 -0
  18. ims_sso-1.0.0a1/ims/sso/browser/templates/logout_warning_viewlet.pt +26 -0
  19. ims_sso-1.0.0a1/ims/sso/browser/templates/newuser_form.pt +69 -0
  20. ims_sso-1.0.0a1/ims/sso/browser/templates/reactivation.pt +26 -0
  21. ims_sso-1.0.0a1/ims/sso/browser/templates/relog.pt +30 -0
  22. ims_sso-1.0.0a1/ims/sso/browser/templates/unauthorized_shib_authenticated.pt +58 -0
  23. ims_sso-1.0.0a1/ims/sso/browser/templates/unauthorized_shib_unauthenticated.pt +57 -0
  24. ims_sso-1.0.0a1/ims/sso/browser/templates/usergroups_usersoverview.pt +568 -0
  25. ims_sso-1.0.0a1/ims/sso/browser/templates/warnings.pt +44 -0
  26. ims_sso-1.0.0a1/ims/sso/browser/unauthorized.py +7 -0
  27. ims_sso-1.0.0a1/ims/sso/browser/usergroups_usersoverview.py +322 -0
  28. ims_sso-1.0.0a1/ims/sso/browser/viewlets.py +55 -0
  29. ims_sso-1.0.0a1/ims/sso/configs.py +18 -0
  30. ims_sso-1.0.0a1/ims/sso/configure.zcml +30 -0
  31. ims_sso-1.0.0a1/ims/sso/controlpanels/__init__.py +0 -0
  32. ims_sso-1.0.0a1/ims/sso/controlpanels/configure.zcml +13 -0
  33. ims_sso-1.0.0a1/ims/sso/controlpanels/settings.pt +24 -0
  34. ims_sso-1.0.0a1/ims/sso/controlpanels/sso.py +22 -0
  35. ims_sso-1.0.0a1/ims/sso/errors.py +6 -0
  36. ims_sso-1.0.0a1/ims/sso/events.py +31 -0
  37. ims_sso-1.0.0a1/ims/sso/events.zcml +12 -0
  38. ims_sso-1.0.0a1/ims/sso/indexes.py +9 -0
  39. ims_sso-1.0.0a1/ims/sso/interfaces.py +168 -0
  40. ims_sso-1.0.0a1/ims/sso/mailing/__init__.py +1 -0
  41. ims_sso-1.0.0a1/ims/sso/mailing/configure.zcml +48 -0
  42. ims_sso-1.0.0a1/ims/sso/mailing/mailers.py +13 -0
  43. ims_sso-1.0.0a1/ims/sso/mailing/mass_relink.pt +36 -0
  44. ims_sso-1.0.0a1/ims/sso/mailing/notify.py +101 -0
  45. ims_sso-1.0.0a1/ims/sso/mailing/relink.py +103 -0
  46. ims_sso-1.0.0a1/ims/sso/permissions.zcml +40 -0
  47. ims_sso-1.0.0a1/ims/sso/plugin.py +195 -0
  48. ims_sso-1.0.0a1/ims/sso/profiles/default/actions.xml +59 -0
  49. ims_sso-1.0.0a1/ims/sso/profiles/default/browserlayer.xml +6 -0
  50. ims_sso-1.0.0a1/ims/sso/profiles/default/controlpanel.xml +43 -0
  51. ims_sso-1.0.0a1/ims/sso/profiles/default/metadata.xml +4 -0
  52. ims_sso-1.0.0a1/ims/sso/profiles/default/registry/bundle.xml +12 -0
  53. ims_sso-1.0.0a1/ims/sso/profiles/default/registry/settings.xml +4 -0
  54. ims_sso-1.0.0a1/ims/sso/profiles/default/rolemap.xml +43 -0
  55. ims_sso-1.0.0a1/ims/sso/profiles/uninstall/actions.xml +7 -0
  56. ims_sso-1.0.0a1/ims/sso/profiles/uninstall/browserlayer.xml +6 -0
  57. ims_sso-1.0.0a1/ims/sso/profiles/uninstall/controlpanel.xml +10 -0
  58. ims_sso-1.0.0a1/ims/sso/profiles/uninstall/registry.xml +6 -0
  59. ims_sso-1.0.0a1/ims/sso/profiles.zcml +23 -0
  60. ims_sso-1.0.0a1/ims/sso/setuphandlers.py +139 -0
  61. ims_sso-1.0.0a1/ims/sso/static/bundle/chunks/881.f292144b2330c5cc4d06.min.js +2 -0
  62. ims_sso-1.0.0a1/ims/sso/static/bundle/chunks/881.f292144b2330c5cc4d06.min.js.LICENSE.txt +1 -0
  63. ims_sso-1.0.0a1/ims/sso/static/bundle/chunks/975.1473fe663da179434339.min.js +2 -0
  64. ims_sso-1.0.0a1/ims/sso/static/bundle/chunks/975.1473fe663da179434339.min.js.LICENSE.txt +1 -0
  65. ims_sso-1.0.0a1/ims/sso/static/bundle/ims.sso.min.js +2 -0
  66. ims_sso-1.0.0a1/ims/sso/static/bundle/ims.sso.min.js.LICENSE.txt +1 -0
  67. ims_sso-1.0.0a1/ims/sso/static/main.css +0 -0
  68. ims_sso-1.0.0a1/ims/sso/static/main.less +0 -0
  69. ims_sso-1.0.0a1/ims/sso/static/multiplugin.gif +0 -0
  70. ims_sso-1.0.0a1/ims/sso/testing.py +55 -0
  71. ims_sso-1.0.0a1/ims/sso/upgrades.py +3 -0
  72. ims_sso-1.0.0a1/ims/sso/upgrades.zcml +8 -0
  73. ims_sso-1.0.0a1/ims/sso/utilities.zcml +21 -0
  74. ims_sso-1.0.0a1/ims/sso/utility.py +468 -0
  75. ims_sso-1.0.0a1/ims/sso/vocabulary.py +22 -0
  76. ims_sso-1.0.0a1/ims/sso/vocabulary.zcml +15 -0
  77. ims_sso-1.0.0a1/pyproject.toml +133 -0
@@ -0,0 +1,59 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # C extensions
6
+ *.so
7
+
8
+ # Distribution / packaging
9
+ .Python
10
+ env/
11
+ build/
12
+ _build/
13
+ public/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ .DS_Store
27
+
28
+ # PyInstaller
29
+ # Usually these files are written by a python script from a template
30
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .coverage
42
+ .cache
43
+ nosetests.xml
44
+ coverage.xml
45
+
46
+ # Translations
47
+ *.mo
48
+ *.pot
49
+
50
+ # Django stuff:
51
+ *.log
52
+
53
+ # Sphinx documentation
54
+ docs/_build/
55
+
56
+ # PyBuilder
57
+ target/
58
+
59
+ /node_modules/
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: ims.sso
3
+ Version: 1.0.0a1
4
+ Summary: Shibboleth integration and security considerations
5
+ Project-URL: homepage, https://github.com/imsweb/ims.sso
6
+ Author-email: Eric Wohnlich <wohnlice@imsweb.com>, Taina Dumay <dumayt@imsweb.com>
7
+ License: GPL
8
+ Classifier: Framework :: Plone :: 6.1
9
+ Classifier: Programming Language :: Python
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: plone
12
+ Provides-Extra: test
13
+ Requires-Dist: formencode; extra == 'test'
14
+ Requires-Dist: plone-app-robotframework; extra == 'test'
15
+ Requires-Dist: plone-app-testing; extra == 'test'
16
+ Requires-Dist: plone-mocktestcase; extra == 'test'
17
+ Requires-Dist: pytest-plone; extra == 'test'
@@ -0,0 +1,16 @@
1
+ from Products.PluggableAuthService import registerMultiPlugin
2
+
3
+ from . import plugin
4
+
5
+ __version__ = "1.0.0a1"
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()