Removing keys in variable size, depth or shape objects
It's not uncommon for us to want to be able to delete an object property from an api response and when you have complete control over the api request/response cycle this may not be an issue. But, when you're consuming third-party services where the shape of the object may be of an unknown size or depth, or variable in shape, ensuring that sensitive data is not being leaked out is important.
This short snippet will show you how you can remove keys of any depth in an object by taking advantage of a nifty little helper library omit-deep-lodash (that only has lodash as a dependency).
I recently ran into a business use-case where I needed api response data around a resource to be public (as in, a non-logged-in user can still fetch/view the content) so that it can be displayed for preview by the site user, as well as be used for static generation/server rendering for SEO optimisation, but the resource contained fields that needed to be protected unless the user had the required role/permissions.
Usually, if you have control over the REST or Graphql api you're consuming, you can simply take advantage of your database query, resolvers or ORM (Object Relational Mapping) library to only return the data you want, or omit certain values. This is fine when you know beforehand the precise structure of the resource and what you're returning to the client so that you don't expose any protected or sensitive data. Things can become a little bit more complicated when you're using something like a headless CMS where a user might be able to dynamically add relationships to resources with protected content to other pieces of content or pages. In this case you might need to find a different solution to protecting sensitive data from ever reaching the client - enter omit-deep-lodash.
Let's say you have a resource for videos/audio/documents or anything else that you might want certain fields protected from the end user unless they have subscribed or purchased something from your site. The problem is that these resources may also house all of the marketing information and metadata around these resources and you need that to be publicly accessible so that you can server render or statically generate your marketing pages for maximum SEO exposure or to enhance sharing across social media platforms. We want to allow this resource to be public, but also not leak protected fields to the client - for most users this wouldn't be an issue since the vast majority of web users don't often pop open the developer tools and inspect network requests to see what data is being sent to and from the browser. But in any case, we should be preventing protected or private fields from ever reaching the client unless the user has permission to view them.
My rule of thumb when working with headless CMS is to name fields that won't be public as something unique that won't clash with other field names (and be removed when didn't want them to) and to prepend it with something like protected_.
For example, if I had a content type for a piece of music, it might have some content fields for title, artist, image and genre, but for the link to the actual file itself, it could be called something like protected_audio_file. Then, when fetching the data on the server side, we can use our helper libraries to remove this bespoke field from any api calls using our helper libraries.
The following snippet is an example from a Strapi controller file for a theoretical Song collection type, which is set up to introduce custom logic for our find method. This controller method maps to a particular route for the type in Strapi, which sets up the necessary boilerplate CRUD (create/read/update/delete) routes when you create a new type.
"use strict";
const omitDeep = require("omit-deep-lodash");
const { get: _get } = require("lodash");
module.exports = {
async find(context) {
// We define any role types that we want to allow access to the protected file
// For example, a premium member that has purchased a subscription to our service
const authorizedRoles = ["premium_member"];
// Using lodash to grab the currently logged in user's role type to compare against
// the authorized roles, or defaults to null - we could also use features like
// optional chaining here as well if supported
const userRole = _get(context, "state.user.role.type", null);
// Fetch the songs with any query params passed along to the route
let entities = await strapi.services.song.find(context.query);
// Here we check if the user's role is included in the list of authorized roles to access
// all the fields - if so, send back everything as is
if (authorizedRoles.includes(userRole)) {
return entities;
} else {
// Use omit-deep-lodash to strip out any keys that match the field we don't want
// unauthorized users to have access to - even if your frontend has safeguards against
// viewing/downloading/accessing the file, they may still be savvy enough to inspect the
// json response in the network request to the backend
return omitDeep(entities, "protected_audio_file");
}
},
};
As mentioned in the code snippet comments, there are a few optimisations we could add to this depending on our setup and features we have available to us, but I just wanted to present the basic premise of how you could go about removing fields from resources that an unauthorised user should not have access to. The example also only shows using it in the find controller method - this same logic would also need to be applied to a findOne method if your app allows fetching information for an individual resource (song), or any other custom methods you may have set up for fetching the data around a type which is consumed by the frontend.
There will always be edge cases with these kinds of things, so it's important to ensure you have tests set up to simulate the many and varied inputs and outputs that may be needed in your app, as this example only visits stripping off a single text key/value.
If you'd like to read more about omit-deep-lodash, you can find the Github repository here.
Thanks for reading!