Field-level Enforcement

Perhaps you’re building a /profile endpoint, and you’d like to exclude the profile’s email address unless the current user specifically has access to it. To build this type of authorization, it often makes sense to use “field-level” enforcement, by explicitly allowing access to certain fields on your domain objects.

Field-level authorization gives you fine-grained control over who can access exactly what bits of information. In Polar, you can write field-level rules like this:

allow_field(user, "read", profile: Profile, "email") if
    user = profile.user or
    user.is_admin;

Notice that an allow_field rule is just like an allow rule, except that it takes an additional argument: the field name.

Authorize one field at a time

To enforce field-level authorization in your app, you use the Oso.authorize_field() method.

def get_email(profile, current_user):
    oso.authorize_field(current_user, "read", profile, "email")
    return profile.email

Like authorize, authorize_field will raise an an authorization error when the user is not allowed to perform the given action. This is an error that you should handle globally in your app. You can read more details about this in the Resource-level Enforcement Guide.

Get all authorized fields

Sometimes it is helpful to get all fields that a user can access, and for this there is a separate method called Oso.authorized_fields() :

# Serialize only the fields of profile that the current user is allowed to read
def serialize_profile(profile, current_user):
    fields = oso.authorized_fields(current_user, "read", profile)
    return {field: profile[field] for field in fields}

The authorized_fields method can be used to send only the fields that the user is explicitly allowed to read, or can similarly be used to filter incoming parameters from a user for a call to, say, an update method. In that case, you might use an "update" action in the call to authorized_fields:

# Filter raw_update_params by the fields on profile that the user can update
def filter_update_params(profile, raw_update_params, current_user):
    fields = oso.authorized_fields(current_user, "update", profile)
    return {field: raw_update_params[field] for field in fields}

Authorizing many fields

Perhaps you have many fields on each object, and you’d like to allow access to them in groups. For example, a Profile object might have some public fields, some fields viewable only by friends, and some fields viewable by admins only.

You can do this with Polar’s in operator:

# Allow friends access to friend-only fields
allow_field(user: User, "read", profile: Profile, field) if
    field in ["last_check_in_location", "favorite_animal"] and
    user in profile.friends;

# Allow admins access to admin-only fields
allow_field(user: User, "read", profile: Profile, field) if
    field in ["email", "last_login"] and
    user.is_admin;

Or, if you have trouble listing all fields in your Polar policy files, and you’d prefer to list fields in your application code, you can also use a constant defined on the class, like this:

allow_field(user: User, "read", profile: Profile, field) if
    field in Profile.FRIENDS_ONLY_FIELDS and
    user in profile.friends;

allow_field(user: User, "read", profile: Profile, field) if
    field in Profile.ADMIN_ONLY_FIELDS and
    user.is_admin;

Doing so would require you to add the FRIENDS_ONLY_FIELDS and ADMIN_ONLY_FIELDS constants to your Profile class:

class Profile:
    ADMIN_ONLY_FIELDS = ["email", "last_login"]
    FRIENDS_ONLY_FIELDS = ["last_check_in_location", "favorite_animal"]

That way, you can add new fields and authorize access to them without touching your Polar policy code.

Connect with us on Slack

If you have any questions, or just want to talk something through, jump into Slack. An Oso engineer or one of the thousands of developers in the growing community will be happy to help.