hydra-access-controls 0.0.2 → 0.0.3

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.
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>