We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Members of `/usergroup/read-only` can edit covers
<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->
Members of the `read-only` usergroup are still able to upload cover images and URLs via the manage covers modal.

### Evidence / Screenshot (if possible)

### Relevant url?
<!-- `https://openlibrary.org/...` -->

### Steps to Reproduce
<!-- What steps caused you to find the bug? -->
While logged-in as a member of the read-only group:
1. Go to any book page
2. Attempt to add a new cover via the manage covers modal

<!-- What actually happened after these steps? What did you expect to happen? -->
* Actual: The cover is updated.
* Expected: The update fails gracefully.

### Details

- **Logged in (Y/N)?**Y
- **Browser type/version?**Any
- **Operating system?**Any
- **Environment (prod/dev/local)?** prod
<!-- If not sure, put prod -->

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->
Check for membership in the read-only group before persisting the new cover.  If submitter is a member, do not store the image.

### Related files
<!-- Files related to this issue; this is super useful for new contributors who might want to help! If you're not sure, leave this blank; a maintainer will add them. -->
POST handler for manage cover form: https://github.com/internetarchive/openlibrary/blob/master/openlibrary/plugins/upstream/covers.py#L34-L53


### Stakeholders
<!-- @ tag stakeholders of this bug -->
@seabelis 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `openlibrary/plugins/upstream/covers.py`
Content:
```
1 """Handle book cover/author photo upload.
2 """
3 from logging import getLogger
4 
5 import requests
6 import web
7 from io import BytesIO
8 
9 from infogami.utils import delegate
10 from infogami.utils.view import safeint
11 from openlibrary import accounts
12 from openlibrary.plugins.upstream.models import Image
13 from openlibrary.plugins.upstream.utils import (
14     get_coverstore_url,
15     get_coverstore_public_url,
16     render_template,
17 )
18 
19 logger = getLogger("openlibrary.plugins.upstream.covers")
20 
21 
22 def setup():
23     pass
24 
25 
26 class add_cover(delegate.page):
27     path = r"(/books/OL\d+M)/add-cover"
28     cover_category = "b"
29 
30     def GET(self, key):
31         book = web.ctx.site.get(key)
32         return render_template('covers/add', book)
33 
34     def POST(self, key):
35         book = web.ctx.site.get(key)
36         if not book:
37             raise web.notfound("")
38 
39         i = web.input(file={}, url="")
40 
41         # remove references to field storage objects
42         web.ctx.pop("_fieldstorage", None)
43 
44         data = self.upload(key, i)
45 
46         if coverid := data.get('id'):
47             if isinstance(i.url, bytes):
48                 i.url = i.url.decode("utf-8")
49             self.save(book, coverid, url=i.url)
50             cover = Image(web.ctx.site, "b", coverid)
51             return render_template("covers/saved", cover)
52         else:
53             return render_template("covers/add", book, {'url': i.url}, data)
54 
55     def upload(self, key, i):
56         """Uploads a cover to coverstore and returns the response."""
57         olid = key.split("/")[-1]
58 
59         if i.file is not None and hasattr(i.file, 'value'):
60             data = i.file.value
61         else:
62             data = None
63 
64         if i.url and i.url.strip() == "https://":
65             i.url = ""
66 
67         user = accounts.get_current_user()
68         params = {
69             "author": user and user.key,
70             "source_url": i.url,
71             "olid": olid,
72             "ip": web.ctx.ip,
73         }
74 
75         upload_url = f'{get_coverstore_url()}/{self.cover_category}/upload2'
76 
77         if upload_url.startswith("//"):
78             upload_url = "http:" + upload_url
79 
80         try:
81             files = {'data': BytesIO(data)}
82             response = requests.post(upload_url, data=params, files=files)
83             return web.storage(response.json())
84         except requests.HTTPError as e:
85             logger.exception("Covers upload failed")
86             return web.storage({'error': str(e)})
87 
88     def save(self, book, coverid, url=None):
89         book.covers = [coverid] + [cover.id for cover in book.get_covers()]
90         book._save(
91             f'{get_coverstore_public_url()}/b/id/{coverid}-S.jpg',
92             action="add-cover",
93             data={"url": url},
94         )
95 
96 
97 class add_work_cover(add_cover):
98     path = r"(/works/OL\d+W)/add-cover"
99     cover_category = "w"
100 
101     def upload(self, key, i):
102         if "coverid" in i and safeint(i.coverid):
103             return web.storage(id=int(i.coverid))
104         else:
105             return add_cover.upload(self, key, i)
106 
107 
108 class add_photo(add_cover):
109     path = r"(/authors/OL\d+A)/add-photo"
110     cover_category = "a"
111 
112     def save(self, author, photoid, url=None):
113         author.photos = [photoid] + [photo.id for photo in author.get_photos()]
114         author._save("Added new photo", action="add-photo", data={"url": url})
115 
116 
117 class manage_covers(delegate.page):
118     path = r"(/books/OL\d+M)/manage-covers"
119 
120     def GET(self, key):
121         book = web.ctx.site.get(key)
122         if not book:
123             raise web.notfound()
124         return render_template("covers/manage", key, self.get_images(book))
125 
126     def get_images(self, book):
127         return book.get_covers()
128 
129     def get_image(self, book):
130         return book.get_cover()
131 
132     def save_images(self, book, covers):
133         book.covers = covers
134         book._save('Update covers')
135 
136     def POST(self, key):
137         book = web.ctx.site.get(key)
138         if not book:
139             raise web.notfound()
140 
141         images = web.input(image=[]).image
142         if '-' in images:
143             images = [int(id) for id in images[: images.index('-')]]
144             self.save_images(book, images)
145             return render_template("covers/saved", self.get_image(book), showinfo=False)
146         else:
147             # ERROR
148             pass
149 
150 
151 class manage_work_covers(manage_covers):
152     path = r"(/works/OL\d+W)/manage-covers"
153 
154 
155 class manage_photos(manage_covers):
156     path = r"(/authors/OL\d+A)/manage-photos"
157 
158     def get_images(self, author):
159         return author.get_photos()
160 
161     def get_image(self, author):
162         return author.get_photo()
163 
164     def save_images(self, author, photos):
165         author.photos = photos
166         author._save('Update photos')
167
```
--- 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/openlibrary/plugins/upstream/covers.py b/openlibrary/plugins/upstream/covers.py
--- a/openlibrary/plugins/upstream/covers.py
+++ b/openlibrary/plugins/upstream/covers.py
@@ -36,6 +36,10 @@
         if not book:
             raise web.notfound("")
 
