vines-services 0.1.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.
- data/LICENSE +19 -0
- data/README +40 -0
- data/Rakefile +130 -0
- data/bin/vines-services +95 -0
- data/conf/config.rb +25 -0
- data/lib/vines/services/command/init.rb +209 -0
- data/lib/vines/services/command/restart.rb +14 -0
- data/lib/vines/services/command/start.rb +30 -0
- data/lib/vines/services/command/stop.rb +20 -0
- data/lib/vines/services/command/views.rb +26 -0
- data/lib/vines/services/component.rb +26 -0
- data/lib/vines/services/config.rb +105 -0
- data/lib/vines/services/connection.rb +120 -0
- data/lib/vines/services/controller/attributes_controller.rb +19 -0
- data/lib/vines/services/controller/base_controller.rb +99 -0
- data/lib/vines/services/controller/disco_info_controller.rb +61 -0
- data/lib/vines/services/controller/labels_controller.rb +17 -0
- data/lib/vines/services/controller/members_controller.rb +44 -0
- data/lib/vines/services/controller/messages_controller.rb +66 -0
- data/lib/vines/services/controller/probes_controller.rb +45 -0
- data/lib/vines/services/controller/services_controller.rb +81 -0
- data/lib/vines/services/controller/subscriptions_controller.rb +39 -0
- data/lib/vines/services/controller/systems_controller.rb +45 -0
- data/lib/vines/services/controller/transfers_controller.rb +58 -0
- data/lib/vines/services/controller/uploads_controller.rb +62 -0
- data/lib/vines/services/controller/users_controller.rb +127 -0
- data/lib/vines/services/core_ext/blather.rb +46 -0
- data/lib/vines/services/core_ext/couchrest.rb +33 -0
- data/lib/vines/services/indexer.rb +195 -0
- data/lib/vines/services/priority_queue.rb +94 -0
- data/lib/vines/services/roster.rb +70 -0
- data/lib/vines/services/storage/couchdb/fragment.rb +23 -0
- data/lib/vines/services/storage/couchdb/service.rb +170 -0
- data/lib/vines/services/storage/couchdb/system.rb +141 -0
- data/lib/vines/services/storage/couchdb/upload.rb +66 -0
- data/lib/vines/services/storage/couchdb/user.rb +137 -0
- data/lib/vines/services/storage/couchdb/vcard.rb +13 -0
- data/lib/vines/services/storage/couchdb.rb +157 -0
- data/lib/vines/services/storage.rb +33 -0
- data/lib/vines/services/throttle.rb +26 -0
- data/lib/vines/services/version.rb +7 -0
- data/lib/vines/services/vql/compiler.rb +94 -0
- data/lib/vines/services/vql/vql.citrus +115 -0
- data/lib/vines/services/vql/vql.rb +186 -0
- data/lib/vines/services.rb +71 -0
- data/test/config_test.rb +242 -0
- data/test/priority_queue_test.rb +23 -0
- data/test/storage/couchdb_test.rb +30 -0
- data/test/vql/compiler_test.rb +96 -0
- data/test/vql/vql_test.rb +233 -0
- data/web/coffeescripts/api.coffee +51 -0
- data/web/coffeescripts/commands.coffee +18 -0
- data/web/coffeescripts/files.coffee +315 -0
- data/web/coffeescripts/init.coffee +21 -0
- data/web/coffeescripts/services.coffee +356 -0
- data/web/coffeescripts/setup.coffee +503 -0
- data/web/coffeescripts/systems.coffee +371 -0
- data/web/images/default-service.png +0 -0
- data/web/images/linux.png +0 -0
- data/web/images/mac.png +0 -0
- data/web/images/run.png +0 -0
- data/web/images/windows.png +0 -0
- data/web/index.html +17 -0
- data/web/stylesheets/common.css +52 -0
- data/web/stylesheets/files.css +218 -0
- data/web/stylesheets/services.css +181 -0
- data/web/stylesheets/setup.css +117 -0
- data/web/stylesheets/systems.css +142 -0
- metadata +230 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Vines
|
3
|
+
class Storage
|
4
|
+
class CouchDB < Storage
|
5
|
+
|
6
|
+
ALL_SERVICES = '/_design/Service/_view/by_name'.freeze
|
7
|
+
|
8
|
+
# The existing storage class needs another method for our custom roster queries
|
9
|
+
# The default escaping in URLs makes this method necessary
|
10
|
+
def get_services
|
11
|
+
http = EM::HttpRequest.new("#{@url}#{ALL_SERVICES}").get
|
12
|
+
http.errback { yield }
|
13
|
+
http.callback do
|
14
|
+
doc = if http.response_header.status == 200
|
15
|
+
JSON.parse(http.response) rescue nil
|
16
|
+
end
|
17
|
+
yield doc
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
#In order to supply the correct vines roster logic, we need to over ride the default
|
22
|
+
#User creation of in the vines server. This method is much more effecient than
|
23
|
+
#looking up roster memberships each time the roster is sent.
|
24
|
+
def find_user(jid)
|
25
|
+
jid = JID.new(jid || '').bare.to_s
|
26
|
+
if jid.empty? then yield; return end
|
27
|
+
get("user:#{jid}") do |doc|
|
28
|
+
user = if doc && doc['type'] == 'User'
|
29
|
+
User.new(:jid => jid).tap do |user|
|
30
|
+
user.name, user.password = doc.values_at('name', 'password')
|
31
|
+
if doc['roster'] != ""
|
32
|
+
(doc['roster'] || {}).each_pair do |jid, props|
|
33
|
+
user.roster << Contact.new(
|
34
|
+
:jid => jid,
|
35
|
+
:name => props['name'],
|
36
|
+
:subscription => props['subscription'],
|
37
|
+
:ask => props['ask'],
|
38
|
+
:groups => props['groups'] || [])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
add_user_roster_services(user)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
yield user
|
45
|
+
end
|
46
|
+
end
|
47
|
+
fiber :find_user
|
48
|
+
|
49
|
+
# We will go find each service that contains this user jid in the
|
50
|
+
# users of the service document.
|
51
|
+
def add_user_roster_services(user)
|
52
|
+
self.get_services do |cdoc|
|
53
|
+
if cdoc
|
54
|
+
rows = cdoc['rows'].map do |row|
|
55
|
+
if row['value']['users'].include?(user.jid.to_s)
|
56
|
+
jid = JID.new("#{row['value']['jid']}").bare.to_s
|
57
|
+
user.roster << Contact.new(
|
58
|
+
:jid => jid,
|
59
|
+
:name => row['value']['name'],
|
60
|
+
:subscription => "both",
|
61
|
+
:ask => "subscribe",
|
62
|
+
:groups => ["Vines"])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module CouchModels
|
6
|
+
class Fragment < CouchRest::Model::Base
|
7
|
+
extend Storage::CouchDB::ClassMethods
|
8
|
+
|
9
|
+
property :xml, String
|
10
|
+
|
11
|
+
design do
|
12
|
+
view :by_jid,
|
13
|
+
map: %q{
|
14
|
+
function(doc) {
|
15
|
+
if (doc.type != 'Fragment') return;
|
16
|
+
emit(doc['_id'].split(':')[1], null);
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module CouchModels
|
6
|
+
class Service < CouchRest::Model::Base
|
7
|
+
extend Storage::CouchDB::ClassMethods
|
8
|
+
|
9
|
+
KEYS = %w[_id name code accounts users jid created_at modified_at].freeze
|
10
|
+
VIEW_ID = "_design/System".freeze
|
11
|
+
VIEW_NAME = "System/memberships".freeze
|
12
|
+
|
13
|
+
attr_writer :size
|
14
|
+
|
15
|
+
after_save :update_views
|
16
|
+
after_destroy :update_views
|
17
|
+
|
18
|
+
property :name, String
|
19
|
+
property :code, String
|
20
|
+
property :accounts, [String], :default => []
|
21
|
+
property :users, [String], :default => []
|
22
|
+
property :jid, String
|
23
|
+
|
24
|
+
timestamps!
|
25
|
+
|
26
|
+
validates_uniqueness_of :name
|
27
|
+
validates_presence_of :name
|
28
|
+
validates_presence_of :code
|
29
|
+
validates_uniqueness_of :jid
|
30
|
+
validates_presence_of :jid
|
31
|
+
validate :compile_view
|
32
|
+
|
33
|
+
design do
|
34
|
+
view :by_name,
|
35
|
+
map: %q{
|
36
|
+
function(doc) {
|
37
|
+
if (doc.type != 'Service' || !doc.name) return;
|
38
|
+
emit(doc.name, doc);
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
view :by_jid,
|
43
|
+
map: %q{
|
44
|
+
function(doc) {
|
45
|
+
if (doc.type != 'Service' || !doc.jid) return;
|
46
|
+
emit(doc.jid, doc);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
view :by_account,
|
51
|
+
map: %q{
|
52
|
+
function(doc) {
|
53
|
+
if (doc.type != 'Service' || !doc.accounts) return;
|
54
|
+
doc.accounts.forEach(function(account) {
|
55
|
+
emit(account, doc);
|
56
|
+
});
|
57
|
+
}
|
58
|
+
},
|
59
|
+
reduce: '_count'
|
60
|
+
|
61
|
+
view :by_user,
|
62
|
+
map: %q{
|
63
|
+
function(doc) {
|
64
|
+
if (doc.type != 'Service' || !doc.users) return;
|
65
|
+
doc.users.forEach(function(jid) {
|
66
|
+
emit(jid, doc);
|
67
|
+
});
|
68
|
+
}
|
69
|
+
},
|
70
|
+
reduce: '_count'
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return true if this JID is allowed access to this service.
|
74
|
+
def user?(jid)
|
75
|
+
users.include?(jid.to_s.downcase)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Allow the user, specified by their JID, to access the members of this
|
79
|
+
# service. Adds the JID to the list and ensures the list stays sorted
|
80
|
+
# and unique.
|
81
|
+
def add_user(jid)
|
82
|
+
users << jid.to_s.downcase
|
83
|
+
users.sort!
|
84
|
+
users.uniq!
|
85
|
+
end
|
86
|
+
|
87
|
+
# Remove this user's permission to access this service.
|
88
|
+
def remove_user(jid)
|
89
|
+
users.delete(jid.to_s.downcase)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return the number of members in this service's view. This is faster
|
93
|
+
# than calling Service#members#size because this reduces the view,
|
94
|
+
# so all members aren't loaded from the database. The size is cached so
|
95
|
+
# subsequent calls to this method do not query the view.
|
96
|
+
def size
|
97
|
+
unless @size
|
98
|
+
rows = database.view(VIEW_NAME, reduce: true, key: [0, id])['rows'] rescue []
|
99
|
+
@size = rows.first ? rows.first['value'] : 0
|
100
|
+
end
|
101
|
+
@size
|
102
|
+
end
|
103
|
+
|
104
|
+
# Query the members view and return an Array of Hashes like this:
|
105
|
+
# [{name: 'www.wonderland.lit', os: 'linux'}]. The members are cached
|
106
|
+
# so subsequent calls to this method do not query the view.
|
107
|
+
def members
|
108
|
+
unless @members
|
109
|
+
rows = database.view(VIEW_NAME, reduce: false, key: [0, id])['rows'] rescue []
|
110
|
+
@members = rows.map {|row| row['value'] }
|
111
|
+
end
|
112
|
+
@members
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_result
|
116
|
+
to_hash.clone.keep_if {|k, v| KEYS.include?(k) }
|
117
|
+
.tap {|h| h['id'] = h.delete('_id') }
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.find_by_name(name)
|
121
|
+
first_from_view('by_name', name)
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.find_by_jid(jid)
|
125
|
+
first_from_view('by_jid', jid.to_s.downcase)
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.find_by_user(jid)
|
129
|
+
by_user.key(jid.to_s.downcase).to_a
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.find_all
|
133
|
+
sizes = find_sizes
|
134
|
+
by_name.map do |doc|
|
135
|
+
doc.size = sizes[doc.id] || 0
|
136
|
+
doc
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return a Hash of service ID to member count.
|
141
|
+
def self.find_sizes
|
142
|
+
{}.tap do |hash|
|
143
|
+
rows = database.view(VIEW_NAME, reduce: true, group: true, startkey: [0], endkey: [1])['rows'] rescue []
|
144
|
+
rows.each do |row|
|
145
|
+
hash[row['key'][1]] = row['value']
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def compile_view
|
153
|
+
VQL::Compiler.new.to_js(code)
|
154
|
+
rescue Exception => e
|
155
|
+
errors.add(:base, e.message)
|
156
|
+
end
|
157
|
+
|
158
|
+
def update_views
|
159
|
+
js = VQL::Compiler.new.to_full_js(self.class.by_name.to_a)
|
160
|
+
design = database.get(VIEW_ID) rescue nil
|
161
|
+
design ||= {'_id' => VIEW_ID, 'views' => {}}
|
162
|
+
design['views']['memberships'] = {map: js, reduce: '_count'}
|
163
|
+
database.save_doc(design)
|
164
|
+
# trigger view update, discard results
|
165
|
+
EM::HttpRequest.new("#{database.root}/#{VIEW_ID}/_view/memberships").get
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module CouchModels
|
6
|
+
class System < CouchRest::Model::Base
|
7
|
+
extend Storage::CouchDB::ClassMethods
|
8
|
+
|
9
|
+
KEYS = %w[_id ohai created_at modified_at].freeze
|
10
|
+
VIEW_NAME = "System/memberships".freeze
|
11
|
+
|
12
|
+
attr_writer :services
|
13
|
+
|
14
|
+
property :ohai, Hash
|
15
|
+
|
16
|
+
timestamps!
|
17
|
+
|
18
|
+
validates_presence_of :ohai
|
19
|
+
|
20
|
+
design do
|
21
|
+
# System docs are large so don't include them in the view. Use
|
22
|
+
# include_docs=true when querying the view, if the full document
|
23
|
+
# is needed.
|
24
|
+
view :by_name,
|
25
|
+
map: %q{
|
26
|
+
function(doc) {
|
27
|
+
if (doc.type != 'System') return;
|
28
|
+
emit(doc['_id'].replace('system:', ''), null);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
view :attributes,
|
33
|
+
map: %q{
|
34
|
+
function(doc) {
|
35
|
+
if (doc.type != 'System' || !doc.ohai) return;
|
36
|
+
Object.keys(doc.ohai).forEach(function(key) {
|
37
|
+
emit(key, null);
|
38
|
+
});
|
39
|
+
}
|
40
|
+
},
|
41
|
+
reduce: '_count'
|
42
|
+
end
|
43
|
+
|
44
|
+
def name
|
45
|
+
id.sub('system:', '')
|
46
|
+
end
|
47
|
+
|
48
|
+
# Query the members view and return the Service objects to which this
|
49
|
+
# System belongs. The services are cached so subsequent calls to this
|
50
|
+
# method do not query the view.
|
51
|
+
def services
|
52
|
+
unless @services
|
53
|
+
rows = database.view(VIEW_NAME, reduce: false, key: [1, name])['rows'] rescue []
|
54
|
+
ids = rows.map {|row| row['value'] }
|
55
|
+
@services = Service.all.keys(ids).to_a
|
56
|
+
end
|
57
|
+
@services
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return a Hash of unix user ID to an Array of JID's allowed to access
|
61
|
+
# that account. For example:
|
62
|
+
# {'apache' => ['alice@wonderland.lit'],
|
63
|
+
# 'root' => ['hatter@wonderland.lit]}
|
64
|
+
def permissions
|
65
|
+
{}.tap do |perms|
|
66
|
+
services.each do |service|
|
67
|
+
service.accounts.each do |unix_id|
|
68
|
+
jids = (perms[unix_id] ||= [])
|
69
|
+
jids << service.users
|
70
|
+
jids.flatten!
|
71
|
+
jids.sort!.uniq!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_result
|
78
|
+
to_hash.clone.keep_if {|k, v| KEYS.include?(k) }.tap do |h|
|
79
|
+
h['name'] = h.delete('_id').sub('system:', '')
|
80
|
+
h['services'] = services.map {|s| {id: s.id, jid: s.jid, name: s.name} }
|
81
|
+
h['permissions'] = permissions
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Send updated permissions to each system that belongs to the service
|
86
|
+
# (or belonged to it before the save).
|
87
|
+
def self.notify_members(stream, from, members)
|
88
|
+
return if members.empty?
|
89
|
+
names = members.map {|m| m['name'] }.uniq
|
90
|
+
systems = System.find_by_names(names)
|
91
|
+
nodes = systems.map do |system|
|
92
|
+
Blather::Stanza::Iq::Query.new(:set).tap do |result|
|
93
|
+
result.to = Blather::JID.new(system.name, from.domain)
|
94
|
+
result.query.content = system.to_result.to_json
|
95
|
+
result.query.namespace = 'http://getvines.com/protocol/systems'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
Throttle.new(stream).async_send(nodes)
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.find_attributes
|
102
|
+
view = attributes.reduce.group
|
103
|
+
view.rows.map {|row| row['key'] }
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.find_all
|
107
|
+
by_name.rows.map do |row|
|
108
|
+
{name: row['key']}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.find_by_name(name)
|
113
|
+
find("system:#{name.downcase}")
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return an Array of Systems with the given names. This method
|
117
|
+
# efficiently bulk loads systems and their services much more quickly
|
118
|
+
# than loading systems one by one. Note that the systems returned by
|
119
|
+
# this method do not have their ohai data loaded because it's expensive.
|
120
|
+
def self.find_by_names(names)
|
121
|
+
keys = names.map {|name| [1, name.downcase] }
|
122
|
+
rows = database.view(VIEW_NAME, reduce: false, keys: keys)['rows'] rescue []
|
123
|
+
ids = rows.map {|row| row['value'] }.uniq
|
124
|
+
services = Service.all.keys(ids).to_a
|
125
|
+
by_id = Hash[services.map {|s| [s.id, s] }]
|
126
|
+
by_name = Hash.new do |h, k|
|
127
|
+
h[k] = System.new(id: "system:#{k}").tap do |system|
|
128
|
+
system.services = []
|
129
|
+
end
|
130
|
+
end
|
131
|
+
rows.each do |row|
|
132
|
+
name = row['key'][1]
|
133
|
+
service = by_id[row['value']]
|
134
|
+
by_name[name].services << service
|
135
|
+
end
|
136
|
+
by_name.values
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module CouchModels
|
6
|
+
class Upload < CouchRest::Model::Base
|
7
|
+
extend Storage::CouchDB::ClassMethods
|
8
|
+
|
9
|
+
KEYS = %w[_id name size labels created_at modified_at].freeze
|
10
|
+
|
11
|
+
property :name, String
|
12
|
+
property :size, Integer
|
13
|
+
property :labels, [String], :default => []
|
14
|
+
|
15
|
+
timestamps!
|
16
|
+
|
17
|
+
validates_uniqueness_of :name
|
18
|
+
validates_presence_of :name
|
19
|
+
validates_numericality_of :size, only_integer: true, greater_than: -1
|
20
|
+
|
21
|
+
design do
|
22
|
+
view :by_name,
|
23
|
+
map: %q{
|
24
|
+
function(doc) {
|
25
|
+
if (doc.type != 'Upload' || !doc.name) return;
|
26
|
+
emit(doc.name, doc);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
view :by_label,
|
31
|
+
map: %q{
|
32
|
+
function(doc) {
|
33
|
+
if (doc.type != 'Upload' || !doc.labels) return;
|
34
|
+
doc.labels.forEach(function(label) {
|
35
|
+
emit(label, doc);
|
36
|
+
});
|
37
|
+
}
|
38
|
+
},
|
39
|
+
reduce: '_count'
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_result
|
43
|
+
to_hash.clone.keep_if {|k, v| KEYS.include?(k) }
|
44
|
+
.tap {|h| h['id'] = h.delete('_id') }
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.find_labels
|
48
|
+
view = by_label.reduce.group
|
49
|
+
view.rows.map {|row| {name: row['key'], size: row['value']} }
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.find_by_label(label)
|
53
|
+
by_label.key(label).map {|doc| doc.to_result }
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.find_by_name(name)
|
57
|
+
first_from_view('by_name', name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.find_all
|
61
|
+
by_name.map {|doc| doc.to_result }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
module Services
|
5
|
+
module CouchModels
|
6
|
+
class User < CouchRest::Model::Base
|
7
|
+
extend Storage::CouchDB::ClassMethods
|
8
|
+
|
9
|
+
KEYS = %w[_id name permissions system created_at modified_at].freeze
|
10
|
+
|
11
|
+
before_save :enforce_constraints
|
12
|
+
after_destroy :remove_references
|
13
|
+
|
14
|
+
property :name, String
|
15
|
+
property :password, String
|
16
|
+
property :roster, Hash, :default => {}
|
17
|
+
property :permissions, Hash, :default => {}
|
18
|
+
property :system, TrueClass, :default => false
|
19
|
+
|
20
|
+
timestamps!
|
21
|
+
|
22
|
+
validates_presence_of :password
|
23
|
+
|
24
|
+
design do
|
25
|
+
view :by_jid,
|
26
|
+
map: %q{
|
27
|
+
function(doc) {
|
28
|
+
if (doc.type != 'User') return;
|
29
|
+
emit(doc['_id'].replace('user:', ''), doc);
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
view :subscribers,
|
34
|
+
map: %q{
|
35
|
+
function(doc) {
|
36
|
+
if (doc.type != 'User' || !doc.roster) return;
|
37
|
+
Object.keys(doc.roster).forEach(function(jid) {
|
38
|
+
emit(doc['_id'].replace('user:', ''), jid);
|
39
|
+
});
|
40
|
+
}
|
41
|
+
},
|
42
|
+
reduce: '_count'
|
43
|
+
end
|
44
|
+
|
45
|
+
%w[systems services users files].each do |name|
|
46
|
+
define_method "manage_#{name}?" do
|
47
|
+
!!read_attribute('permissions')[name]
|
48
|
+
end
|
49
|
+
define_method "manage_#{name}=" do |value|
|
50
|
+
read_attribute('permissions')[name] = !!value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def permissions=(perms)
|
55
|
+
perms ||= {}
|
56
|
+
self.manage_systems = perms['systems']
|
57
|
+
self.manage_services = perms['services']
|
58
|
+
self.manage_files = perms['files']
|
59
|
+
self.manage_users = perms['users']
|
60
|
+
end
|
61
|
+
|
62
|
+
def password=(desired)
|
63
|
+
desired = (desired || '').strip
|
64
|
+
raise 'password too short' if desired.size < (system ? 128 : 8)
|
65
|
+
write_attribute('password', BCrypt::Password.create(desired))
|
66
|
+
end
|
67
|
+
|
68
|
+
def change_password(previous, desired)
|
69
|
+
hash = BCrypt::Password.new(password) rescue nil
|
70
|
+
raise 'password failure' unless hash && hash == previous
|
71
|
+
self.password = desired
|
72
|
+
end
|
73
|
+
|
74
|
+
def jid
|
75
|
+
id ? id.sub('user:', '') : nil
|
76
|
+
end
|
77
|
+
|
78
|
+
# Query the Service/by_user view and return the Service objects to which
|
79
|
+
# this User has access. The services are cached so subsequent calls to
|
80
|
+
# this method do not query the view.
|
81
|
+
def services
|
82
|
+
@services ||= Service.find_by_user(jid)
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_result
|
86
|
+
to_hash.clone.keep_if {|k, v| KEYS.include?(k) }.tap do |h|
|
87
|
+
h['jid'] = h.delete('_id').sub('user:', '')
|
88
|
+
h['services'] = h['system'] ? []: services.map {|s| s.id }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.find_all
|
93
|
+
by_jid.map do |doc|
|
94
|
+
{jid: doc.jid, name: doc.name, system: doc.system}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.find_by_jid(jid)
|
99
|
+
first_from_view('by_jid', jid.to_s.downcase)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# System users are not allowed to manage any other objects.
|
105
|
+
def enforce_constraints
|
106
|
+
if system
|
107
|
+
write_attribute('name', nil)
|
108
|
+
write_attribute('permissions', {})
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# After the User document is deleted, remove references to the user from
|
113
|
+
# related documents (rosters, services, vcards and XML fragments).
|
114
|
+
def remove_references
|
115
|
+
if card = Vcard.find("vcard:#{jid}")
|
116
|
+
card.destroy
|
117
|
+
end
|
118
|
+
|
119
|
+
Fragment.by_jid.key(jid).each do |doc|
|
120
|
+
doc.destroy
|
121
|
+
end
|
122
|
+
|
123
|
+
Service.find_by_user(jid).each do |service|
|
124
|
+
service.remove_user(jid)
|
125
|
+
service.save
|
126
|
+
end
|
127
|
+
|
128
|
+
jids = User.subscribers.key(jid).rows.map {|row| row['value'] }
|
129
|
+
User.by_jid.keys(jids).each do |subscriber|
|
130
|
+
subscriber.roster.delete(jid)
|
131
|
+
subscriber.save
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|