ddr-core 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +12 -0
  3. data/README.md +27 -0
  4. data/Rakefile +30 -0
  5. data/app/assets/config/ddr_core_manifest.js +0 -0
  6. data/app/controllers/users/omniauth_callbacks_controller.rb +11 -0
  7. data/app/controllers/users/sessions_controller.rb +15 -0
  8. data/app/models/concerns/ddr/captionable.rb +25 -0
  9. data/app/models/concerns/ddr/describable.rb +108 -0
  10. data/app/models/concerns/ddr/governable.rb +25 -0
  11. data/app/models/concerns/ddr/has_admin_metadata.rb +141 -0
  12. data/app/models/concerns/ddr/has_attachments.rb +10 -0
  13. data/app/models/concerns/ddr/has_children.rb +10 -0
  14. data/app/models/concerns/ddr/has_content.rb +132 -0
  15. data/app/models/concerns/ddr/has_extracted_text.rb +10 -0
  16. data/app/models/concerns/ddr/has_intermediate_file.rb +25 -0
  17. data/app/models/concerns/ddr/has_multires_image.rb +14 -0
  18. data/app/models/concerns/ddr/has_parent.rb +18 -0
  19. data/app/models/concerns/ddr/has_struct_metadata.rb +21 -0
  20. data/app/models/concerns/ddr/has_thumbnail.rb +33 -0
  21. data/app/models/concerns/ddr/solr_document_behavior.rb +429 -0
  22. data/app/models/concerns/ddr/streamable.rb +25 -0
  23. data/app/models/ddr/admin_set.rb +28 -0
  24. data/app/models/ddr/attachment.rb +14 -0
  25. data/app/models/ddr/collection.rb +28 -0
  26. data/app/models/ddr/component.rb +31 -0
  27. data/app/models/ddr/contact.rb +23 -0
  28. data/app/models/ddr/digest.rb +8 -0
  29. data/app/models/ddr/file.rb +40 -0
  30. data/app/models/ddr/item.rb +36 -0
  31. data/app/models/ddr/language.rb +31 -0
  32. data/app/models/ddr/media_type.rb +22 -0
  33. data/app/models/ddr/resource.rb +94 -0
  34. data/app/models/ddr/rights_statement.rb +25 -0
  35. data/app/models/ddr/target.rb +17 -0
  36. data/config/initializers/devise.rb +262 -0
  37. data/config/locales/ddr-core.en.yml +85 -0
  38. data/config/routes.rb +3 -0
  39. data/db/migrate/20141104181418_create_users.rb +34 -0
  40. data/db/migrate/20141107124012_add_columns_to_user.rb +46 -0
  41. data/lib/ddr-core.rb +1 -0
  42. data/lib/ddr/auth.rb +80 -0
  43. data/lib/ddr/auth/ability.rb +18 -0
  44. data/lib/ddr/auth/ability_definitions.rb +26 -0
  45. data/lib/ddr/auth/ability_definitions/admin_set_ability_definitions.rb +9 -0
  46. data/lib/ddr/auth/ability_definitions/alias_ability_definitions.rb +23 -0
  47. data/lib/ddr/auth/ability_definitions/attachment_ability_definitions.rb +13 -0
  48. data/lib/ddr/auth/ability_definitions/collection_ability_definitions.rb +28 -0
  49. data/lib/ddr/auth/ability_definitions/component_ability_definitions.rb +13 -0
  50. data/lib/ddr/auth/ability_definitions/item_ability_definitions.rb +13 -0
  51. data/lib/ddr/auth/ability_definitions/lock_ability_definitions.rb +13 -0
  52. data/lib/ddr/auth/ability_definitions/publication_ability_definitions.rb +16 -0
  53. data/lib/ddr/auth/ability_definitions/role_based_ability_definitions.rb +39 -0
  54. data/lib/ddr/auth/ability_definitions/superuser_ability_definitions.rb +9 -0
  55. data/lib/ddr/auth/ability_factory.rb +10 -0
  56. data/lib/ddr/auth/abstract_ability.rb +48 -0
  57. data/lib/ddr/auth/affiliation.rb +14 -0
  58. data/lib/ddr/auth/affiliation_groups.rb +20 -0
  59. data/lib/ddr/auth/anonymous_ability.rb +7 -0
  60. data/lib/ddr/auth/auth_context.rb +109 -0
  61. data/lib/ddr/auth/auth_context_factory.rb +13 -0
  62. data/lib/ddr/auth/detached_auth_context.rb +19 -0
  63. data/lib/ddr/auth/dynamic_groups.rb +13 -0
  64. data/lib/ddr/auth/effective_permissions.rb +12 -0
  65. data/lib/ddr/auth/effective_roles.rb +9 -0
  66. data/lib/ddr/auth/failure_app.rb +16 -0
  67. data/lib/ddr/auth/group.rb +40 -0
  68. data/lib/ddr/auth/grouper_gateway.rb +70 -0
  69. data/lib/ddr/auth/groups.rb +32 -0
  70. data/lib/ddr/auth/ldap_gateway.rb +74 -0
  71. data/lib/ddr/auth/permissions.rb +18 -0
  72. data/lib/ddr/auth/remote_groups.rb +14 -0
  73. data/lib/ddr/auth/role_based_access_controls_enforcement.rb +56 -0
  74. data/lib/ddr/auth/roles.rb +28 -0
  75. data/lib/ddr/auth/roles/role.rb +121 -0
  76. data/lib/ddr/auth/roles/role_type.rb +23 -0
  77. data/lib/ddr/auth/roles/role_types.rb +52 -0
  78. data/lib/ddr/auth/superuser_ability.rb +7 -0
  79. data/lib/ddr/auth/test_helpers.rb +22 -0
  80. data/lib/ddr/auth/user.rb +54 -0
  81. data/lib/ddr/auth/web_auth_context.rb +29 -0
  82. data/lib/ddr/core.rb +110 -0
  83. data/lib/ddr/core/engine.rb +8 -0
  84. data/lib/ddr/core/version.rb +5 -0
  85. data/lib/ddr/error.rb +16 -0
  86. data/lib/ddr/files.rb +13 -0
  87. data/lib/ddr/fits.rb +189 -0
  88. data/lib/ddr/index.rb +29 -0
  89. data/lib/ddr/index/abstract_query_result.rb +22 -0
  90. data/lib/ddr/index/connection.rb +38 -0
  91. data/lib/ddr/index/csv_query_result.rb +84 -0
  92. data/lib/ddr/index/document_builder.rb +9 -0
  93. data/lib/ddr/index/field.rb +35 -0
  94. data/lib/ddr/index/field_attribute.rb +22 -0
  95. data/lib/ddr/index/fields.rb +154 -0
  96. data/lib/ddr/index/filter.rb +139 -0
  97. data/lib/ddr/index/query.rb +82 -0
  98. data/lib/ddr/index/query_builder.rb +185 -0
  99. data/lib/ddr/index/query_clause.rb +112 -0
  100. data/lib/ddr/index/query_params.rb +40 -0
  101. data/lib/ddr/index/query_result.rb +102 -0
  102. data/lib/ddr/index/response.rb +30 -0
  103. data/lib/ddr/index/sort_order.rb +28 -0
  104. data/lib/ddr/index/unique_key_field.rb +12 -0
  105. data/lib/ddr/managers.rb +9 -0
  106. data/lib/ddr/managers/manager.rb +13 -0
  107. data/lib/ddr/managers/technical_metadata_manager.rb +141 -0
  108. data/lib/ddr/structure.rb +188 -0
  109. data/lib/ddr/structures/agent.rb +49 -0
  110. data/lib/ddr/structures/component_type_term.rb +29 -0
  111. data/lib/ddr/structures/div.rb +64 -0
  112. data/lib/ddr/structures/f_locat.rb +54 -0
  113. data/lib/ddr/structures/file.rb +52 -0
  114. data/lib/ddr/structures/file_grp.rb +35 -0
  115. data/lib/ddr/structures/file_sec.rb +22 -0
  116. data/lib/ddr/structures/fptr.rb +31 -0
  117. data/lib/ddr/structures/mets_hdr.rb +37 -0
  118. data/lib/ddr/structures/mptr.rb +49 -0
  119. data/lib/ddr/structures/struct_map.rb +40 -0
  120. data/lib/ddr/utils.rb +185 -0
  121. data/lib/ddr/vocab.rb +22 -0
  122. data/lib/ddr/vocab/asset.rb +51 -0
  123. data/lib/ddr/vocab/contact.rb +9 -0
  124. data/lib/ddr/vocab/display.rb +9 -0
  125. data/lib/ddr/vocab/duke_terms.rb +13 -0
  126. data/lib/ddr/vocab/rdf_vocabulary_parser.rb +43 -0
  127. data/lib/ddr/vocab/roles.rb +25 -0
  128. data/lib/ddr/vocab/sources/duketerms.rdf +870 -0
  129. data/lib/ddr/vocab/vocabulary.rb +37 -0
  130. data/lib/ddr/workflow.rb +8 -0
  131. data/lib/tasks/ddr/core_tasks.rake +4 -0
  132. metadata +428 -0
