conjur-asset-dsl2 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +2 -0
  3. data/.gitignore +14 -0
  4. data/.project +18 -0
  5. data/.rspec +1 -0
  6. data/.travis.yml +4 -0
  7. data/CHANGELOG +1 -0
  8. data/Dockerfile.dev +19 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +248 -0
  12. data/Rakefile +18 -0
  13. data/backup.tar +0 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +7 -0
  16. data/conjur-asset-dsl2.gemspec +32 -0
  17. data/jenkins.sh +36 -0
  18. data/lib/conjur/command/dsl2.rb +175 -0
  19. data/lib/conjur/dsl2/executor/base.rb +50 -0
  20. data/lib/conjur/dsl2/executor/create.rb +117 -0
  21. data/lib/conjur/dsl2/executor/deny.rb +13 -0
  22. data/lib/conjur/dsl2/executor/give.rb +12 -0
  23. data/lib/conjur/dsl2/executor/grant.rb +13 -0
  24. data/lib/conjur/dsl2/executor/permit.rb +16 -0
  25. data/lib/conjur/dsl2/executor/retire.rb +7 -0
  26. data/lib/conjur/dsl2/executor/revoke.rb +11 -0
  27. data/lib/conjur/dsl2/executor/update.rb +31 -0
  28. data/lib/conjur/dsl2/executor.rb +99 -0
  29. data/lib/conjur/dsl2/invalid.rb +12 -0
  30. data/lib/conjur/dsl2/plan.rb +49 -0
  31. data/lib/conjur/dsl2/planner/base.rb +215 -0
  32. data/lib/conjur/dsl2/planner/grants.rb +85 -0
  33. data/lib/conjur/dsl2/planner/permissions.rb +80 -0
  34. data/lib/conjur/dsl2/planner/record.rb +102 -0
  35. data/lib/conjur/dsl2/planner.rb +38 -0
  36. data/lib/conjur/dsl2/ruby/loader.rb +263 -0
  37. data/lib/conjur/dsl2/types/base.rb +376 -0
  38. data/lib/conjur/dsl2/types/create.rb +15 -0
  39. data/lib/conjur/dsl2/types/deny.rb +17 -0
  40. data/lib/conjur/dsl2/types/give.rb +14 -0
  41. data/lib/conjur/dsl2/types/grant.rb +24 -0
  42. data/lib/conjur/dsl2/types/member.rb +14 -0
  43. data/lib/conjur/dsl2/types/permit.rb +22 -0
  44. data/lib/conjur/dsl2/types/policy.rb +129 -0
  45. data/lib/conjur/dsl2/types/records.rb +243 -0
  46. data/lib/conjur/dsl2/types/retire.rb +14 -0
  47. data/lib/conjur/dsl2/types/revoke.rb +14 -0
  48. data/lib/conjur/dsl2/types/update.rb +16 -0
  49. data/lib/conjur/dsl2/yaml/handler.rb +400 -0
  50. data/lib/conjur/dsl2/yaml/loader.rb +29 -0
  51. data/lib/conjur-asset-dsl2-version.rb +7 -0
  52. data/lib/conjur-asset-dsl2.rb +27 -0
  53. data/syntax.md +147 -0
  54. metadata +237 -0
