Skip to content

Commit

Permalink
feat: Add methods for documenting authorization.
Browse files Browse the repository at this point in the history
  • Loading branch information
a-chacon committed Jul 30, 2024
1 parent 7fe53f3 commit f543146
Show file tree
Hide file tree
Showing 20 changed files with 253 additions and 47 deletions.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ gem 'sprockets-rails'

gem 'rack-cors'

gem "bcrypt", "~> 3.1.7"

gem 'jwt'

# Start debugger with binding.b [https://github.com/ruby/debug]
# gem "debug", ">= 1.0.0"
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ GEM
mutex_m
tzinfo (~> 2.0)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.8)
builder (3.3.0)
concurrent-ruby (1.3.3)
Expand All @@ -103,6 +104,8 @@ GEM
irb (1.14.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jwt (2.8.2)
base64
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
Expand Down Expand Up @@ -201,6 +204,8 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
bcrypt (~> 3.1.7)
jwt
oas_rails!
puma
rack-cors
Expand Down
51 changes: 45 additions & 6 deletions lib/generators/oas_rails/config/templates/oas_rails_initializer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# config/initializers/oas_rails.rb
OasRails.configure do |config|
# Basic Information about the API
config.info.title = 'OasRails'
config.info.summary = 'OasRails: Automatic Interactive API Documentation for Rails'
config.info.description = <<~HEREDOC
Expand Down Expand Up @@ -34,17 +35,55 @@
Explore your API documentation and enjoy the power of OasRails!
For more information and advanced usage, visit the [OasRails GitHub repository](https://github.com/a-chacon/oas_rails).
HEREDOC
config.info.contact.name = 'a-chacon'
config.info.contact.email = '[email protected]'
config.info.contact.url = 'https://a-chacon.com'

# Servers Information. For more details follow: https://spec.openapis.org/oas/latest.html#server-object
config.servers = [{ url: 'http://localhost:3000', description: 'Local' }]

# Tag Information. For more details follow: https://spec.openapis.org/oas/latest.html#tag-object
config.tags = [{ name: "Users", description: "Manage the `amazing` Users table." }]

# config.default_tags_from = :namespace # Could be: :namespace or :controller
# config.autodiscover_request_body = true # Try to get request body for create and update methods based on the controller name.
# config.autodiscover_responses = true # Looks for renders in your source code and try to generate the responses.
# config.api_path = "/" # set this config if your api is in a different namespace other than /
# Optional Settings (Uncomment to use)

# Extract default tags of operations from namespace or controller. Can be set to :namespace or :controller
# config.default_tags_from = :namespace

# Automatically detect request bodies for create/update methods
# Default: true
# config.autodiscover_request_body = false

# Automatically detect responses from controller renders
# Default: true
# config.autodiscover_responses = false

# API path configuration if your API is under a different namespace
# config.api_path = "/"

# #######################
# Authentication Settings
# #######################

# Whether to authenticate all routes by default
# Default is true; set to false if you don't want all routes to include secutrity schemas by default
# config.authenticate_all_routes_by_default = true

# Default security schema used for authentication
# Choose a predefined security schema
# [:api_key_cookie, :api_key_header, :api_key_query, :basic, :bearer, :bearer_jwt, :mutual_tls]
# config.security_schema = :bearer

# Custom security schemas
# You can uncomment and modify to use custom security schemas
# Please follow the documentation: https://spec.openapis.org/oas/latest.html#security-scheme-object
#
# config.security_schemas = {
# bearer:{
# "type": "apiKey",
# "name": "api_key",
# "in": "header"
# }
# }
end
51 changes: 28 additions & 23 deletions lib/oas_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,33 @@
require "method_source"
require "esquema"

require_relative 'oas_rails/version'
require_relative 'oas_rails/engine'
require_relative 'oas_rails/oas_base'
require_relative 'oas_rails/configuration'
require_relative 'oas_rails/specification'
require_relative 'oas_rails/route_extractor'
require_relative 'oas_rails/oas_route'
require_relative 'oas_rails/operation'
module OasRails
require "oas_rails/version"
require "oas_rails/engine"

require_relative 'oas_rails/info'
require_relative 'oas_rails/contact'
require_relative 'oas_rails/paths'
require_relative 'oas_rails/path_item'
require_relative 'oas_rails/parameter'
require_relative 'oas_rails/tag'
require_relative 'oas_rails/license'
require_relative 'oas_rails/server'
require_relative "oas_rails/request_body"
require_relative "oas_rails/media_type"
require_relative 'oas_rails/yard/oas_yard_factory'
require_relative "oas_rails/response"
require_relative "oas_rails/responses"
autoload :OasBase, "oas_rails/oas_base"
autoload :Configuration, "oas_rails/configuration"
autoload :Specification, "oas_rails/specification"
autoload :RouteExtractor, "oas_rails/route_extractor"
autoload :OasRoute, "oas_rails/oas_route"
autoload :Operation, "oas_rails/operation"
autoload :Info, "oas_rails/info"
autoload :Contact, "oas_rails/contact"
autoload :Paths, "oas_rails/paths"
autoload :PathItem, "oas_rails/path_item"
autoload :Parameter, "oas_rails/parameter"
autoload :Tag, "oas_rails/tag"
autoload :License, "oas_rails/license"
autoload :Server, "oas_rails/server"
autoload :RequestBody, "oas_rails/request_body"
autoload :MediaType, "oas_rails/media_type"
autoload :Response, "oas_rails/response"
autoload :Responses, "oas_rails/responses"

module Yard
autoload :OasYardFactory, 'oas_rails/yard/oas_yard_factory'
end

module OasRails
class << self
def configure
yield config
Expand All @@ -43,7 +46,9 @@ def configure_yard!
'Parameter' => [:parameter, :with_parameter],
'Response' => [:response, :with_response],
'Endpoint Tags' => [:tags],
'Summary' => [:summary]
'Summary' => [:summary],
'No Auth' => [:no_auth],
'Auth methods' => [:auth, :with_types]
}
yard_tags.each do |tag_name, (method_name, handler)|
::YARD::Tags::Library.define_tag(tag_name, method_name, handler)
Expand Down
58 changes: 54 additions & 4 deletions lib/oas_rails/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
module OasRails
class Configuration
attr_accessor :info, :default_tags_from, :autodiscover_request_body, :autodiscover_responses, :api_path
attr_reader :servers, :tags
attr_accessor :info, :default_tags_from, :autodiscover_request_body, :autodiscover_responses, :api_path, :security_schemas, :authenticate_all_routes_by_default
attr_reader :servers, :tags, :security_schema

def initialize(**kwargs)
def initialize
@info = Info.new
@servers = kwargs[:servers] || default_servers
@servers = default_servers
@tags = []
@swagger_version = '3.1.0'
@default_tags_from = "namespace"
@autodiscover_request_body = true
@autodiscover_responses = true
@api_path = "/"
@authenticate_all_routes_by_default = true
@security_schema = nil
@security_schemas = {}
end

def security_schema=(value)
return unless (security_schema = DEFAULT_SECURITY_SCHEMES[value])

@security_schemas = { value => security_schema }
end

def default_servers
Expand All @@ -26,4 +35,45 @@ def tags=(value)
@tags = value.map { |t| Tag.new(name: t[:name], description: t[:description]) }
end
end

DEFAULT_SECURITY_SCHEMES = {
api_key_cookie: {
type: "apiKey",
in: "cookie",
name: "api_key",
description: "An API key that will be supplied in a named cookie."
},
api_key_header: {
type: "apiKey",
in: "header",
name: "X-API-Key",
description: "An API key that will be supplied in a named header."
},
api_key_query: {
type: "apiKey",
in: "query",
name: "apiKey",
description: "An API key that will be supplied in a named query parameter."
},
basic: {
type: "http",
scheme: "basic",
description: "Basic auth that takes a base64'd combination of `user:password`."
},
bearer: {
type: "http",
scheme: "bearer",
description: "A bearer token that will be supplied within an `Authorization` header as `bearer <token>`."
},
bearer_jwt: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description: "A bearer token that will be supplied within an `Authorization` header as `bearer <token>`. In this case, the format of the token is specified as JWT."
},
mutual_tls: {
type: "mutualTLS",
description: "Requires a specific mutual TLS certificate to use when making an HTTP request."
}
}.freeze
end
3 changes: 2 additions & 1 deletion lib/oas_rails/oas_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def to_spec
value
end

