conjur-asset-dsl2 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4e808b958cc3f5faac0227b00000b8b11d5ad255
4
- data.tar.gz: 9f4f5bb57310b929f8b7d8f3e7785e39cb8441b1
3
+ metadata.gz: 57aa36f4305e541f313b8a833623d6100929ae50
4
+ data.tar.gz: dcabd391389d702f971b3a12aee54afadf53c914
5
5
  SHA512:
6
- metadata.gz: f7f304a0432b4cb7fbea8b079c8b690c081c073833b257c8f5fb14e8b17fc3ba28f63436e33e506ec5f812ef6c6a60d64ae4e23eb5b35b22ea0ba20255846728
7
- data.tar.gz: a52561942757c5fef6c95dfdcb83eb8c85b864e1d738337d4e72e5bcd587057610dd2d42efbd68a8f11853a911eaa8deb4a9940d5fe51ce248048c32e6f58776
6
+ metadata.gz: 62e2362f4202eaa0d48b0db4c597515531c592b63abcc8118bd88c1b8ce5ee9e4354e895ca1cf43dbea79e4a823d1d8c250c1fa5b724fca284a501fdbad695fb
7
+ data.tar.gz: 08a1ec598a5d4f23e1bb27dd939f5eaddeff097bcf0d9aebaf1ca8fabf6949134827daaf656d5a27c3d384b07dc7f5bf6b9e6a579a65f264109744f0e9dddb70
@@ -1,6 +1,13 @@
1
+ # 0.6.0
2
+
3
+ * Implement the !deny statement.
4
+ * Eliminate un-necessary privilege and role revocations.
5
+
1
6
  # 0.5.0
2
7
 
3
8
  * Refactor how the policy statements are validated and normalized, fixing some bugs in the process.
9
+ * In record ids, replace the string '$namespace' with the policy namespace. This enables cross-policy
10
+ entitlements to be made more flexibly.
4
11
 
5
12
  # 0.4.4
6
13
 
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in conjur-asset-dsl2.gemspec
4
4
  gemspec
5
+
6
+ gem 'simplecov', require: false
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "ci_reporter_rspec"
31
31
  spec.add_development_dependency "aruba"
32
32
  spec.add_development_dependency 'io-grab'
33
+ spec.add_development_dependency 'simplecov'
33
34
  end
@@ -1,7 +1,7 @@
1
1
  module Conjur
2
2
  module Asset
