hydra-access-controls 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +1 -0
  2. data/.gitmodules +3 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +6 -0
  5. data/README.textile +100 -0
  6. data/Rakefile +6 -3
  7. data/config/fedora.yml +8 -0
  8. data/config/jetty.yml +5 -0
  9. data/config/solr.yml +6 -0
  10. data/hydra-access-controls.gemspec +3 -2
  11. data/lib/ability.rb +1 -0
  12. data/lib/hydra-access-controls.rb +10 -0
  13. data/lib/hydra-access-controls/version.rb +1 -1
  14. data/lib/hydra/ability.rb +14 -13
  15. data/lib/hydra/access_controls_enforcement.rb +27 -16
  16. data/lib/hydra/admin_policy.rb +81 -0
  17. data/lib/hydra/datastream.rb +1 -0
  18. data/lib/hydra/datastream/inheritable_rights_metadata.rb +22 -0
  19. data/lib/hydra/policy_aware_ability.rb +128 -0
  20. data/lib/hydra/policy_aware_access_controls_enforcement.rb +70 -0
  21. data/lib/hydra/role_mapper_behavior.rb +16 -2
  22. data/lib/hydra/user.rb +42 -0
  23. data/lib/tasks/hydra-access-controls.rake +18 -0
  24. data/lib/tasks/hydra_jetty.rake +55 -0
  25. data/solr_conf/conf/schema.xml +124 -0
  26. data/solr_conf/conf/solrconfig.xml +329 -0
  27. data/solr_conf/solr.xml +35 -0
  28. data/spec/factories.rb +101 -0
  29. data/spec/spec_helper.rb +28 -0
  30. data/spec/support/blacklight.rb +7 -0
  31. data/spec/support/config/solr.yml +4 -0
  32. data/spec/support/mods_asset.rb +4 -1
  33. data/spec/support/rails.rb +10 -0
  34. data/spec/support/solr_document.rb +13 -0
  35. data/spec/support/user.rb +32 -0
  36. data/spec/unit/ability_spec.rb +338 -56
  37. data/spec/unit/access_controls_enforcement_spec.rb +180 -0
  38. data/spec/unit/admin_policy_spec.rb +89 -0
  39. data/spec/unit/inheritable_rights_metadata_spec.rb +66 -0
  40. data/spec/unit/policy_aware_ability_spec.rb +92 -0
  41. data/spec/unit/policy_aware_access_controls_enforcement_spec.rb +109 -0
  42. metadata +59 -4
  43. data/README.md +0 -29
@@ -2,5 +2,6 @@ module Hydra
2
2
  module Datastream
3
3
  extend ActiveSupport::Autoload
4
4
  autoload :RightsMetadata
5
+ autoload :InheritableRightsMetadata
5
6
  end
6
7
  end
