Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require:
plugins:
- rubocop-packaging
- rubocop-performance
- rubocop-rails
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ end

group :development, :test do
gem "bullet", "~> 8.1"
gem "byebug", "~> 13.0", platforms: %i[mri mingw x64_mingw]
gem "byebug", "~> 13.0", platforms: %i[mri windows]
gem "rspec-rails", "~> 8.0", ">= 8.0.4"
end

Expand Down
3 changes: 2 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ GEM
crack (1.0.1)
bigdecimal
rexml
crass (1.0.6)
crass (1.0.7)
crawler_detect (1.2.11)
qonfig (>= 0.24)
csl (2.2.1)
Expand Down Expand Up @@ -771,6 +771,7 @@ GEM
PLATFORMS
aarch64-linux
arm64-darwin-23
arm64-darwin-25
universal-darwin-21
x86_64-darwin-20
x86_64-linux
Expand Down
98 changes: 98 additions & 0 deletions app/controllers/api_keys_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

class ApiKeysController < ApplicationController
before_action :authenticate_user!
before_action :reject_api_key_credentials!

load_and_authorize_resource except: %i[index create]

def index
load_client_context

include_revoked = params[:include_revoked]

if @client.nil?
if current_user&.is_admin_or_staff?
authorize! :manage, ApiKey
api_keys = include_revoked ? ApiKey.all : ApiKey.active
else
raise CanCan::AccessDenied
end
else
authorize_api_key_index!(@client)
api_keys = include_revoked ? @client.api_keys : @client.api_keys.active
end

api_keys = order_api_keys(api_keys, include_revoked)

options = {
meta: { total: api_keys.count },
params: { current_ability: current_ability },
}

render json: ApiKeySerializer.new(api_keys, options).serializable_hash
end

def create
load_client_context
raise ActiveRecord::RecordNotFound unless @client

api_key = @client.api_keys.build(safe_params)
authorize! :create, api_key

if api_key.save
options = {
params: {
current_ability: current_ability,
include_plain_key: true,
},
}
render json: ApiKeySerializer.new(api_key, options).serializable_hash,
status: :created
else
render json: {
errors: api_key.errors.full_messages.map do |m|
{ status: "422", title: m }
end,
}, status: :unprocessable_entity
end
end

def destroy
@api_key.revoke!
head :no_content
end

private
def reject_api_key_credentials!
return unless current_user&.api_key_authenticated?

raise CanCan::AccessDenied,
"API keys cannot manage credentials; use the client password."
end

def authorize_api_key_index!(client)
probe = client.api_keys.build
unless can?(:manage, probe) || can?(:read, probe)
raise CanCan::AccessDenied
end
end

def load_client_context
@client = current_user&.client_id.present? ? Client.find_by(symbol: current_user.client_id.upcase) : nil
end

def order_api_keys(scope, include_revoked)
if include_revoked
scope.order(
Arel.sql("revoked_at IS NULL DESC, revoked_at DESC, created_at DESC"),
)
else
scope.order(created_at: :desc)
end
end

def safe_params
params.require(:data).require(:attributes).permit(:name)
end
end
19 changes: 18 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def authenticate_user!

fail CanCan::AuthorizationNotPerformed if @current_user.errors.present?

set_api_key_sentry_tags
@current_user
end

Expand Down Expand Up @@ -175,7 +176,13 @@ def is_admin_or_staff?
private
def append_info_to_payload(payload)
super
payload[:uid] = current_user.uid.downcase if current_user.try(:uid)
return unless current_user.try(:uid)

payload[:uid] = current_user.uid.downcase
if current_user.try(:api_key_authenticated?)
payload[:auth_method] = current_user.auth_method
payload[:api_key_prefix] = current_user.api_key_prefix
end
end

def set_raven_context
Expand All @@ -186,5 +193,15 @@ def set_raven_context
{ ip_address: request.ip }
end
)
set_api_key_sentry_tags
end

def set_api_key_sentry_tags
return unless current_user.try(:api_key_authenticated?)

Sentry.set_tags(
auth_method: current_user.auth_method,
api_key_prefix: current_user.api_key_prefix,
)
end
end
4 changes: 4 additions & 0 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def create_token
error_response("Wrong account ID or password.") && return
end

if user.api_key_authenticated? || user.jwt.blank?
error_response("Wrong account ID or password.") && return
end

render json: {
"access_token" => user.jwt, "expires_in" => 3_600 * 24 * 30
}.to_json,
Expand Down
50 changes: 50 additions & 0 deletions app/models/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def initialize(user)

