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

Api key implementation #466

Open
wants to merge 39 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
636367a
Implementing search feature
benoitlx May 29, 2024
d390724
add apikey controller
AliasXoX Jun 2, 2024
55ab055
adding duration test the 31 of month #issue-2328471615
benoitlx Jun 2, 2024
3b2c707
add form and create feature for ApiKey
AliasXoX Jun 3, 2024
9cfb6ee
add apikey route
AliasXoX Jun 5, 2024
cf14ca4
add new page and functionnal form to add api key
AliasXoX Jun 5, 2024
98e5f38
delete api key create view
AliasXoX Jun 5, 2024
d46f1d1
deleted function in api key helper
AliasXoX Jun 5, 2024
a24f1f9
add first tests
AliasXoX Jun 5, 2024
99385e2
test added for api key | checks if it can create an ApiKey
AliasXoX Jun 5, 2024
62ef0c7
New page for creating api keys
AliasXoX Jun 7, 2024
a814f92
add tests for api key index and destroy actions
AliasXoX Jun 7, 2024
896d616
add fixture for api key
AliasXoX Jun 7, 2024
0a84054
adding access via api authentication
AliasXoX Jun 14, 2024
0e60fad
oups
AliasXoX Jun 14, 2024
5be2472
adding cancancan abilities to api authentication
AliasXoX Jun 14, 2024
be2b48a
oups 2
AliasXoX Jun 14, 2024
3b22322
adding documentation
AliasXoX Jun 16, 2024
c94ec41
adding hashed api keys
AliasXoX Jun 17, 2024
5823b20
really hashing the keys
AliasXoX Jun 17, 2024
7007ebc
don't really know what's going on but tests finally passed
AliasXoX Jun 17, 2024
543e5ad
adding credentials for test environment
AliasXoX Jun 30, 2024
23f2551
Yes
AliasXoX Jun 30, 2024
b912261
Merge branch 'master' of https://github.com/rezoleo/lea5
AliasXoX Jun 30, 2024
a6b181d
test credentials are public
AliasXoX Jun 30, 2024
2f19aa3
adding tests
AliasXoX Jul 2, 2024
bc914f9
Branch API_Key_implementation up to date with master
AliasXoX Jul 2, 2024
fd171e1
adding locals
AliasXoX Jul 2, 2024
2431af6
adding locals
AliasXoX Jul 2, 2024
8e7d33f
adding locals
AliasXoX Jul 2, 2024
c6258a9
adding tests to check rights for api keys
AliasXoX Jul 3, 2024
0a5fde7
adding authorizations tests
AliasXoX Jul 3, 2024
2917a3c
adding test for a session opened by an api key authentication
AliasXoX Jul 3, 2024
3587ade
adding tests
AliasXoX Jul 3, 2024
862cab8
creation of /api endpoint
AliasXoX Oct 26, 2024
f403dda
adding machines to /api scope & reformating api keys jbuilder view
AliasXoX Oct 26, 2024
5737cc0
Merge branch 'master' of https://github.com/rezoleo/lea5
AliasXoX Oct 26, 2024
a89d1cd
Merge branch 'master' into API_Key_implementation
AliasXoX Oct 26, 2024
d1c5a4c
Rubocop...
AliasXoX Oct 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/abilities/api_key_ability.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class ApiKeyAbility
include CanCan::Ability
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.

end
end
2 changes: 1 addition & 1 deletion app/models/ability.rb → app/abilities/user_ability.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class Ability
class UserAbility
include CanCan::Ability

def initialize(user)
Expand Down
47 changes: 47 additions & 0 deletions app/controllers/api_keys_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

class ApiKeysController < ApplicationController
include ApiKeyAuthenticatable
include SessionsHelper

# Require token authentication for index

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

end

def new
@api_key = ApiKey.new
authorize! :new, @api_key
end

def create
@api_key = ApiKey.new(api_key_params)
authorize! :create, @api_key
respond_to do |format|
if @api_key.save
format.html do
flash[:success] = "ApiKey added! It is #{@api_key.key}"
redirect_to api_keys_url
end
else
format.html { render 'new', status: :unprocessable_entity }
end
end
end

def destroy
@api_key = ApiKey.find(params[:id])
authorize! :destroy, @api_key
@api_key.destroy
flash[:success] = 'ApiKey deleted!'
redirect_to api_keys_url
end

private

def api_key_params
params.require(:api_key).permit(:bearer_name)
end
end
20 changes: 20 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,32 @@

class ApplicationController < ActionController::Base
include SessionsHelper
include ApiKeyAuthenticatable

