From 52dfea9bef68017031f73dfd3dcf947fcfd055f0 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 17 Jul 2019 15:36:26 -0400 Subject: [PATCH 1/5] Factor resource kind into a method to make it easier to override In controllers were the `kind` isn't given in the path parameters, we want an easy way for the controller to provide it to the `find_resource` concern. --- app/controllers/concerns/find_resource.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/find_resource.rb b/app/controllers/concerns/find_resource.rb index fa278258ce..938d4ffa12 100644 --- a/app/controllers/concerns/find_resource.rb +++ b/app/controllers/concerns/find_resource.rb @@ -4,7 +4,11 @@ module FindResource extend ActiveSupport::Concern def resource_id - [ params[:account], params[:kind], params[:identifier] ].join(":") + [ params[:account], resource_kind, params[:identifier] ].join(":") + end + + def resource_kind + params[:kind] end protected From d195d115cf878420cdc7530b38ed48fe1c055489 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Fri, 18 Dec 2020 11:47:30 -0500 Subject: [PATCH 2/5] Add PolicyFactory policy type --- .../lib/conjur/policy/types/base.rb | 5 +++ .../lib/conjur/policy/types/policy.rb | 5 +++ .../lib/conjur/policy/types/records.rb | 37 ++++++++++++++++++- .../resolver-fixtures/absolute-members.yml | 20 ++++++++++ .../yaml/all-types-all-fields.expected.yml | 17 +++++++++ .../round-trip/yaml/all-types-all-fields.yml | 14 ++++++- .../spec/types/policy_factory_spec.rb | 36 ++++++++++++++++++ 7 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 gems/policy-parser/spec/types/policy_factory_spec.rb diff --git a/gems/policy-parser/lib/conjur/policy/types/base.rb b/gems/policy-parser/lib/conjur/policy/types/base.rb index 7f1041e80c..a954430351 100644 --- a/gems/policy-parser/lib/conjur/policy/types/base.rb +++ b/gems/policy-parser/lib/conjur/policy/types/base.rb @@ -92,6 +92,11 @@ def expect_record name, value def expect_layer name, value expect_type(name, value, "Layer", ->{ value.is_a?(Layer) }) end + + # If it's a Policy + def expect_policy name, value + expect_type name, value, "Policy", lambda{ value.is_a?(Policy) } + end # If it looks like a resource. def expect_resource name, value diff --git a/gems/policy-parser/lib/conjur/policy/types/policy.rb b/gems/policy-parser/lib/conjur/policy/types/policy.rb index c0ccf0d194..013ef2ee04 100644 --- a/gems/policy-parser/lib/conjur/policy/types/policy.rb +++ b/gems/policy-parser/lib/conjur/policy/types/policy.rb @@ -87,6 +87,11 @@ class Body < YAMLList include Permissions end + class Template < YAMLList + include Grants + include Permissions + end + # Policy includes the functionality of Entitlements, wrapped in a # policy role, policy resource, policy id and policy version. class Policy < Record diff --git a/gems/policy-parser/lib/conjur/policy/types/records.rb b/gems/policy-parser/lib/conjur/policy/types/records.rb index a4dc481a9d..8af19cc047 100644 --- a/gems/policy-parser/lib/conjur/policy/types/records.rb +++ b/gems/policy-parser/lib/conjur/policy/types/records.rb @@ -227,7 +227,42 @@ def role *args end end end - + + class PolicyFactory < Record + include ActsAsResource + include ActsAsRole + + attribute :role, kind: :role, singular: true, dsl_accessor: true + attribute :base, kind: :policy, singular: true, dsl_accessor: true + + alias role_accessor role + + def role *args + if args.empty? + role_accessor || self.owner + else + role_accessor(*args) + end + end + + # Don't include template records, these are pointers to + # future records, not records in this policy + def referenced_records + super - Array(@template) + end + + def template &block + if block_given? + singleton :template, lambda { Template.new }, &block + end + @template ||= [] + end + + def template= template + @template = template + end + end + class AutomaticRole < Base include ActsAsRole diff --git a/gems/policy-parser/spec/resolver-fixtures/absolute-members.yml b/gems/policy-parser/spec/resolver-fixtures/absolute-members.yml index 520260b915..aa7d72a8cb 100644 --- a/gems/policy-parser/spec/resolver-fixtures/absolute-members.yml +++ b/gems/policy-parser/spec/resolver-fixtures/absolute-members.yml @@ -12,6 +12,14 @@ policy: | members: - !user /alice + - !policy-factory + id: foo-factory + template: + - !variable + id: test + owner: !user + id: /test + expectation: | --- - !user @@ -51,3 +59,15 @@ expectation: | role: !group account: the-account id: foo/bar/users + - !policy-factory + account: the-account + id: foo-factory + owner: !role + account: rspec + id: default-owner + kind: user + template: + - !variable + id: test + owner: !user + id: /test \ No newline at end of file diff --git a/gems/policy-parser/spec/round-trip/yaml/all-types-all-fields.expected.yml b/gems/policy-parser/spec/round-trip/yaml/all-types-all-fields.expected.yml index f05855f118..7e23849ef2 100644 --- a/gems/policy-parser/spec/round-trip/yaml/all-types-all-fields.expected.yml +++ b/gems/policy-parser/spec/round-trip/yaml/all-types-all-fields.expected.yml @@ -35,3 +35,20 @@ id: release-bot - !webservice id: quake2-server +- !policy + id: base-policy + body: [] +- !policy-factory + id: certificates + base: + id: base-policy + template: + - !policy + id: sub-policy +- !policy-factory + id: root-factory + owner: + id: bob + template: + - !policy + id: sub-policy \ No newline at end of file diff --git a/gems/policy-parser/spec/round-trip/yaml/all-types-all-fields.yml b/gems/policy-parser/spec/round-trip/yaml/all-types-all-fields.yml index 842f9eb0cc..bb7441008c 100644 --- a/gems/policy-parser/spec/round-trip/yaml/all-types-all-fields.yml +++ b/gems/policy-parser/spec/round-trip/yaml/all-types-all-fields.yml @@ -40,4 +40,16 @@ - !webservice id: quake2-server - \ No newline at end of file +- !policy base-policy + +- !policy-factory + id: certificates + base: !policy base-policy + template: + - !policy sub-policy + +- !policy-factory + id: root-factory + owner: !user bob + template: + - !policy sub-policy diff --git a/gems/policy-parser/spec/types/policy_factory_spec.rb b/gems/policy-parser/spec/types/policy_factory_spec.rb new file mode 100644 index 0000000000..16517eedf3 --- /dev/null +++ b/gems/policy-parser/spec/types/policy_factory_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper.rb' + +describe Conjur::PolicyParser::Types::PolicyFactory do + + subject(:records) { Conjur::PolicyParser::YAML::Loader.load policy} + + context "when policy factory doesn't specify a base" do + let(:policy) { <<-POLICY +- !policy-factory + id: my-factory + template: + - !variable test +POLICY + } + + it "has a nil base" do + expect(records.first.base).to be(nil) + end + end + + context "when policy factory specicies a base" do + let(:policy) { <<-POLICY +- !policy test +- !policy-factory + id: another-factory + base: !policy test + template: + - !variable test2 +POLICY + } + + it "has the base policy set" do + expect(records.second.base.id).to eq('test') + end + end +end From 11f4cd1f3c05458fdd0bd7eb31665461f71e3df6 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 17 Jul 2019 15:45:44 -0400 Subject: [PATCH 3/5] Add Policy Factory database model The Policy Factory in the database stores the policy template for the factory, as well as the policy branch it should load to. --- app/models/policy_factory.rb | 11 +++ .../20190717131940_update_policy_log_enum.rb | 13 +++ .../20190717131941_create_policy_factories.rb | 95 +++++++++++++++++++ spec/models/policy_factory_spec.rb | 22 +++++ 4 files changed, 141 insertions(+) create mode 100644 app/models/policy_factory.rb create mode 100644 db/migrate/20190717131940_update_policy_log_enum.rb create mode 100644 db/migrate/20190717131941_create_policy_factories.rb create mode 100644 spec/models/policy_factory_spec.rb diff --git a/app/models/policy_factory.rb b/app/models/policy_factory.rb new file mode 100644 index 0000000000..832eac4537 --- /dev/null +++ b/app/models/policy_factory.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class PolicyFactory < Sequel::Model + include HasId + + unrestrict_primary_key + + one_to_one :role, class: :Role + many_to_one :base_policy, class: :Resource +end + diff --git a/db/migrate/20190717131940_update_policy_log_enum.rb b/db/migrate/20190717131940_update_policy_log_enum.rb new file mode 100644 index 0000000000..3af2ec756e --- /dev/null +++ b/db/migrate/20190717131940_update_policy_log_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +Sequel.migration do + # ALTER TYPE ... ADD VALUE.. cannot run in a transaction block + no_transaction + + up do + execute <<-SQL + -- Add new table to policy log types + ALTER TYPE policy_log_kind ADD VALUE IF NOT EXISTS 'policy_factories'; + SQL + end +end diff --git a/db/migrate/20190717131941_create_policy_factories.rb b/db/migrate/20190717131941_create_policy_factories.rb new file mode 100644 index 0000000000..e7b4233435 --- /dev/null +++ b/db/migrate/20190717131941_create_policy_factories.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +Sequel.migration do + up do + create_table :policy_factories do + foreign_key :role_id, :roles, type: String, primary_key: true, null: false, on_delete: :cascade + foreign_key :policy_id, :resources, type: String, on_delete: :cascade + + # Can't delete policy if there is a policy factory writing to it + foreign_key :base_policy_id, :resources, type: String, on_delete: :restrict + + column :template, :text + end + + primary_key_columns = schema(:policy_factories).select{|x,s|s[:primary_key]}.map(&:first).map(&:to_s).pg_array + + execute <<-SQL + ALTER TABLE policy_factories + ADD CONSTRAINT verify_policy_kind CHECK (kind(policy_id) = 'policy'); + + ALTER TABLE policy_factories + ADD CONSTRAINT verify_base_policy_kind CHECK (kind(base_policy_id) = 'policy'); + + CREATE OR REPLACE FUNCTION policy_log_policy_factories() RETURNS TRIGGER AS $$ + DECLARE + subject policy_factories; + current policy_versions; + skip boolean; + BEGIN + IF (TG_OP = 'DELETE') THEN + subject := OLD; + ELSE + subject := NEW; + END IF; + + BEGIN + skip := current_setting('conjur.skip_insert_policy_log_trigger'); + EXCEPTION WHEN OTHERS THEN + skip := false; + END; + + IF skip THEN + RETURN subject; + END IF; + + current = current_policy_version(); + IF current.resource_id = subject.policy_id THEN + INSERT INTO policy_log( + policy_id, version, + operation, kind, + subject) + SELECT + (policy_log_record( + 'policy_factories', + #{literal primary_key_columns}, + hstore(subject), + current.resource_id, + current.version, + TG_OP + )).*; + ELSE + RAISE WARNING 'modifying data outside of policy load: %', subject.policy_id; + END IF; + RETURN subject; + END; + $$ LANGUAGE plpgsql + SET search_path FROM CURRENT; + + CREATE TRIGGER policy_log + AFTER INSERT OR UPDATE ON policy_factories + FOR EACH ROW + WHEN (NEW.policy_id IS NOT NULL) + EXECUTE PROCEDURE policy_log_policy_factories(); + + CREATE TRIGGER policy_log_d + AFTER DELETE ON policy_factories + FOR EACH ROW + WHEN (OLD.policy_id IS NOT NULL) + EXECUTE PROCEDURE policy_log_policy_factories(); + + + + SQL + end + + down do + execute """ + DROP TRIGGER policy_log ON policy_factories; + DROP TRIGGER policy_log_d ON policy_factories; + DROP FUNCTION policy_log_policy_factories(); + """ + + drop_table(:policy_factories) + end +end diff --git a/spec/models/policy_factory_spec.rb b/spec/models/policy_factory_spec.rb new file mode 100644 index 0000000000..589475ca39 --- /dev/null +++ b/spec/models/policy_factory_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +shared_context "create policy factory role" do + + # 'let!' always runs before the example; 'let' is lazily evaluated. + let!(:the_user) { + Role.create(role_id: "rspec:policy_factory:#{identifier}") + } +end + +describe PolicyFactory, :type => :model do + include_context "create policy factory role" + + let(:identifier) { 'my-policy-factory' } + + it "policy role is required" do + expect{ PolicyFactory.create(role_id: "") }.to raise_error(Sequel::ForeignKeyConstraintViolation, /(policy_factories_role_id_fkey)/) + end + +end From c6971f22421fe080f9d8f48d3920853d2cfb9878 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 17 Jul 2019 15:47:35 -0400 Subject: [PATCH 4/5] Add policy load orchestration for policy factories --- app/models/audit/subject.rb | 6 +++ app/models/loader/orchestrate.rb | 5 ++- app/models/loader/types.rb | 31 +++++++++++++++ .../models/loader_expectations/base/empty.txt | 3 ++ .../loader_expectations/updated/extended.txt | 3 ++ .../updated/extended_simple_base.txt | 3 ++ .../updated/extended_without_deletion.txt | 3 ++ .../updated/host_factory.txt | 3 ++ .../updated/host_factory_new_layer.txt | 3 ++ .../updated/policy_factory.txt | 38 +++++++++++++++++++ .../loader_expectations/updated/simple.txt | 3 ++ .../updated/simple_with_foreign_role.txt | 3 ++ .../models/loader_fixtures/policy_factory.yml | 13 +++++++ spec/models/loader_orchestrate_spec.rb | 4 ++ 14 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 spec/models/loader_expectations/updated/policy_factory.txt create mode 100644 spec/models/loader_fixtures/policy_factory.yml diff --git a/app/models/audit/subject.rb b/app/models/audit/subject.rb index 1c384d0de2..f390bd6580 100644 --- a/app/models/audit/subject.rb +++ b/app/models/audit/subject.rb @@ -39,5 +39,11 @@ def type ownership == 't' ? :owner : :member end end + + class PolicyFactory < Subject + field :role_id + to_h {{ role: role_id }} + to_s { format "policy_factory %s", role_id } + end end end diff --git a/app/models/loader/orchestrate.rb b/app/models/loader/orchestrate.rb index b855bb5ccf..b3282a036f 100644 --- a/app/models/loader/orchestrate.rb +++ b/app/models/loader/orchestrate.rb @@ -61,7 +61,7 @@ class Orchestrate attr_reader :policy_version, :create_records, :delete_records, :new_roles, :schemata - TABLES = %i[roles role_memberships resources permissions annotations] + TABLES = %i(roles role_memberships resources permissions annotations policy_factories) # Columns to compare across schemata to find exact duplicates. TABLE_EQUIVALENCE_COLUMNS = { @@ -69,7 +69,8 @@ class Orchestrate resources: [ :resource_id, :owner_id ], role_memberships: [ :role_id, :member_id, :admin_option, :ownership ], permissions: [ :resource_id, :privilege, :role_id ], - annotations: [ :resource_id, :name, :value ] + annotations: [ :resource_id, :name, :value ], + policy_factories: [ :role_id, :base_policy_id, :template ] } def initialize( diff --git a/app/models/loader/types.rb b/app/models/loader/types.rb index d86658c0ac..1254b6b8f4 100644 --- a/app/models/loader/types.rb +++ b/app/models/loader/types.rb @@ -347,6 +347,37 @@ def create! end end + class PolicyFactory < Record + def_delegators :@policy_object, :base, :template + + def create! + super + + base_policy_id = find_resourceid(base_resource_id) + + ::PolicyFactory.create( + role_id: roleid, + policy_id: policy_id, + base_policy_id: base_policy_id, + template: template.to_yaml + ) + end + + def identifier + self.roleid.split(':', 3)[2] + end + + def verify; end + + private + + def base_resource_id + # If no base policy Id is provided, the template is loaded + # into the root policy. + base&.resourceid || "#{policy_object.account}:policy:root" + end + end + # Deletions class Deletion < Types::Base diff --git a/spec/models/loader_expectations/base/empty.txt b/spec/models/loader_expectations/base/empty.txt index cfff85c3b8..c2f14136f2 100644 --- a/spec/models/loader_expectations/base/empty.txt +++ b/spec/models/loader_expectations/base/empty.txt @@ -23,3 +23,6 @@ No data. annotations No data. +policy_factories +No data. + diff --git a/spec/models/loader_expectations/updated/extended.txt b/spec/models/loader_expectations/updated/extended.txt index 3ffe26ef71..9371424ac7 100644 --- a/spec/models/loader_expectations/updated/extended.txt +++ b/spec/models/loader_expectations/updated/extended.txt @@ -48,3 +48,6 @@ RESOURCE_ID | NAME | VALUE | POLICY_ID rspec:variable:the-policy/the-secret | description | it's a secret | rspec:policy:the-policy rspec:variable:the-policy/the-secret | length | 20 | rspec:policy:the-policy +policy_factories +No data. + diff --git a/spec/models/loader_expectations/updated/extended_simple_base.txt b/spec/models/loader_expectations/updated/extended_simple_base.txt index cdd80a4596..808ebe99bc 100644 --- a/spec/models/loader_expectations/updated/extended_simple_base.txt +++ b/spec/models/loader_expectations/updated/extended_simple_base.txt @@ -54,3 +54,6 @@ rspec:variable:the-policy/the-secret | description | the-secret | rspec:policy:r rspec:variable:the-policy/the-secret | kind | plain text | rspec:policy:root rspec:variable:the-policy/the-secret | length | 20 | rspec:policy:the-policy +policy_factories +No data. + diff --git a/spec/models/loader_expectations/updated/extended_without_deletion.txt b/spec/models/loader_expectations/updated/extended_without_deletion.txt index 39951a8a84..073ebd4f65 100644 --- a/spec/models/loader_expectations/updated/extended_without_deletion.txt +++ b/spec/models/loader_expectations/updated/extended_without_deletion.txt @@ -54,3 +54,6 @@ rspec:variable:the-policy/the-secret | description | it's a secret | rspec:polic rspec:variable:the-policy/the-secret | kind | plain text | rspec:policy:the-policy rspec:variable:the-policy/the-secret | length | 20 | rspec:policy:the-policy +policy_factories +No data. + diff --git a/spec/models/loader_expectations/updated/host_factory.txt b/spec/models/loader_expectations/updated/host_factory.txt index c2877d50c8..078fca8ffc 100644 --- a/spec/models/loader_expectations/updated/host_factory.txt +++ b/spec/models/loader_expectations/updated/host_factory.txt @@ -30,3 +30,6 @@ No data. annotations No data. +policy_factories +No data. + diff --git a/spec/models/loader_expectations/updated/host_factory_new_layer.txt b/spec/models/loader_expectations/updated/host_factory_new_layer.txt index 5e2a0e371d..3518273c40 100644 --- a/spec/models/loader_expectations/updated/host_factory_new_layer.txt +++ b/spec/models/loader_expectations/updated/host_factory_new_layer.txt @@ -34,3 +34,6 @@ No data. annotations No data. +policy_factories +No data. + diff --git a/spec/models/loader_expectations/updated/policy_factory.txt b/spec/models/loader_expectations/updated/policy_factory.txt new file mode 100644 index 0000000000..544584a67f --- /dev/null +++ b/spec/models/loader_expectations/updated/policy_factory.txt @@ -0,0 +1,38 @@ +roles +ROLE_ID | POLICY_ID +---------------------------------------------|------------------------ +rspec:policy_factory:the-policy/certificates | rspec:policy:the-policy +rspec:policy:root | +rspec:policy:the-policy | rspec:policy:root +rspec:policy:the-policy/certificates | rspec:policy:the-policy +rspec:user:admin | + +role_memberships +ROLE_ID | MEMBER_ID | ADMIN_OPTION | OWNERSHIP | POLICY_ID +---------------------------------------------|-------------------------|--------------|-----------|------------------------ +rspec:policy_factory:the-policy/certificates | rspec:policy:the-policy | true | true | rspec:policy:the-policy +rspec:policy:root | rspec:user:admin | true | true | +rspec:policy:the-policy | rspec:user:admin | true | true | rspec:policy:root +rspec:policy:the-policy/certificates | rspec:policy:the-policy | true | true | rspec:policy:the-policy + +resources +RESOURCE_ID | OWNER_ID | POLICY_ID +---------------------------------------------|-------------------------|------------------------ +rspec:policy_factory:the-policy/certificates | rspec:policy:the-policy | rspec:policy:the-policy +rspec:policy:root | rspec:user:admin | +rspec:policy:the-policy | rspec:user:admin | rspec:policy:root +rspec:policy:the-policy/certificates | rspec:policy:the-policy | rspec:policy:the-policy + +permissions +No data. + +annotations +RESOURCE_ID | NAME | VALUE | POLICY_ID +---------------------------------------------|-------------|----------------------------------------|------------------------ +rspec:policy_factory:the-policy/certificates | description | Policy factory for server certificates | rspec:policy:the-policy + +policy_factories +ROLE_ID | BASE_POLICY_ID | TEMPLATE | POLICY_ID +---------------------------------------------|--------------------------------------|-------------------------------------------------------------------------------------------|------------------------ +rspec:policy_factory:the-policy/certificates | rspec:policy:the-policy/certificates | --- - !policy id: "<%=role.identifier%>" body: - !variable id: ssl-private-key | rspec:policy:the-policy + diff --git a/spec/models/loader_expectations/updated/simple.txt b/spec/models/loader_expectations/updated/simple.txt index d5891ab1b4..ca857f166a 100644 --- a/spec/models/loader_expectations/updated/simple.txt +++ b/spec/models/loader_expectations/updated/simple.txt @@ -41,3 +41,6 @@ RESOURCE_ID | NAME | VALUE | POLICY_ID rspec:variable:the-policy/the-secret | description | the-secret | rspec:policy:the-policy rspec:variable:the-policy/the-secret | kind | plain text | rspec:policy:the-policy +policy_factories +No data. + diff --git a/spec/models/loader_expectations/updated/simple_with_foreign_role.txt b/spec/models/loader_expectations/updated/simple_with_foreign_role.txt index d5891ab1b4..ca857f166a 100644 --- a/spec/models/loader_expectations/updated/simple_with_foreign_role.txt +++ b/spec/models/loader_expectations/updated/simple_with_foreign_role.txt @@ -41,3 +41,6 @@ RESOURCE_ID | NAME | VALUE | POLICY_ID rspec:variable:the-policy/the-secret | description | the-secret | rspec:policy:the-policy rspec:variable:the-policy/the-secret | kind | plain text | rspec:policy:the-policy +policy_factories +No data. + diff --git a/spec/models/loader_fixtures/policy_factory.yml b/spec/models/loader_fixtures/policy_factory.yml new file mode 100644 index 0000000000..54e28226a8 --- /dev/null +++ b/spec/models/loader_fixtures/policy_factory.yml @@ -0,0 +1,13 @@ +- !policy certificates + +- !policy-factory + id: certificates + annotations: + description: Policy factory for server certificates + base: !policy certificates + template: + - !policy + id: <%=role.identifier%> + body: + - !variable + id: ssl-private-key diff --git a/spec/models/loader_orchestrate_spec.rb b/spec/models/loader_orchestrate_spec.rb index 3b31d32fc1..f342edb751 100644 --- a/spec/models/loader_orchestrate_spec.rb +++ b/spec/models/loader_orchestrate_spec.rb @@ -120,6 +120,10 @@ def verify_data(path) replace_policy_with 'simple.yml' verify_data 'updated/simple.txt' end + it "creates a policy factory" do + replace_policy_with 'policy_factory.yml' + verify_data 'updated/policy_factory.txt' + end it "creates a host factory" do replace_policy_with 'host_factory.yml' verify_data 'updated/host_factory.txt' From 41334a9b7771cd2a3e8417f345b94ab1453dd3ed Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 17 Jul 2019 15:47:57 -0400 Subject: [PATCH 5/5] Add policy factory route and controller --- .../policy_factories_controller.rb | 159 +++++++++++++++ config/routes.rb | 11 + cucumber/api/features/policy_factory.feature | 192 ++++++++++++++++++ .../support/policy-factory-context.txt | 5 + 4 files changed, 367 insertions(+) create mode 100644 app/controllers/policy_factories_controller.rb create mode 100644 cucumber/api/features/policy_factory.feature create mode 100644 cucumber/api/features/support/policy-factory-context.txt diff --git a/app/controllers/policy_factories_controller.rb b/app/controllers/policy_factories_controller.rb new file mode 100644 index 0000000000..0fa8ae910e --- /dev/null +++ b/app/controllers/policy_factories_controller.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +# This controller is responsible for creating host records using +# host factory tokens for authorization. +class PolicyFactoriesController < ApplicationController + include FindResource + include AuthorizeResource + + RenderContext = Struct.new(:role, :params) do + def get_binding + binding + end + end + + def create_policy + authorize :execute + + factory = ::PolicyFactory[resource_id] + + template = Conjur::PolicyParser::YAML::Loader.load(factory.template) + + context = RenderContext.new(current_user, params) + + template = update_array(template, context) + + policy_text = template.to_yaml + + response = load_policy(factory.base_policy, policy_text, policy_context) unless dry_run? + + response = { + policy_text: policy_text, + load_to: factory.base_policy.identifier, + dry_run: dry_run?, + response: response + } + render json: response, status: :created + end + + def update_record(record, context) + fields = record.class.fields.keys + + if record.is_a?(Conjur::PolicyParser::Types::Policy) + fields << 'body' + end + + fields.each do |name| + record_value = record.send(name) + + if record_value.class < Conjur::PolicyParser::Types::Base + update_record(record_value, context) + elsif record_value.is_a?(Array) + update_array(record_value, context) + elsif record_value.is_a?(Hash) + update_hash(record_value, context) + elsif record_value.is_a?(String) + rendered_value = ERB.new(record_value).result(context.get_binding) + record.send("#{name}=", rendered_value) + end + end + + record + end + + def update_array(arr, context) + arr.map! do |item| + if item.class < Conjur::PolicyParser::Types::Base + update_record(item, context) + elsif item.is_a?(Array) + update_array(item, context) + elsif item.is_a?(Hash) + update_hash(item, context) + elsif item.is_a?(String) + ERB.new(item).result(context.get_binding) + else + item + end + end + + arr + end + + def update_hash(hsh, context) + hsh.each do |k, val| + if val.class < Conjur::PolicyParser::Types::Base + update_record(val, context) + elsif val.is_a?(Array) + update_array(val, context) + elsif val.is_a?(Hash) + update_hash(val, context) + elsif val.is_a?(String) + hsh[k] = ERB.new(val).result(context.get_binding) + end + end + end + + def get_template + authorize :read + + factory = ::PolicyFactory[resource_id] + + response = { + body: factory.template + } + + render json: response + end + + def update_template + authorize :update + + factory = ::PolicyFactory[resource_id] + + factory.template = request.body.read + factory.save + + response = { + body: factory.template + } + + render json: response, status: :accepted + end + + protected + + def dry_run? + params[:dry_run].present? + end + + def resource_kind + 'policy_factory' + end + + def load_policy(load_to, policy_text, policy_context) + policy_version = PolicyVersion.new( + role: current_user, + policy: load_to, + policy_text: policy_text, + client_ip: request.ip + ) + policy_version.delete_permitted = false + policy = policy_version.save + + policy_action = Loader::CreatePolicy.from_policy(policy, context: policy_context) + policy_action.call + + created_roles = policy_action.new_roles.select do |role| + %w(user host).member?(role.kind) + end.inject({}) do |memo, role| + credentials = Credentials[role: role] || Credentials.create(role: role) + memo[role.id] = { id: role.id, api_key: credentials.api_key } + memo + end + + { + created_roles: created_roles, + version: policy_version.version + } + end +end diff --git a/config/routes.rb b/config/routes.rb index e1f4db4a66..004c5961ab 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -83,6 +83,17 @@ def matches?(request) get "/public_keys/:account/:kind/*identifier" => 'public_keys#show' post "/ca/:account/:service_id/sign" => 'certificate_authority#sign' + + # Policy Factory routes + scope '/policy_factories/:account/*identifier' do + # The `/template` routes need to be listed before create policy, so + # that `create_policy` doesn't attempt to include `/template` in the + # policy factory ID. + get '/template' => 'policy_factories#get_template' + put '/template' => 'policy_factories#update_template' + + post '/' => 'policy_factories#create_policy' + end end post "/host_factories/hosts" => 'host_factories#create_host' diff --git a/cucumber/api/features/policy_factory.feature b/cucumber/api/features/policy_factory.feature new file mode 100644 index 0000000000..edfa907323 --- /dev/null +++ b/cucumber/api/features/policy_factory.feature @@ -0,0 +1,192 @@ +Feature: Policy Factory + + Background: + Given I am the super-user + And I create a new user "alice" + And I create a new user "bob" + And I successfully PATCH "/policies/cucumber/policy/root" with body: + """ + - !policy certificates + - !policy-factory + id: certificates + base: !policy certificates + template: + - !variable + id: <%=role.identifier%> + annotations: + provision/provisioner: context + provision/context/parameter: value + + - !permit + role: !user + id: /<%=role.identifier%> + resource: !variable + id: <%=role.identifier%> + privileges: [ read, execute ] + + - !policy nested-policy + - !policy-factory + id: nested-policy + owner: !user alice + base: !policy nested-policy + template: + - !host + id: outer-<%=role.identifier%> + owner: !user /<%=role.identifier%> + annotations: + outer: <%=role.identifier%> + + - !policy + id: inner + owner: !user /<%=role.identifier%> + body: + - !host + id: inner-<%=role.identifier%> + annotations: + inner: <%=role.identifier%> + + - !policy edit-template + - !policy-factory + id: edit-template + owner: !user alice + base: !policy edit-template + template: + - !variable to-be-edited + + - !policy-factory + id: root-factory + template: + - !variable created-in-root + + - !policy annotated-variables + - !policy-factory + id: parameterized + base: !policy annotated-variables + template: + - !variable + id: <%=role.identifier%> + annotations: + description: <%=params[:description]%> + + - !permit + role: !user bob + resource: !policy-factory parameterized + privileges: [ read ] + + - !permit + role: !user alice + resource: !policy-factory certificates + privileges: [ read, execute ] + + - !permit + role: !user alice + resource: !policy-factory parameterized + privileges: [ read, execute ] + """ + + Scenario: Dry run loading policy using a factory + Given I login as "alice" + + When I POST "/policy_factories/cucumber/certificates?dry_run=true" + Then the JSON should be: + """ + { + "policy_text": "---\n- !variable\n id: alice\n annotations:\n provision/provisioner: context\n provision/context/parameter: value\n- !permit\n privilege:\n - read\n - execute\n role: !user\n id: \"/alice\"\n resource: !variable\n id: alice\n", + "load_to": "certificates", + "dry_run": true, + "response": null + } + """ + + Scenario: Nested policy within factory template + Given I login as "alice" + When I successfully POST "/policy_factories/cucumber/nested-policy" + Then I successfully GET "/resources/cucumber/host/nested-policy/outer-alice" + Then I successfully GET "/resources/cucumber/host/nested-policy/inner/inner-alice" + + Scenario: Load policy using a factory + Given I login as "alice" + And I set the "Content-Type" header to "multipart/form-data; boundary=demo" + When I successfully POST "/policy_factories/cucumber/certificates" with body from file "policy-factory-context.txt" + Then the JSON should be: + """ + { + "policy_text": "---\n- !variable\n id: alice\n annotations:\n provision/provisioner: context\n provision/context/parameter: value\n- !permit\n privilege:\n - read\n - execute\n role: !user\n id: \"/alice\"\n resource: !variable\n id: alice\n", + "load_to": "certificates", + "dry_run": false, + "response": { + "created_roles": { + }, + "version": 1 + } + } + """ + And I successfully GET "/secrets/cucumber/variable/certificates/alice" + Then the JSON should be: + """ + "test value" + """ + + Scenario: Load parameterized policy using a factory + Given I login as "alice" + + When I POST "/policy_factories/cucumber/parameterized?description=first%20description" + Then the JSON should be: + """ + { + "policy_text": "---\n- !variable\n id: alice\n annotations:\n description: first description\n", + "load_to": "annotated-variables", + "dry_run": false, + "response": { + "created_roles": { + }, + "version": 1 + } + } + """ + + Scenario: Get a 404 response without read permission + Given I login as "bob" + When I POST "/policy_factories/cucumber/certificates" + Then the HTTP response status code is 404 + + Scenario: Get a 403 response without execute permission + Given I login as "bob" + When I POST "/policy_factories/cucumber/parameterized" + Then the HTTP response status code is 403 + + Scenario: A policy factory without a base loads into the root policy + Given I POST "/policy_factories/cucumber/root-factory" + And the HTTP response status code is 201 + Then I successfully GET "/resources/cucumber/variable/created-in-root" + + Scenario: I retrieve the policy factory template through the API + Given I login as "alice" + When I GET "/policy_factories/cucumber/edit-template/template" + Then the HTTP response status code is 200 + And the JSON response should be: + """ + { + "body": "---\n- !variable\n id: to-be-edited\n" + } + """ + + Scenario: I update the policy factory template through the API + Given I login as "alice" + When I PUT "/policy_factories/cucumber/edit-template/template" with body: + """ + ---\n- !variable replaced + """ + Then the HTTP response status code is 202 + When I GET "/policy_factories/cucumber/edit-template/template" + Then the JSON response should be: + """ + { + "body": "---\\n- !variable replaced" + } + """ + + Scenario: I don't have permission to retrieve the policy factory template + Given I login as "bob" + When I GET "/policy_factories/cucumber/edit-template/template" + Then the HTTP response status code is 404 diff --git a/cucumber/api/features/support/policy-factory-context.txt b/cucumber/api/features/support/policy-factory-context.txt new file mode 100644 index 0000000000..9defffb2e7 --- /dev/null +++ b/cucumber/api/features/support/policy-factory-context.txt @@ -0,0 +1,5 @@ +--demo +Content-Disposition: form-data; name="value" + +test value +--demo--