3
3
  module DSL2
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,179 @@
1
+ module Conjur
2
+ module DSL2
3
+ module Planner
4
+ # Stores the state of existing and requested grants (roles or privileges).
5
+ #
6
+ # The difference between the existing and requested grants can be used to determine
7
+ # specifically what actions should be performed in order to bring the state of the server
8
+ # into compliance with the policy.
9
+ class BaseFacts
10
+ attr_accessor :planner, :existing, :requested, :existing_with_admin_flag, :requested_with_admin_flag
11
+
12
+ # Whether to sort the grants. By default this is off; turning it on makes the output
13
+ # deterministic which is nice for testing.
14
+ cattr_accessor :sort
15
+
16
+ def initialize planner
17
+ @planner = planner
18
+ @requested = Set.new
19
+ @requested_with_admin_flag = Set.new
20
+ @existing = Set.new
21
+ @existing_with_admin_flag = Set.new
22
+ end
23
+
24
+ def api
25
+ planner.api
26
+ end
27
+
28
+ # Return the set of grants which are requested but not already held.
29
+ #
30
+ # Note that if a grant is held with a different admin option than requested,
31
+ # re-applying with the new admin option will update the grant and create
32
+ # the desired state.
33
+ def grants_to_apply
34
+ sort(requested_with_admin_flag - existing_with_admin_flag)
35
+ end
36
+
37
+ # Return the set of grants which are held but not requested.
38
+ #
39
+ # The admin flag is ignored by this method. So, if a grant exists (with or without
40
+ # admin), and it is not requested (with or without admin), it is revoked. The
41
+ # case in which the grant is held with a different admin option than requested
42
+ # is handled by +grants_to_apply+.
43
+ def grants_to_revoke
44
+ sort(existing - requested)
45
+ end
46
+
47
+ def validate_role_exists! role
48
+ error("Role not found: #{role}") unless planner.role_exists?(role)
49
+ end
50
+
51
+ def validate_resource_exists! resource
52
+ error("Resource not found: #{resource}") unless planner.resource_exists?(resource)
53
+ end
54
+
55
+ protected
56
+
57
+ # Sort a result if +sort+ is enabled.
58
+ def sort result
59
+ self.class.sort ? result.to_a.sort : result
60
+ end
61
+ end
62
+
63
+ # Role grants are a tuple of [ roleid, member_roleid, admin_option ].
64
+ class RoleFacts < BaseFacts
65
+
66
+ # Enumerate all existing grants on the specified +role+.
67
+ # Each grant is yielded to the block.
68
+ def role_grants role, &block
69
+ begin
70
+ api.role(role.roleid).members
71
+ rescue RestClient::ResourceNotFound
72
+ if api.role(role.roleid).exists?
73
+ $stderr.puts "WARNING: Unable to fetch members of role #{role.roleid}. Use 'elevate' mode, or at least 'reveal' mode, for policy management."
74
+ end
75
+ []
76
+ end.each do |grant|
77
+ yield grant
78
+ end
79
+ end
80
+
81
+ # Validate that all the requested roles exist.
82
+ def validate!
83
+ requested.to_a.flatten.uniq.each do |roleid|
84
+ validate_role_exists! roleid
85
+ end
86
+ end
87
+
88
+ # Add a Types::Grant to the set of requested grants.
89
+ def add_requested_grant grant
90
+ Array(grant.roles).each do |role|
91
+ Array(grant.members).each do |member|
92
+ requested.add [ role.roleid, member.role.roleid ]
93
+ requested_with_admin_flag.add [ role.roleid, member.role.roleid, !!member.admin ]
94
+ end
95
+ end
96
+ end
97
+
98
+ # Removes a Types::Revoke from the set of requested grants.
99
+ def remove_revoked_grant revoke
100
+ Array(revoke.roles).each do |role|
101
+ Array(revoke.members).each do |member|
102
+ requested.delete [ role.roleid, member.roleid ]
103
+ requested_with_admin_flag.delete [ role.roleid, member.roleid, true ]
104
+ requested_with_admin_flag.delete [ role.roleid, member.roleid, false ]
105
+ end
106
+ end
107
+ end
108
+
109
+ # Add a Conjur::API::Rolerevoke that is already held.
110
+ def add_existing_grant role, grant
111
+ existing.add [ role.roleid, grant.member.roleid ]
112
+ existing_with_admin_flag.add [ role.roleid, grant.member.roleid, grant.admin_option ]
113
+ end
114
+ end
115
+
116
+ # Privilege grants are [ roleid, privilege, resourceid, grant_option ].
117
+ class PrivilegeFacts < BaseFacts
118
+
119
+ # Enumerate all existing permissions for the specified +resource+.
120
+ # Only permissions that apply the specified +privilege+ are considered.
121
+ # Each permission is yielded to the block.
122
+ def resource_permissions resource, privileges, &block
123
+ permissions = begin
124
+ JSON.parse(api.resource(resource.resourceid).get)['permissions']
125
+ rescue RestClient::ResourceNotFound
126
+ if api.resource(resource.resourceid).exists?
127
+ $stderr.puts "WARNING: Unable to fetch permissions of resource #{resource.resourceid}. Use 'elevate' mode, or at least 'reveal' mode, for policy management."
128
+ end
129
+ []
130
+ end
131
+ permissions.select{|p| privileges.member?(p['privilege'])}.each do |permission|
132
+ yield permission
133
+ end
134
+ end
135
+
136
+ # Validate that all the requested roles exist.
137
+ def validate!
138
+ requested.to_a.map{|row| row[0]}.uniq.each do |roleid|
139
+ validate_role_exists! roleid
140
+ end
141
+ requested.to_a.map{|row| row[2]}.uniq.each do |resourceid|
142
+ validate_resource_exists! resourceid
143
+ end
144
+ end
145
+
146
+ # Add a Types::deny to the set of requested grants.
147
+ def add_requested_permission permit
148
+ Array(permit.roles).each do |member|
149
+ Array(permit.privileges).each do |privilege|
150
+ Array(permit.resources).each do |resource|
151
+ requested.add [ member.role.roleid, privilege, resource.resourceid ]
152
+ requested_with_admin_flag.add [ member.role.roleid, privilege, resource.resourceid, !!member.admin ]
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ # Removes a Types::Deny from the set of requested grants.
159
+ def remove_revoked_permission deny
160
+ Array(deny.roles).each do |role|
161
+ Array(deny.privileges).each do |privilege|
162
+ Array(deny.resources).each do |resource|
163
+ requested.delete [ role.roleid, privilege, resource.resourceid ]
164
+ requested_with_admin_flag.delete [ role.roleid, privilege, resource.resourceid, true ]
165
+ requested_with_admin_flag.delete [ role.roleid, privilege, resource.resourceid, false ]
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ # Add a permission that is already held.
172
+ def add_existing_permission permission
173
+ existing.add [ permission['role'], permission['privilege'], permission['resource'] ]
174
+ existing_with_admin_flag.add [ permission['role'], permission['privilege'], permission['resource'], permission['grant_option'] ]
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -1,110 +1,77 @@
1
1
  require 'conjur/dsl2/planner/base'
