-
Notifications
You must be signed in to change notification settings - Fork 438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API Access to Moderation Queue #1028
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,10 +5,12 @@ | |
import re | ||
import tempfile | ||
import unittest | ||
import base64 | ||
|
||
from urllib.parse import urlencode | ||
|
||
from werkzeug.wrappers import Response | ||
from werkzeug.datastructures import Headers | ||
|
||
from isso import Isso, core, config | ||
from isso.utils import http | ||
|
@@ -689,6 +691,20 @@ def testLatestNotEnabled(self): | |
response = self.get('/latest?limit=5') | ||
self.assertEqual(response.status_code, 404) | ||
|
||
def testPendingNotFound(self): | ||
# load some comments in a mix of posts | ||
saved = [] | ||
for idx, post_id in enumerate([1, 2, 2, 1, 2, 1, 3, 1, 4, 2, 3, 4, 1, 2]): | ||
text = 'text-{}'.format(idx) | ||
post_uri = 'test-{}'.format(post_id) | ||
self.post('/new?uri=' + post_uri, data=json.dumps({'text': text})) | ||
saved.append((post_uri, text)) | ||
|
||
response = self.get('/pending?limit=5') | ||
|
||
# If the admin interface was not enabled we should get a 404. | ||
self.assertEqual(response.status_code, 404) | ||
|
||
|
||
class TestHostDependent(unittest.TestCase): | ||
|
||
|
@@ -763,13 +779,17 @@ def setUp(self): | |
conf.set("moderation", "enabled", "true") | ||
conf.set("guard", "enabled", "off") | ||
conf.set("hash", "algorithm", "none") | ||
conf.set("admin", "enabled", "true") | ||
self.conf = conf | ||
|
||
class App(Isso, core.Mixin): | ||
pass | ||
|
||
self.app = App(conf) | ||
self.app.wsgi_app = FakeIP(self.app.wsgi_app, "192.168.1.1") | ||
self.client = JSONClient(self.app, Response) | ||
self.post = self.client.post | ||
self.get = self.client.get | ||
|
||
def tearDown(self): | ||
os.unlink(self.path) | ||
|
@@ -844,6 +864,64 @@ def testModerateComment(self): | |
# Comment should no longer exist | ||
self.assertEqual(self.app.db.comments.get(id_), None) | ||
|
||
def testPendingWithoutAdmin(self): | ||
self.conf.set("admin", "enabled", "false") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these tests guaranteed to be run in order? I'd prefer you un-set the |
||
response = self.get('/pending?limit=5') | ||
self.assertEqual(response.status_code, 404) | ||
|
||
def testPendingUnauthorized(self): | ||
response = self.get('/pending?limit=5') | ||
self.assertEqual(response.status_code, 401) | ||
|
||
def getAuthenticated(self, url, username, password): | ||
credentials = f"{username}:{password}" | ||
encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8') | ||
headers = Headers() | ||
headers.add('Authorization', f'Basic {encoded_credentials}') | ||
|
||
return self.client.get(url, headers=headers) | ||
|
||
def testPendingNotEnabled(self): | ||
password = "s3cr3t" | ||
self.conf.set("admin", "enabled", "true") | ||
self.conf.set("admin", "password", password) | ||
response = self.getAuthenticated('/pending?limit=5', 'admin', password) | ||
self.assertEqual(response.status_code, 404) | ||
|
||
def testPendingNotEnabled(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be |
||
password = "s3cr3t" | ||
self.conf.set("admin", "enabled", "true") | ||
self.conf.set("admin", "password", password) | ||
self.conf.set("general", "pending-enabled", "true") | ||
response = self.getAuthenticated('/pending?limit=5', 'admin', password) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
body = loads(response.data) | ||
self.assertEqual(body, []) | ||
|
||
def testPendingPosts(self): | ||
# load some comments in a mix of posts | ||
saved = [] | ||
for idx, post_id in enumerate([1, 2, 2, 1, 2, 1, 3, 1, 4, 2, 3, 4, 1, 2]): | ||
text = 'text-{}'.format(idx) | ||
post_uri = 'test-{}'.format(post_id) | ||
self.post('/new?uri=' + post_uri, data=json.dumps({'text': text})) | ||
saved.append((post_uri, text)) | ||
|
||
password = "s3cr3t" | ||
self.conf.set("admin", "enabled", "true") | ||
self.conf.set("admin", "password", password) | ||
self.conf.set("general", "pending-enabled", "true") | ||
response = self.getAuthenticated('/pending?limit=5', 'admin', password) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
body = loads(response.data) | ||
expected_items = saved[-5:] # latest 5 | ||
for reply, expected in zip(body, expected_items): | ||
expected_uri, expected_text = expected | ||
self.assertIn(expected_text, reply['text']) | ||
self.assertEqual(expected_uri, reply['uri']) | ||
|
||
|
||
class TestUnsubscribe(unittest.TestCase): | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -132,6 +132,33 @@ def get_uri_from_url(url): | |
return uri | ||
|
||
|
||
def requires_auth(method): | ||
def decorated(self, *args, **kwargs): | ||
request = args[1] | ||
auth = request.authorization | ||
if not auth: | ||
return Response( | ||
"Unauthorized", 401, | ||
{'WWW-Authenticate': 'Basic realm="Authentication Required"'}) | ||
if not self.check_auth(auth.username, auth.password): | ||
return Response( | ||
"Wrong username or password", 401, | ||
{'WWW-Authenticate': 'Basic realm="Authentication Required"'}) | ||
return method(self, *args, **kwargs) | ||
return decorated | ||
|
||
|
||
def requires_admin(method): | ||
def decorated(self, *args, **kwargs): | ||
if not self.isso.conf.getboolean("admin", "enabled"): | ||
return NotFound( | ||
"Unavailable because 'admin' not enabled by site admin" | ||
) | ||
|
||
return method(self, *args, **kwargs) | ||
return decorated | ||
|
||
|
||
class API(object): | ||
|
||
FIELDS = set(['id', 'parent', 'text', 'author', 'website', | ||
|
@@ -146,6 +173,7 @@ class API(object): | |
('counts', ('POST', '/count')), | ||
('feed', ('GET', '/feed')), | ||
('latest', ('GET', '/latest')), | ||
('pending', ('GET', '/pending')), | ||
('view', ('GET', '/id/<int:id>')), | ||
('edit', ('PUT', '/id/<int:id>')), | ||
('delete', ('DELETE', '/id/<int:id>')), | ||
|
@@ -1515,12 +1543,12 @@ def admin(self, env, req): | |
@apiName latest | ||
@apiVersion 0.12.6 | ||
@apiDescription | ||
Get the latest comments from the system, no matter which thread. Only available if `[general] latest-enabled` is set to `true` in server config. | ||
Get the latest accepted comments from the system, no matter which thread. Only available if `[general] latest-enabled` is set to `true` in server config. | ||
|
||
@apiQuery {Number} limit | ||
The quantity of last comments to retrieve | ||
|
||
@apiExample {curl} Get the latest 5 comments | ||
@apiExample {curl} Get the latest 5 accepted comments | ||
curl 'https://comments.example.com/latest?limit=5' | ||
|
||
@apiUse commentResponse | ||
|
@@ -1562,6 +1590,79 @@ def latest(self, environ, request): | |
"Unavailable because 'latest-enabled' not set by site admin" | ||
) | ||
|
||
return self._latest(environ, request, "1") | ||
|
||
|
||
def check_auth(self, username, password): | ||
admin_password = self.isso.conf.get("admin", "password") | ||
|
||
return username == 'admin' and password == admin_password | ||
|
||
|
||
""" | ||
@api {get} /pending pending | ||
@apiGroup Comment | ||
@apiName pending | ||
@apiVersion 0.13.1 | ||
@apiDescription | ||
Get the latest comments waiting moderation from the system, no matter which thread. Only available if `[general] pending-enabled` is set to `true` and `[admin] enabled is set to `true` in server config. | ||
|
||
@apiHeader {String="Basic BASE64_CREDENTIALS"} authorization Base64 encoded "USERNAME:PASSWORD" | ||
|
||
@apiQuery {Number} limit | ||
The quantity of last comments to retrieve | ||
|
||
@apiExample {curl} Get the latest 5 pending comments | ||
curl -u 'admin:ADMIN_PASSWORD' 'https://comments.example.com/pending?limit=5' | ||
|
||
@apiUse commentResponse | ||
|
||
@apiSuccessExample Example result: | ||
[ | ||
{ | ||
"website": null, | ||
"uri": "/some", | ||
"author": null, | ||
"parent": null, | ||
"created": 1464912312.123416, | ||
"text": " <p>I want to use MySQL</p>", | ||
"dislikes": 0, | ||
"modified": null, | ||
"mode": 2, | ||
"id": 3, | ||
"likes": 1 | ||
}, | ||
{ | ||
"website": null, | ||
"uri": "/other", | ||
"author": null, | ||
"parent": null, | ||
"created": 1464914341.312426, | ||
"text": " <p>I want to use MySQL</p>", | ||
"dislikes": 0, | ||
"modified": null, | ||
"mode": 2, | ||
"id": 4, | ||
"likes": 0 | ||
} | ||
] | ||
""" | ||
# If the admin interface is not enabled, people may have not changed | ||
# the default password. We therefore disallow the /pending endpoint, | ||
# as well. | ||
@requires_admin | ||
@requires_auth | ||
def pending(self, environ, request): | ||
# if the feature is not allowed, don't present the endpoint | ||
if not self.conf.getboolean("pending-enabled"): | ||
return NotFound( | ||
"Unavailable because 'pending-enabled' not set by site admin" | ||
) | ||
|
||
return self._latest(environ, request, "2") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than adding a new endpoint, wouldn't it make more sense to instead pass through a |
||
|
||
|
||
def _latest(self, environ, request, mode): | ||
# get and check the limit | ||
bad_limit_msg = "Query parameter 'limit' is mandatory (integer, >0)" | ||
try: | ||
|
@@ -1572,7 +1673,7 @@ def latest(self, environ, request): | |
return BadRequest(bad_limit_msg) | ||
|
||
# retrieve the latest N comments from the DB | ||
all_comments_gen = self.comments.fetchall(limit=None, order_by='created', mode='1') | ||
all_comments_gen = self.comments.fetchall(limit=None, order_by='created', mode=mode) | ||
comments = collections.deque(all_comments_gen, maxlen=limit) | ||
|
||
# prepare a special set of fields (except text which is rendered specifically) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"that works like"
"awaiting"