Skip to content

Instantly share code, notes, and snippets.

@pixelastic
Forked from aymorgan/Introduction
Last active January 9, 2018 07:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pixelastic/17ec843f8e36f97d295d9602f0d318ed to your computer and use it in GitHub Desktop.
Save pixelastic/17ec843f8e36f97d295d9602f0d318ed to your computer and use it in GitHub Desktop.
Guidance for implementing Algolia.generate_secured_api_key
# Answer
Thanks for the question, I'll do my best to guide you the best way I can.
I think you're very close to have a working implementation.
Algolia secured API keys are API keys with *hard* filters already embedded
inside. You generic Search API key will let you search through all the content
in your index, but when you create a secured API key, you create a specific
version of your search API key that can only search into a subset of records.
You can think of it as having already some filters checked (search only for
records with this attribute, or with a price above a threshold, etc). And there
is no way to remove those filters. It's perfect for multitenancy, when you don't
want user A to have access to data of user B.
In your case, you have each Report that belongs to a Company. If you're
currently loggued in as User A (which is part of Company 42), you'll want
a secured API key that can only access reports that belongs to Company 42. You
would then call `Algolia.generate_secured_api_key 'YourSearchAPIKey', {'filters'
=> 'company_id:42'}`.
In that example, every User of Company 42 will use the same key. Generating
those keys can be done entirely offline (meaning, you don't need to contact the
Algolia servers to do so), and is idempotent (generating a new key with the same
parameters, will always return the same value).
The key you generated that way should then be used to contact our servers, and
used as the `apiKey` in InstantSearch. I suggest that you generate this key in
your controller and pass it directly to your view. You don't need to store it in
your database, you can simply regenerate it everytime you need it.
So, to answer your questions more specifically:
- Where does public_key = Algolia.generate_secured_api_key
'YourSearchOnlyApiKey', {'filters'=> '_tags:user_42'} go? In my Report model?
This should go in your controller, and you should pass the result to your view,
to populate the `apiKey` of InstantSearch, instead of
`ENV['ALGOLIASEARCH_API_KEY_SEARCH']`. This secured API key is more specific
than tha generic Search API key, as it already has some filters embedded inside.
- Do I need to add a migration to my Report model to include a public_key?
No. As you're not storing the key in your model, you don't need to. Just
generate the key each time you need it. You *can* if you want generate it once
and then store it, but it seems overkill.
- To display Reports where and indexed Report.company_id
= current_user.company.id what do I put in the 'filters' section? Or is it not
a filter?
It is a filter :). In it, you should follow this syntax:
`{attribute_name}:{value}`. So if you want only the Reports that belong to
a specific company, you should add `company_id:42`. If you want only the one
that belong to a specific user, it would be `user_id:42`. Note that you must
have `company_id`/`user_id` defined as `attributesForFaceting` in your settings
for this to work.
- Do I need to change anything in my index.html.erb view to display?
Yes, you need to get the secured API key and pass it instead of the `apiKey`.
...
$(document).ready(function() {
var hitTemplate = '<a href="reports/{{{slug}}}" class="hit-report-card">' +
'<div id="preview_image" class="hit-report-img" style="content:url(<%= @attachments.find(6).image_url(:preview) %>);">ATTACHMENT ID: {{attachment.id}}</div>' +
'<div class="hit-report-title dont-break-out">{{{_highlightResult.title.value}}}</div>' +
'<div class="hit-report-date">{{{date_in_words}}}</div>' +
'<div class="hit-report-user">{{user.first_name}} {{user.last_name}}</div>' +
'<div class="hit-report-type">A/B Test</div>' +
'</a>';
var search = instantsearch({
appId: '<%= ENV[ 'ALGOLIASEARCH_APPLICATION_ID'] %>',
apiKey: '<%= ENV['ALGOLIASEARCH_API_KEY_SEARCH'] %>',
indexName: '<%= Report %>',
urlSync: true
});
search.addWidget(
instantsearch.widgets.searchBox({
container: '#q',
placeholder: 'Search for reports',
autofocus: false,
poweredBy: true
})
);
...
DISCLAIMER:
Sorry if this is a little long winded but I just want to ensure I communicate it properly (also I'm still learning Ruby & Rails so sorry if some issues are Rails questions than Algolia specific).
MY APP:
My Rails app has the following models:
- Company (has_many :users, has_many :reports)
- User (belongs_to :company, has_many :reports)
- Report (belongs_to :company, belongs_to :user)
I have managed to successfull indexed my Report model along with some nested attributes as shown below in the report.rb file.
I read this: https://discourse.algolia.com/t/multi-tenant-algolia-index/147 and a few other articals that kind of gave me the impression
that the best way to multitenancy with Algolia was to add the company_id to the indexed Reports model and then user the generate_secured_api_key
to filter results from the backend so a user only see's their companies reports in the search results.
QUESTIONS:
- Where does public_key = Algolia.generate_secured_api_key 'YourSearchOnlyApiKey', {'filters'=> '_tags:user_42'} go? In my Report model?
- Do I need to add a migration to my Report model to include a public_key?
- To display Reports where and indexed Report.company_id = current_user.company.id what do I put in the 'filters' section? Or is it not a filter?
- Do I need to change anything in my index.html.erb view to display?
...
# Algolia Integration - https://www.algolia.com/doc/api-client/rails/usage/
include AlgoliaSearch
algoliasearch do
# all attributes will be sent
attribute :id, :slug, :title, :overview, :result, :company_id, :user_id, :created_at, :updated_at, :date_in_words
attribute :user do
# Adds a nested model for User but restricts to first_name + last_name + email
{ first_name: user.first_name, last_name: user.last_name, email: user.email }
end
attribute :attachment do
# Adds a nested model for Attachment
{ id: attachment_ids }
end
end
...
@aymorgan
Copy link

aymorgan commented Jan 8, 2018

Thank you so much, I owe you big time!

Just one more thing (which I think might be ruby syntax I don't know)!

{'filters'=> 'company_id:1'} works as expected now I've added it to 'attributesForFaceting' thanks, but how would I change that to {'filters'=> 'company_id:current_user.company_id'} ? Thanks again

@pixelastic
Copy link
Author

In Ruby, you can use "#{}" for variable interpolation.

name = "Tim"
sentence = "Hello, my name is #{name}" 
puts sentence
# Will output "Hello, my name is Tim"

So you can follow the same logic to set:

Algolia.generate_secured_api_key 'YourSearchOnlyApiKey', {'filters' => "company_id:#{current_user.company_id}"}

Be careful, it only works in double quotes ("), not single quotes (').

Hope that helps :)

@aymorgan
Copy link

aymorgan commented Jan 9, 2018

No way, I was so close! Haha! I tried that but was using single quotes, just presumed that syntax couldn't be used there with it not working. Thanks again!

One more question and I promise I'll stop pestering you after this, haha! If you don't mind.. please 😄

QUESTION:
Each Report (that's indexed with Algolia) will have a number of attachments (I'm using Amazon S3 to store them), due to the AWS permissions I have set I can't index the attachment url (as they time out) so have indexed the attachment ID's then get the url from that.

As you can see below I have <%= @attachments.find(6).image_url(:preview) %> which displays the same attachment (attachment.id = 6) image for each indexed Report displayed in the results. I wish to show just the first attachment id from the indexed array of attachment ids for each of the indexed reports.

The attachment.ids are indexed as a nested element within report. I'm mainly confused as it's switching from .erb to js to ruby then trying to get the first id from the Algolia index instead of just attachment.id 6.

INDEXED DATA - nested attachment array of id's within the report:

"attachment": {
        "id": [
          167,
          168,
          166
        ]
      }

INDEX.HTML>ERB

var hitTemplate = '<a href="reports/{{{slug}}}" class="hit-report-card">' +
                          '<div id="preview_image" class="hit-report-img" style="content:url(<%= @attachments.find(6).image_url(:preview) %>);">ATTACHMENT ID: {{attachment.id}}</div>' +
                          '<div class="hit-report-title dont-break-out">{{{_highlightResult.title.value}}}</div>' +
                          '<div class="hit-report-date">{{{date_in_words}}}</div>' +
                          '<div class="hit-report-user">{{user.first_name}} {{user.last_name}}</div>' +
                          '<div class="hit-report-type">A/B Test</div>' +
                        '</a>';

...

I hope that make sense - I promise that's it now. I'll do my best to sort out pagination and other stuff without you. haha! Thank you so much for your help with all this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment