Skip to content

Commit

Permalink
API Key Management Backend (#1862)
Browse files Browse the repository at this point in the history
* API Management Backend: Add title and description to api_keys

Model changes
 - Add title and description fields to api_keys model.
 - Add a user_id field which automatically takes the current user's id.
 - Add a team_id field

GraphQL Changes
Add mutations to create and delete api keys. User has to be an admin
in the current workspace in order to be able to administer api keys.

* Add GraphQL query to fetch team api keys

Add ability to list all api keys in a team, and the ability to fetch one api key given its id.

* Add mutation and test to delete team api keys

* Add UserType to ApiKeyType

* Create bot user when creating api keys via GraphQL

When the CreateApiKey mutation is called, we will automatically create a new BotUser linked to the team. This bot user will host and own the api key.

* Polish api-key creation permissions

* Add reviewer feedback

* Generate relay/graphql schema

* Add team and user to api keys (in original migration)

* Add reviewer feedback

- Clean up ApiKeyMutations
- Change team and user columns in api_keys table to references
- Add maximum number of api_keys in a team
- Increase default expiry limit of api_keys

* Fix failing tests

* Fix deletion of api keys, and bot_user name

* Set default role for TeamUser when creating api_key

When creating api_key, set the role for the created TeamUser to 'editor'

* Don't create bot_user when creating api keys without team

Don't create bot_user when creating api keys without team

* Create bot user only when needed

* Get API key expiry from application configuration

* Log all graphql activity

Log all calls to the graphql endpoint

* ApiKey title should not be mandatory

* Fix failing tests

* Order team api keys by creation date

* Revert unintended changes to schema.rb

* Log graphql activity only when a user is available

---------

Co-authored-by: Alexandre Amorim <[email protected]>
  • Loading branch information
jayjay-w and amoedoamorim authored May 8, 2024
1 parent 1cbb141 commit 2c531f4
Show file tree
Hide file tree
Showing 20 changed files with 1,120 additions and 65 deletions.
12 changes: 12 additions & 0 deletions app/controllers/api/v1/graphql_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def batch
protected

def parse_graphql_result
log_graphql_activity
context = { ability: @ability, file: parse_uploaded_files }
@output = nil
begin
Expand All @@ -64,6 +65,17 @@ def parse_graphql_result
end
end

def log_graphql_activity
return if User.current.nil?

uid = User.current.id
user_name = User.current.name
team = Team.current || User.current.current_team
team = team.nil? ? '' : team.name
role = User.current.role
Rails.logger.info("[Graphql] Logging activity: uid: #{uid} user_name: #{user_name} team: #{team} role: #{role}")
end

def parse_uploaded_files
file_param = request.params[:file]
file = file_param
Expand Down
11 changes: 11 additions & 0 deletions app/graph/mutations/api_key_mutations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module ApiKeyMutations
MUTATION_TARGET = 'api_key'.freeze
PARENTS = ['team'].freeze

class Create < Mutations::CreateMutation
argument :title, GraphQL::Types::String, required: false
argument :description, GraphQL::Types::String, required: false
end

class Destroy < Mutations::DestroyMutation; end
end
17 changes: 17 additions & 0 deletions app/graph/types/api_key_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class ApiKeyType < DefaultObject
description "ApiKey type"

implements GraphQL::Types::Relay::Node

field :dbid, GraphQL::Types::Int, null: true
field :team_id, GraphQL::Types::Int, null: true
field :user_id, GraphQL::Types::Int, null: true
field :title, GraphQL::Types::String, null: true
field :access_token, GraphQL::Types::String, null: true
field :description, GraphQL::Types::String, null: true
field :application, GraphQL::Types::String, null: true
field :expire_at, GraphQL::Types::String, null: true

field :team, TeamType, null: true
field :user, UserType, null: true
end
3 changes: 3 additions & 0 deletions app/graph/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,7 @@ class MutationType < BaseObject
field :createExplainer, mutation: ExplainerMutations::Create
field :updateExplainer, mutation: ExplainerMutations::Update
field :destroyExplainer, mutation: ExplainerMutations::Destroy

field :createApiKey, mutation: ApiKeyMutations::Create
field :destroyApiKey, mutation: ApiKeyMutations::Destroy
end
20 changes: 20 additions & 0 deletions app/graph/types/team_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,24 @@ def tipline_messages(uid:)
def articles(article_type:)
object.explainers if article_type == 'explainer'
end

field :api_key, ApiKeyType, null: true do
argument :dbid, GraphQL::Types::Int, required: true
end

def api_key(dbid:)
ability = context[:ability] || Ability.new
api_key = object.get_api_key(dbid)
ability.can?(:read, api_key) ? api_key : nil
end

field :api_keys, ApiKeyType.connection_type, null: true
def api_keys
ability = context[:ability] || Ability.new
api_keys = object.api_keys.order(created_at: :desc)

api_keys.select do |api_key|
ability.can?(:read, api_key)
end
end
end
1 change: 1 addition & 0 deletions app/models/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def admin_perms
can :destroy, FeedTeam do |obj|
obj.team_id == @context_team.id || obj.feed.team_id == @context_team.id
end
can [:create, :update, :read, :destroy], ApiKey, :team_id => @context_team.id
end

def editor_perms
Expand Down
35 changes: 33 additions & 2 deletions app/models/api_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ class Check::TooManyRequestsError < StandardError
end

class ApiKey < ApplicationRecord
belongs_to :team, optional: true
belongs_to :user, optional: true

validates_presence_of :access_token, :expire_at
validates_uniqueness_of :access_token
validates :title, uniqueness: { scope: :team }

before_validation :generate_access_token, on: :create
before_validation :calculate_expiration_date, on: :create
before_validation :set_user_and_team
after_create :create_bot_user

validate :validate_team_api_keys_limit, on: :create

has_one :bot_user
has_one :bot_user, dependent: :destroy

# Reimplement this method in your application
def self.applications
Expand All @@ -34,7 +42,30 @@ def generate_access_token
end
end

def create_bot_user
if self.bot_user.blank? && self.team.present?
bot_name = "#{self.team.slug}-bot-#{self.title}"
new_bot_user = BotUser.new(api_key: self, name: bot_name, login: bot_name)
new_bot_user.skip_check_ability = true
new_bot_user.set_role 'editor'
new_bot_user.save!
end
end

def set_user_and_team
self.user = User.current unless User.current.nil?
self.team = Team.current unless Team.current.nil?
end

def calculate_expiration_date
self.expire_at ||= Time.now.since(30.days)
api_default_expiry_days = CheckConfig.get('api_default_expiry_days', 30).to_i
self.expire_at ||= Time.now.since(api_default_expiry_days.days)
end

def validate_team_api_keys_limit
return unless team

max_team_api_keys = CheckConfig.get('max_team_api_keys', 20).to_i
errors.add(:base, "Maximum number of API keys exceeded") if team.api_keys.count >= max_team_api_keys
end
end
3 changes: 2 additions & 1 deletion app/models/bot_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,9 @@ def set_default_identifier

def create_api_key
if self.api_key_id.blank?
api_key = ApiKey.new
api_key = ApiKey.new(bot_user: self)
api_key.skip_check_ability = true
api_key.title = self.name
api_key.save!
api_key.expire_at = api_key.expire_at.since(100.years)
api_key.save!
Expand Down
1 change: 1 addition & 0 deletions app/models/concerns/team_associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module TeamAssociations
has_many :tipline_newsletters
has_many :tipline_requests, as: :associated
has_many :explainers, dependent: :destroy
has_many :api_keys

has_annotations
end
Expand Down
4 changes: 4 additions & 0 deletions app/models/team.rb
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,10 @@ def get_feed(feed_id)
self.feeds.where(id: feed_id.to_i).last
end

def get_api_key(api_key_id)
self.api_keys.where(id: api_key_id.to_i).last
end

# A newsletter header type is available only if there are WhatsApp templates for it
def available_newsletter_header_types
available = []
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ToSOrPrivacyPolicyReadError < StandardError; end
has_many :feeds
has_many :feed_invitations
has_many :tipline_requests
has_many :api_keys

devise :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable,
Expand Down
3 changes: 3 additions & 0 deletions db/migrate/20150729232909_create_api_keys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ class CreateApiKeys < ActiveRecord::Migration[4.2]
def change
create_table :api_keys do |t|
t.string :access_token, null: false, default: ''
t.string :title
t.references :user, null: true
t.references :team, null: true
t.datetime :expire_at
t.jsonb :rate_limits, default: {}
t.string :application
Expand Down
8 changes: 8 additions & 0 deletions db/migrate/20240420104318_add_title_desc_to_api_keys.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddTitleDescToApiKeys < ActiveRecord::Migration[6.1]
def change
add_column(:api_keys, :title, :string) unless column_exists?(:api_keys, :title)
add_column :api_keys, :description, :string
add_reference(:api_keys, :team, foreign_key: true, null: true) unless column_exists?(:api_keys, :team_id)
add_reference(:api_keys, :user, foreign_key: true, null: true) unless column_exists?(:api_keys, :user_id)
end
end
6 changes: 5 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2024_04_17_140727) do
ActiveRecord::Schema.define(version: 2024_04_20_104318) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -172,11 +172,15 @@

create_table "api_keys", id: :serial, force: :cascade do |t|
t.string "access_token", default: "", null: false
t.string "title"
t.integer "user_id"
t.integer "team_id"
t.datetime "expire_at"
t.jsonb "rate_limits", default: {}
t.string "application"
t.datetime "created_at"
t.datetime "updated_at"
t.string "description"
end

create_table "assignments", id: :serial, force: :cascade do |t|
Expand Down
Loading

0 comments on commit 2c531f4

Please sign in to comment.