@@ -0,0 +1,22 @@
1
+ require 'active_support/core_ext/string'
2
+ module Hydra
3
+ module Datastream
4
+ # Implements Hydra RightsMetadata XML terminology for asserting access permissions
5
+ class InheritableRightsMetadata < Hydra::Datastream::RightsMetadata
6
+
7
+ @terminology = Hydra::Datastream::RightsMetadata.terminology
8
+
9
+ def to_solr(solr_doc=Hash.new)
10
+ solr_doc["inheritable_access_t"] = access.machine.group.val + access.machine.person.val
11
+ solr_doc["inheritable_discover_access_group_t"] = discover_access.machine.group
12
+ solr_doc["inheritable_discover_access_person_t"] = discover_access.machine.person
13
+ solr_doc["inheritable_read_access_group_t"] = read_access.machine.group
14
+ solr_doc["inheritable_read_access_person_t"] = read_access.machine.person
15
+ solr_doc["inheritable_edit_access_group_t"] = edit_access.machine.group
16
+ solr_doc["inheritable_edit_access_person_t"] = edit_access.machine.person
17
+ solr_doc["inheritable_embargo_release_date_dt"] = embargo_release_date
18
+ return solr_doc
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,128 @@
1
+ # Repeats access controls evaluation methods, but checks against a governing "Policy" object (or "Collection" object) that provides inherited access controls.
2
+ module Hydra::PolicyAwareAbility
3
+
4
+ # Extends Hydra::Ability.test_edit to try policy controls if object-level controls deny access
5
+ def test_edit(pid, user, session)
6
+ result = super
7
+ if result
8
+ return result
9
+ else
10
+ return test_edit_from_policy(pid, user, session)
11
+ end
12
+ end
13
+
14
+ # Extends Hydra::Ability.test_read to try policy controls if object-level controls deny access
15
+ def test_read(pid, user, session)
16
+ result = super
17
+ if result
18
+ return result
19
+ else
20
+ return test_read_from_policy(pid, user, session)
21
+ end
22
+ end
23
+
24
+ # Returns the pid of policy object (is_governed_by) for the specified object
25
+ # Assumes that the policy object is associated by an is_governed_by relationship (Whis is stored as "is_governed_by_s" in object's solr document)
26
+ # Returns nil if no policy associated with the object
27
+ def policy_pid_for(object_pid)
28
+ return @policy_pid if @policy_pid
29
+ solr_result = ActiveFedora::Base.find_with_conditions({:id=>object_pid}, :fl=>'is_governed_by_s')
30
+ begin
31
+ @policy_pid = value_from_solr_field(solr_result, 'is_governed_by_s').first.gsub("info:fedora/", "")
32
+ rescue NoMethodError
33
+ @policy_pid = nil
34
+ end
35
+ return @policy_pid
36
+ end
37
+
38
+ # Returns the permissions solr document for policy_pid
39
+ # The document is stored in an instance variable, so calling this multiple times will only query solr once.
40
+ # To force reload, set @policy_permissions_solr_document to nil
41
+ def policy_permissions_doc(policy_pid)
42
+ return @policy_permissions_solr_document if @policy_permissions_solr_document
43
+ response, @policy_permissions_solr_document = get_permissions_solr_response_for_doc_id(policy_pid)
44
+ @policy_permissions_solr_document
45
+ end
46
+
47
+ # Tests whether the object's governing policy object grants edit access for the current user
48
+ def test_edit_from_policy(object_pid, user, session)
49
+ policy_pid = policy_pid_for(object_pid)
50
+ if policy_pid.nil?
51
+ return false
52
+ else
53
+ logger.debug("[CANCAN] -policy- Does the POLICY #{policy_pid} provide EDIT permissions for #{user_key(user)}?")
54
+ group_intersection = user_groups(user, session) & edit_groups_from_policy( policy_pid )
55
+ result = !group_intersection.empty? || edit_persons_from_policy( policy_pid ).include?(user_key(user))
56
+ logger.debug("[CANCAN] -policy- decision: #{result}")
57
+ return result
58
+ end
59
+ end
60
+
61
+ # Tests whether the object's governing policy object grants read access for the current user
62
+ def test_read_from_policy(object_pid, user, session)
63
+ policy_pid = policy_pid_for(object_pid)
64
+ if policy_pid.nil?
65
+ return false
66
+ else
67
+ logger.debug("[CANCAN] -policy- Does the POLICY #{policy_pid} provide READ permissions for #{user_key(user)}?")
68
+ group_intersection = user_groups(user, session) & read_groups_from_policy( policy_pid )
69
+ result = !group_intersection.empty? || read_persons_from_policy( policy_pid ).include?(user_key(user))
70
+ logger.debug("[CANCAN] -policy- decision: #{result}")
71
+ result
72
+ end
73
+ end
74
+
75
+ # Returns the list of groups granted edit access by the policy object identified by policy_pid
76
+ def edit_groups_from_policy(policy_pid)
77
+ policy_permissions = policy_permissions_doc(policy_pid)
78
+ edit_group_field = Hydra.config[:permissions][:inheritable][:edit][:group]
79
+ eg = ((policy_permissions == nil || policy_permissions.fetch(edit_group_field,nil) == nil) ? [] : policy_permissions.fetch(edit_group_field,nil))
80
+ logger.debug("[CANCAN] -policy- edit_groups: #{eg.inspect}")
81
+ return eg
82
+ end
83
+
84
+ # Returns the list of groups granted read access by the policy object identified by policy_pid
85
+ # Note: edit implies read, so read_groups is the union of edit and read groups
86
+ def read_groups_from_policy(policy_pid)
87
+ policy_permissions = policy_permissions_doc(policy_pid)
88
+ read_group_field = Hydra.config[:permissions][:inheritable][:read][:group]
89
+ rg = edit_groups_from_policy(policy_pid) | ((policy_permissions == nil || policy_permissions.fetch(read_group_field,nil) == nil) ? [] : policy_permissions.fetch(read_group_field,nil))
90
+ logger.debug("[CANCAN] -policy- read_groups: #{rg.inspect}")
91
+ return rg
92
+ end
93
+
94
+ # Returns the list of individuals granted edit access by the policy object identified by policy_pid
95
+ def edit_persons_from_policy(policy_pid)
96
+ policy_permissions = policy_permissions_doc(policy_pid)
97
+ edit_person_field = Hydra.config[:permissions][:inheritable][:edit][:individual]
98
+ ep = ((policy_permissions == nil || policy_permissions.fetch(edit_person_field,nil) == nil) ? [] : policy_permissions.fetch(edit_person_field,nil))
99
+ logger.debug("[CANCAN] -policy- edit_persons: #{ep.inspect}")
100
+ return ep
101
+ end
102
+
103
+ # Returns the list of individuals granted read access by the policy object identified by policy_pid
104
+ # Noate: edit implies read, so read_persons is the union of edit and read persons
105
+ def read_persons_from_policy(policy_pid)
106
+ policy_permissions = policy_permissions_doc(policy_pid)
107
+ read_individual_field = Hydra.config[:permissions][:inheritable][:read][:individual]
108
+ rp = edit_persons_from_policy(policy_pid) | ((policy_permissions == nil || policy_permissions.fetch(read_individual_field,nil) == nil) ? [] : policy_permissions.fetch(read_individual_field,nil))
109
+ logger.debug("[CANCAN] -policy- read_persons: #{rp.inspect}")
110
+ return rp
111
+ end
112
+
113
+ private
114
+
115
+ # Grabs the value of field_name from solr_result
116
+ # @example
117
+ # solr_result = Multiresimage.find_with_conditions({:id=>object_pid}, :fl=>'is_governed_by_s')
118
+ # value_from_solr_field(solr_result, 'is_governed_by_s')
119
+ # => ["info:fedora/changeme:2278"]
120
+ def value_from_solr_field(solr_result, field_name)
121
+ field_from_result = solr_result.select {|x| x.has_key?(field_name)}.first
122
+ if field_from_result.nil?
123
+ return nil
124
+ else
125
+ return field_from_result[field_name]
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,70 @@
1
+ # Repeats access controls evaluation methods, but checks against a governing "Policy" object (or "Collection" object) that provides inherited access controls.
2
+ module Hydra::PolicyAwareAccessControlsEnforcement
3
+
4
+ # Extends Hydra::AccessControlsEnforcement.apply_gated_discovery to reflect policy-provided access
5
+ # appends the result of policy_clauses into the :fq
6
+ def apply_gated_discovery(solr_parameters, user_parameters)
7
+ super
8
+ additional_clauses = policy_clauses
9
+ unless additional_clauses.nil? || additional_clauses.empty?
10
+ solr_parameters[:fq].first << " OR " + policy_clauses
11
+ logger.debug("POLICY-aware Solr parameters: #{ solr_parameters.inspect }")
12
+ end
13
+ end
14
+
15
+ # returns solr query for finding all objects whose policies grant discover access to current_user
16
+ def policy_clauses
17
+ policy_pids = policies_with_access
18
+ return nil if policy_pids.empty?
19
+ '(' + policy_pids.map {|pid| "is_governed_by_s:info\\:fedora/#{pid.gsub(/:/, '\\\\:')}"}.join(' OR ') + ')'
20
+ end
21
+
22
+
23
+ # find all the policies that grant discover/read/edit permissions to this user or any of it's groups
24
+ def policies_with_access
25
+ #### TODO -- Memoize this and put it in the session?
26
+ return [] unless current_user
27
+ user_access_filters = []
28
+ # Grant access based on user id & role
29
+ unless current_user.nil?
30
+ user_access_filters += apply_policy_role_permissions(discovery_permissions)
31
+ user_access_filters += apply_policy_individual_permissions(discovery_permissions)
32
+ end
33
+ result = policy_class.find_with_conditions( user_access_filters.join(" OR "), :fl => "id" )
34
+ logger.debug "get policies: #{result}\n\n"
35
+ result.map {|h| h['id']}
36
+ end
37
+
38
+
39
+ def apply_policy_role_permissions(permission_types)
40
+ # for roles
41
+ user_access_filters = []
42
+ ::RoleMapper.roles(user_key).each_with_index do |role, i|
43
+ discovery_permissions.each do |type|
44
+ user_access_filters << "inheritable_#{type}_access_group_t:#{role}"
45
+ end
46
+ end
47
+ user_access_filters
48
+ end
49
+
50
+ def apply_policy_individual_permissions(permission_types)
51
+ # for individual person access
52
+ user_access_filters = []
53
+ discovery_permissions.each do |type|
54
+ user_access_filters << "inheritable_#{type}_access_person_t:#{user_key}"
55
+ end
56
+ user_access_filters
57
+ end
58
+
59
+ # Returns the Model used for AdminPolicy objects.
60
+ # You can set this by overriding this method or setting Hydra.config[:permissions][:policy_class]
61
+ # Defults to Hydra::AdminPolicy
62
+ def policy_class
63
+ if Hydra.config[:permissions][:policy_class].nil?
64
+ return Hydra::AdminPolicy
65
+ else
66
+ return Hydra.config[:permissions][:policy_class]
67
+ end
68
+ end
69
+
70
+ end
@@ -7,8 +7,21 @@ module Hydra::RoleMapperBehavior
7
7
  def role_names
8
8
  map.keys
9
9
  end
10
- def roles(username)
11
- byname[username]||[]
10
+
11
+ #
12
+ # @param user_or_uid either the User object or user id
13
+ # If you pass in a nil User object (ie. user isn't logged in), or a uid that doesn't exist, it will return an empty array
14
+ def roles(user_or_uid)
15
+ if user_or_uid.kind_of?(String)
16
+ user = User.find_by_user_key(user_or_uid)
17
+ user_id = user_or_uid
18
+ elsif user_or_uid.kind_of?(User) && !user_or_uid.uid.nil?
19
+ user = user_or_uid
20
+ user_id = user.user_key
21
+ end
22
+ array = byname[user_id]||[]
23
+ array = array << 'registered' unless (user.nil? || user.new_record?)
24
+ array
12
25
  end
13
26
 
14
27
  def whois(r)
@@ -28,6 +41,7 @@ module Hydra::RoleMapperBehavior
28
41
  memo
29
42
  end
30
43
  end
44
+
31
45
  end
32
46
  end
33
47
 
data/lib/hydra/user.rb ADDED
@@ -0,0 +1,42 @@
1
+ # Injects behaviors into User model so that it will work with Hydra Access Controls
2
+ # By default, this module assumes you are using the User model created by Blacklight, which uses Devise.
3
+ # To integrate your own User implementation into Hydra, override this Module or define your own User model in app/models/user.rb within your Hydra head.
4
+ require 'deprecation'
5
+ module Hydra::User
6
+ extend Deprecation
7
+
8
+ def self.included(klass)
9
+ # Other modules to auto-include
10
+ klass.extend(ClassMethods)
11
+ end
12
+
13
+ # This method should display the unique identifier for this user as defined by devise.
14
+ # The unique identifier is what access controls will be enforced against.
15
+ def user_key
16
+ send(Devise.authentication_keys.first)
17
+ end
18
+
19
+ module ClassMethods
20
+ # This method should find User objects using the user_key you've chosen.
21
+ # By default, uses the unique identifier specified in by devise authentication_keys (ie. find_by_id, or find_by_email).
22
+ # You must have that find method implemented on your user class, or must override find_by_user_key
23
+ def find_by_user_key(key)
24
+ self.send("find_by_#{Devise.authentication_keys.first}".to_sym, key)
25
+ end
26
+ end
27
+
28
+ # This method should display the unique identifier for this user
29
+ # the unique identifier is what access controls will be enforced against.
30
+ def unique_id
31
+ return to_s
32
+ end
33
+ deprecation_deprecate :unique_id
34
+
35
+
36
+ # For backwards compatibility with the Rails2 User models in Hydra/Blacklight
37
+ def login
38
+ return unique_id
39
+ end
40
+ deprecation_deprecate :login
41
+
42
+ end
@@ -0,0 +1,18 @@
1
+ namespace "hydra-access" do
2
+ desc "Run Continuous Integration"
3
+ task "ci" do
4
+ require 'jettywrapper'
5
+ jetty_params = Jettywrapper.load_config.merge(
6
+ {:jetty_home => File.expand_path(File.dirname(__FILE__) + '/../../jetty'),
7
+ :startup_wait => 15,
8
+ :jetty_port => ENV['TEST_JETTY_PORT'] || 8983
9
+ }
10
+ )
11
+ Rake::Task['jetty:config'].invoke
12
+ error = nil
13
+ error = Jettywrapper.wrap(jetty_params) do
14
+ Rake::Task['spec'].invoke
15
+ end
16
+ raise "test failures: #{error}" if error
17
+ end
18
+ end
@@ -0,0 +1,55 @@
1
+ # re-using hydra_jetty.rake from hydra-head
2
+
3
+ namespace :jetty do
4
+ desc "Apply all configs to Testing Server (relies on hydra:jetty:config tasks unless you override it)"
5
+ task :config do
6
+ Rake::Task["hydra:jetty:config"].invoke
7
+ end
8
+ end
9
+
10
+ namespace :hydra do
11
+ namespace :jetty do
12
+ desc "Copies the default Solr & Fedora configs into the bundled Hydra Testing Server"
13
+ task :config do
14
+ Rake::Task["hydra:jetty:config_fedora"].invoke
15
+ Rake::Task["hydra:jetty:config_solr"].invoke
16
+ end
17
+
18
+ desc "Copies the contents of solr_conf into the Solr development-core and test-core of Testing Server"
19
+ task :config_solr do
20
+ FileList['solr_conf/conf/*'].each do |f|
21
+ cp("#{f}", 'jetty/solr/development-core/conf/', :verbose => true)
22
+ cp("#{f}", 'jetty/solr/test-core/conf/', :verbose => true)
23
+ end
24
+ end
25
+
26
+ desc "Copies a custom fedora config for the bundled Hydra Testing Server"
27
+ task :config_fedora do
28
+ # load a custom fedora.fcfg -
29
+ if defined?(Rails.root)
30
+ app_root = Rails.root
31
+ else
32
+ app_root = File.join(File.dirname(__FILE__),"..")
33
+ end
34
+
35
+ fcfg = File.join(app_root,"fedora_conf","conf","development","fedora.fcfg")
36
+ if File.exists?(fcfg)
37
+ puts "copying over development/fedora.fcfg"
38
+ cp("#{fcfg}", 'jetty/fedora/default/server/config/', :verbose => true)
39
+ else
40
+ puts "#{fcfg} file not found -- skipping fedora config"
41
+ end
42
+ fcfg = File.join(app_root,"fedora_conf","conf","test","fedora.fcfg")
43
+ if File.exists?(fcfg)
44
+ puts "copying over test/fedora.fcfg"
45
+ cp("#{fcfg}", 'jetty/fedora/test/server/config/', :verbose => true)
46
+ else
47
+ puts "#{fcfg} file not found -- skipping fedora config"
48
+ end
49
+ end
50
+
51
+ desc "Copies the default SOLR config files and starts up the fedora instance."
52
+ task :load => [:config, 'jetty:start']
53
+
54
+ end
55
+ end
@@ -0,0 +1,124 @@
1
+ <?xml version="1.0" encoding="UTF-8" ?>
2
+ <!--
3
+ IMPORTANT
4
+ This copy of the solr schema is only used in the context of testing hydra-head. If you want to make changes available to individual hydra heads, you must apply them to the template in lib/generators/hydra/templates/solr_config
5
+ -->
6
+ <schema name="Hydra" version="1.1">
7
+ <!-- For complete comments from the Solr project example schema.xml:
8
+ http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/solr/conf/schema.xml?view=markup
9
+ See also:
10
+ http://wiki.apache.org/solr/SchemaXml
11
+ -->
12
+ <types>
13
+ <fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
14
+ <fieldType name="boolean" class="solr.BoolField" sortMissingLast="true" omitNorms="true"/>
15
+ <fieldType name="integer" class="solr.IntField" omitNorms="true"/>
16
+ <fieldType name="long" class="solr.LongField" omitNorms="true"/>
17
+ <fieldType name="float" class="solr.FloatField" omitNorms="true"/>
18
+ <fieldType name="double" class="solr.DoubleField" omitNorms="true"/>
19
+ <fieldType name="sint" class="solr.SortableIntField" sortMissingLast="true" omitNorms="true"/>
20
+ <fieldType name="slong" class="solr.SortableLongField" sortMissingLast="true" omitNorms="true"/>
21
+ <fieldType name="sfloat" class="solr.SortableFloatField" sortMissingLast="true" omitNorms="true"/>
22
+ <fieldType name="sdouble" class="solr.SortableDoubleField" sortMissingLast="true" omitNorms="true"/>
23
+ <fieldType name="date" class="solr.DateField" sortMissingLast="true" omitNorms="true"/>
24
+ <fieldType name="random" class="solr.RandomSortField" indexed="true" />
25
+
26
+ <fieldType name="text_ws" class="solr.TextField" positionIncrementGap="100">
27
+ <analyzer>
28
+ <tokenizer class="solr.WhitespaceTokenizerFactory"/>
29
+ </analyzer>
30
+ </fieldType>
31
+
32
+ <fieldType name="text" class="solr.TextField" positionIncrementGap="100">
33
+ <analyzer type="index">
34
+ <tokenizer class="solr.WhitespaceTokenizerFactory"/>
35
+ <filter class="solr.StopFilterFactory"
36
+ ignoreCase="true"
37
+ words="stopwords.txt"
38
+ enablePositionIncrements="true"
39
+ />
40
+ <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
41
+ <filter class="solr.LowerCaseFilterFactory"/>
42
+ <filter class="solr.EnglishPorterFilterFactory" protected="protwords.txt"/>
43
+ <filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
44
+ </analyzer>
45
+ <analyzer type="query">
46
+ <tokenizer class="solr.WhitespaceTokenizerFactory"/>
47
+ <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true"/>
48
+ <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
49
+ <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="0" catenateNumbers="0" catenateAll="0" splitOnCaseChange="1"/>
50
+ <filter class="solr.LowerCaseFilterFactory"/>
51
+ <filter class="solr.EnglishPorterFilterFactory" protected="protwords.txt"/>
52
+ <filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
53
+ </analyzer>
54
+ </fieldType>
55
+
56
+ <fieldType name="textTight" class="solr.TextField" positionIncrementGap="100" >
57
+ <analyzer>
58
+ <tokenizer class="solr.WhitespaceTokenizerFactory"/>
59
+ <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="false"/>
60
+ <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/>
61
+ <filter class="solr.WordDelimiterFilterFactory" generateWordParts="0" generateNumberParts="0" catenateWords="1" catenateNumbers="1" catenateAll="0"/>
62
+ <filter class="solr.LowerCaseFilterFactory"/>
63
+ <filter class="solr.EnglishPorterFilterFactory" protected="protwords.txt"/>
64
+ <filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
65
+ </analyzer>
66
+ </fieldType>
67
+
68
+ <fieldType name="alphaOnlySort" class="solr.TextField" sortMissingLast="true" omitNorms="true">
69
+ <analyzer>
70
+ <tokenizer class="solr.KeywordTokenizerFactory"/>
71
+ <filter class="solr.LowerCaseFilterFactory" />
72
+ <filter class="solr.TrimFilterFactory" />
73
+ <filter class="solr.PatternReplaceFilterFactory"
74
+ pattern="([^a-z])" replacement="" replace="all"
75
+ />
76
+ </analyzer>
77
+ </fieldType>
78
+
79
+ <fieldtype name="ignored" stored="false" indexed="false" class="solr.StrField" />
80
+
81
+ </types>
82
+
83
+ <!-- For complete comments from the Solr project example schema.xml:
84
+ http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/solr/conf/schema.xml?view=markup
85
+ See also:
86
+ http://wiki.apache.org/solr/SchemaXml
87
+ -->
88
+ <fields>
89
+
90
+ <field name="id" type="string" indexed="true" stored="true" required="true" />
91
+ <field name="text" type="text" indexed="true" stored="true" multiValued="true"/>
92
+ <field name="timestamp" type="date" indexed="true" stored="true" default="NOW" multiValued="false"/>
93
+
94
+ <!-- format is used for facet, display, and choosing which partial to use for the show view, so it must be stored and indexed -->
95
+ <field name="format" type="string" indexed="true" stored="true"/>
96
+ <!-- pub_date is assumed by Blacklight's default configuration, so we must define it here to avoid errors -->
97
+ <field name="pub_date" type="string" indexed="true" stored="true" multiValued="true"/>
98
+
99
+ <dynamicField name="*_i" type="sint" indexed="true" stored="true"/>
100
+ <dynamicField name="*_s" type="string" indexed="true" stored="true" multiValued="true"/>
101
+ <dynamicField name="*_l" type="slong" indexed="true" stored="true"/>
102
+ <dynamicField name="*_t" type="text" indexed="true" stored="true" multiValued="true"/>
103
+ <dynamicField name="*_b" type="boolean" indexed="true" stored="true"/>
104
+ <dynamicField name="*_f" type="sfloat" indexed="true" stored="true"/>
105
+ <dynamicField name="*_d" type="sdouble" indexed="true" stored="true"/>
106
+ <dynamicField name="*_dt" type="date" indexed="true" stored="true"/>
107
+
108
+ <dynamicField name="random*" type="random" />
109
+ <!-- FIXME: Solr can't sort on a multivalued field -->
110
+ <dynamicField name="*_sort" type="string" indexed="true" stored="false" multiValued="true"/>
111
+ <!-- FIXME: generally, facet fields are not stored -->
112
+ <dynamicField name="*_facet" type="string" indexed="true" stored="true" multiValued="true" />
113
+ <dynamicField name="*_display" type="string" indexed="false" stored="true" multiValued="true" />
114
+
115
+ </fields>
116
+
117
+ <uniqueKey>id</uniqueKey>
118
+ <defaultSearchField>text</defaultSearchField>
119
+ <solrQueryParser defaultOperator="AND" />
120
+ <copyField source="*_facet" dest="text" />
121
+ <copyField source="*_t" dest="text" />
122
+ <copyField source="*_s" dest="text" />
123
+
124
+ </schema>