hash[camel_case_key] = processed_value unless (processed_value.is_a?(Hash) || processed_value.is_a?(Array)) && processed_value.empty?
# hash[camel_case_key] = processed_value unless (processed_value.is_a?(Hash) || processed_value.is_a?(Array)) && processed_value.empty?
hash[camel_case_key] = processed_value
end
hash
end
Expand Down
20 changes: 18 additions & 2 deletions lib/oas_rails/operation.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module OasRails
class Operation < OasBase
attr_accessor :tags, :summary, :description, :operation_id, :parameters, :method, :docstring, :request_body, :responses
attr_accessor :tags, :summary, :description, :operation_id, :parameters, :method, :docstring, :request_body, :responses, :security

def initialize(method:, summary:, operation_id:, **kwargs)
super()
Expand All @@ -12,6 +12,7 @@ def initialize(method:, summary:, operation_id:, **kwargs)
@parameters = kwargs[:parameters] || []
@request_body = kwargs[:request_body] || {}
@responses = kwargs[:responses] || {}
@security = kwargs[:security] || []
end

class << self
Expand All @@ -23,7 +24,8 @@ def from_oas_route(oas_route:)
parameters = extract_parameters(oas_route:)
request_body = extract_request_body(oas_route:)
responses = extract_responses(oas_route:)
new(method: oas_route.verb.downcase, summary:, operation_id:, tags:, description:, parameters:, request_body:, responses:)
security = extract_security(oas_route:)
new(method: oas_route.verb.downcase, summary:, operation_id:, tags:, description:, parameters:, request_body:, responses:, security:)
end