@@ -0,0 +1,50 @@
1
+ module Conjur::DSL2
2
+ module Executor
3
+ # Builds a list of execution actions for a statement. The statement
4
+ # is an object from Conjur::DSL2::Types. Each execution action is
5
+ # an HTTP method, a request path, and request parameters.
6
+ class Base
7
+ attr_reader :statement, :actions, :default_account
8
+
9
+ def initialize statement, actions, default_account
10
+ @statement = statement
11
+ @actions = actions
12
+ @default_account = default_account
13
+ end
14
+
15
+ def action obj
16
+ @actions.push obj
17
+ end
18
+
19
+ def execute
20
+ raise "execute not implemented in #{self.class.name}"
21
+ end
22
+
23
+ def resource_path record = nil
24
+ record ||= self.statement
25
+ [ "authz", record.account || default_account, "resources", record.resource_kind, record.id ].join('/')
26
+ end
27
+
28
+ def role_path record = nil
29
+ record ||= self.statement
30
+ [ "authz", record.account || default_account, "roles", record.role_kind, record.id ].join('/')
31
+ end
32
+ end
33
+
34
+ module Annotate
35
+ def annotate
36
+ Array(annotate_record.annotations).each do |k,v|
37
+ action({
38
+ 'method' => 'put',
39
+ 'path' => update_annotation_path,
40
+ 'parameters' => { "name" => k, "value" => v }
41
+ })
42
+ end
43
+ end
44
+
45
+ def update_annotation_path
46
+ [ "authz", annotate_record.account || default_account, "annotations", annotate_record.resource_kind, annotate_record.id ].join('/')
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,117 @@
1
+ module Conjur::DSL2::Executor
2
+ # Abstract base class for creating a new record.
3
+ class Create < Base
4
+ def record
5
+ statement.record
6
+ end
7
+
8
+ def account
9
+ record.account || default_account
10
+ end
11
+ end
12
+
13
+ # Generic 'create' implementation which POSTs to a resources URL.
14
+ class CreateRecord < Create
15
+ include Annotate
16
+
17
+ def execute
18
+ action({
19
+ 'method' => 'post',
20
+ 'path' => create_path,
21
+ 'parameters' => create_parameters
22
+ })
23
+ annotate
24
+ end
25
+
26
+ def annotate_record
27
+ record
28
+ end
29
+
30
+ def create_path
31
+ [ kind_path ].join('/')
32
+ end
33
+
34
+ def kind_path
35
+ record.resource_kind.pluralize
36
+ end
37
+
38
+ # Each record is assumed to have an 'id' attribute required for creation.
39
+ # In addition, other create parameters can be specified by the +custom_attribute_names+
40
+ # method on the record.
41
+ def create_parameters
42
+ {
43
+ record.id_attribute => record.id
44
+ }.tap do |params|
45
+ custom_attrs = record.custom_attribute_names.inject({}) do |memo, attr|
46
+ value = record.send(attr)
47
+ memo[attr.to_s] = value if value
48
+ memo
49
+ end
50
+ params.merge! custom_attrs
51
+ params["ownerid"] = record.owner.roleid(record.owner.account || default_account) if record.owner
52
+ end
53
+ end
54
+ end
55
+
56
+ # When creating a host factory, the +roleid+ and +layer+ are required.
57
+ class CreateHostFactory < CreateRecord
58
+ def create_parameters
59
+ super.tap do |params|
60
+ params['roleid'] = record.role.roleid(default_account)
61
+ params['layers'] = Array(record.layers).map(&:id)
62
+ end
63
+ end
64
+ end
65
+
66
+ # When creating a variable, set default values for the +mime_type+ and +kind+.
67
+ class CreateVariable < CreateRecord
68
+ def create_parameters
69
+ super.tap do |params|
70
+ params['mime_type'] ||= 'text/plain'
71
+ params['kind'] ||= 'secret'
72
+ end
73
+ end
74
+ end
75
+
76
+ # When creating a raw Role or Resource, the owner of the new record is specified by
77
+ # the +acting_as+ parameter.
78
+ module ActingAs
79
+ def acting_as_parameters
80
+ {}.tap do |params|
81
+ params["acting_as"] = record.owner.roleid(default_account) if record.owner
82
+ end
83
+ end
84
+ end
85
+
86
+ # Create a new Resource with a PUT request to the resource path.
87
+ class CreateResource < Create
88
+ include ActingAs
89
+ include Annotate
90
+
91
+ def execute
92
+ action({
93
+ 'method' => 'put',
94
+ 'path' => resource_path(statement.record),
95
+ 'parameters' => acting_as_parameters
96
+ })
97
+ annotate
98
+ end
99
+
100
+ def annotate_record
101
+ statement.record
102
+ end
103
+ end
104
+
105
+ # Create a new Role with a PUT request to the role path.
106
+ class CreateRole < Create
107
+ include ActingAs
108
+
109
+ def execute
110
+ action({
111
+ 'method' => 'put',
112
+ 'path' => role_path(statement.record),
113
+ 'parameters' => acting_as_parameters
114
+ })
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,13 @@
1
+ module Conjur::DSL2::Executor
2
+ # Deny a privilege with a POST request to the +deny+ url of the resource, with the privilege
3
+ # and role as parameters.
4
+ class Deny < Base
5
+ def execute
6
+ action({
7
+ 'method' => 'post',
8
+ 'path' => "#{resource_path(statement.resource)}?deny",
9
+ 'parameters' => { "privilege" => statement.privilege, "role" => statement.role.roleid(default_account) }
10
+ })
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module Conjur::DSL2::Executor
2
+ # Change the owner of a resource with a PUT request to the resource path, specifying the new owner.
3
+ class Give < Base
4
+ def execute
5
+ action({
6
+ 'method' => 'put',
7
+ 'path' => resource_path(statement.resource),
8
+ 'parameters' => { "owner" => statement.owner.roleid(default_account) }
9
+ })
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Conjur::DSL2::Executor
2
+ class Grant < Base
3
+ def execute
4
+ parameters = { "member" => statement.member.role.roleid(default_account) }
5
+ parameters['admin_option'] = statement.member.admin unless statement.member.admin.nil?
6
+ action({
7
+ 'method' => 'put',
8
+ 'path' => "authz/#{statement.role.account || default_account}/roles/#{statement.role.role_kind}/#{statement.role.id}?members",
9
+ 'parameters' => parameters
10
+ })
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ module Conjur::DSL2::Executor
2
+ # Permit a privilege with a POST request to the +permit+ url of the resource, with the privilege
3
+ # and role as parameters. +grant_option+ is also provided if it is explicitly stated on the Permit
4
+ # record.
5
+ class Permit < Base
6
+ def execute
7
+ parameters = { "privilege" => statement.privilege, "role" => statement.role.role.roleid(default_account) }
8
+ parameters['grant_option'] = admin unless statement.role.admin.nil?
9
+ action({
10
+ 'method' => 'post',
11
+ 'path' => "#{resource_path(statement.resource)}?permit",
12
+ 'parameters' => parameters
13
+ })
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ module Conjur::DSL2::Executor
2
+ class Retire < Base
3
+ def execute
4
+ raise "Retire is not implemented"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module Conjur::DSL2::Executor
2
+ class Revoke < Base
3
+ def execute
4
+ action({
5
+ 'method' => 'delete',
6
+ 'path' => "#{role_path(statement.role)}?members",
7
+ 'parameters' => { "member" => statement.member.roleid(default_account)}
8
+ })
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ module Conjur::DSL2::Executor
2
+ class Update < Base
3
+ include Annotate
4
+
5
+ def execute
6
+ statement.record.custom_attribute_names.each do |attr|
7
+ value = statement.record.send(attr)
8
+ action({
9
+ 'method' => 'put',
10
+ 'path' => update_path,
11
+ 'parameters' => { attr.to_s => value }
12
+ })
13
+ end
14
+
15
+ annotate
16
+ end
17
+
18
+ def kind_path
19
+ statement.record.resource_kind.pluralize
20
+ end
21
+
22
+ def update_path
23
+ require 'cgi'
24
+ [ kind_path, CGI.escape(statement.record.id) ].join('/')
25
+ end
26
+
27
+ def annotate_record
28
+ statement.record
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,99 @@
1
+ module Conjur
2
+ module DSL2
3
+ module Executor
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'conjur/dsl2/executor/base'
9
+ require 'conjur/dsl2/executor/create'
10
+ require 'conjur/dsl2/executor/give'
11
+ require 'conjur/dsl2/executor/grant'
12
+ require 'conjur/dsl2/executor/revoke'
13
+ require 'conjur/dsl2/executor/permit'
14
+ require 'conjur/dsl2/executor/deny'
15
+ require 'conjur/dsl2/executor/retire'
16
+ require 'conjur/dsl2/executor/update'
17
+
18
+ module Conjur
19
+ module DSL2
20
+ module Executor
21
+ class << self
22
+ def class_for action
23
+ if action.is_a?(Conjur::DSL2::Types::Create)
24
+ class_name = action.record.class.name.split("::")[-1]
25
+ begin
26
+ Conjur::DSL2::Executor.const_get([ "Create", class_name ].join)
27
+ rescue NameError
28
+ Conjur::DSL2::Executor::CreateRecord
29
+ end
30
+ else
31
+ class_name = action.class.name.split("::")[-1]
32
+ Conjur::DSL2::Executor.const_get(class_name)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ class HTTPExecutor
39
+ def initialize api
40
+ @api = api
41
+ end
42
+
43
+ def execute actions
44
+ require 'net/https'
45
+ uri = URI.parse(Conjur.configuration.appliance_url)
46
+ @base_path = uri.path
47
+ Net::HTTP.start uri.host, uri.port, use_ssl: true do |http|
48
+ @http = http
49
+ actions.each do |step|
50
+ invoke step
51
+ end
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ def invoke step
58
+ send step['method'], step['path'], step['parameters']
59
+ end
60
+
61
+ def create path, parameters
62
+ request = Net::HTTP::Post.new [ @base_path, path ].join('/')
63
+ request.set_form_data parameters
64
+ send_request request
65
+ end
66
+
67
+ alias post create
68
+
69
+ def update path, parameters
70
+ request = Net::HTTP::Put.new [ @base_path, path ].join('/')
71
+ request.set_form_data parameters
72
+ send_request request
73
+ end
74
+
75
+ alias put update
76
+
77
+ def delete path, parameters
78
+ uri = URI.parse([ @base_path, path ].join('/'))
79
+ uri.query = [uri.query, parameters.map{|k,v| [ k, URI.escape(v) ].join('=')}.join("&")].compact.join('&')
80
+ request = Net::HTTP::Delete.new [ uri.path, '?', uri.query ].join
81
+
82
+ send_request request
83
+ end
84
+
85
+ def send_request request
86
+ # $stderr.puts "#{request.method.upcase} #{request.path} #{request.body}"
87
+ require 'base64'
88
+ request['Authorization'] = "Token token=\"#{Base64.strict_encode64 @api.token.to_json}\""
89
+ response = @http.request request
90
+ # $stderr.puts response.code
91
+ if response.code.to_i >= 300
92
+ $stderr.puts "#{request.method.upcase} #{request.path} #{request.body} failed with error #{response.code}:"
93
+ # $stderr.puts "Request failed with error #{response.code}:"
94
+ $stderr.puts response.body
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,12 @@
1
+ module Conjur
2
+ module DSL2
3
+ class Invalid < Exception
4
+ attr_reader :mark
5
+
6
+ def initialize message, filename, mark
7
+ super [ "Error at line #{mark.line}, column #{mark.column} in #{filename}", message ].join(' : ')
8
+ @mark = mark
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ module Conjur
2
+ module DSL2
3
+ class Plan
4
+ attr_reader :actions, :policy, :roles_created, :resources_created
5
+ attr_accessor :namespace, :ownerid
6
+
7
+
8
+ def initialize namespace = nil
9
+ @namespace = namespace
10
+ @actions = []
11
+ @policy = nil
12
+ @roles_created = Set.new
13
+ @resources_created = Set.new
14
+ end
15
+
16
+ def scoped_id id
17
+ id = id.id if id.respond_to?(:id)
18
+
19
+ # id is nil means it should have the same id as the policy
20
+ id = '' if id.nil?
21
+
22
+ if id[0] == '/'
23
+ id[1..-1]
24
+ else
25
+
26
+ tokens = []
27
+ tokens.push @namespace if @namespace
28
+ tokens.push @policy.id if @policy
29
+
30
+ if id.start_with?(tokens.join('/') + '/')
31
+ id
32
+ else
33
+ tokens.push id unless id.empty?
34
+ tokens.join('/')
35
+ end
36
+ end
37
+ end
38
+
39
+ def policy= policy
40
+ raise "Plan policy is already specified" if @policy && policy
41
+ @policy = policy
42
+ end
43
+
44
+ def action a
45
+ @actions.push a
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,215 @@
1
+ module Conjur
2
+ module DSL2
3
+ module Planner
4
+ class Base
5
+
6
+ attr_reader :record, :api
7
+ attr_accessor :plan
8
+
9
+
10
+ def initialize record, api
11
+ @record = record
12
+ @api = api
13
+ end
14
+
15
+ def action a
16
+ @plan.action a
17
+ end
18
+
19
+ def scoped_id id
20
+ @plan.scoped_id id
21
+ end
22
+
23
+ def scoped_roleid record
24
+ record = record.roleid(default_account) unless record.kind_of?(String)
25
+ account, kind, id = record.split(':', 3)
26
+ [ account, kind, scoped_id(id) ].join(":")
27
+ end
28
+
29
+ def scoped_resourceid record
30
+ record = record.resourceid(default_account) unless record.kind_of?(String)
31
+ account, kind, id = record.split(':', 3)
32
+ [ account, kind, scoped_id(id) ].join(":")
33
+ end
34
+
35
+ def account
36
+ (record.account rescue nil) || default_account
37
+ end
38
+
39
+ def default_account
40
+ Conjur.configuration.account
41
+ end
42
+
43
+ def role_record fullid
44
+ account, kind, id = fullid.split(':', 3)
45
+ if kind == '@'
46
+ Conjur::DSL2::Types::ManagedRole.build fullid, default_account
47
+ else
48
+ if record_class = record_type(kind)
49
+ record_class.new.tap do |record|
50
+ record.account = account unless account == default_account
51
+ unless record.is_a?(Conjur::DSL2::Types::Variable)
52
+ record.kind = kind if record.respond_to?(:kind=)
53
+ end
54
+ record.id = id
55
+ end
56
+ else
57
+ Conjur::DSL2::Types::Role.new(fullid, default_account: default_account)
58
+ end
59
+ end
60
+ end
61
+
62
+ def record_type kind
63
+ begin
64
+ Conjur::DSL2::Types.const_get(kind.classify)
65
+ rescue NameError
66
+ nil
67
+ end
68
+ end
69
+
70
+ alias resource_record role_record
71
+
72
+ def resource
73
+ api.resource(scoped_resourceid record)
74
+ end
75
+
76
+ def role
77
+ api.role(scoped_roleid record)
78
+ end
79
+
80
+ # Sort in canonical order -- basically, a `Record` or `Create` comes before everything
81
+ # else. So the base class's sort just places those before us, and anything else gets 0.
82
+ def <=> other
83
+ other.kind_of?(Conjur::DSL2::Planner::ActsAsRecord) ? 1 : 0
84
+ end
85
+
86
+ def resource_exists? resource
87
+ resource_id = resource.kind_of?(String) ? resource : scoped_resourceid(resource)
88
+ (plan.resources_created.include?(resource_id) || api.resource(resource_id).exists?)
89
+ end
90
+
91
+ def role_exists? role
92
+ role_id = role.kind_of?(String) ? role : scoped_roleid(role)
93
+ # I believe it's correct to assume manged roles exist?
94
+ return true if role_id.split(':',2).last.start_with?('@')
95
+
96
+ plan.roles_created.include?(role_id) || api.role(role_id).exists?
97
+ end
98
+
99
+ def error message
100
+ # For now raise it, we can think about trying to recover down the road
101
+ raise message
102
+ end
103
+
104
+ def trace message
105
+ if trace_enabled?
106
+ $stderr.puts "[trace #{record}] #{message}"
107
+ end
108
+ end
109
+
110
+ def trace_enabled?
111
+ ENV["DSL_PLANNER_TRACE"] || !!@trace_enabled
112
+ end
113
+
114
+ def trace_enabled= enabled
115
+ @trace_enabled = enabled
116
+ end
117
+
118
+
119
+ def update_record
120
+ update = Conjur::DSL2::Types::Update.new
121
+ update.record = record
122
+ record.id = scoped_id(record)
123
+
124
+ changed = false
125
+ record.custom_attribute_names.each do |attr|
126
+ existing_value = object.attributes[attr]
127
+ new_value = record.send(attr)
128
+ if new_value
129
+ if new_value == existing_value
130
+ record.send "#{attr}=", nil
131
+ else
132
+ raise "Cannot modify immutable attribute '#{record.resource_kind}.#{attr}'" if record.immutable_attribute_names.member?(attr)
133
+ changed = true
134
+ end
135
+ end
136
+ end
137
+
138
+ if record.resource?
139
+ existing = resource.exists? ? resource.annotations : {}
140
+ current = record.annotations.kind_of?(::Array) ? record.annotations[0] : record.annotations
141
+ (record.annotations||{}).keys.each do |attr|
142
+ existing_value = existing[attr]
143
+ new_value = record.annotations[attr]
144
+ if new_value == existing_value
145
+ record.annotations.delete attr
146
+ else
147
+ changed = true
148
+ end
149
+ end
150
+
151
+ if record.owner && resource.owner != scoped_roleid(record.owner)
152
+ give = Conjur::DSL2::Types::Give.new
153
+ give.resource = Conjur::DSL2::Types::Resource.new(record.resourceid(default_account), default_account: default_account)
154
+ give.owner = Conjur::DSL2::Types::Role.new(scoped_roleid(record.owner), default_account: default_account)
155
+ action give
156
+
157
+ if record.role?
158
+ grant = Conjur::DSL2::Types::Grant.new
159
+ grant.role = Conjur::DSL2::Types::Role.new(record.roleid(default_account), default_account: default_account)
160
+ grant.member = Conjur::DSL2::Types::Member.new
161
+ grant.member.role = Conjur::DSL2::Types::Role.new(scoped_roleid(record.owner), default_account: default_account)
162
+ grant.member.admin = true
163
+ action grant
164
+ end
165
+ end
166
+ end
167
+
168
+ action update if changed
169
+ end
170
+
171
+ def create_record
172
+ create = Conjur::DSL2::Types::Create.new
173
+ create.record = record
174
+ record.id = scoped_id(record)
175
+ if record.owner
176
+ record.owner = Conjur::DSL2::Types::Role.new(scoped_roleid(record.owner), default_account: default_account)
177
+ elsif plan.ownerid
178
+ record.owner = Conjur::DSL2::Types::Role.new(plan.ownerid, default_account: default_account)
179
+ end
180
+
181
+ if record.resource?
182
+ existing = resource.exists? ? resource.annotations : {}
183
+ # And this is why we don't name a class Array.
184
+ current = record.annotations.kind_of?(::Array) ? record.annotations[0] : record.annotations
185
+ (current||{}).keys.each do |attr|
186
+ existing_value = existing[attr]
187
+ new_value = current[attr]
188
+ if new_value == existing_value
189
+ current.delete attr
190
+ end
191
+ end
192
+ end
193
+
194
+ plan.roles_created.add(record.roleid(account)) if record.role?
195
+ plan.resources_created.add(record.resourceid(account)) if record.resource?
196
+ action create
197
+ end
198
+ end
199
+
200
+ class Array < Base
201
+
202
+ def do_plan
203
+ planners = record.map do |item|
204
+ Planner.planner_for(item, api)
205
+ end.sort
206
+
207
+ planners.each do |planner|
208
+ planner.plan = self.plan
209
+ planner.do_plan
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end