before_action :still_authenticated?
before_action :api_auth

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

ApiKeyAbility.new(current_bearer)
elsif !session[:user_id].nil?
UserAbility.new(current_user)
else
UserAbility.new(nil)
end
end

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


current_bearer = authenticate_or_request_with_http_token { |token, _options| authenticator(token) }
log_in_api current_bearer
end
end
20 changes: 20 additions & 0 deletions app/controllers/concerns/api_key_authenticatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module ApiKeyAuthenticatable
extend ActiveSupport::Concern

include ActionController::HttpAuthentication::Basic::ControllerMethods
include ActionController::HttpAuthentication::Token::ControllerMethods

attr_reader :current_api_key

private

attr_writer :current_api_key

def authenticator(http_token)
@current_api_key = ApiKey.authenticate_by_token! http_token

current_api_key
end
end
1 change: 1 addition & 0 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line 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

def create
user = User.upsert_from_auth_hash(request.env['omniauth.auth'])
log_in user
Expand Down
4 changes: 4 additions & 0 deletions app/helpers/api_keys_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

module ApiKeysHelper
end
15 changes: 14 additions & 1 deletion app/helpers/sessions_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ def current_user
@current_user
end

def current_bearer
return nil if session[:api_key_id].nil?

@current_bearer = ApiKey.find(session[:api_key_id])
@current_bearer
end

def logged_in?
!current_user.nil?
!current_user.nil? || !current_bearer.nil?
end

def log_in(user)
Expand All @@ -20,6 +27,12 @@ def log_in(user)
session[:groups] = user.groups
end

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

end

# TODO: also logout of sso
def log_out
reset_session
Expand Down
26 changes: 26 additions & 0 deletions app/models/api_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

class ApiKey < ApplicationRecord
HMAC_SECRET_KEY = Rails.application.credentials.api_key_hmac_secret_key!

validates :bearer_name, presence: true, allow_blank: false

before_create :generate_token_hmac_digest

attr_accessor :key

def self.authenticate_by_token!(key)
digest = OpenSSL::HMAC.hexdigest 'SHA256', HMAC_SECRET_KEY, key

find_by! api_key: digest
end

private

def generate_token_hmac_digest
@key = SecureRandom.hex(32)

digest = OpenSSL::HMAC.hexdigest 'SHA256', HMAC_SECRET_KEY, @key
self.api_key = digest
end
end
11 changes: 11 additions & 0 deletions app/views/api_keys/_api_key.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%# locals: (api_key:) -%>

<div class="api_key">
<span><%= api_key.bearer_name %></span>
<span><%=api_key.created_at %></span>
<% if can?(:destroy, api_key) %>
<%= button_to(api_key,method: :delete, data: { turbo_confirm: "Are you sure ?" }, 'aria-label': 'Delete this api key') do %>
<%= svg_icon_tag 'icon_delete' %>
<% end %>
<% end %>
</div>
11 changes: 11 additions & 0 deletions app/views/api_keys/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%# locals: () -%>

<%= form_with(model: @api_key, class: 'form') do |f| %>
<div>
<%= f.label :bearer_name %>
<%= f.text_field :bearer_name, required: true %>
</div>
<div>
<%= f.submit yield(:button_text) %>
</div>
<% end %>
30 changes: 30 additions & 0 deletions app/views/api_keys/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<%# locals: () -%>

<main>
<div class="container">
<div class="card-details-container card-api-keys card-details-api-keys">
<div class="card card-details">

<div class="card-title">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path xmlns="http://www.w3.org/2000/svg" d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0S96 57.3 96 128s57.3 128 128 128zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/>
</svg>
<h2>Api Keys</h2>
</div>

<%# Need to pass an instance for an ability with block https://github.com/CanCanCommunity/cancancan/blob/develop/docs/define_abilities_with_blocks.md %>
<% if can?(:create, ApiKey) %>
<%= render "components/buttons/button_primary_create_api", text: "new api key", path: new_api_key_path, aria_label: "Add a new api key" %>
<% end %>

</div>

<div class="divider"></div>
</div>
<div class="card card-api-key card-content">
<div class="card-content-machines">
<%= render(@api_keys) || "No bearers of an api key" %>
</div>
</div>
</div>
</main>
5 changes: 5 additions & 0 deletions app/views/api_keys/index.json.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

# locals: ()

json.array! @api_keys, :id, :bearer_name, :created_at, :updated_at
7 changes: 7 additions & 0 deletions app/views/api_keys/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%# locals: () -%>

