I am developing an API using Flask-RESTful, and my application has three roles.
site_admin
department_admin
basic
For any given resource, the JSON object returned has a different set of keys based on each role.
For example, if you hit /orders as "site_admin", the result could look like this:
{
"orders": [
{"id": 1, "user": "foo", "paid": True, "department": "A", "code": 456},
{"id": 2, "user": "bar", "paid": False, "department": "A", "code": 567},
{"id": 3, "user": "meh", "paid": False, "department": "B", "code": 678}
]
}
However, if you hit /orders as "department_admin", the result could look like this:
{
"orders": [
{"id": 3, "user": "meh", "paid": False}
]
}
And if you hit /orders as "basic" it would be a very minimal JSON response like this:
{
"orders": [
{"id": 2, "paid": True}
]
}
What is the RESTful way of implementing this?
I can come up with three ways of doing it.
(1) using a request arg and filtering on that:
class Orders(restful.Resource):
def get(self):
if request.args['role'] == 'site_admin':
return admin_JSON_response()
elif request.args['role'] == 'department_admin':
return dept_admin_JSON_response()
else:
return basic_JSON_response()
api.add_resource(Orders, '/orders')
(2) filtering on the session object:
class Orders(restful.Resource):
def get(self):
if session['role'] == 'site_admin':
return admin_JSON_response()
elif session['role'] == 'department_admin':
return dept_admin_JSON_response()
else:
return basic_JSON_response()
api.add_resource(Orders, '/orders')
(3) having a different route for each role:
class OrdersSiteAdmin(restful.Resource):
def get(self):
return admin_JSON_response()
api.add_resource(OrdersSiteAdmin, '/orders_site_admin')
class OrdersDeptAdmin(restful.Resource):
def get(self):
return dept_admin_JSON_response()
api.add_resource(OrdersDeptAdmin, '/orders_dept_admin')
class OrdersBasic(restful.Resource):
def get(self):
return basic_JSON_response()
api.add_resource(OrdersBasic, '/orders_basic')
... Is there a consensus on which is the preferred way RESTfully?
Thanks so much!
Your option #2 violates the the "stateless" constraint, the use of user sessions is not a good idea in a REST API, and instead you should require your clients to provide authentication with every request.
Let's assume you fix #2 and instead of a user session you now have a current_user variable, which is populated during authentication. Then you could rewrite that example as follows:
class Orders(restful.Resource):
def get(self):
if current_user.role == 'site_admin':
return admin_JSON_response()
elif current_user.role == 'department_admin':
return dept_admin_JSON_response()
else:
return basic_JSON_response()
api.add_resource(Orders, '/orders')
Let's look at your three options one by one:
(1) specifies the role in the query string, which would enable any user to request any representation, just by passing the desired role. But why put the role in the query string? I assume you will authenticate your users, so knowing your user you also know the role. This seems unnecessary and will give you extra validation effort.
(3) creates different resources for each role. Once again, you have to ensure that a "basic" user does not have access to the two URLs that apply to the higher roles, so you have also some validation work here.
(2) assumes the user database stores the role of each user, so once the user is authenticated the correct representation for his/her role is returned based on the assigned role. This is, I think, the best option, as users have really no way to hack their way into data they are not allowed to see.
Speaking of being RESTful, I would also look at your representations, which can be improved. Consider implementing links to other resources instead of providing IDs, to comply with the HATEOAS constraint.
Related
Is there a way to modify these fields through the Admin SDK without manually doing it through the admin console? Specifically under the employee information, the job title, type of employee, manager's email and department.
I've looked at https://developers.google.com/admin-sdk/directory/reference/rest/v1/users and https://googleapis.github.io/google-api-python-client/docs/dyn/admin_directory_v1.users.html#insert but I don't see any parameter that you can specify these fields in.
Any help would be appreciated, thank you!
The fields are kind of scattered around but we can figure it out by filling them manually on a test user and running a users.get on them.
So for an employee that looks like this:
The API result is the following (after removing the irrelevant fields):
{
"externalIds": [
{
"value": "ID-123",
"type": "organization"
}
],
"relations": [
{
"value": "example#domain.com",
"type": "manager"
}
],
"organizations": [
{
"title": "My Job Title",
"primary": true,
"customType": "",
"department": "My Department",
"description": "My Employee Type",
"costCenter": "My Cost Center"
}
],
"locations": [
{
"buildingId": "My Building ID",
"floorName": "My Floor Name",
"floorSection": "My Floor Section"
}
]
}
So from this we can deduce that Job Title corresponds to organizations.title, Type of employee corresponds to organizations.description, Manager's email corresponds to relations.value that also has a relations.type = manager and Department corresponds to organizations.department.
Do note that as explained in the documentation that you were looking at, these fields can hold multiple values, so the user can have multiple organizations, but the one that shows in the Admin console is the one marked as primary, they can have multiple relations other than manager such as assistant, spouse, etc. They can have multiple locations and so on.
Generally what will be displayed in the Admin console are the primary fields. Any secondary values may not show up but you can still retrieve them via the API. Most likely they're meant for custom applications that can take advantage of reading these multiple fields, while the console only shows the most relevant fields (as decided by Google).
I have a simple Users resource with a put method to update all user information except user password. According to Flask-Restx docs when a model has set the strict and validation params to true, a validation error will be thrown if an unspecified param is provided in the request. However, this doesn't seem to be working for me.
Model definition:
from flask_restx import Namespace, Resource, fields, marshal
users_ns = Namespace("users")
user = users_ns.model(
"user",
{
"user_name": fields.String(example="some_user", required=True),
"email": fields.String(example="some.user#email", required=True),
"is_admin": fields.Boolean(example="False"),
"is_deactivated": fields.Boolean(example="False"),
"created_date": fields.DateTime(example="2020-12-01T01:59:39.297904"),
"last_modified_date": fields.DateTime(example="2020-12-01T01:59:39.297904"),
"uri": fields.Url("api.user"),
},
strict=True,
)
user_post = users_ns.inherit(
"user_post", user, {"password": fields.String(required=True)}
) # Used for when
Resource and method definition:
from api.models import Users
class User(Resource):
#users_ns.marshal_with(user)
#users_ns.expect(user, validate=True)
def put(self, id):
"""
Update a specified user.
"""
user = Users.query.get_or_404(id)
body = request.get_json()
user.update(body)
return user
Failing Test:
def test_update_user_invalid_password_param(self, client, db):
""" User endpoint should return 400 when user attempts to pass password param to update. """
data = {
"user_name": "some_user",
"email": "some.user#email.com",
"password": "newpassword",
}
response = client.put(url_for("api.user", id=1), json=data)
assert response.status_code == 400
The response.status_code here is 200 because no validation error is thrown for the unspecified param passed in the body of the request.
Am I using the strict param improperly? Am I misunderstanding the behavior of strict?
UPDATED: I've added the test for strict model param from Flask-RestX repo (can be found here) for more context on expected behavior:
def test_api_payload_strict_verification(self, app, client):
api = restx.Api(app, validate=True)
ns = restx.Namespace("apples")
api.add_namespace(ns)
fields = ns.model(
"Person",
{
"name": restx.fields.String(required=True),
"age": restx.fields.Integer,
"birthdate": restx.fields.DateTime,
},
strict=True,
)
#ns.route("/validation/")
class Payload(restx.Resource):
payload = None
#ns.expect(fields)
def post(self):
Payload.payload = ns.payload
return {}
data = {
"name": "John Doe",
"agge": 15, # typo
}
resp = client.post_json("/apples/validation/", data, status=400)
assert re.match("Additional properties are not allowed \(u*'agge' was unexpected\)", resp["errors"][""])
I resolved my issue by pulling the latest version of Flask-RESTX from Github. The strict parameter for models was merged after Flask-RESTX version 0.2.0 was released on Pypi in March of 2020 (see the closed issue in Flask-RESTX repo for more context). My confusion arose because the documentation appears to represent the latest state of master and not the last Pypi release.
It's been a while since I touched on this but from what I can tell, I don't think you are using the strict param correctly. From the documentation here, the :param bool strict is defined as
:param bool strict: validation should raise an error when there is param not provided in schema
But in your last snippet of code, you are trying to validate with the dictionary data with the body of the request.
If I recall well again, for this sort of task you need to use (and as you mentioned) a RequestParser. There's a good example of it here - flask - something more strict than #api.expect for input data?
So basically I have an "/add" route where it is used to manually seed in test user data to mongodb and I want to be able to pass that user's ID to the URL for the route "/agreement" so when the user is on the agreement page, their "_id" will be mapped to that URI and the info they enter on the agreement page will update their data that we seeded in the "/add" route.
#app.route('/add')
def add():
users = mongo.db.users
users.insert({
"_id" : "autogenID",
"_comment" : " File structure for files in client collection",
"client_name" : "Name_of_firm",
"contact_name" : "Name_of_contact",
"contact_email" : "Email_ID_of_contact",
"contact_password" : "passowrd_encryped_bcrypt",
"client_industry" : "client_industry",
"monthly_checks_processed": "monthly_checks_processed",
"parent_id" : "client_id",
"child_id" : [{
"child_id": "client_id",
"child_id": "client_id"
}],
"agreement_authorised" : "boolean_yes_no",
"agreement" : "agreement_text",
"client_card" : [{
"name_on_card": "client_name",
"credit_card_number": "credit_card_number_bycrypt",
"expiration_date" : "expiration_date_bycrypt",
"credit_card_cvc" : "credit_card_cvc_bycrypt",
"active" : "boolean_yes_no"
}]
})
all_users = users.find()
for user in all_users:
print user
#app.route('/agreement', methods=['GET', 'POST'])
def agreenment(id):
user = mongo.db.users.find("_id")
print user
return render_template('agreement.html', user=user)
I think the issue lies in the agreement resource where maybe I should write /agreement/<id> but I think I am missing a fundamental understanding of where the <id> is instantiated. I also don't completely understand what the function of the agreement parameter is but I put (id) because this seems like something I would have to do in order to get the user's info passed to another resource.
I also think user = mongo.db.users.find("_id") might not be correct as well as return render_template('agreement.html', user=user). Part of me thinks that maybe I should do redirect instead of render but if anyone can lend a hand I would greatly appreciate it.
Try this:
#app.route('/agreement/<user_id>', methods=['GET', 'POST'])
def agreement(user_id):
user = mongo.db.users.find_one({"_id": user_id})
print user
return render_template('agreement.html', user=user)
URL parameter names in Flask are specified in the route parameter, and they have to match the decorated function's signature.
Querying in Mongo is done by passing a dict specifying the constraints, so db.users.find_one({"_id": user_id}) will return a single result with _id matching the user_id variable.
Edit: Actually, as per the documentation, you can just do:
db.users.find_one(user_id).
After adding a member to a group:
groupMember = {
"email": sender,
"role": "MEMBER",
}
directory_service.members().insert(groupKey=groupName,body=groupMember).execute()
I would like to set the following attributes for that same member:
- Email delivery (we want to set it to no mail)
- Posting permission (we want to set it to "Override: member is allowed to post")
I can't find python (or other languages) API for setting group member attributes other than these member resource attributes:
{
"kind": "admin#directory#member",
"etag": etag,
"id": string,
"email": string,
"role": string,
"type": string
}
Is there an API (python or other languages) to set "Email delivery" and "Posting permission" attributes for a specific group member?
Thank you for your help!
There is no API to set group member email preferences.
I'm trying to build in a way to handle a large number of posted options, e.g.
my_posted_data = {"item": "value", "item_options":{"a":2, "b":2} }
This would be coming from somewhere else in an api situation where I'm not in control of the environment and it is simulated for now. I'll post that through the requests library; and moving server-side, I try to get this from the route/view in my application. request.form gets read into a variable(form) which is passed to a task_manager queue. In the task I'll try to do:
options = form.get("item_options", None)
options always ends up as NoneType. Why is this not selecting the dict(like) value of {"a": 2, "b": 2}? I guess I'm doing it wrong, but what at this point I am unable to pinpoint.
Based on this scant picture I've provided, how do I post and and retrieve nested values with Flask request in the most effective way?
EDIT: I had to go a different way, using JSON data because I realized for the situation that is best, the form is more for user input from an html page, and this is something that requires a different approach.
By using Flask-WTF with Field Enclosures in WTForms, you can easily handle nested post data.
class ListField(Field):
def process_formdata(self, valuelist):
self.data = valuelist
class ItemsForm(Form):
a = StringField()
b = StringField()
class TestForm(FlaskForm):
item = StringField()
product = ListField(FormField(ItemsForm))
Since FormField add default prefix, the JSON will looks like
{
"item": "value",
"product": {
"product-a": "string",
"product-b": "string",
}
}