2
+ require 'conjur/dsl2/planner/facts'
2
3
 
3
4
  module Conjur
4
5
  module DSL2
5
6
  module Planner
6
- class RoleAction < Base
7
- def verify_roles_available roles
8
- # Check all roles / members involved
9
- roles.each do |role|
10
- error("role not found: #{role.roleid} in #{plan.roles_created.to_a}") unless role_exists?(role)
11
- end
12
- end
13
- end
14
-
15
- class Grant < RoleAction
7
+ class Grant < Base
16
8
  # Plans a role grant.
17
9
  #
18
10
  # The Grant record can list multiple roles and members. Each member should
19
11
  # be granted every role. If the +replace+ option is set, then any existing
20
12
  # grant on a role that is *not* given should be revoked, except for role admins.
21
13
  def do_plan
22
- roles = Array(record.roles)
23
- members = Array(record.members)
24
- given_grants = Hash.new { |hash, key| hash[key] = [] }
25
- given_admins = Set.new
26
- requested_grants = Hash.new { |hash, key| hash[key] = [] }
27
-
28
- verify_roles_available roles + members.map(&:role)
29
-
30
- roles.each do |role|
31
- grants = begin
32
- api.role(role.roleid).members
33
- rescue RestClient::ResourceNotFound
34
- []
35
- end
36
-
37
- grants.each do |grant|
38
- member_roleid = grant.member.roleid
39
- given_grants[role.roleid].push [ member_roleid, grant.admin_option ]
40
- given_admins << member_roleid if grant.admin_option
41
- end
42
- members.each do |member|
43
- requested_grants[role.roleid].push [ member.role.roleid, !!member.admin ]
14
+ facts = RoleFacts.new self
15
+
16
+ facts.add_requested_grant record
17
+
18
+ Array(record.roles).each do |role|
19
+ facts.role_grants(role) do |grant|
20
+ facts.add_existing_grant role, grant
44
21
  end
45
22
  end
46
23
 