<% provide :button_text, "Create" %>

<h1>ApiKey#new</h1>

<%= render 'form' %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<%# locals: (aria_label:, path:, text:) -%>

<%= button_to(path, method: :get, class: "button-primary", 'aria-label': aria_label) do %>
<%= text %>
<%= svg_icon_tag 'icon_plus' %>
<% end %>
3 changes: 3 additions & 0 deletions app/views/layouts/_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<li>
<%= link_to "Users", users_path %>
</li>
<li>
<%= link_to "Api Keys", api_keys_url %>
</li>
<% end %>
</ul>
</nav>
Expand Down
2 changes: 1 addition & 1 deletion config/credentials.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VfC7hNP55EjMyi4vt8jK8lAaRmOkql+wsyAFxSVsQdVT8tbZk1l0dgyELsvoVfHwU9kqzI+wMIFyuH6MoXBhcghZfa6m5g683FM06BDkYPwwDflEVeox0DvWmgGji4qzk3oFe57T9qQT736mz3dWfeQTzvjHnaUpq27gYQTpQHOuELjYwKsMXbFRFiNNMmG5phiG2k0Asc7dqZ8CRPwmhJYPm5aJBc9Bzrz3ebBnhxmZ+JzZ8AsZnnvnAFnylm2jRwgN91kJE9l3fkWVTlF6rm0EoCJ9r3neibTeDu2PtNnkYISp3O7chrBJKo6FS9GFAcOxgkxB8QPiX2TV2s3jPiNyxffxLqUe+jziPP4dDiHdZvKrONIltblTJV2WTzur7/82fCEhQUf2oM7uwavq/yvQNOlay4nUD9TyBGZrs1fyFuulT4Ik2/w4T7usUC6am1xIzB2ITF8IuAgolkT/5EPsIs45gJEpJy9Fqlg1PAv01NN9hhViEl+A--tARvrsUmwyVJ9/XI--ytpULZnEcYeOSqfUYhHAnA==
Ctjn6rzrT8lN8apnt2Pd02BtoVstxzsZTgu7DACFn3A6pXt5rbEb1Z79Tcx5R6t2lB9MbLauB/DwUyS9JdzjzKeQQnGvGDHo0j9/vR1I1N3m3+sEXOuPNihRzzCLgePkh4jYtqNZ8NK1MdjmeG6kfPvB4fYNgLz+LWHudMLDhW98Ee3U6yso/edEMlXolmXyp3C4LeyqW/4qVPqaonUU4LJpYeG+UZndxjifEU8vpIhY4dW10mVpdO5JSnteNbtRBc/ZojuVla69hLEgYyr1uiyoy23ArtztfpyXhZEc6cmuJoXi7gpJd9sIDOZf2/xwasRW1EkI3NgpREj4Gy1t7lSwWLf8vp4j3qpC3/ifnGUUNAoJfBjax18WVOfzf2z051Q1TRN0tMhTknihNcUjAer54eT+JYHv7PLbxACcHs4evI/mnuglBOvI9DggODsUIe/7BqM0oQ0j1lj0/m7FHnexdc8FFHrrZ5IXN3VvZ6E0dAEMXNY8gS66mNjNEf2jfXKRicG/eu0FHYRiR00QgtVKsrggUy5IQnLc5Nj3lAq74Bi53UFDQI/AnaIA6uGQtf2lG3XTyfaadwMA6hOFKSQtVd1/OVJAptFIyJkkMiBiJAcU--rNRzrZK7K/fPFPxc--fNlBrjqZoGt/U748IRtjng==
2 changes: 1 addition & 1 deletion config/credentials/production.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
QF2+HIDkGNTPjx81hqlGjQAZyIzZVAp9c7J+dWPqlggPt9u/LWhxWWi6vY7KXGYe3opGafwsWlOgrO0BXtzbOg+RK+Bq9a8rUgsKchqHi44B487Y9/rTvS5bcCTJj5c78AJFdZCsEdOXqUv6khBGHB2i8fGQ5LZyh/SFJhmyt3QqGuL6Ig4MsuD/275o2M94CjrlXT2nNGe6BPl94GurtQv7s67bbgrSy3N2f0Kc+VR4VfrIhTwgkpXxEBv7vk/ok/1WvYpqRbFNuyPhiqkZ6Rj+0oK7JCxtFDL15tBtrIRbdpNuBeWO8BQwf/CfvcgBsJnOGg//LU/AJe+ndMCdUzdfVpuB1QKG4A62lmrLNOEwlw/K3JdPsWqPEh6rSVIr5nAzl4oaxMo0NZs7VK7ZhNpGxkvdNVQEZDxchZkaanQOIFU02240w3nMHYx5aed1MEj2ZpuR26Jg+W85m3K0BvsCDcXAouLTvMCB8kGZ8H6SOWExIdbIhmM3--x6H3t+K9w244qMLp--x+vXvS3cObYjuhqsAb4HYA==
Zmnkjz2oYau2HVmSP0GPQ/fzdTezrNhjFyIiP7okk4UZoOATR8X1mbTwulJF0/1vmtmoCr8jTAPWLfRY+0SwZAidd7yh16vhWCy6mpd5/OMeupQencSQCl3T0wgkkMtIa/J64DbfRav8BPhPBHi4St+1x62FM+WRw0udfj+xA2AUPxtIy/FZXIm+WUHcj6wQKXvIY2xJ1YcpA2WAsmow+1d1Aps1UWzMm0pwysm4SsUCDVS3nloaNTQ25g5+0HZ49biY7nbU6E1IM6dsgwK2/yRrrPxgDpv22L/r3BNp1wv0u4XP5LG+/yfaRfcdaO34WegffGsGEbuBf+hJ/fmxEJ3kq3Uk7DVlbFft94kIfzhHPOU/fi6EX4pWwrS4hExJ43X6/nEMtUY2b6ZXQDLazHv6rI2FcVzsSP+CRQwFc3VU2sjiJbHoe9hh1Jz27d/8rc+BjAwiwwpzZyTsAUlFy92qLZIENJskOhiowZewErJIUmV31L1ImRw/NCCC/Qt+hE8VvTkYmlWFU++9v4wddd5u2GUHzmHh7h7QFUCgKYFeqZiZbi58ihfIn/j7mpdJVk6lc73C+zAMI52rN65bZ6pGySU1/IJMGkhPc181hVC8tdiryw==--ss5nXaT6eOm/wEgh--oHW8aumKNzcCbzU7XFQyXg==
1 change: 1 addition & 0 deletions config/credentials/test.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
151fe6f76326223cfb5f7cef5369c504
1 change: 1 addition & 0 deletions config/credentials/test.yml.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mrjWeVMW0ZzGnTS1Yw+YRq/mJUTEyb7vTGp781GRYBv7s4XMK4eRkNrcG3jlb6StbugQFUa3yKSSUac1E/LHrtRgDS8FO7lkN3m3gnD/apzZ6QFQ+ub/Gzhgg9htCB2OSTwKwHINeCuog/wlxeGd3VGon0nNltGlYKv8Fet2W8KyMaOBNSFTwf6zv0jPrke1LRGiqAlNuaA=--w7kZ8Fb41WHlHnQB--mk0Xq4zeXvm7j1VYpZ39jQ==
3 changes: 3 additions & 0 deletions config/initializers/_constants.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# frozen_string_literal: true

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