if user.role_id == "staff_admin"
can :manage, :all
can :manage, ApiKey
cannot %i[new create], Doi do |doi|
doi.client.blank? ||
!(
Expand Down Expand Up @@ -77,6 +78,10 @@ def initialize(user)
end
cannot %i[transfer], Client
can %i[manage], ClientPrefix # , :client_id => user.provider_id
can :manage, ApiKey do |api_key|
client = api_key.client
client && client.provider_id == user.provider_id
end

# if Flipper[:delete_doi].enabled?(user)
# can [:manage], Doi, :provider_id => user.provider_id
Expand Down Expand Up @@ -115,6 +120,9 @@ def initialize(user)
can %i[read], Provider
can %i[read update read_contact_information read_analytics], Client, symbol: user.client_id.upcase
can %i[read], ClientPrefix, client_id: user.client_id
can :manage, ApiKey do |api_key|
api_key.client&.symbol&.downcase == user.client_id && user.client&.is_active == "\x01"
end

# if Flipper[:delete_doi].enabled?(user)
# can [:manage], Doi, :client_id => user.client_id
Expand Down Expand Up @@ -145,10 +153,49 @@ def initialize(user)
activity.doi.findable? || activity.doi.client_id == user.client_id
end
can %i[read], :access_datafile
elsif user.role_id == "client_api" && user.client.present? && user.client.is_active == "\x01"
can %i[read], Provider
can %i[read], Client, symbol: user.client_id.upcase
can %i[read], ClientPrefix, client_id: user.client_id
can %i[
read
destroy
update
register_url
validate
undo
get_url
read_landing_page_results
],
Doi,
client_id: user.client_id
can %i[new create], Doi do |doi|
doi.client.prefixes.where(uid: doi.prefix).present? ||
doi.type == "OtherDoi"
end
can %i[read], Doi
can %i[read], User
can %i[read], Activity do |activity|
activity.doi.findable? || activity.doi.client_id == user.client_id
end
elsif user.role_id == "client_api" && user.client.present?
can %i[read], Provider
can %i[read], Client, symbol: user.client_id.upcase
can %i[read], ClientPrefix, client_id: user.client_id
can %i[read], Doi, client_id: user.client_id
can %i[read], Doi
can %i[read], User
can %i[read], :access_datafile
can %i[read], Activity do |activity|
activity.doi.findable? || activity.doi.client_id == user.client_id
end
elsif user.role_id == "client_admin" && user.client.present?
can %i[read], Provider
can %i[read read_contact_information read_analytics], Client, symbol: user.client_id.upcase
can %i[read], ClientPrefix, client_id: user.client_id
can :read, ApiKey do |api_key|
api_key.client&.symbol&.downcase == user.client_id
end
can %i[read], Doi, client_id: user.client_id
can %i[read], Doi
can %i[read], User
Expand All @@ -160,6 +207,9 @@ def initialize(user)
can %i[read], Provider
can %i[read read_contact_information read_analytics], Client, symbol: user.client_id.upcase
can %i[read], ClientPrefix, client_id: user.client_id
can :read, ApiKey do |api_key|
api_key.client&.symbol&.downcase == user.client_id
end
can %i[read get_url read_landing_page_results],
Doi,
client_id: user.client_id
Expand Down
56 changes: 56 additions & 0 deletions app/models/api_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
class ApiKey < ApplicationRecord
include Passwordable

attr_accessor :key

belongs_to :client, touch: true

# Used by ApiKeySerializer belongs_to :client (id_method_name: :symbol)
# so relationship linkage IDs are client symbols (e.g. DATACITE.TESTKEY).
delegate :symbol, to: :client, allow_nil: true

validates_presence_of :client, :name

before_create :initialize_api_key

scope :active, -> { where(revoked_at: nil) }

def revoke!
update!(revoked_at: Time.zone.now)
end

def revoked?
revoked_at != nil
end

def self.authenticate(token)
return nil if token.blank?

prefix = token.to_s[0, 11]
candidates = active.where(key_prefix: prefix)

candidates.find do |api_key|
secure_compare(api_key.key_hash, api_key.encrypt_password_sha256(token))
end
end

private
def initialize_api_key
generate_id
generate_key
end

def generate_id
self.id ||= SecureRandom.uuid
end

def generate_key
prefix = "DC."
secret = SecureRandom.alphanumeric(32)
plain = prefix + secret

self.key = plain
self.key_prefix = prefix + secret[0, 8]
self.key_hash = encrypt_password_sha256(plain)
end
end
1 change: 1 addition & 0 deletions app/models/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class Client < ApplicationRecord
has_many :client_prefixes, dependent: :destroy
has_many :prefixes, through: :client_prefixes
has_many :provider_prefixes, through: :client_prefixes
has_many :api_keys, dependent: :destroy
has_many :activities, as: :auditable, dependent: :destroy

before_validation :set_defaults
Expand Down
Loading
Loading