Skip to content

Commit

Permalink
Add support for security barrier and invoker
Browse files Browse the repository at this point in the history
  • Loading branch information
hiemanshu committed Nov 29, 2024
1 parent 1666aed commit 52f7758
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 17 deletions.
32 changes: 26 additions & 6 deletions lib/scenic/adapters/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ def views
#
# @param name The name of the view to create
# @param sql_definition The SQL schema for the view.
# @param security_barrier If we should enable security_barrier
# @param security_invoker If we should enable security_invoker
#
# @return [void]
def create_view(name, sql_definition)
execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};"
def create_view(name, sql_definition, security_barrier, security_invoker)
with_statement = build_with_statement(security_barrier, security_invoker)
execute "CREATE VIEW #{quote_table_name(name)} #{with_statement} AS #{sql_definition};"
end

# Updates a view in the database.
Expand All @@ -75,11 +78,13 @@ def create_view(name, sql_definition)
#
# @param name The name of the view to update
# @param sql_definition The SQL schema for the updated view.
# @param security_barrier If we should enable security_barrier
# @param security_invoker If we should enable security_invoker
#
# @return [void]
def update_view(name, sql_definition)
def update_view(name, sql_definition, security_barrier, security_invoker)
drop_view(name)
create_view(name, sql_definition)
create_view(name, sql_definition, security_barrier, security_invoker)
end

# Replaces a view in the database using `CREATE OR REPLACE VIEW`.
Expand All @@ -101,10 +106,13 @@ def update_view(name, sql_definition)
#
# @param name The name of the view to update
# @param sql_definition The SQL schema for the updated view.
# @param security_barrier If we should enable security_barrier
# @param security_invoker If we should enable security_invoker
#
# @return [void]
def replace_view(name, sql_definition)
execute "CREATE OR REPLACE VIEW #{quote_table_name(name)} AS #{sql_definition};"
def replace_view(name, sql_definition, security_barrier, security_invoker)
with_statement = build_with_statement(security_barrier, security_invoker)
execute "CREATE OR REPLACE VIEW #{quote_table_name(name)} #{with_statement} AS #{sql_definition};"
end

# Drops the named view from the database
Expand Down Expand Up @@ -276,6 +284,18 @@ def refresh_dependencies_for(name, concurrently: false)
concurrently: concurrently
)
end

def build_with_statement(security_barrier, security_invoker)
if security_invoker && security_barrier
return "WITH (security_barrier, security_invoker = true)"
elsif security_invoker
return "WITH (security_invoker = true)"
elsif security_barrier
return "WITH (security_barrier)"
end

return ""
end
end
end
end
12 changes: 11 additions & 1 deletion lib/scenic/adapters/postgres/views.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def views_from_postgres
c.relname as viewname,
pg_get_viewdef(c.oid) AS definition,
c.relkind AS kind,
c.reloptions AS options,
n.nspname AS namespace
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
Expand All @@ -43,6 +44,14 @@ def views_from_postgres
def to_scenic_view(result)
namespace, viewname = result.values_at "namespace", "viewname"

security_invoker = result["options"].include?("security_invoker=true")
security_barrier = result["options"].include?("security_barrier=true")

options = {
security_invoker:,
security_barrier:
}

namespaced_viewname = if namespace != "public"
"#{pg_identifier(namespace)}.#{pg_identifier(viewname)}"
else
Expand All @@ -52,7 +61,8 @@ def to_scenic_view(result)
Scenic::View.new(
name: namespaced_viewname,
definition: result["definition"].strip,
materialized: result["kind"] == "m"
materialized: result["kind"] == "m",
options:
)
end

Expand Down
33 changes: 26 additions & 7 deletions lib/scenic/statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ module Statements
# @param materialized [Boolean, Hash] Set to true to create a materialized
# view. Set to { no_data: true } to create materialized view without
# loading data. Defaults to false.
# @param security_barrier [Boolean] Set to true to enable the security barrier
# option on the view. Defaults to false.
# @param security_invoker [Boolean] Set to true to enable the security invoker
# option on the view. Defaults to false.
# @return The database response from executing the create statement.
#
# @example Create from `db/views/searches_v02.sql`
Expand All @@ -22,7 +26,8 @@ module Statements
# SELECT * FROM users WHERE users.active = 't'
# SQL
#
def create_view(name, version: nil, sql_definition: nil, materialized: false)
def create_view(name, version: nil, sql_definition: nil, materialized: false,
security_barrier: false, security_invoker: false)
if version.present? && sql_definition.present?
raise(
ArgumentError,
Expand All @@ -43,7 +48,7 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false)
no_data: no_data(materialized)
)
else
Scenic.database.create_view(name, sql_definition)
Scenic.database.create_view(name, sql_definition, security_barrier, security_invoker)
end
end

