We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Debian package securedrop-app-code is not getting upgraded
# Bug

## Description

In my prod vm `securedrop-app-code` package is not getting upgraded.

## Steps to Reproduce

Follow the QA steps to fetch the latest packages from apt-test repo.

## Expected Behavior

We should get the 0.7~RC1 packages.

## Actual Behavior

```
$ dpkg -al | grep secure
ii  openssh-client                       1:6.6p1-2ubuntu2.10                        amd64        secure shell (SSH) client, for secure access to remote machines
ii  openssh-server                       1:6.6p1-2ubuntu2.10                        amd64        secure shell (SSH) server, for secure access from remote machines
ii  openssh-sftp-server                  1:6.6p1-2ubuntu2.10                        amd64        secure shell (SSH) sftp server module, for SFTP access from remote machines
ii  secure-delete                        3.1-6                                      amd64        tools to wipe files, free disk space, swap and memory
ii  securedrop-app-code                  0.6                                        amd64        Packages the SecureDrop application code pip dependencies and apparmor profiles. This package will put the apparmor profiles in enforce mode. This package does use pip to install the pip wheelhouse
ii  securedrop-config                    0.1.1+0.7.0~rc1                            all          Establishes baseline system state for running SecureDrop.
ii  securedrop-grsec                     4.4.115+r1                                 amd64        Metapackage providing a grsecurity-patched Linux kernel for use
ii  securedrop-keyring                   0.1.1+0.7.0~rc1                            amd64        Provides an apt keyring for SecureDrop-related packages, so the master signing key used for SecureDrop packages can be updated via apt.
ii  securedrop-ossec-agent               2.8.2+0.7.0~rc1                            amd64        Installs the securedrop pre-configured OSSEC agent
ii  ssh-import-id                        3.21-0ubuntu1                              all          securely retrieve an SSH public key and install it locally
```