47
- roles.each do |role|
48
- roleid = role.roleid
49
- given = given_grants[roleid]
50
- requested = requested_grants[roleid]
51
-
52
- (Set.new(requested) - Set.new(given)).each do |p|
53
- member, admin = p
54
- grant = Conjur::DSL2::Types::Grant.new
55
- grant.role = role_record roleid
56
- grant.member = Conjur::DSL2::Types::Member.new role_record(member)
57
- grant.member.admin = true if admin
58
- action grant
59
- end
24
+ facts.validate!
25
+
26
+ facts.grants_to_apply.each do |grant|
27
+ roleid, memberid, admin = grant
28
+ grant = Conjur::DSL2::Types::Grant.new
29
+ grant.role = role_record roleid
30
+ grant.member = Conjur::DSL2::Types::Member.new role_record(memberid)
31
+ grant.member.admin = admin
32
+ action grant
33
+ end
60
34
 
61
- if record.replace
62
- (Set.new(given) - Set.new(requested)).each do |p|
63
- member, _ = p
64
- member_roleid = role_record(member).roleid
65
- next if given_admins.member?(member_roleid)
66
- revoke = Conjur::DSL2::Types::Revoke.new
67
- revoke.role = role_record roleid
68
- revoke.member = role_record(member)
69
- action revoke
70
- end
35
+ if record.replace
36
+ facts.grants_to_revoke.each do |grant|
37
+ roleid, memberid = grant
38
+ revoke = Conjur::DSL2::Types::Revoke.new
39
+ revoke.role = role_record roleid
40
+ revoke.member = role_record(memberid)
41
+ action revoke
71
42
  end
72
43
  end
73
44
  end
74
45
  end
75
46
 
76
- class Revoke < RoleAction
47
+ class Revoke < Base
77
48
  def do_plan
78
- roles = Array(record.roles)
79
- members = Array(record.members)
80
- given_grants = Hash.new { |hash, key| hash[key] = [] }
81
-
82
- verify_roles_available roles + members
83
-
84
- roles.each do |role|
85
- grants = begin
86
- api.role(role.roleid).members
87
- rescue RestClient::ResourceNotFound
88
- []
89
- end
90
-
91
- grants.each do |grant|
92
- member_roleid = grant.member.roleid
93
- given_grants[role.roleid].push member_roleid
49
+ facts = RoleFacts.new self
50
+
51
+ # Load all the role members as both requested and existing grants.
52
+ # Then revoke the Grant record, and see what's left.
53
+ Array(record.roles).each do |role|
54
+ facts.role_grants(role) do |grant|
55
+ grant_record = Types::Grant.new
56
+ grant_record.role = Types::Role.new(role.roleid)
57
+ grant_record.member = Types::Member.new Types::Role.new(grant.member.roleid)
58
+ grant_record.member.admin = grant.admin_option
59
+ facts.add_requested_grant grant_record
60
+
61
+ facts.add_existing_grant role, grant
94
62
  end
95
63
  end
64
+
65
+ facts.remove_revoked_grant record
96
66
 
97
- roles.each do |role|
98
- roleid = role.roleid
99
- given = given_grants[roleid]
100
- members.each do |member|
101
- next unless given.member?(member.roleid)
67
+ facts.validate!
102
68
 
103
- revoke = Conjur::DSL2::Types::Revoke.new
104
- revoke.role = role
105
- revoke.member = member
106
- action revoke
107
- end
69
+ facts.grants_to_revoke.each do |grant|
70
+ roleid, memberid = grant
71
+ revoke = Conjur::DSL2::Types::Revoke.new
72
+ revoke.role = role_record roleid
73
+ revoke.member = role_record(memberid)
74
+ action revoke
108
75
  end
109
76
  end
110
77
  end
@@ -1,5 +1,5 @@
1
1
  require 'conjur/dsl2/planner/base'
2
- require 'set'
2
+ require 'conjur/dsl2/planner/facts'
3
3
 
4
4
  module Conjur
5
5
  module DSL2
@@ -11,65 +11,78 @@ module Conjur
11
11
  # privilege on an existing resource that is *not* given should be denied.
12
12
  class Permit < Base