def extract_summary(oas_route:)
Expand Down Expand Up @@ -112,6 +114,20 @@ def extract_responses(oas_route:)
responses
end

def extract_security(oas_route:)
return [] if oas_route.docstring.tags(:no_auth).any?

if (methods = oas_route.docstring.tags(:auth).first)
OasRails.config.security_schemas.keys.map { |key| { key => [] } }.select do |schema|
methods.types.include?(schema.keys.first.to_s)
end
elsif OasRails.config.authenticate_all_routes_by_default
OasRails.config.security_schemas.keys.map { |key| { key => [] } }
else
[]
end
end

def external_docs; end
end
end
Expand Down
32 changes: 26 additions & 6 deletions lib/oas_rails/specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,48 @@ def to_json(*_args)
{}
end

# Create the Base of the OAS hash.
# @see https://spec.openapis.org/oas/latest.html#schema
def base_spec
{
openapi: '3.1.0',
info: OasRails.config.info.to_spec,
servers: OasRails.config.servers.map(&:to_spec),
paths: paths_spec,
components: components_spec,
security: [],
paths:,
components:,
security:,
tags: OasRails.config.tags.map(&:to_spec),
externalDocs: {}
}
end

def paths_spec
# Create the Security Requirement Object.
# @see https://spec.openapis.org/oas/latest.html#security-requirement-object
def security
return [] unless OasRails.config.authenticate_all_routes_by_default

OasRails.config.security_schemas.map { |key, _| { key => [] } }
end

# Create the Paths Object For the Root of the OAS.
# @see https://spec.openapis.org/oas/latest.html#paths-object
def paths
Paths.from_string_paths(string_paths: RouteExtractor.host_paths).to_spec
end

def components_spec
# Created the Components Object For the Root of the OAS.
# @see https://spec.openapis.org/oas/latest.html#components-object
def components
{
schemas: {}, parameters: {}, securitySchemas: {}, requestBodies: {}, responses: {},
schemas: {}, parameters: {}, securitySchemes: security_schemas, requestBodies: {}, responses: {},
headers: {}, examples: {}, links: {}, callbacks: {}
}
end

# Create the Security Schemas Array inside components field of the OAS.
# @see https://spec.openapis.org/oas/latest.html#security-scheme-object
def security_schemas
OasRails.config.security_schemas
end
end
end
7 changes: 6 additions & 1 deletion test/controllers/oas_rails/oas_rails_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ module OasRails
class OasRailsControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers

test 'should return the front' do
get '/docs'
assert_response :ok
end

test 'should return the oas' do
get '/docs/oas'
get '/docs.json'
assert_response :ok
end
end
Expand Down
4 changes: 3 additions & 1 deletion test/dummy/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ gem 'jbuilder'
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: %i[windows jruby]
Expand Down Expand Up @@ -56,3 +56,5 @@ group :test do
gem 'capybara'
gem 'selenium-webdriver'
end

gem 'jwt'
Loading

0 comments on commit f543146

Please sign in to comment.