Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added
- Telemetry support
[cyberark/conjur#2854](https://github.com/cyberark/conjur/pull/2854)
- Introduces support for Policy Factory, which enables resource creation
through a new `factories` API.
[cyberark/conjur#2855](https://github.com/cyberark/conjur/pull/2855/files)

## [1.19.6] - 2023-07-05

### Added
- New flag to `conjurctl server` command called `--no-migrate` which allows for skipping
the database migration step when starting the server.
[cyberark/conjur#2895](https://github.com/cyberark/conjur/pull/2895)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ gem 'openid_connect'

gem "anyway_config"
gem 'i18n', '~> 1.8.11'

gem 'json_schemer'
gem 'prometheus-client'

group :development, :test do
Expand Down
23 changes: 17 additions & 6 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ GEM
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
anyway_config (2.2.3)
ruby-next-core (>= 0.14.0)
Expand Down Expand Up @@ -116,7 +116,7 @@ GEM
coderay (1.1.3)
command_class (0.0.2)
concurrent-ruby (1.2.2)
conjur-api (5.3.8.pre.194)
conjur-api (5.4.0)
activesupport (>= 4.2)
addressable (~> 2.0)
rest-client
Expand Down Expand Up @@ -207,6 +207,8 @@ GEM
dry-core (~> 0.5, >= 0.5)
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 1.0, >= 1.0.2)
ecma-re-validator (0.4.0)
regexp_parser (~> 2.2)
erubi (1.12.0)
event_emitter (0.2.6)
eventmachine (1.2.7)
Expand All @@ -222,6 +224,7 @@ GEM
globalid (1.1.0)
activesupport (>= 5.0)
haikunator (1.1.1)
hana (1.3.7)
hashdiff (1.0.1)
highline (2.0.3)
http (4.2.0)
Expand All @@ -230,7 +233,7 @@ GEM
http-form_data (~> 2.0)
http-parser (~> 1.2.0)
http-accept (1.7.0)
http-cookie (1.0.4)
http-cookie (1.0.5)
domain_name (~> 0.5)
http-form_data (2.3.0)
http-parser (1.2.3)
Expand All @@ -249,6 +252,11 @@ GEM
activesupport (>= 4.2)
aes_key_wrap
bindata
json_schemer (0.2.24)
ecma-re-validator (~> 0.3)
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
json_spec (1.1.5)
multi_json (~> 1.0)
rspec (>= 2.0, < 4.0)
Expand Down Expand Up @@ -324,7 +332,7 @@ GEM
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
public_suffix (5.0.1)
puma (5.6.4)
nio4r (~> 2.0)
racc (1.7.1)
Expand Down Expand Up @@ -387,6 +395,7 @@ GEM
kwalify (~> 0.7.0)
parser (~> 3.0.0)
rainbow (>= 2.0, < 4.0)
regexp_parser (2.7.0)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
Expand Down Expand Up @@ -471,8 +480,9 @@ GEM
concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.1)
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
uri_template (0.7.0)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
Expand Down Expand Up @@ -531,6 +541,7 @@ DEPENDENCIES
i18n (~> 1.8.11)
iso8601
jbuilder (~> 2.7.0)
json_schemer
json_spec (~> 1.1)
jwt (= 2.2.2)
kubeclient
Expand Down
69 changes: 69 additions & 0 deletions app/controllers/policy_factories_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

require './app/domain/responses'

class PolicyFactoriesController < RestController
include AuthorizeResource

before_action :current_user

def create
response = DB::Repository::PolicyFactoryRepository.new.find(
role: current_user,
**relevant_params(%i[account kind version id])
).bind do |factory|
Factories::CreateFromPolicyFactory.new.call(
account: params[:account],
factory_template: factory,
request_body: request.body.read,
authorization: request.headers["Authorization"]
)
end

render_response(response) do
render(json: response.result)
end
end

def show
allowed_params = %i[account kind version id]
response = DB::Repository::PolicyFactoryRepository.new.find(
role: current_user,
**relevant_params(allowed_params)
)

render_response(response) do
presenter = Presenter::PolicyFactories::Show.new(factory: response.result)
render(json: presenter.present)
end
end

def index
response = DB::Repository::PolicyFactoryRepository.new.find_all(
role: current_user,
account: params[:account]
)
render_response(response) do
presenter = Presenter::PolicyFactories::Index.new(factories: response.result)
render(json: presenter.present)
end
end

private

def render_response(response, &block)
if response.success?
block.call
else
presenter = Presenter::PolicyFactories::Error.new(response: response)
render(
json: presenter.present,
status: response.status
)
end
end

def relevant_params(allowed_params)
params.permit(*allowed_params).slice(*allowed_params).to_h.symbolize_keys
end
end
131 changes: 131 additions & 0 deletions app/db/repository/policy_factory_repository.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
require 'base64'
require 'json'

require './app/domain/responses'

module DB
module Repository
module DataObjects
PolicyFactory = Struct.new(
:name,
:classification,
:version,
:policy,
:policy_branch,
:schema,
:description,
keyword_init: true
)
end

class PolicyFactoryRepository
def initialize(
data_object: DataObjects::PolicyFactory,
resource: ::Resource,
logger: Rails.logger
)
@resource = resource
@data_object = data_object
@logger = logger
@success = ::SuccessResponse
@failure = ::FailureResponse
end

def find_all(account:, role:)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method find_all has 26 lines of code (exceeds 25 allowed). Consider refactoring.

factories = @resource.visible_to(role).where(
Sequel.like(
:resource_id,
"#{account}:variable:conjur/factories/%"
)
).all
.select { |factory| role.allowed_to?(:execute, factory) }
.group_by do |item|
# form is: 'conjur/factories/core/v1/groups'
_, _, classification, _, factory = item.resource_id.split('/')
[classification, factory].join('/')
end
.map do |_, versions|
versions.max { |a, b| factory_version(a.id) <=> factory_version(b.id) }
end
.map do |factory|
response = secret_to_data_object(factory)
response.result if response.success?
end
.compact

if factories.empty?
return @failure.new(
'Role does not have permission to use Factories',
status: :forbidden
)
end

@success.new(factories)
end

def find(kind:, id:, account:, role:, version: nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method find has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method find has 26 lines of code (exceeds 25 allowed). Consider refactoring.

factory = if version.present?
@resource["#{account}:variable:conjur/factories/#{kind}/#{version}/#{id}"]
else
@resource.where(
Sequel.like(
:resource_id,
"#{account}:variable:conjur/factories/#{kind}/%"
)
).all
.select { |i| i.resource_id.split('/').last == id }
.max { |a, b| factory_version(a.id) <=> factory_version(b.id) }
end

resource_id = "#{kind}/#{version || 'v1'}/#{id}"

if factory.blank?
@failure.new(
{ resource: resource_id, message: 'Requested Policy Factory does not exist' },
status: :not_found
)
elsif !role.allowed_to?(:execute, factory)
@failure.new(
{ resource: resource_id, message: 'Requested Policy Factory is not available' },
status: :forbidden
)
else
secret_to_data_object(factory)
end
end

private

def factory_version(factory_id)
version_match = factory_id.match(%r{/v(\d+)/[\w-]+})
return 0 if version_match.nil?

version_match[1].to_i
end

def secret_to_data_object(variable)
_, _, classification, version, id = variable.resource_id.split('/')
factory = variable.secret&.value
if factory
decoded_factory = JSON.parse(Base64.decode64(factory))
@success.new(
@data_object.new(
policy: Base64.decode64(decoded_factory['policy']),
policy_branch: decoded_factory['policy_branch'],
schema: decoded_factory['schema'],
version: version,
name: id,
classification: classification,
description: decoded_factory['schema']&.dig('description').to_s
)
)
else
@failure.new(
{ resource: "#{classification}/#{version}/#{id}", message: 'Requested Policy Factory is not available' },
status: :bad_request
)
Comment on lines +123 to +126

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this failure case truly a :bad_request? Doesn't that imply client error, where this would be a server error (the variable exists, but doesn't contain the information we expect)?

@jvanderhoof jvanderhoof Aug 4, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about 404 Not Found instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is rough because it's dependent on prior actions (as you've pointed out). Micah and I have been talking about phase two of these factories, which makes them first class citizens at the policy level (work here). Much of the pre-population issues will go away with this work.

As this is only an alpha level feature, how do you feel about addressing this work as part of the next phase?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me 👍

end
end
end
end
end
Loading