Skip to content

Api key implementation #466

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

Merged
merged 52 commits into from
May 10, 2025
Merged

Api key implementation #466

merged 52 commits into from
May 10, 2025

Conversation

AliasXoX
Copy link
Contributor

@AliasXoX AliasXoX commented Jul 2, 2024

This pull request implements a new authentication method for lea5 using API Keys.
The authentication procedure using an API key has been documented.
Here's an overview of the changes coming with this pull request :

  • A new controller and view in order for admin to see, create and delete API keys
  • A new rails concern to authorize API key authentication (see api_key_authenticatable.rb)
  • A new /api endpoint to login as an api key bearer (see routes.rb ;)
  • A new rails secret to hash API keys (see api_key.rb)
  • An encoded file for credentials for the test environment
  • A new api namespace (under /api/)
  • A new application controller for api requests only (see api/api_application_controller.rb ; api/users_controller.rb ; api/machines_controller.rb ; api/api_keys_controller.rb)
  • Modifying the way Cancancan abilities work in lea5 (see user_ability.rb ; api_key_ability.rb ; api/api_application_controller.rb ; application_controller.rb)
  • Some tests that cover abilities, login with an api key and of course the api key controller's actions
  • Custom cop for RuboCop

The method of implemention is highly inspired by https://keygen.sh/blog/how-to-implement-api-key-authentication-in-rails-without-devise/

def initialize(api_key)
return if api_key.blank?

can :read, :all
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need in a near future to refine the permissions of each key, maybe by listing all the endpoints when creating one and let the user select which he wants the bearer to access (read, create, update, destroy), and change the permissions in the edit view.


def index
@api_keys = ApiKey.accessible_by(current_ability)
authorize! :index, @api_keys
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: differs from how we manage index in user_controller


private

def still_authenticated?
log_out if should_log_out?
end

def api_auth
return unless request.path[0, 5] == '/api/'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely useless comment, but there is hard-coded text right here

def log_in_api(bearer)
reset_session # For security reasons, we clear the session data before login
session[:api_key_id] = bearer.id
session[:expires_at] = Time.current
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't understand why it's done this way


def current_ability
@current_ability ||= if !session[:api_key_id].nil?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

@@ -1,6 +1,7 @@
# frozen_string_literal: true

class SessionsController < ApplicationController
include ApiKeyAuthenticatable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
include ApiKeyAuthenticatable

I'm not sure this is still useful

Comment on lines 3 to 5
# Path to login via api key
AUTH_API_PATH = '/auth/api'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Path to login via api key
AUTH_API_PATH = '/auth/api'

I think it's an artifact of the previous version that uses full connection and not Authorization header

config/routes.rb Outdated
Comment on lines 19 to 21
resources :users, controller: :users, as: 'api_users'
resources :api_keys, controller: :api_keys, as: 'api_api_keys'
resources :machines, controller: :machines, as: 'api_machines'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely things to do about these endpoints but that's not the goal of this PR

Comment on lines 7 to 9
@api_key = api_keys(:FakeRadius)
@real_key = '5fcdb374f0a70e9ff0675a0ce4acbdf6d21225fe74483319c2766074732d6d80'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe test if a wrong api key is provided ?

Copy link
Member

@nymous nymous left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great PR! Limited in scope, with tests, docs and example sources...

I left some comments on the files, and maybe a more "general" thing to consider:
Maybe we should rework a little bit the authenticate_with_http_token and its callback authenticator, which doesn't have a ! but raises an exception if the API key is not found, but because we only use authenticate_with_http_token and not authenticate_or_request_with_http_token Rails doesn't return a 401 if the API key is wrong, it's just a 404... Maybe get a bit closer from the example, where they use the or_request method, and the find_by (without !) to return nil if no API key matches instead of an exception, that way we have proper HTTP status codes (and we can always implement the "optional" auth if needed...)

@github-project-automation github-project-automation bot moved this from Reviewer approved to Review in progress in Lea5 Apr 14, 2025
class ApiKeysController < ApiApplicationController
def index
@api_keys = ApiKey.accessible_by(current_ability)
authorize! :index, @api_keys
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure you can call authorize! :index with a list of objects, or if it works like you expect. Can you try to remove the permissions in your ability.rb and make sure this breaks as expected?
(But if it works, great! We will add it to the other "index" actions in all controllers ^^)

format.html { render 'new', status: :unprocessable_entity }
end
if @api_key.save
flash[:new_api_key] = "ApiKey a dded! It is #{@api_key.api_key}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you specify the entire flash text, I think you can keep the :success key. I was thinking of using a custom flash key to allow for a specific presentation in the view, where we show the API key in a code block.

assert_raises CanCan::AccessDenied do
get "#{api_api_keys_path}.json"
end
get "#{api_api_keys_path}.json"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can now remove these .json in paths, with the default fomat: :json!

Comment on lines 14 to 16
MOD_DIRECTORY = 'app/controllers/api/'
MOD_CONTROLLER = 'ApiApplicationController'
MSG = "All controllers in the Mod namespace should inherit from #{MOD_CONTROLLER}" # rubocop:disable Style/MutableConstant
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MOD_DIRECTORY = 'app/controllers/api/'
MOD_CONTROLLER = 'ApiApplicationController'
MSG = "All controllers in the Mod namespace should inherit from #{MOD_CONTROLLER}" # rubocop:disable Style/MutableConstant
API_DIRECTORY = 'app/controllers/api/'
API_CONTROLLER = 'ApiApplicationController'
MSG = "All controllers in the Api namespace should inherit from #{API_CONTROLLER}".freeze

parent_class = node.parent_class&.const_name || ''

if parent_class == MOD_CONTROLLER ||
(parent_module_name.split('::').include?('Mod') && "Mod::#{parent_class}" == MOD_CONTROLLER)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(parent_module_name.split('::').include?('Mod') && "Mod::#{parent_class}" == MOD_CONTROLLER)
(parent_module_name.split('::').include?('Api') && "Api::#{parent_class}" == MOD_CONTROLLER)

Copy link
Member

@nymous nymous left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's gooooo

@github-project-automation github-project-automation bot moved this from Review in progress to Reviewer approved in Lea5 May 10, 2025
@AliasXoX AliasXoX merged commit 9da0348 into master May 10, 2025
7 checks passed
@AliasXoX AliasXoX deleted the API_Key_implementation branch May 10, 2025 13:40
@github-project-automation github-project-automation bot moved this from Reviewer approved to Done in Lea5 May 10, 2025
@@ -107,4 +110,8 @@ def setup
test 'admin can do everything' do
assert @admin_ability.can?(:manage, :all)
end

test 'shoul be able to read everything with an api key' do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo shoul instead of should

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

6 participants