```
$ sudo apt-get upgrade
Reading package lists... Done
Building dependency tree       
Reading state information... Done
Calculating upgrade... Done
The following packages have been kept back:
  securedrop-app-code
0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.

```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/journalist_app/forms.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 from flask_babel import lazy_gettext as gettext
4 from flask_wtf import FlaskForm
5 from flask_wtf.file import FileField, FileAllowed, FileRequired
6 from wtforms import (TextAreaField, TextField, BooleanField, HiddenField,
7                      ValidationError)
8 from wtforms.validators import InputRequired, Optional
9 
10 from models import Journalist
11 
12 
13 def otp_secret_validation(form, field):
14     strip_whitespace = field.data.replace(' ', '')
15     if len(strip_whitespace) != 40:
16         raise ValidationError(gettext(
17             'HOTP secrets are 40 characters long - '
18             'you have entered {num_chars}.'.format(
19                 num_chars=len(strip_whitespace)
20             )))
21 
22 
23 def minimum_length_validation(form, field):
24     if len(field.data) < Journalist.MIN_USERNAME_LEN:
25         raise ValidationError(
26             gettext('Field must be at least {min_chars} '
27                     'characters long but only got '
28                     '{num_chars}.'.format(
29                         min_chars=Journalist.MIN_USERNAME_LEN,
30                         num_chars=len(field.data))))
31 
32 
33 class NewUserForm(FlaskForm):
34     username = TextField('username', validators=[
35         InputRequired(message=gettext('This field is required.')),
36         minimum_length_validation
37     ])
38     password = HiddenField('password')
39     is_admin = BooleanField('is_admin')
40     is_hotp = BooleanField('is_hotp')
41     otp_secret = TextField('otp_secret', validators=[
42         otp_secret_validation,
43         Optional()
44     ])
45 
46 
47 class ReplyForm(FlaskForm):
48     message = TextAreaField(
49         u'Message',
50         id="content-area",
51         validators=[
52             InputRequired(message=gettext(
53                 'You cannot send an empty reply.')),
54         ],
55     )
56 
57 
58 class LogoForm(FlaskForm):
59     logo = FileField(validators=[
60         FileRequired(message=gettext('File required.')),
61         FileAllowed(['jpg', 'png', 'jpeg'],
62                     message=gettext("You can only upload JPG/JPEG"
63                                     " or PNG image files."))
64     ])
65
```

Path: `securedrop/journalist_app/admin.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 from PIL import Image
4 
5 import os
6 
7 from flask import (Blueprint, render_template, request, url_for, redirect, g,
8                    current_app, flash, abort)
9 from flask_babel import gettext
10 from sqlalchemy.exc import IntegrityError
11 from sqlalchemy.orm.exc import NoResultFound
12 
13 from db import db
14 from models import Journalist, InvalidUsernameException, PasswordError
15 from journalist_app.decorators import admin_required
16 from journalist_app.utils import (make_password, commit_account_changes,
17                                   set_diceware_password, validate_hotp_secret)
18 from journalist_app.forms import LogoForm, NewUserForm
19 
20 
21 def make_blueprint(config):
22     view = Blueprint('admin', __name__)
23 
24     @view.route('/', methods=('GET', 'POST'))
25     @admin_required
26     def index():
27         users = Journalist.query.all()
28         return render_template("admin.html", users=users)
29 
30     @view.route('/config', methods=('GET', 'POST'))
31     @admin_required
32     def manage_config():
33         form = LogoForm()
34         if form.validate_on_submit():
35             f = form.logo.data
36             custom_logo_filepath = os.path.join(config.SECUREDROP_ROOT,
37                                                 "static/i/custom_logo.png")
38             try:
39                 with Image.open(f) as im:
40                     im.thumbnail((500, 450), resample=3)
41                     im.save(custom_logo_filepath, "PNG")
42                 flash(gettext("Image updated."), "logo-success")
43             except Exception:
44                 flash("Unable to process the image file."
45                       " Try another one.", "logo-error")
46             finally:
47                 return redirect(url_for("admin.manage_config"))
48 
49         else:
50             for field, errors in form.errors.items():
51                 for error in errors:
52                     flash(error, "logo-error")
53             return render_template("config.html", form=form)
54 
55     @view.route('/add', methods=('GET', 'POST'))
56     @admin_required
57     def add_user():
58         form = NewUserForm()
59         if form.validate_on_submit():
60             form_valid = True
61             username = request.form['username']
62             password = request.form['password']
63             is_admin = bool(request.form.get('is_admin'))
64 
65             try:
66                 otp_secret = None
67                 if request.form.get('is_hotp', False):
68                     otp_secret = request.form.get('otp_secret', '')
69                 new_user = Journalist(username=username,
70                                       password=password,
71                                       is_admin=is_admin,
72                                       otp_secret=otp_secret)
73                 db.session.add(new_user)
74                 db.session.commit()
75             except PasswordError:
76                 flash(gettext(
77                       'There was an error with the autogenerated password. '
78                       'User not created. Please try again.'), 'error')
79                 form_valid = False
80             except InvalidUsernameException as e:
81                 form_valid = False
82                 flash('Invalid username: ' + str(e), "error")
83             except IntegrityError as e:
84                 db.session.rollback()
85                 form_valid = False
86                 if "UNIQUE constraint failed: journalists.username" in str(e):
87                     flash(gettext('Username "{user}" already taken.'.format(
88                         user=username)), "error")
89                 else:
90                     flash(gettext("An error occurred saving this user"
91                                   " to the database."
92                                   " Please inform your administrator."),
93                           "error")
94                     current_app.logger.error("Adding user "
95                                              "'{}' failed: {}".format(
96                                                  username, e))
97 
98             if form_valid:
99                 return redirect(url_for('admin.new_user_two_factor',
100                                         uid=new_user.id))
101 
102         return render_template("admin_add_user.html",
103                                password=make_password(config),
104                                form=form)
105 
106     @view.route('/2fa', methods=('GET', 'POST'))
107     @admin_required
108     def new_user_two_factor():
109         user = Journalist.query.get(request.args['uid'])
110 
111         if request.method == 'POST':
112             token = request.form['token']
113             if user.verify_token(token):
114                 flash(gettext(
115                     "Token in two-factor authentication "
116                     "accepted for user {user}.").format(
117                         user=user.username),
118                     "notification")
119                 return redirect(url_for("admin.index"))
120             else:
121                 flash(gettext(
122                     "Could not verify token in two-factor authentication."),
123                       "error")
124 
125         return render_template("admin_new_user_two_factor.html", user=user)
126 
127     @view.route('/reset-2fa-totp', methods=['POST'])
128     @admin_required
129     def reset_two_factor_totp():
130         uid = request.form['uid']
131         user = Journalist.query.get(uid)
132         user.is_totp = True
133         user.regenerate_totp_shared_secret()
134         db.session.commit()
135         return redirect(url_for('admin.new_user_two_factor', uid=uid))
136 
137     @view.route('/reset-2fa-hotp', methods=['POST'])
138     @admin_required
139     def reset_two_factor_hotp():
140         uid = request.form['uid']
141         otp_secret = request.form.get('otp_secret', None)
142         if otp_secret:
143             user = Journalist.query.get(uid)
144             if not validate_hotp_secret(user, otp_secret):
145                 return render_template('admin_edit_hotp_secret.html', uid=uid)
146             db.session.commit()
147             return redirect(url_for('admin.new_user_two_factor', uid=uid))
148         else:
149             return render_template('admin_edit_hotp_secret.html', uid=uid)
150 
151     @view.route('/edit/<int:user_id>', methods=('GET', 'POST'))
152     @admin_required
153     def edit_user(user_id):
154         user = Journalist.query.get(user_id)
155 
156         if request.method == 'POST':
157             if request.form.get('username', None):
158                 new_username = request.form['username']
159 
160                 try:
161                     Journalist.check_username_acceptable(new_username)
162                 except InvalidUsernameException as e:
163                     flash('Invalid username: ' + str(e), 'error')
164                     return redirect(url_for("admin.edit_user",
165                                             user_id=user_id))
166 
167                 if new_username == user.username:
168                     pass
169                 elif Journalist.query.filter_by(
170                         username=new_username).one_or_none():
171                     flash(gettext(
172                         'Username "{user}" already taken.').format(
173                             user=new_username),
174                         "error")
175                     return redirect(url_for("admin.edit_user",
176                                             user_id=user_id))
177                 else:
178                     user.username = new_username
179 
180             user.is_admin = bool(request.form.get('is_admin'))
181 
182             commit_account_changes(user)
183 
184         password = make_password(config)
185         return render_template("edit_account.html", user=user,
186                                password=password)
187 
188     @view.route('/edit/<int:user_id>/new-password', methods=('POST',))
189     @admin_required
190     def set_password(user_id):
191         try:
192             user = Journalist.query.get(user_id)
193         except NoResultFound:
194             abort(404)
195 
196         password = request.form.get('password')
197         set_diceware_password(user, password)
198         return redirect(url_for('admin.edit_user', user_id=user_id))
199 
200     @view.route('/delete/<int:user_id>', methods=('POST',))
201     @admin_required
202     def delete_user(user_id):
203         user = Journalist.query.get(user_id)
204         if user_id == g.user.id:
205             # Do not flash because the interface already has safe guards.
206             # It can only happen by manually crafting a POST request
207             current_app.logger.error(
208                 "Admin {} tried to delete itself".format(g.user.username))
209             abort(403)
210         elif user:
211             db.session.delete(user)
212             db.session.commit()
213             flash(gettext("Deleted user '{user}'").format(
214                 user=user.username), "notification")
215         else:
216             current_app.logger.error(
217                 "Admin {} tried to delete nonexistent user with pk={}".format(
218                     g.user.username, user_id))
219             abort(404)
220 
221         return redirect(url_for('admin.index'))
222 
223     @view.route('/edit/<int:user_id>/new-password', methods=('POST',))
224     @admin_required
225     def new_password(user_id):
226         try:
227             user = Journalist.query.get(user_id)
228         except NoResultFound:
229             abort(404)
230 
231         password = request.form.get('password')
232         set_diceware_password(user, password)
233         return redirect(url_for('admin.edit_user', user_id=user_id))
234 
235     @view.route('/ossec-test')
236     @admin_required
237     def ossec_test():
238         current_app.logger.error('This is a test OSSEC alert')
239         flash(gettext('Test alert sent. Please check your email.'),
240               'notification')
241         return redirect(url_for('admin.manage_config'))
242 
243     return view
244
```
--- 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/securedrop/journalist_app/admin.py b/securedrop/journalist_app/admin.py
--- a/securedrop/journalist_app/admin.py
+++ b/securedrop/journalist_app/admin.py
@@ -1,7 +1,5 @@
 # -*- coding: utf-8 -*-
 
-from PIL import Image
-
 import os
 
 from flask import (Blueprint, render_template, request, url_for, redirect, g,
@@ -33,19 +31,16 @@
         form = LogoForm()
         if form.validate_on_submit():
             f = form.logo.data
-            custom_logo_filepath = os.path.join(config.SECUREDROP_ROOT,
-                                                "static/i/custom_logo.png")
+            custom_logo_filepath = os.path.join(current_app.static_folder, 'i',
+                                                'custom_logo.png')
             try:
-                with Image.open(f) as im:
-                    im.thumbnail((500, 450), resample=3)
-                    im.save(custom_logo_filepath, "PNG")
+                f.save(custom_logo_filepath)
                 flash(gettext("Image updated."), "logo-success")
             except Exception:
                 flash("Unable to process the image file."
                       " Try another one.", "logo-error")
             finally:
                 return redirect(url_for("admin.manage_config"))
-
         else:
             for field, errors in form.errors.items():
                 for error in errors:
diff --git a/securedrop/journalist_app/forms.py b/securedrop/journalist_app/forms.py
--- a/securedrop/journalist_app/forms.py
+++ b/securedrop/journalist_app/forms.py
@@ -58,7 +58,6 @@
 class LogoForm(FlaskForm):
     logo = FileField(validators=[
         FileRequired(message=gettext('File required.')),
-        FileAllowed(['jpg', 'png', 'jpeg'],
-                    message=gettext("You can only upload JPG/JPEG"
-                                    " or PNG image files."))
+        FileAllowed(['png'],
+                    message=gettext("Upload images only."))
     ])

{"golden_diff": "diff --git a/securedrop/journalist_app/admin.py b/securedrop/journalist_app/admin.py\n--- a/securedrop/journalist_app/admin.py\n+++ b/securedrop/journalist_app/admin.py\n@@ -1,7 +1,5 @@\n # -*- coding: utf-8 -*-\n \n-from PIL import Image\n-\n import os\n \n from flask import (Blueprint, render_template, request, url_for, redirect, g,\n@@ -33,19 +31,16 @@\n         form = LogoForm()\n         if form.validate_on_submit():\n             f = form.logo.data\n-            custom_logo_filepath = os.path.join(config.SECUREDROP_ROOT,\n-                                                \"static/i/custom_logo.png\")\n+            custom_logo_filepath = os.path.join(current_app.static_folder, 'i',\n+                                                'custom_logo.png')\n             try:\n-                with Image.open(f) as im:\n-                    im.thumbnail((500, 450), resample=3)\n-                    im.save(custom_logo_filepath, \"PNG\")\n+                f.save(custom_logo_filepath)\n                 flash(gettext(\"Image updated.\"), \"logo-success\")\n             except Exception:\n                 flash(\"Unable to process the image file.\"\n                       \" Try another one.\", \"logo-error\")\n             finally:\n                 return redirect(url_for(\"admin.manage_config\"))\n-\n         else:\n             for field, errors in form.errors.items():\n                 for error in errors:\ndiff --git a/securedrop/journalist_app/forms.py b/securedrop/journalist_app/forms.py\n--- a/securedrop/journalist_app/forms.py\n+++ b/securedrop/journalist_app/forms.py\n@@ -58,7 +58,6 @@\n class LogoForm(FlaskForm):\n     logo = FileField(validators=[\n         FileRequired(message=gettext('File required.')),\n-        FileAllowed(['jpg', 'png', 'jpeg'],\n-                    message=gettext(\"You can only upload JPG/JPEG\"\n-                                    \" or PNG image files.\"))\n+        FileAllowed(['png'],\n+                    message=gettext(\"Upload images only.\"))\n     ])\n", "issue": "Debian package securedrop-app-code is not getting upgraded\n# Bug\r\n\r\n## Description\r\n\r\nIn my prod vm `securedrop-app-code` package is not getting upgraded.\r\n\r\n## Steps to Reproduce\r\n\r\nFollow the QA steps to fetch the latest packages from apt-test repo.\r\n\r\n## Expected Behavior\r\n\r\nWe should get the 0.7~RC1 packages.\r\n\r\n## Actual Behavior\r\n\r\n```\r\n$ dpkg -al | grep secure\r\nii  openssh-client                       1:6.6p1-2ubuntu2.10                        amd64        secure shell (SSH) client, for secure access to remote machines\r\nii  openssh-server                       1:6.6p1-2ubuntu2.10                        amd64        secure shell (SSH) server, for secure access from remote machines\r\nii  openssh-sftp-server                  1:6.6p1-2ubuntu2.10                        amd64        secure shell (SSH) sftp server module, for SFTP access from remote machines\r\nii  secure-delete                        3.1-6                                      amd64        tools to wipe files, free disk space, swap and memory\r\nii  securedrop-app-code                  0.6                                        amd64        Packages the SecureDrop application code pip dependencies and apparmor profiles. This package will put the apparmor profiles in enforce mode. This package does use pip to install the pip wheelhouse\r\nii  securedrop-config                    0.1.1+0.7.0~rc1                            all          Establishes baseline system state for running SecureDrop.\r\nii  securedrop-grsec                     4.4.115+r1                                 amd64        Metapackage providing a grsecurity-patched Linux kernel for use\r\nii  securedrop-keyring                   0.1.1+0.7.0~rc1                            amd64        Provides an apt keyring for SecureDrop-related packages, so the master signing key used for SecureDrop packages can be updated via apt.\r\nii  securedrop-ossec-agent               2.8.2+0.7.0~rc1                            amd64        Installs the securedrop pre-configured OSSEC agent\r\nii  ssh-import-id                        3.21-0ubuntu1                              all          securely retrieve an SSH public key and install it locally\r\n```\r\n\r\n```\r\n$ sudo apt-get upgrade\r\nReading package lists... Done\r\nBuilding dependency tree       \r\nReading state information... Done\r\nCalculating upgrade... Done\r\nThe following packages have been kept back:\r\n  securedrop-app-code\r\n0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.\r\n\r\n```\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\nfrom flask_babel import lazy_gettext as gettext\nfrom flask_wtf import FlaskForm\nfrom flask_wtf.file import FileField, FileAllowed, FileRequired\nfrom wtforms import (TextAreaField, TextField, BooleanField, HiddenField,\n                     ValidationError)\nfrom wtforms.validators import InputRequired, Optional\n\nfrom models import Journalist\n\n\ndef otp_secret_validation(form, field):\n    strip_whitespace = field.data.replace(' ', '')\n    if len(strip_whitespace) != 40:\n        raise ValidationError(gettext(\n            'HOTP secrets are 40 characters long - '\n            'you have entered {num_chars}.'.format(\n                num_chars=len(strip_whitespace)\n            )))\n\n\ndef minimum_length_validation(form, field):\n    if len(field.data) < Journalist.MIN_USERNAME_LEN:\n        raise ValidationError(\n            gettext('Field must be at least {min_chars} '\n                    'characters long but only got '\n                    '{num_chars}.'.format(\n                        min_chars=Journalist.MIN_USERNAME_LEN,\n                        num_chars=len(field.data))))\n\n\nclass NewUserForm(FlaskForm):\n    username = TextField('username', validators=[\n        InputRequired(message=gettext('This field is required.')),\n        minimum_length_validation\n    ])\n    password = HiddenField('password')\n    is_admin = BooleanField('is_admin')\n    is_hotp = BooleanField('is_hotp')\n    otp_secret = TextField('otp_secret', validators=[\n        otp_secret_validation,\n        Optional()\n    ])\n\n\nclass ReplyForm(FlaskForm):\n    message = TextAreaField(\n        u'Message',\n        id=\"content-area\",\n        validators=[\n            InputRequired(message=gettext(\n                'You cannot send an empty reply.')),\n        ],\n    )\n\n\nclass LogoForm(FlaskForm):\n    logo = FileField(validators=[\n        FileRequired(message=gettext('File required.')),\n        FileAllowed(['jpg', 'png', 'jpeg'],\n                    message=gettext(\"You can only upload JPG/JPEG\"\n                                    \" or PNG image files.\"))\n    ])\n", "path": "securedrop/journalist_app/forms.py"}, {"content": "# -*- coding: utf-8 -*-\n\nfrom PIL import Image\n\nimport os\n\nfrom flask import (Blueprint, render_template, request, url_for, redirect, g,\n                   current_app, flash, abort)\nfrom flask_babel import gettext\nfrom sqlalchemy.exc import IntegrityError\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom db import db\nfrom models import Journalist, InvalidUsernameException, PasswordError\nfrom journalist_app.decorators import admin_required\nfrom journalist_app.utils import (make_password, commit_account_changes,\n                                  set_diceware_password, validate_hotp_secret)\nfrom journalist_app.forms import LogoForm, NewUserForm\n\n\ndef make_blueprint(config):\n    view = Blueprint('admin', __name__)\n\n    @view.route('/', methods=('GET', 'POST'))\n    @admin_required\n    def index():\n        users = Journalist.query.all()\n        return render_template(\"admin.html\", users=users)\n\n    @view.route('/config', methods=('GET', 'POST'))\n    @admin_required\n    def manage_config():\n        form = LogoForm()\n        if form.validate_on_submit():\n            f = form.logo.data\n            custom_logo_filepath = os.path.join(config.SECUREDROP_ROOT,\n                                                \"static/i/custom_logo.png\")\n            try:\n                with Image.open(f) as im:\n                    im.thumbnail((500, 450), resample=3)\n                    im.save(custom_logo_filepath, \"PNG\")\n                flash(gettext(\"Image updated.\"), \"logo-success\")\n            except Exception:\n                flash(\"Unable to process the image file.\"\n                      \" Try another one.\", \"logo-error\")\n            finally:\n                return redirect(url_for(\"admin.manage_config\"))\n\n        else:\n            for field, errors in form.errors.items():\n                for error in errors:\n                    flash(error, \"logo-error\")\n            return render_template(\"config.html\", form=form)\n\n    @view.route('/add', methods=('GET', 'POST'))\n    @admin_required\n    def add_user():\n        form = NewUserForm()\n        if form.validate_on_submit():\n            form_valid = True\n            username = request.form['username']\n            password = request.form['password']\n            is_admin = bool(request.form.get('is_admin'))\n\n            try:\n                otp_secret = None\n                if request.form.get('is_hotp', False):\n                    otp_secret = request.form.get('otp_secret', '')\n                new_user = Journalist(username=username,\n                                      password=password,\n                                      is_admin=is_admin,\n                                      otp_secret=otp_secret)\n                db.session.add(new_user)\n                db.session.commit()\n            except PasswordError:\n                flash(gettext(\n                      'There was an error with the autogenerated password. '\n                      'User not created. Please try again.'), 'error')\n                form_valid = False\n            except InvalidUsernameException as e:\n                form_valid = False\n                flash('Invalid username: ' + str(e), \"error\")\n            except IntegrityError as e:\n                db.session.rollback()\n                form_valid = False\n                if \"UNIQUE constraint failed: journalists.username\" in str(e):\n                    flash(gettext('Username \"{user}\" already taken.'.format(\n                        user=username)), \"error\")\n                else:\n                    flash(gettext(\"An error occurred saving this user\"\n                                  \" to the database.\"\n                                  \" Please inform your administrator.\"),\n                          \"error\")\n                    current_app.logger.error(\"Adding user \"\n                                             \"'{}' failed: {}\".format(\n                                                 username, e))\n\n            if form_valid:\n                return redirect(url_for('admin.new_user_two_factor',\n                                        uid=new_user.id))\n\n        return render_template(\"admin_add_user.html\",\n                               password=make_password(config),\n                               form=form)\n\n    @view.route('/2fa', methods=('GET', 'POST'))\n    @admin_required\n    def new_user_two_factor():\n        user = Journalist.query.get(request.args['uid'])\n\n        if request.method == 'POST':\n            token = request.form['token']\n            if user.verify_token(token):\n                flash(gettext(\n                    \"Token in two-factor authentication \"\n                    \"accepted for user {user}.\").format(\n                        user=user.username),\n                    \"notification\")\n                return redirect(url_for(\"admin.index\"))\n            else:\n                flash(gettext(\n                    \"Could not verify token in two-factor authentication.\"),\n                      \"error\")\n\n        return render_template(\"admin_new_user_two_factor.html\", user=user)\n\n    @view.route('/reset-2fa-totp', methods=['POST'])\n    @admin_required\n    def reset_two_factor_totp():\n        uid = request.form['uid']\n        user = Journalist.query.get(uid)\n        user.is_totp = True\n        user.regenerate_totp_shared_secret()\n        db.session.commit()\n        return redirect(url_for('admin.new_user_two_factor', uid=uid))\n\n    @view.route('/reset-2fa-hotp', methods=['POST'])\n    @admin_required\n    def reset_two_factor_hotp():\n        uid = request.form['uid']\n        otp_secret = request.form.get('otp_secret', None)\n        if otp_secret:\n            user = Journalist.query.get(uid)\n            if not validate_hotp_secret(user, otp_secret):\n                return render_template('admin_edit_hotp_secret.html', uid=uid)\n            db.session.commit()\n            return redirect(url_for('admin.new_user_two_factor', uid=uid))\n        else:\n            return render_template('admin_edit_hotp_secret.html', uid=uid)\n\n    @view.route('/edit/<int:user_id>', methods=('GET', 'POST'))\n    @admin_required\n    def edit_user(user_id):\n        user = Journalist.query.get(user_id)\n\n        if request.method == 'POST':\n            if request.form.get('username', None):\n                new_username = request.form['username']\n\n                try:\n                    Journalist.check_username_acceptable(new_username)\n                except InvalidUsernameException as e:\n                    flash('Invalid username: ' + str(e), 'error')\n                    return redirect(url_for(\"admin.edit_user\",\n                                            user_id=user_id))\n\n                if new_username == user.username:\n                    pass\n                elif Journalist.query.filter_by(\n                        username=new_username).one_or_none():\n                    flash(gettext(\n                        'Username \"{user}\" already taken.').format(\n                            user=new_username),\n                        \"error\")\n                    return redirect(url_for(\"admin.edit_user\",\n                                            user_id=user_id))\n                else:\n                    user.username = new_username\n\n            user.is_admin = bool(request.form.get('is_admin'))\n\n            commit_account_changes(user)\n\n        password = make_password(config)\n        return render_template(\"edit_account.html\", user=user,\n                               password=password)\n\n    @view.route('/edit/<int:user_id>/new-password', methods=('POST',))\n    @admin_required\n    def set_password(user_id):\n        try:\n            user = Journalist.query.get(user_id)\n        except NoResultFound:\n            abort(404)\n\n        password = request.form.get('password')\n        set_diceware_password(user, password)\n        return redirect(url_for('admin.edit_user', user_id=user_id))\n\n    @view.route('/delete/<int:user_id>', methods=('POST',))\n    @admin_required\n    def delete_user(user_id):\n        user = Journalist.query.get(user_id)\n        if user_id == g.user.id:\n            # Do not flash because the interface already has safe guards.\n            # It can only happen by manually crafting a POST request\n            current_app.logger.error(\n                \"Admin {} tried to delete itself\".format(g.user.username))\n            abort(403)\n        elif user:\n            db.session.delete(user)\n            db.session.commit()\n            flash(gettext(\"Deleted user '{user}'\").format(\n                user=user.username), \"notification\")\n        else:\n            current_app.logger.error(\n                \"Admin {} tried to delete nonexistent user with pk={}\".format(\n                    g.user.username, user_id))\n            abort(404)\n\n        return redirect(url_for('admin.index'))\n\n    @view.route('/edit/<int:user_id>/new-password', methods=('POST',))\n    @admin_required\n    def new_password(user_id):\n        try:\n            user = Journalist.query.get(user_id)\n        except NoResultFound:\n            abort(404)\n\n        password = request.form.get('password')\n        set_diceware_password(user, password)\n        return redirect(url_for('admin.edit_user', user_id=user_id))\n\n    @view.route('/ossec-test')\n    @admin_required\n    def ossec_test():\n        current_app.logger.error('This is a test OSSEC alert')\n        flash(gettext('Test alert sent. Please check your email.'),\n              'notification')\n        return redirect(url_for('admin.manage_config'))\n\n    return view\n", "path": "securedrop/journalist_app/admin.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\nfrom flask_babel import lazy_gettext as gettext\nfrom flask_wtf import FlaskForm\nfrom flask_wtf.file import FileField, FileAllowed, FileRequired\nfrom wtforms import (TextAreaField, TextField, BooleanField, HiddenField,\n                     ValidationError)\nfrom wtforms.validators import InputRequired, Optional\n\nfrom models import Journalist\n\n\ndef otp_secret_validation(form, field):\n    strip_whitespace = field.data.replace(' ', '')\n    if len(strip_whitespace) != 40:\n        raise ValidationError(gettext(\n            'HOTP secrets are 40 characters long - '\n            'you have entered {num_chars}.'.format(\n                num_chars=len(strip_whitespace)\n            )))\n\n\ndef minimum_length_validation(form, field):\n    if len(field.data) < Journalist.MIN_USERNAME_LEN:\n        raise ValidationError(\n            gettext('Field must be at least {min_chars} '\n                    'characters long but only got '\n                    '{num_chars}.'.format(\n                        min_chars=Journalist.MIN_USERNAME_LEN,\n                        num_chars=len(field.data))))\n\n\nclass NewUserForm(FlaskForm):\n    username = TextField('username', validators=[\n        InputRequired(message=gettext('This field is required.')),\n        minimum_length_validation\n    ])\n    password = HiddenField('password')\n    is_admin = BooleanField('is_admin')\n    is_hotp = BooleanField('is_hotp')\n    otp_secret = TextField('otp_secret', validators=[\n        otp_secret_validation,\n        Optional()\n    ])\n\n\nclass ReplyForm(FlaskForm):\n    message = TextAreaField(\n        u'Message',\n        id=\"content-area\",\n        validators=[\n            InputRequired(message=gettext(\n                'You cannot send an empty reply.')),\n        ],\n    )\n\n\nclass LogoForm(FlaskForm):\n    logo = FileField(validators=[\n        FileRequired(message=gettext('File required.')),\n        FileAllowed(['png'],\n                    message=gettext(\"Upload images only.\"))\n    ])\n", "path": "securedrop/journalist_app/forms.py"}, {"content": "# -*- coding: utf-8 -*-\n\nimport os\n\nfrom flask import (Blueprint, render_template, request, url_for, redirect, g,\n                   current_app, flash, abort)\nfrom flask_babel import gettext\nfrom sqlalchemy.exc import IntegrityError\nfrom sqlalchemy.orm.exc import NoResultFound\n\nfrom db import db\nfrom models import Journalist, InvalidUsernameException, PasswordError\nfrom journalist_app.decorators import admin_required\nfrom journalist_app.utils import (make_password, commit_account_changes,\n                                  set_diceware_password, validate_hotp_secret)\nfrom journalist_app.forms import LogoForm, NewUserForm\n\n\ndef make_blueprint(config):\n    view = Blueprint('admin', __name__)\n\n    @view.route('/', methods=('GET', 'POST'))\n    @admin_required\n    def index():\n        users = Journalist.query.all()\n        return render_template(\"admin.html\", users=users)\n\n    @view.route('/config', methods=('GET', 'POST'))\n    @admin_required\n    def manage_config():\n        form = LogoForm()\n        if form.validate_on_submit():\n            f = form.logo.data\n            custom_logo_filepath = os.path.join(current_app.static_folder, 'i',\n                                                'custom_logo.png')\n            try:\n                f.save(custom_logo_filepath)\n                flash(gettext(\"Image updated.\"), \"logo-success\")\n            except Exception:\n                flash(\"Unable to process the image file.\"\n                      \" Try another one.\", \"logo-error\")\n            finally:\n                return redirect(url_for(\"admin.manage_config\"))\n        else:\n            for field, errors in form.errors.items():\n                for error in errors:\n                    flash(error, \"logo-error\")\n            return render_template(\"config.html\", form=form)\n\n    @view.route('/add', methods=('GET', 'POST'))\n    @admin_required\n    def add_user():\n        form = NewUserForm()\n        if form.validate_on_submit():\n            form_valid = True\n            username = request.form['username']\n            password = request.form['password']\n            is_admin = bool(request.form.get('is_admin'))\n\n            try:\n                otp_secret = None\n                if request.form.get('is_hotp', False):\n                    otp_secret = request.form.get('otp_secret', '')\n                new_user = Journalist(username=username,\n                                      password=password,\n                                      is_admin=is_admin,\n                                      otp_secret=otp_secret)\n                db.session.add(new_user)\n                db.session.commit()\n            except PasswordError:\n                flash(gettext(\n                      'There was an error with the autogenerated password. '\n                      'User not created. Please try again.'), 'error')\n                form_valid = False\n            except InvalidUsernameException as e:\n                form_valid = False\n                flash('Invalid username: ' + str(e), \"error\")\n            except IntegrityError as e:\n                db.session.rollback()\n                form_valid = False\n                if \"UNIQUE constraint failed: journalists.username\" in str(e):\n                    flash(gettext('Username \"{user}\" already taken.'.format(\n                        user=username)), \"error\")\n                else:\n                    flash(gettext(\"An error occurred saving this user\"\n                                  \" to the database.\"\n                                  \" Please inform your administrator.\"),\n                          \"error\")\n                    current_app.logger.error(\"Adding user \"\n                                             \"'{}' failed: {}\".format(\n                                                 username, e))\n\n            if form_valid:\n                return redirect(url_for('admin.new_user_two_factor',\n                                        uid=new_user.id))\n\n        return render_template(\"admin_add_user.html\",\n                               password=make_password(config),\n                               form=form)\n\n    @view.route('/2fa', methods=('GET', 'POST'))\n    @admin_required\n    def new_user_two_factor():\n        user = Journalist.query.get(request.args['uid'])\n\n        if request.method == 'POST':\n            token = request.form['token']\n            if user.verify_token(token):\n                flash(gettext(\n                    \"Token in two-factor authentication \"\n                    \"accepted for user {user}.\").format(\n                        user=user.username),\n                    \"notification\")\n                return redirect(url_for(\"admin.index\"))\n            else:\n                flash(gettext(\n                    \"Could not verify token in two-factor authentication.\"),\n                      \"error\")\n\n        return render_template(\"admin_new_user_two_factor.html\", user=user)\n\n    @view.route('/reset-2fa-totp', methods=['POST'])\n    @admin_required\n    def reset_two_factor_totp():\n        uid = request.form['uid']\n        user = Journalist.query.get(uid)\n        user.is_totp = True\n        user.regenerate_totp_shared_secret()\n        db.session.commit()\n        return redirect(url_for('admin.new_user_two_factor', uid=uid))\n\n    @view.route('/reset-2fa-hotp', methods=['POST'])\n    @admin_required\n    def reset_two_factor_hotp():\n        uid = request.form['uid']\n        otp_secret = request.form.get('otp_secret', None)\n        if otp_secret:\n            user = Journalist.query.get(uid)\n            if not validate_hotp_secret(user, otp_secret):\n                return render_template('admin_edit_hotp_secret.html', uid=uid)\n            db.session.commit()\n            return redirect(url_for('admin.new_user_two_factor', uid=uid))\n        else:\n            return render_template('admin_edit_hotp_secret.html', uid=uid)\n\n    @view.route('/edit/<int:user_id>', methods=('GET', 'POST'))\n    @admin_required\n    def edit_user(user_id):\n        user = Journalist.query.get(user_id)\n\n        if request.method == 'POST':\n            if request.form.get('username', None):\n                new_username = request.form['username']\n\n                try:\n                    Journalist.check_username_acceptable(new_username)\n                except InvalidUsernameException as e:\n                    flash('Invalid username: ' + str(e), 'error')\n                    return redirect(url_for(\"admin.edit_user\",\n                                            user_id=user_id))\n\n                if new_username == user.username:\n                    pass\n                elif Journalist.query.filter_by(\n                        username=new_username).one_or_none():\n                    flash(gettext(\n                        'Username \"{user}\" already taken.').format(\n                            user=new_username),\n                        \"error\")\n                    return redirect(url_for(\"admin.edit_user\",\n                                            user_id=user_id))\n                else:\n                    user.username = new_username\n\n            user.is_admin = bool(request.form.get('is_admin'))\n\n            commit_account_changes(user)\n\n        password = make_password(config)\n        return render_template(\"edit_account.html\", user=user,\n                               password=password)\n\n    @view.route('/edit/<int:user_id>/new-password', methods=('POST',))\n    @admin_required\n    def set_password(user_id):\n        try:\n            user = Journalist.query.get(user_id)\n        except NoResultFound:\n            abort(404)\n\n        password = request.form.get('password')\n        set_diceware_password(user, password)\n        return redirect(url_for('admin.edit_user', user_id=user_id))\n\n    @view.route('/delete/<int:user_id>', methods=('POST',))\n    @admin_required\n    def delete_user(user_id):\n        user = Journalist.query.get(user_id)\n        if user_id == g.user.id:\n            # Do not flash because the interface already has safe guards.\n            # It can only happen by manually crafting a POST request\n            current_app.logger.error(\n                \"Admin {} tried to delete itself\".format(g.user.username))\n            abort(403)\n        elif user:\n            db.session.delete(user)\n            db.session.commit()\n            flash(gettext(\"Deleted user '{user}'\").format(\n                user=user.username), \"notification\")\n        else:\n            current_app.logger.error(\n                \"Admin {} tried to delete nonexistent user with pk={}\".format(\n                    g.user.username, user_id))\n            abort(404)\n\n        return redirect(url_for('admin.index'))\n\n    @view.route('/edit/<int:user_id>/new-password', methods=('POST',))\n    @admin_required\n    def new_password(user_id):\n        try:\n            user = Journalist.query.get(user_id)\n        except NoResultFound:\n            abort(404)\n\n        password = request.form.get('password')\n        set_diceware_password(user, password)\n        return redirect(url_for('admin.edit_user', user_id=user_id))\n\n    @view.route('/ossec-test')\n    @admin_required\n    def ossec_test():\n        current_app.logger.error('This is a test OSSEC alert')\n        flash(gettext('Test alert sent. Please check your email.'),\n              'notification')\n        return redirect(url_for('admin.manage_config'))\n\n    return view\n", "path": "securedrop/journalist_app/admin.py"}]}