Skip to content
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

Add privateAttributes to global and per model response #7331

Merged
merged 18 commits into from
Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/v3.x/concepts/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,25 @@ module.exports = ({ env }) => ({
| `admin.forgotPassword.from` | Sender mail address | string | Default value defined in your [provider configuration](../plugins/email.md#configure-the-plugin) |
| `admin.forgotPassword.replyTo` | Default address or addresses the receiver is asked to reply to | string | Default value defined in your [provider configuration](../plugins/email.md#configure-the-plugin) |

## API

**Path —** `./config/api.js`.

```js
module.exports = ({ env }) => ({
responses: {
privateAttributes: ['_v', 'id', 'created_at'],
},
});
```

**Available options**

| Property | Description | Type | Default |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------- |
| `responses` | Global API response configuration | Object | |
| `responses.privateAttributes` | Set of globally defined attributes to be treated as private. E.g. `_v` when using MongoDb or timestamps like `created_at`, `updated_at` can be treated as private | String array | `[]` |

## Functions

The `./config/functions/` folder contains a set of JavaScript files in order to add dynamic and logic based configurations.
Expand Down
10 changes: 8 additions & 2 deletions docs/v3.x/concepts/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Use the CLI, and run the following command `strapi generate:model restaurant nam

This will create two files located at `./api/restaurant/models`:

- `Restaurant.settings.json`: contains the list of attributes and settings. The JSON format makes the file easily editable.
- `Restaurant.settings.json`: contains the list of attributes and settings. The JSON format makes the file easily editable.
- `Restaurant.js`: imports `Restaurant.settings.json` and extends it with additional settings and life cycle callbacks.

::: tip
Expand Down Expand Up @@ -164,12 +164,18 @@ The options key on the model-json states.

- `timestamps`: This tells the model which attributes to use for timestamps. Accepts either `boolean` or `Array` of strings where first element is create date and second element is update date. Default value when set to `true` for Bookshelf is `["created_at", "updated_at"]` and for MongoDB is `["createdAt", "updatedAt"]`.

- `privateAttributes`: This configuration allows to treat as private a set of attributes, even if they're not actually defined as attributes in the model. Accepts an `Array` of strings. It could be used to remove from API responses timestamps or `_v` when using MongoDB. The set of `privateAttributes` defined in the model are merged with the `privateAttributes` defined in the global Strapi configuration only when `ignoreGlobalPrivateAttributes` is set to `false`.

- `ignoreGlobalPrivateAttributes`: This configuration allows to ignore the global `privateAttributes`. The set of `privateAttributes` defined in the model won't be merged with the `privateAttributes` defined in the global Strapi configuration.
dalbitresb12 marked this conversation as resolved.
Show resolved Hide resolved

**Path —** `User.settings.json`.

```json
{
"options": {
"timestamps": true
"timestamps": true,
"privateAttributes": ["id", "_v", "created_at"],
"ignoreGlobalPrivateAttributes": true
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ describe('Permissions Manager', () => {
options: {},
};
},
config: {
get: jest.fn,
},
};

test('Pick all fields (output) using default model', () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/strapi-plugin-graphql/services/type-definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ const buildTypeDefObj = model => {
delete typeDef[association.alias];
});

// Remove private attributes (which are not attributes) defined per model or globally
const privateAttributes = _.union(
_.get(model, 'options.privateAttributes', []),
_.get(model, 'options.ignoreGlobalPrivateAttributes', false)
dalbitresb12 marked this conversation as resolved.
Show resolved Hide resolved
? []
: strapi.config.get('api.responses.privateAttributes', [])
);
privateAttributes.forEach(attr => {
if (typeDef[attr]) {
delete typeDef[attr];
}
});

return typeDef;
};

Expand Down
217 changes: 217 additions & 0 deletions packages/strapi-utils/lib/__tests__/sanitizeEntity.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
const sanitizeEntity = require('../sanitize-entity');
dalbitresb12 marked this conversation as resolved.
Show resolved Hide resolved

const modelWithPublicOnlyAttributes = {
kind: 'collectionType',
collectionName: 'test_model',
info: {
name: 'Model with public only attributes',
description: '',
},
options: {
increments: true,
timestamps: true,
comment: '',
},
attributes: {
foo: {
type: 'string',
},
bar: {
type: 'string',
},
},
};

const modelWithPrivatesAttributes = {
kind: 'collectionType',
collectionName: 'test_model',
info: {
name: 'Model with private attributes',
description: '',
},
options: {
increments: true,
timestamps: true,
comment: '',
},
attributes: {
foo: {
type: 'string',
private: true,
},
bar: {
type: 'string',
},
},
};

const modelWithPassword = {
kind: 'collectionType',
collectionName: 'test_model',
info: {
name: 'Model with password',
description: '',
},
options: {
increments: true,
timestamps: true,
comment: '',
},
attributes: {
foo: {
type: 'password',
},
bar: {
type: 'string',
},
},
};

const modelWithOptionPrivateAttributes = {
kind: 'collectionType',
collectionName: 'test_model',
info: {
name: 'Model with option.privateAttributes',
description: '',
},
options: {
increments: true,
timestamps: true,
comment: '',
privateAttributes: ['hideme'],
},
attributes: {
foo: {
type: 'string',
},
bar: {
type: 'string',
},
},
};

const modelWithGlobalIgnore = {
kind: 'collectionType',
collectionName: 'test_model',
info: {
name: 'Model with global ignore',
description: '',
},
options: {
increments: true,
timestamps: true,
comment: '',
ignoreGlobalPrivateAttributes: true,
},
attributes: {
foo: {
type: 'string',
},
bar: {
type: 'string',
},
},
};

describe('Sanitize Entity', () => {
beforeEach(() => {
global.strapi = {
config: {
get: jest.fn,
},
};
});

test('When no private attributes in model, then all attributes must be returned', async () => {
let entity = { foo: 'foo', bar: 'bar' };
let sanitized = sanitizeEntity(entity, { model: modelWithPublicOnlyAttributes });

expect(sanitized).toEqual({ foo: 'foo', bar: 'bar' });
});

test('When private attributes in model, then all private attributes must be hidden', async () => {
let entity = { foo: 'foo', bar: 'bar' };
let sanitized = sanitizeEntity(entity, { model: modelWithPrivatesAttributes });

expect(sanitized).toEqual({ bar: 'bar' });
});

test('When withPrivate = true, then all the attributes must be returned', async () => {
let entity = { foo: 'foo', bar: 'bar' };
let sanitized = sanitizeEntity(entity, {
model: modelWithPrivatesAttributes,
withPrivate: true,
});

expect(sanitized).toEqual({ foo: 'foo', bar: 'bar' });
});

test('When isOutput = false, then all the attributes must be returned', async () => {
let entity = { foo: 'foo', bar: 'bar' };
let sanitized = sanitizeEntity(entity, {
model: modelWithPrivatesAttributes,
isOutput: false,
});

expect(sanitized).toEqual({ foo: 'foo', bar: 'bar' });
});

test('When attribute type is password, then it must be hidden', async () => {
let entity = { foo: 'foo', bar: 'bar' };
let sanitized = sanitizeEntity(entity, { model: modelWithPassword });

expect(sanitized).toEqual({ bar: 'bar' });
});

test('When non-attribute fields are present, all must be returned', async () => {
let entity = { foo: 'foo', bar: 'bar', imhere: true };
let sanitized = sanitizeEntity(entity, { model: modelWithPublicOnlyAttributes });

expect(sanitized).toEqual({ foo: 'foo', bar: 'bar', imhere: true });
});

test('When options.privateAttributes in model, non-attribute fields must be hidden', async () => {
let entity = { foo: 'foo', bar: 'bar', imhere: true, hideme: 'should be hidden' };
let sanitized = sanitizeEntity(entity, { model: modelWithOptionPrivateAttributes });

expect(sanitized).toEqual({ imhere: true, foo: 'foo', bar: 'bar' });
});

test('When privateAttributes in model and global config, non-attribute fields must be hidden', async () => {
global.strapi = {
config: {
get: jest.fn(path => {
return path === 'api.responses.privateAttributes' ? ['hidemeglobal'] : [];
}),
},
};
let entity = {
foo: 'foo',
bar: 'bar',
imhere: true,
hideme: 'should be hidden',
hidemeglobal: 'should be hidden',
};
let sanitized = sanitizeEntity(entity, { model: modelWithOptionPrivateAttributes });

expect(sanitized).toEqual({ imhere: true, foo: 'foo', bar: 'bar' });
});

test('When ignoreGlobalPrivateAttributes in model, global private attributes must be returned', async () => {
global.strapi = {
config: {
get: jest.fn(path => {
return path === 'api.responses.privateAttributes' ? ['hidemeglobal'] : [];
}),
},
};
let entity = {
foo: 'foo',
bar: 'bar',
hidemeglobal: 'should be returned',
};
let sanitized = sanitizeEntity(entity, { model: modelWithGlobalIgnore });

expect(sanitized).toEqual({ foo: 'foo', bar: 'bar', hidemeglobal: 'should be returned' });
});
});
11 changes: 10 additions & 1 deletion packages/strapi-utils/lib/sanitize-entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,21 @@ const sanitizeEntity = (dataSource, options) => {

const { attributes } = model;
const allowedFields = getAllowedFields({ includeFields, model, isOutput });
const privateAttributes = _.union(
_.get(model, 'options.privateAttributes', []),
dalbitresb12 marked this conversation as resolved.
Show resolved Hide resolved
_.get(model, 'options.ignoreGlobalPrivateAttributes', false)
? []
: strapi.config.get('api.responses.privateAttributes', [])
);

const reducerFn = (acc, value, key) => {
const attribute = attributes[key];
const allowedFieldsHasKey = allowedFields.includes(key);

if (shouldRemoveAttribute(attribute, { withPrivate, isOutput })) {
if (
shouldRemoveAttribute(attribute, { withPrivate, isOutput }) ||
(privateAttributes.includes(key) && isOutput && !withPrivate)
dalbitresb12 marked this conversation as resolved.
Show resolved Hide resolved
) {
return acc;
}

Expand Down