Comment on lines +3 to +5
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

# Path to login via SSO authentication
AUTH_PATH = '/auth/keycloak'

Expand Down
8 changes: 8 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
resources :free_accesses, shallow: true, except: [:index, :show]
end

resources :api_keys

scope 'api' do
resources :users, controller: :users, as: 'api_users'
resources :api_keys, controller: :api_keys, as: 'api_api_keys'
resources :machines, controller: :machines, as: 'api_machines'
Comment on lines +19 to +21
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

end

get '/search', as: 'search', to: 'search#search'

# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
Expand Down
14 changes: 14 additions & 0 deletions db/migrate/20240531163901_api_keys.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class ApiKeys < ActiveRecord::Migration[7.0]
def change
create_table :api_keys do |t|
t.integer :bearer_id, null: false, index: { unique: true }
t.string :bearer_name, null: false
t.string :api_key, null: false, index: { unique: true }
t.datetime :api_key_start_at, index: { unique: true }

t.timestamps
end
end
end
10 changes: 10 additions & 0 deletions db/migrate/20240602124750_fix_api_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class FixApiKey < ActiveRecord::Migration[7.0]
def change
change_table :api_keys do |t|
t.remove :api_key_start_at,
type: :datetime
end
end
end
10 changes: 10 additions & 0 deletions db/migrate/20240603090057_fix_api_key_bearer_id.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class FixApiKeyBearerId < ActiveRecord::Migration[7.0]
def change
change_table :api_keys do |t|
t.remove :bearer_id,
type: :integer
end
end
end
14 changes: 11 additions & 3 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading