simple_crowd 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,26 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ doc
20
+ pkg
21
+ .idea
22
+ .yardoc
23
+ .bundle
24
+
25
+ ## PROJECT::SPECIFIC
26
+ test/crowd_config.yml
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in simple_crowd.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ Simple Crowd (SOAP Client for Atlassian Crowd)
2
+ =====
3
+
4
+ A basic Atlassian Crowd Client based on their SOAP API.
5
+ All the standard API calls have been implemented to my knowledge as of Crowd 2.0
6
+
7
+ #### Some disclaimers:
8
+
9
+
10
+ - This gem was created before Atlassian created a REST API for Crowd which is why we implemented it in SOAP.
11
+ - This gem was created for Atlassian Crowd 2.0, but it should work with 2.2.
12
+ - We renamed "principal" to "user" in all the API calls for our convenience as this gem was initially created for internal use only.
13
+ - This gem is in use in production and has been fully tested, but we provide no guarantee or support if it does not work for you.
14
+
15
+ #### Service URL Options:
16
+
17
+ - :service_url => "http://localhost:8095/crowd/",
18
+ - :app_name => "crowd",
19
+ - :app_password => ""
20
+
21
+ Ex. `SimpleCrowd::Client.new(:service_url...)`
22
+
23
+ #### Some example calls:
24
+
25
+ - `client.authenticate_user("test@test.com", "testpassword")`
26
+ - returns token if authenticated or nil if not
27
+ - `client.find_user_with_attributes_by_name("test@test.com")`
28
+ - returns user with all custom attributes
29
+ - NOTE: find_user_by_name does not return custom attributes
30
+ - `client.is_valid_user_token?("SOMELARGECROWDTOKEN")`
31
+ - returns true or false
32
+
33
+
34
+ TODO
35
+ --
36
+
37
+ - Add support for arrays in custom attribute values
38
+ - Add exception/error types instead of throwing Simple::CrowdError for all errors
39
+ - Add support for group custom attributes (as of Crowd 2.1 or 2.2)
40
+ - Add more automated tests for groups and validation factors
41
+
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'bundler'
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ desc 'Run tests for InheritedResources.'
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = true
11
+ end
12
+
13
+ desc 'Generate documentation for InheritedResources.'
14
+ Rake::RDocTask.new(:rdoc) do |rdoc|
15
+ rdoc.rdoc_dir = 'rdoc'
16
+ rdoc.title = 'InheritedResources'
17
+ rdoc.options << '--line-numbers' << '--inline-source'
18
+ rdoc.rdoc_files.include('README.rdoc')
19
+ rdoc.rdoc_files.include('lib/**/*.rb')
20
+ end
21
+
@@ -0,0 +1,41 @@
1
+ require 'savon'
2
+ require 'hashie'
3
+ require 'forwardable'
4
+ require 'simple_crowd/crowd_entity'
5
+ require 'simple_crowd/crowd_error'
6
+ require 'simple_crowd/user'
7
+ require 'simple_crowd/group'
8
+ require 'simple_crowd/client'
9
+ require 'simple_crowd/mappers/soap_attributes'
10
+ Dir['simple_crowd/mappers/*.rb'].each {|file| require File.basename(file, File.extname(file)) }
11
+
12
+ module SimpleCrowd
13
+ class << self
14
+ def config &config_block
15
+ config_block.call(options)
16
+ end
17
+ # SimpleCrowd default options
18
+ def options
19
+ @options ||= {
20
+ :service_url => "http://localhost:8095/crowd/",
21
+ :app_name => "crowd",
22
+ :app_password => ""
23
+ }
24
+ end
25
+ def soap_options base_options = self.options
26
+ @soap_options ||= base_options.merge({
27
+ :service_ns => "urn:SecurityServer",
28
+ :service_namespaces => {
29
+ 'xmlns:auth' => 'http://authentication.integration.crowd.atlassian.com',
30
+ 'xmlns:ex' => 'http://exception.integration.crowd.atlassian.com',
31
+ 'xmlns:int' => 'http://soap.integration.crowd.atlassian.com',
32
+ 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
33
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
34
+ }
35
+ })
36
+ @soap_options.merge!({:service_url => base_options[:service_url] + 'services/SecurityServer'})
37
+ end
38
+ end
39
+ end
40
+
41
+
@@ -0,0 +1,295 @@
1
+ module SimpleCrowd
2
+ class Client
3
+ def initialize options = {}
4
+ @options = SimpleCrowd.soap_options SimpleCrowd.options.merge(options)
5
+
6
+ # TODO: Fix error handling
7
+ # Errors do not contained Exception info so we'll handle the errors ourselves
8
+ # Savon::Response.raise_errors = false
9
+ # @client = Savon::Client.new @options[:service_url]
10
+ yield(@options) if block_given?
11
+ end
12
+ def app_token
13
+ @app_token ||= authenticate_application
14
+ end
15
+ attr_writer :app_token
16
+
17
+ def get_cookie_info
18
+ simple_soap_call :get_cookie_info
19
+ end
20
+
21
+ def get_granted_authorities
22
+ groups = simple_soap_call :get_granted_authorities
23
+ groups[:string] unless groups.nil?
24
+ end
25
+
26
+ def authenticate_application(name = @options[:app_name], password = @options[:app_password])
27
+ response = client.authenticate_application! do |soap|
28
+ prepare soap
29
+ soap.body = {:in0 => {
30
+ 'auth:name' => name,
31
+ 'auth:credential' => {'auth:credential' => password}
32
+ }.merge(no_validation_factors)}
33
+ end
34
+ raise CrowdError.new(response.soap_fault, response.to_hash[:fault]) if response.soap_fault?
35
+ response.to_hash[:authenticate_application_response][:out][:token]
36
+ end
37
+
38
+ # Authenticate user by name/pass and retrieve login token
39
+ # @return [String] user token
40
+ def authenticate_user name, password, factors = nil
41
+ if factors
42
+ factors = prepare_validation_factors(factors)
43
+ simple_soap_call :authenticate_principal, {'auth:application' => @options[:app_name], 'auth:name' => name,
44
+ 'auth:credential' => {'auth:credential' => password},
45
+ 'auth:validationFactors' => factors}
46
+ else
47
+ simple_soap_call :authenticate_principal_simple, name, password
48
+ end
49
+ end
50
+
51
+ def create_user_token name
52
+ simple_soap_call :create_principal_token, name, nil
53
+ end
54
+
55
+ # Invalidate an existing user token (log out)
56
+ # NOTE: call will return true even if token is invalid
57
+ # @return [Boolean] success (does not guarantee valid token)
58
+ def invalidate_user_token token
59
+ simple_soap_call :invalidate_principal_token, token do |res|
60
+ !res.soap_fault? && res.to_hash.key?(:invalidate_principal_token_response)
61
+ end
62
+ end
63
+
64
+ def is_valid_user_token? token, factors = nil
65
+ factors = prepare_validation_factors(factors) unless factors.nil?
66
+ simple_soap_call :is_valid_principal_token, token, factors
67
+ end
68
+
69
+ def is_cache_enabled?
70
+ simple_soap_call :is_cache_enabled
71
+ end
72
+
73
+ def is_group_member? group, user
74
+ simple_soap_call :is_group_member, group, user
75
+ end
76
+
77
+ def find_group_by_name name
78
+ SimpleCrowd::Group.parse_from :soap, simple_soap_call(:find_group_by_name, name)
79
+ end
80
+
81
+ def find_all_group_names
82
+ (simple_soap_call :find_all_group_names)[:string]
83
+ end
84
+
85
+ def update_group group, description, active
86
+ simple_soap_call :update_group, group, description, active do |res|
87
+ !res.soap_fault? && res.to_hash.key?(:update_group_response)
88
+ end
89
+ end
90
+
91
+ def add_user_to_group user, group
92
+ simple_soap_call :add_principal_to_group, user, group do |res|
93
+ !res.soap_fault? && res.to_hash.key?(:add_principal_to_group_response)
94
+ end
95
+ end
96
+
97
+ def remove_user_from_group user, group
98
+ simple_soap_call :remove_principal_from_group, user, group do |res|
99
+ !res.soap_fault? && res.to_hash.key?(:remove_principal_from_group_response)
100
+ end
101
+ end
102
+
103
+ def reset_user_password name
104
+ simple_soap_call :reset_principal_credential, name do |res|
105
+ !res.soap_fault? && res.to_hash.key?(:reset_principal_credential_response)
106
+ end
107
+ end
108
+
109
+ def find_all_user_names
110
+ (simple_soap_call :find_all_principal_names)[:string]
111
+ end
112
+
113
+ def find_user_by_name name
114
+ SimpleCrowd::User.parse_from :soap, simple_soap_call(:find_principal_by_name, name) rescue nil
115
+ end
116
+
117
+ def find_user_with_attributes_by_name name
118
+ SimpleCrowd::User.parse_from :soap, simple_soap_call(:find_principal_with_attributes_by_name, name) rescue nil
119
+ end
120
+
121
+ def find_user_by_token token
122
+ SimpleCrowd::User.parse_from :soap, simple_soap_call(:find_principal_by_token, token) rescue nil
123
+ end
124
+
125
+ def find_username_by_token token
126
+ user = find_user_by_token token
127
+ user && user[:username]
128
+ end
129
+
130
+ # Exact email match
131
+ def find_user_by_email email
132
+ search_users_by_email(email).find{|u| u.email == email}
133
+ end
134
+
135
+ # Partial email match
136
+ def search_users_by_email email
137
+ search_users({'principal.email' => email})
138
+ end
139
+
140
+ def search_users restrictions
141
+ soap_restrictions = prepare_search_restrictions restrictions
142
+ users = simple_soap_call :search_principals, soap_restrictions rescue []
143
+ return [] if users.nil? || users[:soap_principal].nil?
144
+ users = users[:soap_principal].is_a?(Array) ? users[:soap_principal] : [users[:soap_principal]]
145
+ users.map{|u| SimpleCrowd::User.parse_from :soap, u}
146
+ end
147
+
148
+ def add_user user, credential
149
+ return if user.nil? || credential.nil?
150
+ [:email, :first_name, :last_name].each do |k|
151
+ user.send(:"#{k}=", "") if user.send(k).nil?
152
+ end
153
+ soap_user = user.map_to :soap
154
+ # We don't use these attributes when creating
155
+ soap_user.delete(:id)
156
+ soap_user.delete(:directory_id)
157
+ # Add blank attributes if missing
158
+
159
+ # Declare require namespaces
160
+ soap_user = soap_user.inject({}) {|hash, (k, v)| hash["int:#{k}"] = v;hash}
161
+ SimpleCrowd::User.parse_from :soap, simple_soap_call(:add_principal, soap_user, {'auth:credential' => credential, 'auth:encryptedCredential' => false})
162
+ end
163
+
164
+ def remove_user name
165
+ simple_soap_call :remove_principal, name do |res|
166
+ !res.soap_fault? && res.to_hash.key?(:remove_principal_response)
167
+ end
168
+ end
169
+
170
+ def update_user_credential user, credential, encrypted = false
171
+ simple_soap_call :update_principal_credential, user,
172
+ {'auth:credential' => credential, 'auth:encryptedCredential' => encrypted} do |res|
173
+ !res.soap_fault? && res.to_hash.key?(:update_principal_credential_response)
174
+ end
175
+ end
176
+
177
+ # Only supports single value attributes
178
+ # TODO: Allow value arrays
179
+ # @param user [String] name of user to update
180
+ # @param name [String] of attribute to update
181
+ # @param value [String] of attribute to update
182
+ def update_user_attribute user, name, value
183
+ return unless (name.is_a?(String) || name.is_a?(Symbol)) && (value.is_a?(String) || value.is_a?(Array))
184
+ soap_attr = SimpleCrowd::Mappers::SoapAttributes.produce({name => value})
185
+ simple_soap_call :update_principal_attribute, user, soap_attr['int:SOAPAttribute'][0] do |res|
186
+ !res.soap_fault? && res.to_hash.key?(:update_principal_attribute_response)
187
+ end
188
+ end
189
+ alias_method :add_user_attribute, :update_user_attribute
190
+
191
+ # @param user [SimpleCrowd::User] dirty user to update
192
+ def update_user user
193
+ return unless user.dirty?
194
+ # Exclude non-attribute properties (only attributes can be updated in crowd)
195
+ attrs_to_update = user.dirty_attributes
196
+ return if attrs_to_update.empty?
197
+
198
+ attrs_to_update.each do |a|
199
+ prop = SimpleCrowd::User.property_by_name a
200
+ soap_prop = prop.maps[:soap].nil? ? prop : prop.maps[:soap]
201
+ self.update_user_attribute user.username, soap_prop, user.send(a)
202
+ end
203
+ end
204
+
205
+ private
206
+
207
+ # Simplify the duplicated soap calls across methods
208
+ # @param [Symbol] action the soap action to call
209
+ # @param data the list of args to pass to the server as "in" args (in1, in2, etc.)
210
+ def simple_soap_call action, *data
211
+ # Take each arg and assign it to "in" keys for SOAP call starting with in1 (in0 is app token)
212
+ soap_args = data.inject({}){|hash, arg| hash[:"in#{hash.length + 1}"] = arg; hash }
213
+ # Ordered "in" keys ex. in1, in2, etc. for SOAP ordering
214
+ in_keys = soap_args.length ? (1..soap_args.length).collect {|v| :"in#{v}" } : []
215
+ # Make the SOAP call to the dynamic action
216
+ response = with_app_token do
217
+ client.send :"#{action}!" do |soap|
218
+ prepare soap
219
+ # Pass in all the args as "in" vars
220
+ soap.body = {:in0 => hash_authenticated_token}.merge(soap_args).merge({:order! => [:in0, *in_keys]})
221
+ end
222
+ end
223
+ # If a block is given then call it and pass in the response object, otherwise get the default out value
224
+ block_given? ? yield(response) : response.to_hash[:"#{action}_response"][:out]
225
+ end
226
+
227
+ def with_app_token retries = 1, &block
228
+ begin
229
+ Savon::Response.raise_errors = false
230
+ response = block.call
231
+ raise CrowdError.new(response.soap_fault, response.to_hash[:fault]) if response.soap_fault?
232
+ return response
233
+ rescue CrowdError => e
234
+ if retries && e.type?(:invalid_authorization_token_exception)
235
+ # Clear token to force a refresh
236
+ self.app_token = nil
237
+ retries -= 1
238
+ retry
239
+ end
240
+ raise
241
+ ensure
242
+ Savon::Response.raise_errors = true
243
+ end
244
+ end
245
+
246
+ # Generate new client on every request (Savon bug?)
247
+ def client
248
+ Savon::Client.new @options[:service_url]
249
+ end
250
+
251
+ # Setup soap object for request
252
+ def prepare soap
253
+ soap.namespace = @options[:service_ns]
254
+ soap.namespaces.merge! @options[:service_namespaces]
255
+ end
256
+
257
+ # Take Crowd SOAP attribute format and return a simple ruby hash
258
+ # @param attributes the soap attributes array
259
+ def process_soap_attributes attributes
260
+ soap = attributes[:soap_attribute]
261
+ (soap && soap.inject({}) {|hash, attr| hash[attr[:name].to_sym] = attr[:values][:string]; hash }) || {}
262
+ end
263
+
264
+ def map_group_hash group
265
+ attributes = process_soap_attributes group[:attributes]
266
+ supported_keys = attributes.keys & SimpleCrowd::Group.mapped_properties(:soap)
267
+ group = group.merge attributes.inject({}) {|map, (k, v)| map[k] = v if supported_keys.include? k; map}
268
+ group[:attributes] = attributes.inject({}) {|map, (k, v)| map[k] = v unless supported_keys.include? k; map}
269
+ group.delete :attributes if group[:attributes].empty?
270
+ SimpleCrowd::Group.new group
271
+ end
272
+
273
+ def prepare_validation_factors factors
274
+ {'auth:validationFactor' =>
275
+ factors.inject([]) {|arr, factor| arr << {'auth:name' => factor[0], 'auth:value' => factor[1]} }
276
+ }
277
+ end
278
+
279
+ def prepare_search_restrictions restrictions
280
+ {'int:searchRestriction' =>
281
+ restrictions.inject([]) {|arr, restrict| arr << {'int:name' => restrict[0], 'int:value' => restrict[1]}}
282
+ }
283
+ end
284
+
285
+ def hash_authenticated_token name = @options[:app_name], token = nil
286
+ token ||= app_token
287
+ {'auth:name' => name, 'auth:token' => token}
288
+ end
289
+
290
+ def no_validation_factors
291
+ {'auth:validationFactors' => {}, :attributes! => {'auth:validationFactors' => {'xsi:nil' => true}}}
292
+ end
293
+
294
+ end
295
+ end
@@ -0,0 +1,199 @@
1
+ module SimpleCrowd
2
+ class ExtendedProperty < Hashie::Dash
3
+ property :name
4
+ property :default
5
+ property :attribute
6
+ property :immutable
7
+ property :maps, :default => {}
8
+ property :mappers, :default => {}
9
+ def immutable?; @immutable; end
10
+ def is_attribute?; @attribute end
11
+ end
12
+ class CrowdEntity < Hashie::Mash
13
+ def initialize(data = {}, notused = nil)
14
+ self.class.properties.each do |prop|
15
+ self.send("#{prop.name}=", self.class.defaults[prop.name.to_sym])
16
+ end
17
+ attrs = data[:attributes].nil? ? [] : data[:attributes].keys
18
+ data.merge! data[:attributes] unless attrs.empty?
19
+ data.delete :attributes
20
+ data.each_pair do |att, value|
21
+ #ruby_att = att_to_ruby att
22
+ ruby_att = att
23
+ real_att = real_key_for ruby_att
24
+ (@attributes ||= []) << real_att if attrs.include?(att)
25
+ prop = self.class.property_by_name(real_att)
26
+ self.send("#{real_att}=", value) unless prop.nil?
27
+ self[real_att] = value if prop.nil?
28
+ end
29
+ # We just initialized the attributes so clear the dirty status
30
+ dirty_properties.clear
31
+ end
32
+ def self.property(property_name, options = {})
33
+ property_name = property_name.to_sym
34
+
35
+ maps = options.inject({}) {|map, (key, value)| map[$1.to_sym] = value.to_sym if key.to_s =~ /^map_(.*)$/; map }
36
+ mappers = options.inject({}) {|map, (key, value)| map[$1.to_sym] = value if key.to_s =~ /^mapper_(.*)$/; map }
37
+ options.reject! {|key, val| key.to_s =~ /^map_(.*)$/ || key.to_s =~ /^mapper_(.*)$/ }
38
+ (@properties ||= []) << ExtendedProperty.new({:name => property_name, :maps => maps, :mappers => mappers}.merge(options))
39
+ (@attributes ||= []) << property_name if options[:attribute]
40
+
41
+ class_eval <<-RUBY
42
+ def #{property_name}
43
+ self[:#{property_name}]
44
+ end
45
+ def #{property_name}=(val)
46
+ (dirty_properties << :#{property_name}).uniq! unless val == self[:#{property_name}]
47
+ self[:#{property_name}] = val
48
+ end
49
+ RUBY
50
+
51
+ maps.each_value do |v|
52
+ alias_method v, property_name
53
+ alias_method :"#{v}=", :"#{property_name}="
54
+ end
55
+ end
56
+
57
+ def self.properties
58
+ properties = []
59
+ ancestors.each do |elder|
60
+ if elder.instance_variable_defined?("@properties")
61
+ properties << elder.instance_variable_get("@properties")
62
+ end
63
+ end
64
+
65
+ properties.reverse.flatten
66
+ end
67
+
68
+ def self.property_by_name(property_name)
69
+ property_name = property_name.to_sym
70
+ properties.detect {|p| p.name == property_name || p.maps.value?(property_name)}
71
+ end
72
+
73
+ def self.properties_by_name(property_name)
74
+ property_name = property_name.to_sym
75
+ properties.select {|p| p.name == property_name || p.maps.value?(property_name)}
76
+ end
77
+
78
+ def self.property?(prop)
79
+ !property_by_name(prop.to_sym).nil?
80
+ end
81
+
82
+ def self.defaults
83
+ properties.inject({}) {|hash, prop| hash[prop.name] = prop['default'] unless prop['default'].nil?; hash }
84
+ end
85
+
86
+ def self.attribute_mappers hash = nil
87
+ @attribute_mappers ||= {:soap => SimpleCrowd::Mappers::SoapAttributes}
88
+ unless hash.nil?
89
+ @attribute_mappers.merge! hash if hash.is_a? Hash
90
+ end
91
+ @attribute_mappers
92
+ end
93
+
94
+ def self.map_for type
95
+ type = type.to_sym
96
+ properties.inject({}) {|hash, prop| hash[prop.name] = prop.maps[type] unless prop.maps[type].nil?; hash }
97
+ end
98
+
99
+ def self.map_to type, entity
100
+ map = map_for type
101
+ attrs = {}
102
+ out = entity.inject({}) do |hash, (key, val)|
103
+ key = key.to_sym
104
+ catch(:skip_prop) do
105
+ unless val.nil?
106
+ mapped_key = map[key].nil? ? key : map[key]
107
+ prop = property_by_name key
108
+ if prop.nil?
109
+ attrs[mapped_key] = val
110
+ throw :skip_prop
111
+ end
112
+ mapper = prop.mappers[type]
113
+ #val = val.inject({}) {|attrs, (k, v)| attrs[property_by_name(k).maps[type]]= v unless v.nil?; attrs} if key == :attributes
114
+ val = mapper.produce val unless mapper.nil?
115
+ if prop.attribute || entity.attributes_keys.include?(key)
116
+ attrs[mapped_key] = val
117
+ else
118
+ hash[mapped_key] = val
119
+ end
120
+ end
121
+ end
122
+ hash
123
+ end
124
+ out[:attributes] = attribute_mappers[type].produce attrs
125
+ out
126
+ end
127
+
128
+ def self.parse_from type, entity
129
+ entity[:attributes] = attribute_mappers[type].parse entity[:attributes]
130
+ parsed_entity = entity.inject({}) do |hash, (key, val)|
131
+ prop = property_by_name key
132
+ unless prop.nil?
133
+ mapper = prop.mappers[type]
134
+ val = mapper.parse val unless mapper.nil?
135
+ end
136
+ hash[key] = val
137
+ hash
138
+ end
139
+ self.new(parsed_entity)
140
+ end
141
+
142
+ def map_to type
143
+ self.class.map_to type, self
144
+ end
145
+
146
+ def attributes_keys
147
+ keys = []
148
+ self.class.ancestors.each do |elder|
149
+ if elder.instance_variable_defined?("@attributes")
150
+ keys << elder.instance_variable_get("@attributes")
151
+ end
152
+ end
153
+ keys << @attributes unless @attributes.nil?
154
+ keys.flatten.uniq
155
+ end
156
+
157
+ def attributes
158
+ self.inject({}) {|hash, (k, v)| hash[k] = v if attributes_keys.include?(k.to_sym); hash}
159
+ end
160
+
161
+ def dirty_properties
162
+ @dirty_properties ||= Array.new
163
+ end
164
+
165
+ def dirty_attributes
166
+ dirty_properties & attributes_keys
167
+ end
168
+
169
+ def dirty? prop = nil
170
+ prop.nil? ? !@dirty_properties.empty? : @dirty_properties.include?(prop)
171
+ end
172
+
173
+ def clean
174
+ @dirty_properties = Array.new
175
+ end
176
+
177
+ def update_with attrs
178
+ current_keys = attributes_keys
179
+ attrs.each_pair {|k, v| self.send(:"#{k}=", v) if current_keys.include?(k.to_sym) && v != self.send(k.to_sym)}
180
+ end
181
+
182
+ def []= key, val
183
+ prop = self.class.property_by_name key
184
+ (@attributes ||= []) << key if prop.nil?
185
+ super
186
+ end
187
+
188
+ private
189
+ def self.real_key_for att
190
+ p = property_by_name att
191
+ p.nil? ? att : p.name
192
+ end
193
+ def self.att_to_ruby att
194
+ att.to_s =~ /^[a-z]*([A-Z][^A-Z]*)*$/ ? att.to_s.underscore.to_sym : att.to_sym
195
+ end
196
+ def real_key_for att; self.class.real_key_for att; end
197
+ def att_to_ruby att; self.class.att_to_ruby att; end
198
+ end
199
+ end