Sometimes, you don’t want your users to search your entire index, but only a subset that concerns them.
Content might be restricted to a specific user as private data, a set of users, a group, or even everybody. Handling access within an index allows to have a fine-grained control over who can search and view what content.
This doesn’t mean you need one index per user. By generating a Secured API key for the current user, you can restrict the records they can retrieve.
Add an attribute for filtering in your dataset
Algolia is schemaless and doesn’t have any concept of relationships between objects, so you need to put all the relevant information in each record.
Take a dataset for corporate documents as an example. The index contains the entire list of documents for the company, but has dedicated access control to restrict who can view content.
Consider different users in this company: Angela, Mike, and Ruth. Angela is an executive, Mike the accountant, and Ruth is an engineer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| [
{
"title": "Financial record Q3 and pipeline forecast",
"visible_by": ["Angela", "group/Finance", "group/Shareholders"],
"objectID": "myID1",
"content": "..."
},
{
"title": "Compliance audit check-list",
"visible_by": ["group/Finance"],
"objectID": "myID2",
"content": "..."
},
{
"title": "Strategic partnership with BigCompany",
"visible_by": ["Angela"],
"objectID": "myID3",
"content": "..."
},
{
"title": "New company-wide healthcare coverage benefits",
"visible_by": ["group/Everybody"],
"objectID": "myID4",
"content": "..."
},
{
"title": "Ruth's personal TODO list",
"visible_by": ["Ruth"],
"objectID": "myID5",
"content": "..."
}
]
|
Each record has a visible_by
attribute, which has a list of users, or groups. Only listed users and groups can see the specific record, with the group Everybody visible by anyone. When searching through it, only allowed people should be able to find those records.
Make the attribute filterable
To make your visible_by
attribute filterable, you should add it in attributesForFaceting
.
1
2
3
4
5
| $index->setSettings([
'attributesForFaceting' => [
'filterOnly(visible_by)'
]
]);
|
1
2
3
4
5
| index.set_settings(
attributesForFaceting: [
'filterOnly(visible_by)'
]
)
|
1
2
3
4
5
| index.setSettings({
attributesForFaceting: [
'filterOnly(visible_by)'
]
});
|
1
2
3
4
5
| index.set_settings({
'attributesForFaceting': [
'filterOnly(visible_by)'
]
})
|
1
2
3
4
5
6
7
| val settings = settings {
attributesForFaceting {
+FilterOnly("visible_by")
}
}
index.setSettings(settings)
|
1
2
3
4
5
6
7
| IndexSettings settings = new IndexSettings
{
AttributesForFaceting = new List<string>
{
"filterOnly(visible_by)"
}
};
|
1
2
| index.setSettings(new IndexSettings()
.setAttributesForFaceting(Collections.singletonList("visible_by")));
|
1
2
3
| res, err := index.SetSettings(search.Settings{
AttributesForFaceting: opt.AttributesForFaceting("filterOnly(visible_by)"),
})
|
1
2
3
4
5
6
7
| client.execute {
setSettings of "your_index_name" `with` IndexSettings(
attributesForFaceting = Some(Seq(
"filterOnly(visible_by)",
))
)
}
|
In this case, you only want to filter on this attribute, and not use it for facet counts. That means you should add the filterOnly
modifier. This improves performance because the engine doesn’t have to compute the count for each value.
If you need faceting on this attribute, you can remove the filterOnly
modifier.
Add and remove users
Whenever someone needs to change the access rights of a record, you need to update the visible_by
attribute.
1
2
3
4
5
6
| $index->partialUpdateObject(
[
'visible_by' => ['Angela', 'group/Finance', 'group/Shareholders'],
'objectID' => 'myID1'
]
);
|
1
2
3
4
| index.partial_update_object({
visible_by: ['Angela', 'group/Finance', 'group/Shareholders'],
objectID: 'myID1'
})
|
1
2
3
4
| index.partialUpdateObject({
visible_by: ['Angela', 'group/Finance', 'group/Shareholders' ],
objectID: 'myID1'
});
|
1
| index.partial_update_object({'objectID': 'myID1', 'visible_by': ['Angela', 'group/Finance', 'group/Shareholders']})
|
1
2
3
4
5
6
7
8
9
10
11
| index.partialUpdateObject(
ObjectID("myID1"),
Partial.Update(
Attribute("visible_by"),
jsonArray {
+("Angela" as String)
+("group/Finance" as String)
+("group/Shareholders" as String)
}
)
)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| using System.Collections.Generic;
using Newtonsoft.Json;
using Algolia.Search.Clients;
public class Post
{
[JsonProperty(PropertyName = "objectID")]
public string ObjectId { get; set; }
[JsonProperty("visible_by")]
public List<string> ViewableBy { get; set; }
}
public class Program
{
public static void Main()
{
List<Post> posts = new List<Post>
{
new Post { ObjectId = "myID1", ViewableBy = new List<string> { "Angela", "group/Finance", "group/Shareholders" } },
};
SearchClient client = new SearchClient("YourApplicationID", "YourWriteAPIKey");
SearchIndex index = client.InitIndex("your_index_name");
index.PartialUpdateObjects(posts);
}
}
|
1
2
3
4
5
6
7
8
| class DemoClass {
// getters setters omitted
@JsonProperty("visible_by")
private List<String> viewableBy;
}
index.partialUpdateObject(new DemoClass()
.setViewableBy(Arrays.aslist("Angela", "group/Finance", "group/Shareholders")));
|
1
2
3
4
| res, err := index.PartialUpdateObject(map[string]interface{}{
"visible_by": []string{"Angela", "group/Finance", "group/Shareholders" },
"objectID": "myID1",
})
|
1
2
3
4
5
| client.execute {
partialUpdate from "index" objects Seq(
Post("myID1", Some(Seq("Angela", "group/Finance", "group/Shareholders")))
)
}
|
The partialUpdateObjects
lets you partially update an attribute instead of replacing the entire record, or even the entire index.
Generate a Secured API key
Frontend search can be vulnerable to malicious users who can tweak the request to impersonate another user and see content they shouldn’t have access to.
To prevent this, generate a Secured API key on the backend with filters (users can’t alter these filters).
1
2
3
4
5
6
7
8
9
| $currentUserID = 'Angela';
$currentGroupId = 'Board';
$securedApiKey = \Algolia\AlgoliaSearch\SearchClient::generateSecuredApiKey(
'YourSearchOnlyAPIKey', // A search key that you keep private
[
'filters' => 'visible_by:'.$currentUserID.' OR visible_by:group/'.$currentGroupId.' OR visible_by:group/Everybody'
]
);
|
1
2
3
4
5
6
7
| current_user_id = 'Angela'
current_group_id = 'Board'
secured_api_key = Algolia::Search::Client.generate_secured_api_key(
'YourSearchOnlyAPIKey', # A search key that you keep private
{ filters: 'visible_by:' + current_user_id + ' OR visible_by:group/' + current_group_id + ' OR visible_by:group/Everybody' }
)
|
1
2
3
4
5
6
7
8
9
10
11
| // Only in Node.js
const currentUserID = 'Angela';
const currentGroupID = 'Board';
const publicKey = client.generateSecuredApiKey(
'YourSearchOnlyAPIKey', // A search key that you keep private
{
filters: `visible_by:${currentUserID} OR visible_by:group/${currentGroupID} OR visible_by:group/Everybody`
}
);
|
1
2
3
4
5
6
7
8
9
10
| from algoliasearch.search_client import SearchClient
current_user_id = 'Angela'
current_group_id = 'Board'
public_key = SearchClient.generate_secured_api_key(
'YourSearchOnlyAPIKey', { # A search key that you keep private
'filters': 'visible_by:' + current_user_id + ' OR visible_by:group/' + current_group_id + ' OR visible_by:group/Everybody'
}
)
|
1
2
3
4
5
6
| val currentUserId = "Angela"
val currentGroupId = "Board"
val restriction = SecuredAPIKeyRestriction(Query(filters = "visible_by:$currentUserId OR visible_by:group/$currentGroupId OR visible_by:group/Everybody"))
// A search key that you keep private
ClientSearch.generateAPIKey(APIKey("YourSearchOnlyAPIKey"), restriction)
|
1
2
3
4
5
6
7
8
9
10
| int currentUserId = "Angela";
int currentGroupId = "Board";
SecuredApiKeyRestriction restriction = new SecuredApiKeyRestriction
{
Query = new Query { Filters = $"visible_by:{currentUserId} OR visible_by:group/{currentGroupId} OR visible_by:group/Everybody" },
};
// A search key that you keep private
client.GenerateSecuredApiKeys("YourSearchOnlyAPIKey", restriction);
|
1
2
3
4
5
6
7
8
9
10
11
| int currentUserID = "Angela";
int currentGroupID = "Board";
SecuredApiKeyRestriction restriction =
new SecuredApiKeyRestriction()
.setQuery(new Query().setFilters(String.format("visible_by:%s OR visible_by:group/%s OR visible_by:group/Everybody", currentUserID, currentGroupID)));
String publicKey = client.generateSecuredAPIKey(
"YourSearchOnlyAPIKey", // A search key that you keep private
restriction
);
|
1
2
3
4
5
6
7
8
| currentUserID := "Angela"
currentGroupID := "Board"
filter := fmt.Sprintf("visible_by:%s OR visible_by:group/%s OR visible_by:group/Everybody", currentUserID, currentGroupID)
key, err := search.GenerateSecuredAPIKey(
"YourYourSearchOnlyAPIKey", // A search key that you keep private
opt.Filters(filter),
)
|
1
2
3
4
5
6
7
| val currentUserID: String = "Angela"
val currentGroupID: String = "Board"
val publicKey = client.generateSecuredApiKey(
"YourSearchOnlyAPIKey", // A search key that you keep private
Query(filters = Some("visible_by:%s OR visible_by:group/%s OR visible_by:group/Everybody".format(currentUserID, currentGroupID)))
)
|
You can get this API key in your frontend from your backend—for example, with an API request.
If a Secured API key is compromised, you should invalidate the search API key used to generate it.
Make sensitive attributes inaccessible
When using a Secured API key with an embedded filter, users can only retrieve content they’re allowed to access to. Since the API returns the visible_by
attribute for each record, they can find out what other users have the same access for this record if they inspect the response.
To mitigate this privacy concern, use the unretrievableAttributes
parameter. It ensures that the visible_by
parameter is never part of the Algolia response, even though you use it for filtering on the engine side.
1
2
3
4
5
| $index->setSettings([
'unretrievableAttributes' => [
'visible_by'
]
]);
|
1
2
3
4
5
| index.set_settings({
unretrievableAttributes: [
'visible_by'
]
})
|
1
2
3
4
5
6
7
| index.setSettings({
unretrievableAttributes: [
'visible_by'
]
}).then(() => {
// done
});
|
1
2
3
4
5
| index.set_settings({
'unretrievableAttributes': [
'visible_by'
]
})
|
1
2
3
4
5
| index.setSettings([
"unretrievableAttributes": [
"visible_by"
]
])
|
1
2
3
4
5
6
7
| val settings = settings {
unretrieveableAttributes {
+"visible_by"
}
}
index.setSettings(settings)
|
1
2
3
4
5
6
| IndexSettings settings = new IndexSettings
{
UnretrievableAttributes = new List<string> { "visible_by" }
};
index.SetSettings(settings);
|
1
2
3
4
5
| index.setSettings(
new IndexSettings().setUnretrievableAttributes(Collections.singletonList(
"visible_by"
))
);
|
1
2
3
4
5
| res, err := index.SetSettings(search.Settings{
UnretrievableAttributes: opt.UnretrievableAttributes(
"visible_by",
),
})
|
1
2
3
4
5
6
7
| client.execute {
setSettings of "myIndex" `with` IndexSettings(
unretrievableAttributes = Some(Seq(
"visible_by"
))
)
}
|
Search the subset
You can now search
on the frontend using the API key generated from your backend. This API key has an embedded filter on the visible_by
attribute, so you have a guarantee that the current user only sees results that they’re allowed to access.