@@ -0,0 +1,74 @@
1
+ require "net-ldap"
2
+
3
+ module Ddr::Auth
4
+ class LdapGateway
5
+
6
+ SCOPE = Net::LDAP::SearchScope_SingleLevel
7
+
8
+ class_attribute :attributes
9
+ self.attributes = [ "edupersonaffiliation", "ismemberof" ]
10
+
11
+ attr_reader :ldap
12
+
13
+ def self.find(user_key)
14
+ new.find(user_key)
15
+ end
16
+
17
+ def initialize
18
+ @ldap = Net::LDAP.new(config)
19
+ end
20
+
21
+ def find(user_key)
22
+ result_set = ldap.search find_params(user_key)
23
+ if result_set
24
+ Result.new result_set.first
25
+ else
26
+ raise ldap.get_operation_result.message
27
+ end
28
+ end
29
+
30
+ class Result
31
+ attr_reader :result
32
+
33
+ def initialize(result)
34
+ @result = result
35
+ end
36
+
37
+ def affiliation
38
+ result ? result[:edupersonaffiliation] : []
39
+ end
40
+
41
+ def ismemberof
42
+ result ? result[:ismemberof] : []
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def find_params(user_key)
49
+ { scope: SCOPE,
50
+ filter: filter(user_key),
51
+ size: 1,
52
+ attributes: attributes
53
+ }
54
+ end
55
+
56
+ def filter(user_key)
57
+ Net::LDAP::Filter.eq("eduPersonPrincipalName", user_key)
58
+ end
59
+
60
+ def config
61
+ { host: ENV["LDAP_HOST"],
62
+ port: ENV["LDAP_PORT"],
63
+ base: ENV["LDAP_BASE"],
64
+ auth:
65
+ { method: :simple,
66
+ username: ENV["LDAP_USER"],
67
+ password: ENV["LDAP_PASSWORD"]
68
+ },
69
+ encryption: { method: :simple_tls }
70
+ }
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,18 @@
1
+ module Ddr::Auth
2
+ class Permissions
3
+
4
+ READ = :read
5
+ DOWNLOAD = :download
6
+ ADD_CHILDREN = :add_children
7
+ UPDATE = :update
8
+ REPLACE = :replace
9
+ ARRANGE = :arrange
10
+ PUBLISH = :publish
11
+ UNPUBLISH = :unpublish
12
+ AUDIT = :audit
13
+ GRANT = :grant
14
+
15
+ ALL = [ READ, DOWNLOAD, ADD_CHILDREN, UPDATE, REPLACE, ARRANGE, PUBLISH, UNPUBLISH, AUDIT, GRANT ]
16
+
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module Ddr::Auth
2
+ class RemoteGroups
3
+
4
+ # @param auth_context [AuthContext]
5
+ # @return [Array<Group>]
6
+ def self.call(auth_context)
7
+ auth_context.ismemberof.map do |id|
8
+ Group.new id.sub(/\Aurn:mace:duke\.edu:groups/, "duke")
9
+ end
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,56 @@
1
+ module Ddr
2
+ module Auth
3
+ #
4
+ # Hydra controller mixin for role-based access control
5
+ #
6
+ # Overrides Hydra::AccessControlsEnforcement#gated_discovery_filters
7
+ # to apply role filters instead of permissions filters.
8
+ #
9
+ module RoleBasedAccessControlsEnforcement
10
+
11
+ def self.included(controller)
12
+ controller.delegate :authorized_to_act_as_superuser?, to: :current_ability
13
+ controller.helper_method :authorized_to_act_as_superuser?
14
+ end
15
+
16
+ def current_ability
17
+ @current_ability ||= AbilityFactory.call(current_user, request.env)
18
+ end
19
+
20
+ # List of URIs for policies on which any of the current user's agent has a role in policy scope
21
+ def policy_role_policies
22
+ @policy_role_policies ||= Array.new.tap do |uris|
23
+ filters = current_ability.agents.map do |agent|
24
+ "#{Ddr::Index::Fields::POLICY_ROLE}:\"#{agent}\""
25
+ end.join(" OR ")
26
+ query = "#{Ddr::Index::Fields::ACTIVE_FEDORA_MODEL}:Collection AND (#{filters})"
27
+ results = ActiveFedora::SolrService.query(query, rows: Collection.count, fl: Ddr::Index::Fields::INTERNAL_URI)
28
+ results.each_with_object(uris) { |r, memo| memo << r[Ddr::Index::Fields::INTERNAL_URI] }
29
+ end
30
+ end
31
+
32
+ def policy_role_filters
33
+ if policy_role_policies.present?
34
+ rels = policy_role_policies.map { |pid| [:is_governed_by, pid] }
35
+ ActiveFedora::SolrService.construct_query_for_rel(rels, "OR")
36
+ end
37
+ end
38
+
39
+ def resource_role_filters
40
+ current_ability.agents.map do |agent|
41
+ ActiveFedora::SolrService.raw_query(Ddr::Index::Fields::RESOURCE_ROLE, agent)
42
+ end.join(" OR ")
43
+ end
44
+
45
+ def gated_discovery_filters
46
+ [resource_role_filters, policy_role_filters].compact
47
+ end
48
+
49
+ # Overrides Hydra::AccessControlsEnforcement
50
+ def enforce_show_permissions
51
+ authorize! :read, params[:id]
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,28 @@
1
+ module Ddr::Auth
2
+ module Roles
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Role
6
+ autoload :RoleType
7
+ autoload :RoleTypes
8
+
9
+ include RoleTypes
10
+
11
+ RESOURCE_SCOPE = "resource".freeze
12
+ POLICY_SCOPE = "policy".freeze
13
+ SCOPES = [RESOURCE_SCOPE, POLICY_SCOPE].freeze
14
+
15
+ class << self
16
+
17
+ def type_map
18
+ @type_map ||= role_types.map { |role_type| [role_type.to_s, role_type] }.to_h
19
+ end
20
+
21
+ def role_types
22
+ @role_types ||= RoleTypes.constants(false).map { |const| RoleTypes.const_get(const) }
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,121 @@
1
+ module Ddr
2
+ module Auth
3
+ module Roles
4
+ #
5
+ # The assignment of a role to an agent within a scope.
6
+ #
7
+ class Role < Valkyrie::Resource
8
+
9
+ DEFAULT_SCOPE = Roles::RESOURCE_SCOPE
10
+
11
+ ValidScope = Valkyrie::Types::Strict::String.enum(*(Roles::SCOPES))
12
+ ValidRoleType = Valkyrie::Types::Strict::String.enum(*(Roles::role_types.map(&:title)))
13
+
14
+ attribute :agent, Valkyrie::Types::Strict::String.constrained(min_size: 1)
15
+ attribute :role_type, ValidRoleType
16
+ attribute :scope, ValidScope
17
+
18
+ class << self
19
+
20
+ # Build a Role instance from hash attributes
21
+ # @param args [Hash] the attributes
22
+ # @return [Role] the role
23
+ # @example
24
+ # Role.build type: "Curator", agent: "bob", scope: "resource"
25
+ def build(args={})
26
+ new.tap do |role|
27
+ args[:role_type] ||= args.delete(:type)
28
+ args[:agent] ||= nil # Triggers a constraint error
29
+ args[:agent] = args[:agent].to_s # Coerce Ddr::Auth:Group to string
30
+
31
+ args.each do |attr, val|
32
+ role.set_value(attr, val)
33
+ end
34
+
35
+ role.scope ||= DEFAULT_SCOPE
36
+ end
37
+ end
38
+
39
+ ###############
40
+ # FIXME or remove serialization/deserialization
41
+ ###############
42
+ #
43
+ # # Deserialize a Role from JSON
44
+ # # @param json [String] the JSON string
45
+ # # @return [Role] the role
46
+ # def from_json(json)
47
+ # build JSON.parse(json)
48
+ # end
49
+
50
+ # alias_method :deserialize, :from_json
51
+
52
+ private
53
+
54
+ #
55
+ # DELETEME
56
+ #
57
+ # def build_attributes(args={})
58
+ # # symbolize keys and stringify values
59
+ # attrs = args.each_with_object({}) do |(k, v), memo|
60
+ # memo[k.to_sym] = Array(v).first.to_s
61
+ # end
62
+ # # set default scope if necessary
63
+ # attrs[:scope] ||= DEFAULT_SCOPE
64
+ # # accept :type key for role_type attribute
65
+ # if attrs.key?(:type)
66
+ # attrs[:role_type] = attrs.delete(:type)
67
+ # end
68
+ # attrs
69
+ # end
70
+
71
+ end # class << self
72
+
73
+ # Roles are considered equal (==) if they
74
+ # are of the same type and have the same agent and scope.
75
+ # @param other [Object] the object of comparison
76
+ # @return [Boolean] the result
77
+ def ==(other)
78
+ self.class == other.class &&
79
+ role_type == other.role_type &&
80
+ scope == other.scope &&
81
+ agent == other.agent
82
+ end
83
+
84
+ alias_method :eql?, :==
85
+
86
+ def in_resource_scope?
87
+ scope == Roles::RESOURCE_SCOPE
88
+ end
89
+
90
+ def in_policy_scope?
91
+ scope == Roles::POLICY_SCOPE
92
+ end
93
+
94
+ def inspect
95
+ "#<#{self.class.name} role_type=#{role_type.inspect}, " \
96
+ "agent=#{agent.inspect}, scope=#{scope.inspect}>"
97
+ end
98
+
99
+ # TODO refactor up?
100
+ def proper_attributes
101
+ attributes.slice(self.class.fields - self.class.reserved_attributes)
102
+ end
103
+
104
+ ###############
105
+ # FIXME or remove serialization/deserialization
106
+ ###############
107
+ #
108
+ # delegate :to_json, to: :proper_attributes
109
+ #
110
+ # alias_method :serialize, :to_json
111
+
112
+ # Returns the permissions associated with the role
113
+ # @return [Array<Symbol>] the permissions
114
+ def permissions
115
+ Roles.type_map[role_type].permissions
116
+ end
117
+
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,23 @@
1
+ module Ddr
2
+ module Auth
3
+ module Roles
4
+ class RoleType
5
+
6
+ attr_reader :title, :description, :permissions
7
+ alias_method :label, :title
8
+
9
+ def initialize(title, description, permissions)
10
+ @title = title.freeze
11
+ @description = description.freeze
12
+ @permissions = permissions.freeze
13
+ freeze
14
+ end
15
+
16
+ def to_s
17
+ title
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,52 @@
1
+ module Ddr
2
+ module Auth
3
+ module Roles
4
+ module RoleTypes
5
+
6
+ CURATOR = RoleType.new(
7
+ "Curator",
8
+ "The Curator role conveys responsibility for curating a resource " \
9
+ "and delegating responsibilities to other agents.",
10
+ Permissions::ALL
11
+ )
12
+
13
+ EDITOR = RoleType.new(
14
+ "Editor",
15
+ "The Editor role conveys reponsibility for managing the content, " \
16
+ "description and structural arrangement of a resource.",
17
+ [ Permissions::READ, Permissions::DOWNLOAD, Permissions::ADD_CHILDREN,
18
+ Permissions::UPDATE, Permissions::REPLACE, Permissions::ARRANGE ]
19
+ )
20
+
21
+ METADATA_EDITOR = RoleType.new(
22
+ "MetadataEditor",
23
+ "The Metadata Editor role conveys responsibility for " \
24
+ "managing the description of a resource.",
25
+ [ Permissions::READ, Permissions::DOWNLOAD, Permissions::UPDATE ]
26
+ )
27
+
28
+ CONTRIBUTOR = RoleType.new(
29
+ "Contributor",
30
+ "The Contributor role conveys responsibility for adding related " \
31
+ "resources to a resource, such as works to a collection.",
32
+ [ Permissions::READ, Permissions::ADD_CHILDREN ]
33
+ )
34
+
35
+ DOWNLOADER = RoleType.new(
36
+ "Downloader",
37
+ "The Downloader role conveys access to the \"master\" file " \
38
+ "(original content bitstream) of a resource.",
39
+ [ Permissions::READ, Permissions::DOWNLOAD ]
40
+ )
41
+
42
+ VIEWER = RoleType.new(
43
+ "Viewer",
44
+ "The Viewer role conveys access to the description and \"access\" " \
45
+ "files (e.g., derivative bitstreams) of a resource.",
46
+ [ Permissions::READ ]
47
+ )
48
+
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,7 @@
1
+ module Ddr::Auth
2
+ class SuperuserAbility < AbstractAbility
3
+
4
+ self.ability_definitions = [ SuperuserAbilityDefinitions ]
5
+
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module Ddr
2
+ module Auth
3
+ module TestHelpers
4
+
5
+ class MockLdapGateway
6
+ def self.find(*args); new.find(*args); end
7
+ def find(user_key); Ddr::Auth::LdapGateway::Result.new(nil); end
8
+ end
9
+
10
+ class MockGrouperGateway
11
+ def self.repository_groups(*args); new.repository_groups(*args); end
12
+ def repository_groups(raw = false); []; end
13
+ def self.user_groups(*args); new.user_groups(*args); end
14
+ def user_groups(user, raw = false); []; end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+
21
+ Ddr::Auth.ldap_gateway = Ddr::Auth::TestHelpers::MockLdapGateway
22
+ Ddr::Auth.grouper_gateway = Ddr::Auth::TestHelpers::MockGrouperGateway
@@ -0,0 +1,54 @@
1
+ require 'devise'
2
+
3
+ module Ddr::Auth
4
+ module User
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ delegate :can, :can?, :cannot, :cannot?, to: :ability
9
+
10
+ validates_uniqueness_of :username, case_sensitive: false
11
+ validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
12
+
13
+ devise :database_authenticatable, :omniauthable, omniauth_providers: [:shibboleth]
14
+
15
+ class_attribute :user_key_attribute
16
+ self.user_key_attribute = Devise.authentication_keys.first
17
+ end
18
+
19
+ module ClassMethods
20
+ def find_by_user_key(key)
21
+ send("find_by_#{user_key_attribute}", key)
22
+ end
23
+
24
+ def from_omniauth(auth)
25
+ user = find_by_user_key(auth.uid) ||
26
+ new(user_key_attribute => auth.uid, :password => Devise.friendly_token)
27
+ user.update!(email: auth.info.email,
28
+ display_name: auth.info.name,
29
+ first_name: auth.info.first_name,
30
+ last_name: auth.info.last_name,
31
+ nickname: auth.info.nickname)
32
+ user
33
+ end
34
+ end
35
+
36
+ # Copied from Hydra::User
37
+ def user_key
38
+ send(user_key_attribute)
39
+ end
40
+
41
+ def to_s
42
+ user_key
43
+ end
44
+
45
+ def agent
46
+ user_key
47
+ end
48
+
49
+ def ability
50
+ @ability ||= AbilityFactory.call(self)
51
+ end
52
+
53
+ end
54
+ end