API based allow list script#223
Conversation
| "required": ["url"], | ||
|
|
||
| "properties": { | ||
| "url": {"type": "string", "format": "public-url"} |
There was a problem hiding this comment.
"format": "public-url" is the most interesting part of this schema.
This is a custom format defined by us. It's totally valid to put whatever you like here in JSON schema. There are a few default formats understood by all standard JSON schema validators, but any they don't understand are just ignored.
There was a problem hiding this comment.
I decided to make the url part of the meta for this object rather than the attributes as:
- The final rule object doesn't actually include the URL value as provided
- This means upon getting a response, you'd get one without this attribute
- It is however required to create it
| "required": ["type", "meta"], | ||
|
|
||
| "properties": { | ||
| "type": {"enum": ["AllowRule"]}, |
There was a problem hiding this comment.
The type is mandatory for JSON:API. I chose the class name as the name for the type as:
- It's not directly tied to the DB
- It might allow us to be fancy one day and automatically pick the right DB class or something
| <style> | ||
| code { | ||
| word-break: break-all; | ||
| } |
There was a problem hiding this comment.
The command gets very long with the session JWT, so enable word wrapping so we can copy with without scrolling horizontally.
|
|
||
| :raise MalformedJSONBody: If the JSON cannot be decoded or the body | ||
| does not conform to the schema provided | ||
| """ |
There was a problem hiding this comment.
I'm sure we could do a fancy decorator like we do in h for applying the schema to a view. This works for now and achieves the main aim of making this not view specific. If we find we are using this a lot I think that would be a good time to think about making it slicker (rule of 3?)
There was a problem hiding this comment.
I don't think we need to do it but a lighter weight nicety could be to add this as a request method: request.validated_json_body(validator). Similar to the built-in request.json_body property
| """Render an HTML version of a blocked URL with explanation.""" | ||
|
|
||
| body = get_validated_json_body(request, _ALLOW_RULE_VALIDATOR) | ||
| url = body["data"]["meta"]["url"] |
There was a problem hiding this comment.
We know this is totally valid as it's just come through the schema. If this was the wrong shape, we couldn't get this far as an exception would already have been raised.
|
|
||
| rule = AllowRule(rule=rule_string, hash=hex_hash, tags=["manual"]) | ||
| request.db.add(rule) | ||
| request.db.flush() # Make sure an id is allocated before we serialise |
There was a problem hiding this comment.
The id field is mandatory in JSON:API
There was a problem hiding this comment.
I think a nice way to do this might be implement a JSON:API renderer so the view just does return rule and the renderer turns that returned object into JSON. Depending on when Pyramid calls renderers the renderer might not even have to call db.flush() if the transaction has already been commited by then. I suspect they probably get called before transaction commit though. As an example we have a custom SVG renderer in h.
I think that'd be trying way too hard for this PR though
|
|
||
| return { | ||
| "type": self.__class__.__name__, | ||
| "id": self.id, |
There was a problem hiding this comment.
This isn't flexible here, and I think it shouldn't be unless it has to be. Having every object have it's primary key as id is a very handy convention.
c3199d0 to
5852bd4
Compare
seanh
left a comment
There was a problem hiding this comment.
As discussed on Slack I'd like us to give Marshmallow a try for the validation as I think it's our chosen validation library. If it turns out not to work very nicely for this then we can reject it.
Other than that just some minor suggestions.
I ran it locally and it seemed to work for me, loaded the rules into the DB, and after running the command the sites from the CSV were now allowed
| class MalformedJSONBody(JSONAPIException): | ||
| """The JSON body is malformed in some way.""" | ||
|
|
||
|
|
There was a problem hiding this comment.
Just so it 400's instead of 500's for a malformed request:
| class MalformedJSONBody(JSONAPIException): | |
| """The JSON body is malformed in some way.""" | |
| class MalformedJSONBody(JSONAPIException): | |
| """The JSON body is malformed in some way.""" | |
| status_code = 400 |
| config.add_route("login_callback", "/ui/api/login_callback") | ||
| config.add_route("logout", "/ui/api/logout") | ||
|
|
||
| config.add_route("add_to_allow_list", "/ui/api/rule", request_method="POST") |
There was a problem hiding this comment.
I think this should probably be /api/rule/, as I don't think we know that we'll ever add an admin page that calls this API. We may never add admin pages for managing the allow list at all if the priority isn't high enough (which seems likely, if we don't get many requests). Even if we do one day end up adding admin pages, we don't know for sure that they'll end up calling this API from JavaScript. So we might end up with an API that never is called by admin pages, but is just confusingly under /ui/api/. It might be better to just put it under /api/ for now and move it in the future if we decide to.
This'd also mean moving add_to_allow_list.py
| effective_principals=[Principals.STAFF], | ||
| ) | ||
| def add_to_allow_list(_context, request): | ||
| """Render an HTML version of a blocked URL with explanation.""" |
There was a problem hiding this comment.
| """Render an HTML version of a blocked URL with explanation.""" | |
| """Add a new rule to the allow list..""" |
|
|
||
| :raise MalformedJSONBody: If the JSON cannot be decoded or the body | ||
| does not conform to the schema provided | ||
| """ |
There was a problem hiding this comment.
I don't think we need to do it but a lighter weight nicety could be to add this as a request method: request.validated_json_body(validator). Similar to the built-in request.json_body property
| # Check this isn't something really dumb like 'co.uk' which will ruin the | ||
| # allow list |
There was a problem hiding this comment.
This check still needs to be added?
| try: | ||
| # We expect a detection from not being on the allow list, so we'll | ||
| # remove it, which will trigger a ValueError if it wasn't there | ||
| reasons.remove(_ALLOW_LIST_DETECTION) | ||
| except ValueError: | ||
| raise ResourceConflict("Requested URL is already allowed") from None |
There was a problem hiding this comment.
Just tweaked the comments to be (I think) a littler clearer:
| try: | |
| # We expect a detection from not being on the allow list, so we'll | |
| # remove it, which will trigger a ValueError if it wasn't there | |
| reasons.remove(_ALLOW_LIST_DETECTION) | |
| except ValueError: | |
| raise ResourceConflict("Requested URL is already allowed") from None | |
| try: | |
| # We expect `reasons` to contain a NOT_ALLOWED detection because | |
| # `url` shouldn't be on the allow list yet. Remove it. | |
| reasons.remove(_ALLOW_LIST_DETECTION) | |
| except ValueError: | |
| # The expected NOT_ALLOWED detection wasn't found: | |
| # `url` must already be on the allow list. | |
| raise ResourceConflict("Requested URL is already allowed") from None |
| # Don't fail fast, so we get all of the detections | ||
| checker = request.find_service(URLCheckerService) | ||
| reasons = list(checker.check_url(url, fail_fast=False)) |
There was a problem hiding this comment.
Making it a bit more explicit what this comment refers to:
| # Don't fail fast, so we get all of the detections | |
| checker = request.find_service(URLCheckerService) | |
| reasons = list(checker.check_url(url, fail_fast=False)) | |
| checker = request.find_service(URLCheckerService) | |
| # Pass fail_fast=True to get *all* the reasons not just the first one. | |
| reasons = list(checker.check_url(url, fail_fast=False)) |
|
|
||
| rule = AllowRule(rule=rule_string, hash=hex_hash, tags=["manual"]) | ||
| request.db.add(rule) | ||
| request.db.flush() # Make sure an id is allocated before we serialise |
There was a problem hiding this comment.
I think a nice way to do this might be implement a JSON:API renderer so the view just does return rule and the renderer turns that returned object into JSON. Depending on when Pyramid calls renderers the renderer might not even have to call db.flush() if the transaction has already been commited by then. I suspect they probably get called before transaction commit though. As an example we have a custom SVG renderer in h.
I think that'd be trying way too hard for this PR though
9fba3ad to
75fe961
Compare
|
This has been superceded in favour of: #224 |
This adds an API and calls it from the script instead of doing things locally. This is of course a lot more complicated, but it has some benefits:
Review notes
POST:/ui/api/rule{ "data": { "type": "AllowRule", "meta": {"url": "http://please.let.me.annotate.com"} } }{ "data": { "type": "AllowRule", "id": 23464, "attributes": { "hash": "97342342347abdef23423423", "tags": ["manual"], "force": false, "rule": "http://please.let.me.annotate.com" } } }Testing notes
allow_list.csvin the root of Checkmatemake services devhttp://localhost:9099Possible improvements
application/vnd.api+json.