We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Bodhi sends notifications to old address after e-mail change
I've changed my e-mail addresses in all locations I could think of:
- [fedmsg](https://apps.fedoraproject.org/notifications)
- [bugzilla](https://bugzilla.redhat.com/)
- [Fedora Admin](https://admin.fedoraproject.org/accounts/)

But I still get notifications from bodhi at my old address about updates I've commented on, see the message below for an example.

It looks like this message doesn't come from fedmsg, the mail doesn't have any X-Fedmsg header fields. If I click on "Manage Alerts" in bodhi, it shows my fedmsg settings (the new e-mail address).

I initially thought this is a caching issue. But I changed my address months ago and I still get notifications to my old address. In addition to that, I also get fedmsg style notifications to my new address, but only about my own comments.

I'm not sure if this is a bug or I forgot to change my address somewhere. If it's the latter, I would expect "Manage alerts" to point to the right location.

Example message:

> Return-Path: updates@fedoraproject.org
> Delivered-To: hofmann@kbsg.rwth-aachen.de
> Received: from mx-out-2.rwth-aachen.de (mx-out-2.rwth-aachen.de [134.130.5.187])
>   (using TLSv1 with cipher DHE-RSA-CAMELLIA256-SHA (256/256 bits))
>   (Client CN "mx-out-2.rwth-aachen.de", Issuer "RWTH Aachen CA" (verified OK))
>   by lagrande.kbsg.rwth-aachen.de (Postfix) with ESMTPS id 7FA403FAD7
>   for hofmann@kbsg.rwth-aachen.de; Fri,  2 Sep 2016 01:00:39 +0200 (CEST)
> X-IronPort-Anti-Spam-Filtered: true
> X-IronPort-Anti-Spam-Result: A0BnAQAJsshXhwK1hNFdHAEBBAEBgywBAQEBAXV8pHaRLIQRJIV4AoIkAQIBAQEBAQITAQEBCgsJCRkvhGICAQOBCSwPFg9IiGEOuwcBAQEBAQEEAQEBAQEBASCGLIIDhnABAQVkgXwLWIIvBZlQhiCJB4F3ToQPgw2GAIZwhViDeYMdEQqBTTw0hE2CHwEBAQ
> X-IPAS-Result: A0BnAQAJsshXhwK1hNFdHAEBBAEBgywBAQEBAXV8pHaRLIQRJIV4AoIkAQIBAQEBAQITAQEBCgsJCRkvhGICAQOBCSwPFg9IiGEOuwcBAQEBAQEEAQEBAQEBASCGLIIDhnABAQVkgXwLWIIvBZlQhiCJB4F3ToQPgw2GAIZwhViDeYMdEQqBTTw0hE2CHwEBAQ
> X-IronPort-AV: E=Sophos;i="5.30,268,1470693600"; 
>    d="scan'208";a="456213363"
> Received: from bastion01.fedoraproject.org (HELO bastion.fedoraproject.org) ([209.132.181.2])
>   by mx-2.rz.rwth-aachen.de with ESMTP; 02 Sep 2016 01:00:39 +0200
> Received: from bodhi03.phx2.fedoraproject.org (bodhi03.phx2.fedoraproject.org [10.5.126.115])
>   by bastion01.phx2.fedoraproject.org (Postfix) with ESMTP id C7A8A6070D39
>   for hofmann@kbsg.rwth-aachen.de; Thu,  1 Sep 2016 23:00:36 +0000 (UTC)
> From: updates@fedoraproject.org
> To: hofmann@kbsg.rwth-aachen.de
> X-Bodhi-Update-Builds: kernel-4.7.2-201.fc24
> In-Reply-To: bodhi-update-66003-labbott-F24@admin.fedoraproject.org
> X-Bodhi-Update-Pushed: True
> X-Bodhi-Update-Type: security
> X-Bodhi-Update-Release: F24
> References: bodhi-update-66003-labbott-F24@admin.fedoraproject.org
> X-Bodhi-Update-Status: testing
> X-Bodhi-Update-Request: stable
> X-Bodhi-Update-Submitter: labbott
> X-Bodhi-Update-Title: kernel-4.7.2-201.fc24
> X-Bodhi: fedoraproject.org
> Subject: [Fedora Update] [CRITPATH] [comment] kernel-4.7.2-201.fc24
> Message-Id: 20160901230036.C7A8A6070D39@bastion01.phx2.fedoraproject.org
> Date: Thu,  1 Sep 2016 23:00:36 +0000 (UTC)
> [message body skipped]


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bodhi/server/security.py`
Content:
```
1 # This program is free software; you can redistribute it and/or
2 # modify it under the terms of the GNU General Public License
3 # as published by the Free Software Foundation; either version 2
4 # of the License, or (at your option) any later version.
5 #
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
9 # GNU General Public License for more details.
10 #
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
14 
15 from cornice.errors import Errors
16 
17 from pyramid.security import (Allow, ALL_PERMISSIONS, DENY_ALL)
18 from pyramid.security import remember, forget
19 from pyramid.httpexceptions import HTTPFound
20 from pyramid.threadlocal import get_current_registry
21 
22 from . import log
23 from .models import User, Group
24 
25 
26 #
27 # Pyramid ACL factories
28 #
29 
30 def admin_only_acl(request):
31     """Generate our admin-only ACL"""
32     return [(Allow, 'group:' + group, ALL_PERMISSIONS) for group in
33             request.registry.settings['admin_groups'].split()] + \
34            [DENY_ALL]
35 
36 
37 def packagers_allowed_acl(request):
38     """Generate an ACL for update submission"""
39     groups = request.registry.settings['mandatory_packager_groups'].split()
40     return [
41         (Allow, 'group:' + group, ALL_PERMISSIONS) for group in groups
42     ] + [DENY_ALL]
43 
44 
45 #
46 # OpenID views
47 #
48 
49 def login(request):
50     login_url = request.route_url('login')
51     referrer = request.url
52     if referrer == login_url:
53         referrer = request.route_url('home')
54     came_from = request.params.get('came_from', referrer)
55     request.session['came_from'] = came_from
56     oid_url = request.registry.settings['openid.url']
57     return HTTPFound(location=request.route_url('verify_openid',
58                                                 _query=dict(openid=oid_url)))
59 
60 
61 def logout(request):
62     headers = forget(request)
63     return HTTPFound(location=request.route_url('home'), headers=headers)
64 
65 
66 #
67 # openid.success_callback
68 #
69 
70 def remember_me(context, request, info, *args, **kw):
71     """ Called upon successful login """
72     log.debug('remember_me(%s)' % locals())
73     log.debug('remember_me: request.params = %r' % request.params)
74     endpoint = request.params['openid.op_endpoint']
75     if endpoint != request.registry.settings['openid.provider']:
76         log.warn('Invalid OpenID provider: %s' % endpoint)
77         request.session.flash('Invalid OpenID provider. You can only use: %s' %
78                               request.registry.settings['openid.provider'])
79         return HTTPFound(location=request.route_url('home'))
80 
81     username = unicode(info['identity_url'].split('http://')[1].split('.')[0])
82     email = info['sreg']['email']
83     log.debug('remember_me: groups = %s' % info['groups'])
84     log.info('%s successfully logged in' % username)
85 
86     # Find the user in our database. Create it if it doesn't exist.
87     db = request.db
88     user = db.query(User).filter_by(name=username).first()
89     if not user:
90         user = User(name=username, email=email)
91         db.add(user)
92         db.flush()
93     else:
94         # We used to not track email addresses, so fill in the fields as people
95         # log back in
96         if not user.email:
97             user.email = email
98             db.flush()
99 
100     # Keep track of what groups the user is a memeber of
101     for group_name in info['groups']:
102         # Drop empty group names https://github.com/fedora-infra/bodhi/issues/306
103         if not group_name.strip():
104             continue
105 
106         group = db.query(Group).filter_by(name=group_name).first()
107         if not group:
108             group = Group(name=group_name)
109             db.add(group)
110             db.flush()
111         if group not in user.groups:
112             log.info('Adding %s to %s group', user.name, group.name)
113             user.groups.append(group)
114 
115     # See if the user was removed from any groups
116     for group in user.groups:
117         if group.name not in info['groups']:
118             log.info('Removing %s from %s group', user.name, group.name)
119             user.groups.remove(group)
120 
121     headers = remember(request, username)
122     came_from = request.session['came_from']
123     del(request.session['came_from'])
124 
125     # Mitigate "Covert Redirect"
126     if not came_from.startswith(request.host_url):
127         came_from = '/'
128 
129     response = HTTPFound(location=came_from)
130     response.headerlist.extend(headers)
131     return response
132 
133 
134 class CorsOrigins(object):
135     """ Proxy-list class to load CORS config after scan-time.
136 
137     This should appear to behave just like a list, but it loads values from the
138     pyramid configuration for its values.  AFAIK, we have to do things this way
139     since Cornice expects its cors configuration to be present at import-time,
140     but the configuration isn't available until later, at Pyramid scan-time.
141     Luckily, Cornice doesn't iterate over that configuration until
142     request-time, so we can load this then.
143 
144         >>> cors_origins_ro = CorsOrigins('cors_origins_ro')
145         >>> cors_origins_ro[0]
146         ['*']
147         >>> cors_origins_rw = CorsOrigins('cors_origins_rw')
148         >>> cors_origins_rw[0]
149         ['bodhi.fedoraproject.org']
150 
151     """
152     def __init__(self, name):
153         self.name = name
154         self.origins = None
155 
156     def initialize(self):
157         if self.origins is None:
158             settings = get_current_registry().settings
159             self.origins = settings.get(self.name, 'localhost').split(',')
160 
161     def __len__(self):
162         if self.origins is None:
163             self.initialize()
164         return len(self.origins)
165 
166     def __getitem__(self, key):
167         if self.origins is None:
168             self.initialize()
169         return self.origins[key]
170 
171     def __iter__(self):
172         if self.origins is None:
173             self.initialize()
174         return iter(self.originals)
175 
176     def __contains__(self, item):
177         if self.origins is None:
178             self.initialize()
179         return item in self.originals
180 
181 
182 cors_origins_ro = CorsOrigins('cors_origins_ro')
183 cors_origins_rw = CorsOrigins('cors_origins_rw')
184 
185 
186 class ProtectedRequest(object):
187     """ A proxy to the request object.
188 
189     The point here is that you can set 'errors' on this request, but they
190     will be sent to /dev/null and hidden from cornice.  Otherwise, this
191     object behaves just like a normal request object.
192     """
193     def __init__(self, real_request):
194         # Hide errors added to this from the real request
195         self.errors = Errors()
196         # But proxy other attributes to the real request
197         self.real_request = real_request
198         for attr in ['db', 'registry', 'validated', 'buildinfo', 'user']:
199             setattr(self, attr, getattr(self.real_request, attr))
200
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bodhi/server/security.py b/bodhi/server/security.py
--- a/bodhi/server/security.py
+++ b/bodhi/server/security.py
@@ -91,9 +91,8 @@
         db.add(user)
         db.flush()
     else:
-        # We used to not track email addresses, so fill in the fields as people
-        # log back in
-        if not user.email:
+        # Update email address if the address changed
+        if user.email != email:
             user.email = email
             db.flush()

{"golden_diff": "diff --git a/bodhi/server/security.py b/bodhi/server/security.py\n--- a/bodhi/server/security.py\n+++ b/bodhi/server/security.py\n@@ -91,9 +91,8 @@\n         db.add(user)\n         db.flush()\n     else:\n-        # We used to not track email addresses, so fill in the fields as people\n-        # log back in\n-        if not user.email:\n+        # Update email address if the address changed\n+        if user.email != email:\n             user.email = email\n             db.flush()\n", "issue": "Bodhi sends notifications to old address after e-mail change\nI've changed my e-mail addresses in all locations I could think of:\n- [fedmsg](https://apps.fedoraproject.org/notifications)\n- [bugzilla](https://bugzilla.redhat.com/)\n- [Fedora Admin](https://admin.fedoraproject.org/accounts/)\n\nBut I still get notifications from bodhi at my old address about updates I've commented on, see the message below for an example.\n\nIt looks like this message doesn't come from fedmsg, the mail doesn't have any X-Fedmsg header fields. If I click on \"Manage Alerts\" in bodhi, it shows my fedmsg settings (the new e-mail address).\n\nI initially thought this is a caching issue. But I changed my address months ago and I still get notifications to my old address. In addition to that, I also get fedmsg style notifications to my new address, but only about my own comments.\n\nI'm not sure if this is a bug or I forgot to change my address somewhere. If it's the latter, I would expect \"Manage alerts\" to point to the right location.\n\nExample message:\n\n> Return-Path: updates@fedoraproject.org\n> Delivered-To: hofmann@kbsg.rwth-aachen.de\n> Received: from mx-out-2.rwth-aachen.de (mx-out-2.rwth-aachen.de [134.130.5.187])\n>   (using TLSv1 with cipher DHE-RSA-CAMELLIA256-SHA (256/256 bits))\n>   (Client CN \"mx-out-2.rwth-aachen.de\", Issuer \"RWTH Aachen CA\" (verified OK))\n>   by lagrande.kbsg.rwth-aachen.de (Postfix) with ESMTPS id 7FA403FAD7\n>   for hofmann@kbsg.rwth-aachen.de; Fri,  2 Sep 2016 01:00:39 +0200 (CEST)\n> X-IronPort-Anti-Spam-Filtered: true\n> X-IronPort-Anti-Spam-Result: A0BnAQAJsshXhwK1hNFdHAEBBAEBgywBAQEBAXV8pHaRLIQRJIV4AoIkAQIBAQEBAQITAQEBCgsJCRkvhGICAQOBCSwPFg9IiGEOuwcBAQEBAQEEAQEBAQEBASCGLIIDhnABAQVkgXwLWIIvBZlQhiCJB4F3ToQPgw2GAIZwhViDeYMdEQqBTTw0hE2CHwEBAQ\n> X-IPAS-Result: A0BnAQAJsshXhwK1hNFdHAEBBAEBgywBAQEBAXV8pHaRLIQRJIV4AoIkAQIBAQEBAQITAQEBCgsJCRkvhGICAQOBCSwPFg9IiGEOuwcBAQEBAQEEAQEBAQEBASCGLIIDhnABAQVkgXwLWIIvBZlQhiCJB4F3ToQPgw2GAIZwhViDeYMdEQqBTTw0hE2CHwEBAQ\n> X-IronPort-AV: E=Sophos;i=\"5.30,268,1470693600\"; \n>    d=\"scan'208\";a=\"456213363\"\n> Received: from bastion01.fedoraproject.org (HELO bastion.fedoraproject.org) ([209.132.181.2])\n>   by mx-2.rz.rwth-aachen.de with ESMTP; 02 Sep 2016 01:00:39 +0200\n> Received: from bodhi03.phx2.fedoraproject.org (bodhi03.phx2.fedoraproject.org [10.5.126.115])\n>   by bastion01.phx2.fedoraproject.org (Postfix) with ESMTP id C7A8A6070D39\n>   for hofmann@kbsg.rwth-aachen.de; Thu,  1 Sep 2016 23:00:36 +0000 (UTC)\n> From: updates@fedoraproject.org\n> To: hofmann@kbsg.rwth-aachen.de\n> X-Bodhi-Update-Builds: kernel-4.7.2-201.fc24\n> In-Reply-To: bodhi-update-66003-labbott-F24@admin.fedoraproject.org\n> X-Bodhi-Update-Pushed: True\n> X-Bodhi-Update-Type: security\n> X-Bodhi-Update-Release: F24\n> References: bodhi-update-66003-labbott-F24@admin.fedoraproject.org\n> X-Bodhi-Update-Status: testing\n> X-Bodhi-Update-Request: stable\n> X-Bodhi-Update-Submitter: labbott\n> X-Bodhi-Update-Title: kernel-4.7.2-201.fc24\n> X-Bodhi: fedoraproject.org\n> Subject: [Fedora Update] [CRITPATH] [comment] kernel-4.7.2-201.fc24\n> Message-Id: 20160901230036.C7A8A6070D39@bastion01.phx2.fedoraproject.org\n> Date: Thu,  1 Sep 2016 23:00:36 +0000 (UTC)\n> [message body skipped]\n\n", "before_files": [{"content": "# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n\nfrom cornice.errors import Errors\n\nfrom pyramid.security import (Allow, ALL_PERMISSIONS, DENY_ALL)\nfrom pyramid.security import remember, forget\nfrom pyramid.httpexceptions import HTTPFound\nfrom pyramid.threadlocal import get_current_registry\n\nfrom . import log\nfrom .models import User, Group\n\n\n#\n# Pyramid ACL factories\n#\n\ndef admin_only_acl(request):\n    \"\"\"Generate our admin-only ACL\"\"\"\n    return [(Allow, 'group:' + group, ALL_PERMISSIONS) for group in\n            request.registry.settings['admin_groups'].split()] + \\\n           [DENY_ALL]\n\n\ndef packagers_allowed_acl(request):\n    \"\"\"Generate an ACL for update submission\"\"\"\n    groups = request.registry.settings['mandatory_packager_groups'].split()\n    return [\n        (Allow, 'group:' + group, ALL_PERMISSIONS) for group in groups\n    ] + [DENY_ALL]\n\n\n#\n# OpenID views\n#\n\ndef login(request):\n    login_url = request.route_url('login')\n    referrer = request.url\n    if referrer == login_url:\n        referrer = request.route_url('home')\n    came_from = request.params.get('came_from', referrer)\n    request.session['came_from'] = came_from\n    oid_url = request.registry.settings['openid.url']\n    return HTTPFound(location=request.route_url('verify_openid',\n                                                _query=dict(openid=oid_url)))\n\n\ndef logout(request):\n    headers = forget(request)\n    return HTTPFound(location=request.route_url('home'), headers=headers)\n\n\n#\n# openid.success_callback\n#\n\ndef remember_me(context, request, info, *args, **kw):\n    \"\"\" Called upon successful login \"\"\"\n    log.debug('remember_me(%s)' % locals())\n    log.debug('remember_me: request.params = %r' % request.params)\n    endpoint = request.params['openid.op_endpoint']\n    if endpoint != request.registry.settings['openid.provider']:\n        log.warn('Invalid OpenID provider: %s' % endpoint)\n        request.session.flash('Invalid OpenID provider. You can only use: %s' %\n                              request.registry.settings['openid.provider'])\n        return HTTPFound(location=request.route_url('home'))\n\n    username = unicode(info['identity_url'].split('http://')[1].split('.')[0])\n    email = info['sreg']['email']\n    log.debug('remember_me: groups = %s' % info['groups'])\n    log.info('%s successfully logged in' % username)\n\n    # Find the user in our database. Create it if it doesn't exist.\n    db = request.db\n    user = db.query(User).filter_by(name=username).first()\n    if not user:\n        user = User(name=username, email=email)\n        db.add(user)\n        db.flush()\n    else:\n        # We used to not track email addresses, so fill in the fields as people\n        # log back in\n        if not user.email:\n            user.email = email\n            db.flush()\n\n    # Keep track of what groups the user is a memeber of\n    for group_name in info['groups']:\n        # Drop empty group names https://github.com/fedora-infra/bodhi/issues/306\n        if not group_name.strip():\n            continue\n\n        group = db.query(Group).filter_by(name=group_name).first()\n        if not group:\n            group = Group(name=group_name)\n            db.add(group)\n            db.flush()\n        if group not in user.groups:\n            log.info('Adding %s to %s group', user.name, group.name)\n            user.groups.append(group)\n\n    # See if the user was removed from any groups\n    for group in user.groups:\n        if group.name not in info['groups']:\n            log.info('Removing %s from %s group', user.name, group.name)\n            user.groups.remove(group)\n\n    headers = remember(request, username)\n    came_from = request.session['came_from']\n    del(request.session['came_from'])\n\n    # Mitigate \"Covert Redirect\"\n    if not came_from.startswith(request.host_url):\n        came_from = '/'\n\n    response = HTTPFound(location=came_from)\n    response.headerlist.extend(headers)\n    return response\n\n\nclass CorsOrigins(object):\n    \"\"\" Proxy-list class to load CORS config after scan-time.\n\n    This should appear to behave just like a list, but it loads values from the\n    pyramid configuration for its values.  AFAIK, we have to do things this way\n    since Cornice expects its cors configuration to be present at import-time,\n    but the configuration isn't available until later, at Pyramid scan-time.\n    Luckily, Cornice doesn't iterate over that configuration until\n    request-time, so we can load this then.\n\n        >>> cors_origins_ro = CorsOrigins('cors_origins_ro')\n        >>> cors_origins_ro[0]\n        ['*']\n        >>> cors_origins_rw = CorsOrigins('cors_origins_rw')\n        >>> cors_origins_rw[0]\n        ['bodhi.fedoraproject.org']\n\n    \"\"\"\n    def __init__(self, name):\n        self.name = name\n        self.origins = None\n\n    def initialize(self):\n        if self.origins is None:\n            settings = get_current_registry().settings\n            self.origins = settings.get(self.name, 'localhost').split(',')\n\n    def __len__(self):\n        if self.origins is None:\n            self.initialize()\n        return len(self.origins)\n\n    def __getitem__(self, key):\n        if self.origins is None:\n            self.initialize()\n        return self.origins[key]\n\n    def __iter__(self):\n        if self.origins is None:\n            self.initialize()\n        return iter(self.originals)\n\n    def __contains__(self, item):\n        if self.origins is None:\n            self.initialize()\n        return item in self.originals\n\n\ncors_origins_ro = CorsOrigins('cors_origins_ro')\ncors_origins_rw = CorsOrigins('cors_origins_rw')\n\n\nclass ProtectedRequest(object):\n    \"\"\" A proxy to the request object.\n\n    The point here is that you can set 'errors' on this request, but they\n    will be sent to /dev/null and hidden from cornice.  Otherwise, this\n    object behaves just like a normal request object.\n    \"\"\"\n    def __init__(self, real_request):\n        # Hide errors added to this from the real request\n        self.errors = Errors()\n        # But proxy other attributes to the real request\n        self.real_request = real_request\n        for attr in ['db', 'registry', 'validated', 'buildinfo', 'user']:\n            setattr(self, attr, getattr(self.real_request, attr))\n", "path": "bodhi/server/security.py"}], "after_files": [{"content": "# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n\nfrom cornice.errors import Errors\n\nfrom pyramid.security import (Allow, ALL_PERMISSIONS, DENY_ALL)\nfrom pyramid.security import remember, forget\nfrom pyramid.httpexceptions import HTTPFound\nfrom pyramid.threadlocal import get_current_registry\n\nfrom . import log\nfrom .models import User, Group\n\n\n#\n# Pyramid ACL factories\n#\n\ndef admin_only_acl(request):\n    \"\"\"Generate our admin-only ACL\"\"\"\n    return [(Allow, 'group:' + group, ALL_PERMISSIONS) for group in\n            request.registry.settings['admin_groups'].split()] + \\\n           [DENY_ALL]\n\n\ndef packagers_allowed_acl(request):\n    \"\"\"Generate an ACL for update submission\"\"\"\n    groups = request.registry.settings['mandatory_packager_groups'].split()\n    return [\n        (Allow, 'group:' + group, ALL_PERMISSIONS) for group in groups\n    ] + [DENY_ALL]\n\n\n#\n# OpenID views\n#\n\ndef login(request):\n    login_url = request.route_url('login')\n    referrer = request.url\n    if referrer == login_url:\n        referrer = request.route_url('home')\n    came_from = request.params.get('came_from', referrer)\n    request.session['came_from'] = came_from\n    oid_url = request.registry.settings['openid.url']\n    return HTTPFound(location=request.route_url('verify_openid',\n                                                _query=dict(openid=oid_url)))\n\n\ndef logout(request):\n    headers = forget(request)\n    return HTTPFound(location=request.route_url('home'), headers=headers)\n\n\n#\n# openid.success_callback\n#\n\ndef remember_me(context, request, info, *args, **kw):\n    \"\"\" Called upon successful login \"\"\"\n    log.debug('remember_me(%s)' % locals())\n    log.debug('remember_me: request.params = %r' % request.params)\n    endpoint = request.params['openid.op_endpoint']\n    if endpoint != request.registry.settings['openid.provider']:\n        log.warn('Invalid OpenID provider: %s' % endpoint)\n        request.session.flash('Invalid OpenID provider. You can only use: %s' %\n                              request.registry.settings['openid.provider'])\n        return HTTPFound(location=request.route_url('home'))\n\n    username = unicode(info['identity_url'].split('http://')[1].split('.')[0])\n    email = info['sreg']['email']\n    log.debug('remember_me: groups = %s' % info['groups'])\n    log.info('%s successfully logged in' % username)\n\n    # Find the user in our database. Create it if it doesn't exist.\n    db = request.db\n    user = db.query(User).filter_by(name=username).first()\n    if not user:\n        user = User(name=username, email=email)\n        db.add(user)\n        db.flush()\n    else:\n        # Update email address if the address changed\n        if user.email != email:\n            user.email = email\n            db.flush()\n\n    # Keep track of what groups the user is a memeber of\n    for group_name in info['groups']:\n        # Drop empty group names https://github.com/fedora-infra/bodhi/issues/306\n        if not group_name.strip():\n            continue\n\n        group = db.query(Group).filter_by(name=group_name).first()\n        if not group:\n            group = Group(name=group_name)\n            db.add(group)\n            db.flush()\n        if group not in user.groups:\n            log.info('Adding %s to %s group', user.name, group.name)\n            user.groups.append(group)\n\n    # See if the user was removed from any groups\n    for group in user.groups:\n        if group.name not in info['groups']:\n            log.info('Removing %s from %s group', user.name, group.name)\n            user.groups.remove(group)\n\n    headers = remember(request, username)\n    came_from = request.session['came_from']\n    del(request.session['came_from'])\n\n    # Mitigate \"Covert Redirect\"\n    if not came_from.startswith(request.host_url):\n        came_from = '/'\n\n    response = HTTPFound(location=came_from)\n    response.headerlist.extend(headers)\n    return response\n\n\nclass CorsOrigins(object):\n    \"\"\" Proxy-list class to load CORS config after scan-time.\n\n    This should appear to behave just like a list, but it loads values from the\n    pyramid configuration for its values.  AFAIK, we have to do things this way\n    since Cornice expects its cors configuration to be present at import-time,\n    but the configuration isn't available until later, at Pyramid scan-time.\n    Luckily, Cornice doesn't iterate over that configuration until\n    request-time, so we can load this then.\n\n        >>> cors_origins_ro = CorsOrigins('cors_origins_ro')\n        >>> cors_origins_ro[0]\n        ['*']\n        >>> cors_origins_rw = CorsOrigins('cors_origins_rw')\n        >>> cors_origins_rw[0]\n        ['bodhi.fedoraproject.org']\n\n    \"\"\"\n    def __init__(self, name):\n        self.name = name\n        self.origins = None\n\n    def initialize(self):\n        if self.origins is None:\n            settings = get_current_registry().settings\n            self.origins = settings.get(self.name, 'localhost').split(',')\n\n    def __len__(self):\n        if self.origins is None:\n            self.initialize()\n        return len(self.origins)\n\n    def __getitem__(self, key):\n        if self.origins is None:\n            self.initialize()\n        return self.origins[key]\n\n    def __iter__(self):\n        if self.origins is None:\n            self.initialize()\n        return iter(self.originals)\n\n    def __contains__(self, item):\n        if self.origins is None:\n            self.initialize()\n        return item in self.originals\n\n\ncors_origins_ro = CorsOrigins('cors_origins_ro')\ncors_origins_rw = CorsOrigins('cors_origins_rw')\n\n\nclass ProtectedRequest(object):\n    \"\"\" A proxy to the request object.\n\n    The point here is that you can set 'errors' on this request, but they\n    will be sent to /dev/null and hidden from cornice.  Otherwise, this\n    object behaves just like a normal request object.\n    \"\"\"\n    def __init__(self, real_request):\n        # Hide errors added to this from the real request\n        self.errors = Errors()\n        # But proxy other attributes to the real request\n        self.real_request = real_request\n        for attr in ['db', 'registry', 'validated', 'buildinfo', 'user']:\n            setattr(self, attr, getattr(self.real_request, attr))\n", "path": "bodhi/server/security.py"}]}