vines-services 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|