13
13
  def do_plan
14
- resources = Array(record.resources)
15
- privileges = Array(record.privilege)
16
- given_permissions = Hash.new { |hash, key| hash[key] = [] }
17
- requested_permissions = Hash.new { |hash, key| hash[key] = [] }
18
-
19
- resources.each do |resource|
20
- permissions = begin
21
- JSON.parse(api.resource(resource.resourceid).get)['permissions']
22
- rescue RestClient::ResourceNotFound
23
- []
24
- end
25
-
26
- permissions.each do |permission|
27
- if privileges.member?(permission['privilege'])
28
- given_permissions[[permission['privilege'], permission['resource']]].push [ permission['role'], permission['grant_option'] ]
29
- end
14
+ facts = PrivilegeFacts.new self
15
+
16
+ facts.add_requested_permission record
17
+
18
+ privileges = Array(record.privileges)
19
+ Array(record.resources).each do |resource|
20
+ facts.resource_permissions(resource, privileges) do |permission|
21
+ facts.add_existing_permission permission
30
22
  end
23
+ end
24
+
25
+ facts.validate!
31
26
 
32
- privileges.each do |privilege|
33
- Array(record.roles).each do |role|
34
- requested_permissions[[privilege, resource.resourceid]].push [ role.role.roleid, !!role.admin ]
35
- end
27
+ facts.grants_to_apply.each do |grant|
28
+ role, privilege, resource, admin = grant
29
+
30
+ permit = Conjur::DSL2::Types::Permit.new
31
+ permit.resource = resource_record resource
32
+ permit.privilege = privilege
33
+ permit.role = Conjur::DSL2::Types::Member.new role_record(role)
34
+ permit.role.admin = true if admin
35
+ action permit
36
+ end
37
+
38
+ if record.replace
39
+ facts.grants_to_revoke.each do |grant|
40
+ roleid, privilege, resourceid = grant
41
+ deny = Conjur::DSL2::Types::Deny.new
42
+ deny.resource = resource_record resourceid
43
+ deny.privilege = privilege
44
+ deny.role = role_record(roleid)
45
+ action deny
36
46
  end
37
47
  end