+        user = accounts.get_current_user()
+        if user and user.is_read_only():
+            raise web.forbidden(message="Patron not permitted to upload images")
+
         i = web.input(file={}, url="")
 
         # remove references to field storage objects

{"golden_diff": "diff --git a/openlibrary/plugins/upstream/covers.py b/openlibrary/plugins/upstream/covers.py\n--- a/openlibrary/plugins/upstream/covers.py\n+++ b/openlibrary/plugins/upstream/covers.py\n@@ -36,6 +36,10 @@\n         if not book:\n             raise web.notfound(\"\")\n \n+        user = accounts.get_current_user()\n+        if user and user.is_read_only():\n+            raise web.forbidden(message=\"Patron not permitted to upload images\")\n+\n         i = web.input(file={}, url=\"\")\n \n         # remove references to field storage objects\n", "issue": "Members of `/usergroup/read-only` can edit covers\n<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->\r\nMembers of the `read-only` usergroup are still able to upload cover images and URLs via the manage covers modal.\r\n\r\n### Evidence / Screenshot (if possible)\r\n\r\n### Relevant url?\r\n<!-- `https://openlibrary.org/...` -->\r\n\r\n### Steps to Reproduce\r\n<!-- What steps caused you to find the bug? -->\r\nWhile logged-in as a member of the read-only group:\r\n1. Go to any book page\r\n2. Attempt to add a new cover via the manage covers modal\r\n\r\n<!-- What actually happened after these steps? What did you expect to happen? -->\r\n* Actual: The cover is updated.\r\n* Expected: The update fails gracefully.\r\n\r\n### Details\r\n\r\n- **Logged in (Y/N)?**Y\r\n- **Browser type/version?**Any\r\n- **Operating system?**Any\r\n- **Environment (prod/dev/local)?** prod\r\n<!-- If not sure, put prod -->\r\n\r\n### Proposal & Constraints\r\n<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->\r\nCheck for membership in the read-only group before persisting the new cover.  If submitter is a member, do not store the image.\r\n\r\n### Related files\r\n<!-- Files related to this issue; this is super useful for new contributors who might want to help! If you're not sure, leave this blank; a maintainer will add them. -->\r\nPOST handler for manage cover form: https://github.com/internetarchive/openlibrary/blob/master/openlibrary/plugins/upstream/covers.py#L34-L53\r\n\r\n\r\n### Stakeholders\r\n<!-- @ tag stakeholders of this bug -->\r\n@seabelis \n", "before_files": [{"content": "\"\"\"Handle book cover/author photo upload.\n\"\"\"\nfrom logging import getLogger\n\nimport requests\nimport web\nfrom io import BytesIO\n\nfrom infogami.utils import delegate\nfrom infogami.utils.view import safeint\nfrom openlibrary import accounts\nfrom openlibrary.plugins.upstream.models import Image\nfrom openlibrary.plugins.upstream.utils import (\n    get_coverstore_url,\n    get_coverstore_public_url,\n    render_template,\n)\n\nlogger = getLogger(\"openlibrary.plugins.upstream.covers\")\n\n\ndef setup():\n    pass\n\n\nclass add_cover(delegate.page):\n    path = r\"(/books/OL\\d+M)/add-cover\"\n    cover_category = \"b\"\n\n    def GET(self, key):\n        book = web.ctx.site.get(key)\n        return render_template('covers/add', book)\n\n    def POST(self, key):\n        book = web.ctx.site.get(key)\n        if not book:\n            raise web.notfound(\"\")\n\n        i = web.input(file={}, url=\"\")\n\n        # remove references to field storage objects\n        web.ctx.pop(\"_fieldstorage\", None)\n\n        data = self.upload(key, i)\n\n        if coverid := data.get('id'):\n            if isinstance(i.url, bytes):\n                i.url = i.url.decode(\"utf-8\")\n            self.save(book, coverid, url=i.url)\n            cover = Image(web.ctx.site, \"b\", coverid)\n            return render_template(\"covers/saved\", cover)\n        else:\n            return render_template(\"covers/add\", book, {'url': i.url}, data)\n\n    def upload(self, key, i):\n        \"\"\"Uploads a cover to coverstore and returns the response.\"\"\"\n        olid = key.split(\"/\")[-1]\n\n        if i.file is not None and hasattr(i.file, 'value'):\n            data = i.file.value\n        else:\n            data = None\n\n        if i.url and i.url.strip() == \"https://\":\n            i.url = \"\"\n\n        user = accounts.get_current_user()\n        params = {\n            \"author\": user and user.key,\n            \"source_url\": i.url,\n            \"olid\": olid,\n            \"ip\": web.ctx.ip,\n        }\n\n        upload_url = f'{get_coverstore_url()}/{self.cover_category}/upload2'\n\n        if upload_url.startswith(\"//\"):\n            upload_url = \"http:\" + upload_url\n\n        try:\n            files = {'data': BytesIO(data)}\n            response = requests.post(upload_url, data=params, files=files)\n            return web.storage(response.json())\n        except requests.HTTPError as e:\n            logger.exception(\"Covers upload failed\")\n            return web.storage({'error': str(e)})\n\n    def save(self, book, coverid, url=None):\n        book.covers = [coverid] + [cover.id for cover in book.get_covers()]\n        book._save(\n            f'{get_coverstore_public_url()}/b/id/{coverid}-S.jpg',\n            action=\"add-cover\",\n            data={\"url\": url},\n        )\n\n\nclass add_work_cover(add_cover):\n    path = r\"(/works/OL\\d+W)/add-cover\"\n    cover_category = \"w\"\n\n    def upload(self, key, i):\n        if \"coverid\" in i and safeint(i.coverid):\n            return web.storage(id=int(i.coverid))\n        else:\n            return add_cover.upload(self, key, i)\n\n\nclass add_photo(add_cover):\n    path = r\"(/authors/OL\\d+A)/add-photo\"\n    cover_category = \"a\"\n\n    def save(self, author, photoid, url=None):\n        author.photos = [photoid] + [photo.id for photo in author.get_photos()]\n        author._save(\"Added new photo\", action=\"add-photo\", data={\"url\": url})\n\n\nclass manage_covers(delegate.page):\n    path = r\"(/books/OL\\d+M)/manage-covers\"\n\n    def GET(self, key):\n        book = web.ctx.site.get(key)\n        if not book:\n            raise web.notfound()\n        return render_template(\"covers/manage\", key, self.get_images(book))\n\n    def get_images(self, book):\n        return book.get_covers()\n\n    def get_image(self, book):\n        return book.get_cover()\n\n    def save_images(self, book, covers):\n        book.covers = covers\n        book._save('Update covers')\n\n    def POST(self, key):\n        book = web.ctx.site.get(key)\n        if not book:\n            raise web.notfound()\n\n        images = web.input(image=[]).image\n        if '-' in images:\n            images = [int(id) for id in images[: images.index('-')]]\n            self.save_images(book, images)\n            return render_template(\"covers/saved\", self.get_image(book), showinfo=False)\n        else:\n            # ERROR\n            pass\n\n\nclass manage_work_covers(manage_covers):\n    path = r\"(/works/OL\\d+W)/manage-covers\"\n\n\nclass manage_photos(manage_covers):\n    path = r\"(/authors/OL\\d+A)/manage-photos\"\n\n    def get_images(self, author):\n        return author.get_photos()\n\n    def get_image(self, author):\n        return author.get_photo()\n\n    def save_images(self, author, photos):\n        author.photos = photos\n        author._save('Update photos')\n", "path": "openlibrary/plugins/upstream/covers.py"}], "after_files": [{"content": "\"\"\"Handle book cover/author photo upload.\n\"\"\"\nfrom logging import getLogger\n\nimport requests\nimport web\nfrom io import BytesIO\n\nfrom infogami.utils import delegate\nfrom infogami.utils.view import safeint\nfrom openlibrary import accounts\nfrom openlibrary.plugins.upstream.models import Image\nfrom openlibrary.plugins.upstream.utils import (\n    get_coverstore_url,\n    get_coverstore_public_url,\n    render_template,\n)\n\nlogger = getLogger(\"openlibrary.plugins.upstream.covers\")\n\n\ndef setup():\n    pass\n\n\nclass add_cover(delegate.page):\n    path = r\"(/books/OL\\d+M)/add-cover\"\n    cover_category = \"b\"\n\n    def GET(self, key):\n        book = web.ctx.site.get(key)\n        return render_template('covers/add', book)\n\n    def POST(self, key):\n        book = web.ctx.site.get(key)\n        if not book:\n            raise web.notfound(\"\")\n\n        user = accounts.get_current_user()\n        if user and user.is_read_only():\n            raise web.forbidden(message=\"Patron not permitted to upload images\")\n\n        i = web.input(file={}, url=\"\")\n\n        # remove references to field storage objects\n        web.ctx.pop(\"_fieldstorage\", None)\n\n        data = self.upload(key, i)\n\n        if coverid := data.get('id'):\n            if isinstance(i.url, bytes):\n                i.url = i.url.decode(\"utf-8\")\n            self.save(book, coverid, url=i.url)\n            cover = Image(web.ctx.site, \"b\", coverid)\n            return render_template(\"covers/saved\", cover)\n        else:\n            return render_template(\"covers/add\", book, {'url': i.url}, data)\n\n    def upload(self, key, i):\n        \"\"\"Uploads a cover to coverstore and returns the response.\"\"\"\n        olid = key.split(\"/\")[-1]\n\n        if i.file is not None and hasattr(i.file, 'value'):\n            data = i.file.value\n        else:\n            data = None\n\n        if i.url and i.url.strip() == \"https://\":\n            i.url = \"\"\n\n        user = accounts.get_current_user()\n        params = {\n            \"author\": user and user.key,\n            \"source_url\": i.url,\n            \"olid\": olid,\n            \"ip\": web.ctx.ip,\n        }\n\n        upload_url = f'{get_coverstore_url()}/{self.cover_category}/upload2'\n\n        if upload_url.startswith(\"//\"):\n            upload_url = \"http:\" + upload_url\n\n        try:\n            files = {'data': BytesIO(data)}\n            response = requests.post(upload_url, data=params, files=files)\n            return web.storage(response.json())\n        except requests.HTTPError as e:\n            logger.exception(\"Covers upload failed\")\n            return web.storage({'error': str(e)})\n\n    def save(self, book, coverid, url=None):\n        book.covers = [coverid] + [cover.id for cover in book.get_covers()]\n        book._save(\n            f'{get_coverstore_public_url()}/b/id/{coverid}-S.jpg',\n            action=\"add-cover\",\n            data={\"url\": url},\n        )\n\n\nclass add_work_cover(add_cover):\n    path = r\"(/works/OL\\d+W)/add-cover\"\n    cover_category = \"w\"\n\n    def upload(self, key, i):\n        if \"coverid\" in i and safeint(i.coverid):\n            return web.storage(id=int(i.coverid))\n        else:\n            return add_cover.upload(self, key, i)\n\n\nclass add_photo(add_cover):\n    path = r\"(/authors/OL\\d+A)/add-photo\"\n    cover_category = \"a\"\n\n    def save(self, author, photoid, url=None):\n        author.photos = [photoid] + [photo.id for photo in author.get_photos()]\n        author._save(\"Added new photo\", action=\"add-photo\", data={\"url\": url})\n\n\nclass manage_covers(delegate.page):\n    path = r\"(/books/OL\\d+M)/manage-covers\"\n\n    def GET(self, key):\n        book = web.ctx.site.get(key)\n        if not book:\n            raise web.notfound()\n        return render_template(\"covers/manage\", key, self.get_images(book))\n\n    def get_images(self, book):\n        return book.get_covers()\n\n    def get_image(self, book):\n        return book.get_cover()\n\n    def save_images(self, book, covers):\n        book.covers = covers\n        book._save('Update covers')\n\n    def POST(self, key):\n        book = web.ctx.site.get(key)\n        if not book:\n            raise web.notfound()\n\n        images = web.input(image=[]).image\n        if '-' in images:\n            images = [int(id) for id in images[: images.index('-')]]\n            self.save_images(book, images)\n            return render_template(\"covers/saved\", self.get_image(book), showinfo=False)\n        else:\n            # ERROR\n            pass\n\n\nclass manage_work_covers(manage_covers):\n    path = r\"(/works/OL\\d+W)/manage-covers\"\n\n\nclass manage_photos(manage_covers):\n    path = r\"(/authors/OL\\d+A)/manage-photos\"\n\n    def get_images(self, author):\n        return author.get_photos()\n\n    def get_image(self, author):\n        return author.get_photo()\n\n    def save_images(self, author, photos):\n        author.photos = photos\n        author._save('Update photos')\n", "path": "openlibrary/plugins/upstream/covers.py"}]}