Expand All @@ -55,12 +60,16 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false)
# `version` argument to {#create_view}.
# @param materialized [Boolean] Set to true if dropping a meterialized view.
# defaults to false.
# @param security_barrier [Boolean] Set to true to enable the security barrier
# option on the view. Defaults to false.
# @param security_invoker [Boolean] Set to true to enable the security invoker
# option on the view. Defaults to false.
# @return The database response from executing the drop statement.
#
# @example Drop a view, rolling back to version 3 on rollback
# drop_view(:users_who_recently_logged_in, revert_to_version: 3)
#
def drop_view(name, revert_to_version: nil, materialized: false)
def drop_view(name, revert_to_version: nil, materialized: false, security_barrier: false, security_invoker: false)
if materialized
Scenic.database.drop_materialized_view(name)
else
Expand All @@ -83,12 +92,17 @@ def drop_view(name, revert_to_version: nil, materialized: false)
# @param materialized [Boolean, Hash] True if updating a materialized view.
# Set to { no_data: true } to update materialized view without loading
# data. Defaults to false.
# @param security_barrier [Boolean] Set to true to enable the security barrier
# option on the view. Defaults to false.
# @param security_invoker [Boolean] Set to true to enable the security invoker
# option on the view. Defaults to false.
# @return The database response from executing the create statement.
#
# @example
# update_view :engagement_reports, version: 3, revert_to_version: 2
#
def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false)
def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false,
security_barrier: false, security_invoker: false)
if version.blank? && sql_definition.blank?
raise(
ArgumentError,
Expand All @@ -112,7 +126,7 @@ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil,
no_data: no_data(materialized)
)
else
Scenic.database.update_view(name, sql_definition)
Scenic.database.update_view(name, sql_definition, security_barrier, security_invoker)
end
end

Expand All @@ -127,12 +141,17 @@ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil,
# @param version [Fixnum] The version number of the view.
# @param revert_to_version [Fixnum] The version number to rollback to on
# `rake db rollback`
# @param security_barrier [Boolean] Set to true to enable the security barrier
# option on the view. Defaults to false.
# @param security_invoker [Boolean] Set to true to enable the security invoker
# option on the view. Defaults to false.
# @return The database response from executing the create statement.
#
# @example
# replace_view :engagement_reports, version: 3, revert_to_version: 2
#
def replace_view(name, version: nil, revert_to_version: nil, materialized: false)
def replace_view(name, version: nil, revert_to_version: nil, materialized: false,
security_barrier: false, security_invoker: false)
if version.blank?
raise ArgumentError, "version is required"
end
Expand All @@ -143,7 +162,7 @@ def replace_view(name, version: nil, revert_to_version: nil, materialized: false

sql_definition = definition(name, version)

Scenic.database.replace_view(name, sql_definition)
Scenic.database.replace_view(name, sql_definition, security_barrier, security_invoker)
end

private
Expand Down
14 changes: 11 additions & 3 deletions lib/scenic/view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,38 @@ class View
# @return [Boolean]
attr_reader :materialized

# Options definition for security_invoker and security_barrier
# @return Hash[Symbol, Boolean]
attr_reader :options

# Returns a new instance of View.
#
# @param name [String] The name of the view.
# @param definition [String] The SQL for the query that defines the view.
# @param materialized [Boolean] `true` if the view is materialized.
def initialize(name:, definition:, materialized:)
def initialize(name:, definition:, materialized:, options:)
@name = name
@definition = definition
@materialized = materialized
@options = options
end

# @api private
def ==(other)
name == other.name &&
definition == other.definition &&
materialized == other.materialized
materialized == other.materialized &&
options == other.options
end

# @api private
def to_schema
materialized_option = materialized ? "materialized: true, " : ""
security_barrier_option = options[:security_barrier] ? "security_barrier: true, " : ""
security_invoker_option = options[:security_invoker] ? "security_invoker: true, " : ""

<<-DEFINITION
create_view #{UnaffixedName.for(name).inspect}, #{materialized_option}sql_definition: <<-\SQL
create_view #{UnaffixedName.for(name).inspect}, #{security_barrier_option}#{security_invoker_option}#{materialized_option}sql_definition: <<-\SQL
#{escaped_definition.indent(2)}
SQL
DEFINITION
Expand Down

0 comments on commit 52f7758

Please sign in to comment.