38
-
39
- resources.each do |resource|
40
- error(%Q("Resource "#{resource}" not found in [#{plan.resources_created.to_a.sort.join(', ')}])) unless resource_exists?(resource)
41
-
42
- privileges.each do |privilege|
43
-
44
- target = resource.resourceid
45
- given = given_permissions[[privilege, target]]
46
- requested = requested_permissions[[privilege, target]]
47
-
48
- (Set.new(requested) - Set.new(given)).each do |p|
49
- role, admin = p
50
-
51
- error(%Q(Role "#{role}" not found")) unless role_exists?(role)
52
-
53
- permit = Conjur::DSL2::Types::Permit.new
54
- permit.resource = resource_record target
55
- permit.privilege = privilege
56
- permit.role = Conjur::DSL2::Types::Member.new role_record(role)
57
- permit.role.admin = true if admin
58
- action permit
59
- end
60
-
61
- if record.replace
62
- (Set.new(given) - Set.new(requested)).each do |p|
63
- role, admin = p
64
- deny = Conjur::DSL2::Types::Deny.new
65
- deny.resource = resource_record target
66
- deny.privilege = privilege
67
- deny.role = role_record(role)
68
- action deny
69
- end
70
- end
48
+ end
49
+ end
50
+
51
+ # Plans a permission denial.
52
+ #
53
+ # A Deny statement is generated if the permission is currently held. Otherwise, its a nop.
54
+ class Deny < Base
55
+ def do_plan
56
+ facts = PrivilegeFacts.new self
57
+
58
+ # Load all the permissions as both requested and existing grants.
59
+ # Then remove the Deny record, and see what's left.
60
+ privileges = Array(record.privileges)
61
+ Array(record.resources).each do |resource|
62
+ facts.resource_permissions(resource, privileges) do |permission|
63
+ permit_record = Types::Permit.new
64
+ permit_record.role = Types::Role.new(permission['role'])
65
+ permit_record.role.admin = permission['grant_option']
66
+ permit_record.privilege = permission['privilege']
67
+ permit_record.resource = Types::Resource.new(permission['resource'])
68
+ facts.add_requested_permission permit_record
69
+
70
+ facts.add_existing_permission permission
71
71
  end
72
72
  end
73
+
74
+ facts.remove_revoked_permission record
75
+
76
+ facts.validate!
77
+
78
+ facts.grants_to_revoke.each do |grant|
79
+ role, privilege, resource = grant
80
+ deny = Conjur::DSL2::Types::Deny.new
81
+ deny.resource = resource_record resource
82
+ deny.privilege = privilege
83
+ deny.role = role_record(role)
84
+ action deny
85
+ end
73
86
  end
74
87
  end
75
88
  end
@@ -69,6 +69,8 @@ module Conjur
69
69
 
70
70
  # Makes all ids absolute, by prepending the namespace (if any) and the enclosing policy (if any).
71
71
  class IdResolver < Resolver
72
+ SUBSTITUTIONS = { "$namespace" => :namespace }
73
+
72
74
  def resolve records
73
75
  traverse records, Set.new, method(:resolve_id), method(:on_resolve_policy)
74
76
  end
@@ -78,12 +80,16 @@ module Conjur
78
80
  id = record.id
79
81
  if id.blank?
80
82
  raise "#{record.to_s} has no id, and no namespace is available to populate it" unless namespace
81
- record.id = namespace
83
+ id = namespace
82
84
  elsif id[0] == '/'
83
- record.id = id[1..-1]
85
+ id = id[1..-1]
84
86
  else
85
- record.id = [ namespace, id ].compact.join('/')
87
+ id = [ namespace, id ].compact.join('/')
86
88
  end
89
+
90
+ substitute! id
91
+
92
+ record.id = id
87
93
  end
88
94
 
89
95
  traverse record.referenced_records, visited, method(:resolve_id), method(:on_resolve_policy)
@@ -96,6 +102,15 @@ module Conjur
96
102
  ensure
97
103
  @namespace = saved_namespace
98
104
  end
105
+
106
+ protected
107
+
108
+ def substitute! id
109
+ SUBSTITUTIONS.each do |k,v|
110
+ next unless value = send(v)
111
+ id.gsub! k, value
112
+ end
113
+ end
99
114
  end
100
115
 
101
116
  # Sets the owner field for any records which support it, and don't have an owner specified.
@@ -8,6 +8,10 @@ module Conjur
8
8
 
9
9
  attribute :role
10
10
  attribute :admin, kind: :boolean, singular: true
11
+
12
+ def to_s
13
+ "#{role} #{admin ? 'with' : 'without'} admin option"
14
+ end
11
15
  end
12
16
  end
13
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conjur-asset-dsl2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gilpin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-02-24 00:00:00.000000000 Z
11
+ date: 2016-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: safe_yaml
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - '>='
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '>='
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  description:
168
182
  email:
169
183
  - kgilpin@conjur.net
@@ -176,7 +190,7 @@ files:
176
190
  - .project
177
191
  - .rspec
178
192
  - .travis.yml
179
- - CHANGELOG
193
+ - CHANGELOG.md
180
194
  - Gemfile
181
195
  - LICENSE.txt
182
196
  - README.md
@@ -205,6 +219,7 @@ files:
205
219
  - lib/conjur/dsl2/plan.rb
206
220
  - lib/conjur/dsl2/planner.rb
207
221
  - lib/conjur/dsl2/planner/base.rb
222
+ - lib/conjur/dsl2/planner/facts.rb
208
223
  - lib/conjur/dsl2/planner/grants.rb
209
224
  - lib/conjur/dsl2/planner/permissions.rb
210
225
  - lib/conjur/dsl2/planner/record.rb