How to use .tap method to build associated record? - ruby

I have an omniauth authentication model I'm building that's associated to a user.
aka user has many authentications.
I wish to build up key-value pairs of this authentication models using tap because twitter provides a secret key while facebook does not.
So if I have this, I want to accomplish the following conditional statement using the .tap method instead.
class User < ActiveRecord::Base
def apply_omniauth(omni)
if omni['credentials']['secret']
self.authentications.build(:provider => omni['provider'],
:uid => omni['uid'],
:token => omni['credentials']['token'],
:token_secret => omni['credentials']['secret']
else
self.authentications.build(:provider => omni['provider'],
:uid => omni['uid'],
:token => omni['credentials']['token']
end
end
end
UPDATE:
I'm trying it this way. Does this accomplish the same as the above?
self.authentications.build.tap do |auth|
auth[:provider] = omni['provider'] if omni['provider']
auth[:uid] = omni['uid'] if omni['uid']
auth[:token] = omni['credentials']['token'] if omni['credentials']['token']
auth[:token_secret] = omni['credentials']['secret'] if omni['credentials']['secret']
end

I think you could simply do (self is obsolete here):
authentications.build(:provider => omni['provider'],
:uid => omni['uid'],
:token => omni['credentials']['token'],
:token_secret => omni['credentials']['secret'])
If any key is missing, this will simply assign nil value. Unless you have some custom logic for setting those attributes, there is no difference between assigning nil or not assigning anything for a new records.

Related

Chef delay attribute assignment via data bag

So i have a bit of a pickle.
I have an encrypted data bag to store LDAP passwords. In my node run list, one of my recipes installs the secret key onto my client machine.
In my problematic cookbook, i have a helper (in /libraries) that pulls data from AD (using LDAP). Problem is, i can't find a way to delay the assignment of my node attribute after initial compile phase.
Take this line of code as example :
node.override['yp_chefserver']['osAdminUser'] = node['yp_chefserver']['osAdminUser'] + get_sam("#{data_bag_item('yp_chefserver', 'ldap', IO.read('/etc/chef/secret/yp_chefserver'))['ldap_password']}")
Im trying to override an attribute by adding an array returned by my helper function "get_sam" which returns an array, but it needs to run AFTER the compile phase since the file "/etc/chef/secret/yp_chefserver" doesnt exist before the convergence of my runlist.
So my question : Is there a way to assign node attributes via data_bag_items during the execution phase?
Some things i've tried :
ruby_block 'attribution' do
only_if { File.exist?('/etc/chef/secret/yp_chefserver')}
block do
node.override['yp_chefserver']['osAdminUser'] = node['yp_chefserver']['osAdminUser'] + get_sam("#{data_bag_item('yp_chefserver', 'ldap', IO.read('/etc/chef/secret/yp_chefserver'))['ldap_password']}")
Chef::Log.warn("content of osAdminUser : #{node['yp_chefserver']['osAdminUser']}")
end
end
This doesn't work because the custom resource ruby_block doesn't have the method "data_bag_item". I've tried using lazy attributes in my "chef_server" custom resource, but same problem.
I also tried having the attribution done directly in my helper module, but since the helper module compiles before the exec phase, the file doesn't exist when it assigns the variable.
Here is the helper function in question should anyone wonder, it pulls the SamAccountName from LDAP to assign admin users to my chef server. :
module YpChefserver
module LDAP
require 'net-ldap'
#ldap
def get_ldap(ldap_password)
if #ldap.nil?
#ldap = Net::LDAP.new :host => "ADSERVER",
:port => 389,
:auth => {
:method => :simple,
:username => "CN=USERNAME,OU=East Service Accounts,OU=System Accounts,DC=ad,DC=ypg,DC=com",
:password => "#{ldap_password}"
}
end
#ldap
end
def get_ldap_users(ldap_password)
filter = Net::LDAP::Filter.eq("cn", "DevOps")
treebase = "dc=ad, dc=ypg, dc=com"
get_ldap(ldap_password).search(:base => treebase, :filter => filter) do |entry|
#puts "DN: #{entry.dn}"
entry.each do |attribute, values|
return values if attribute == :member
end
end
end
def get_sam(ldap_password)
samacc = Array.new
get_ldap_users(ldap_password).entries.each{ |elem|
y = elem.to_s.split(/[,=]/)
filter = Net::LDAP::Filter.eq("cn", y[1])
treebase = "OU=Support Users and Groups,OU=CGI Support,DC=ad,DC=ypg,DC=com"
get_ldap(ldap_password).search(:base => treebase, :filter => filter, :attributes => "SamAccountName") do |entry|
samacc << entry.samaccountname
end
}
return samacc
end
end
end
Turns out you can actually call it inside a ruby block, just by using the actual Chef call instead of the resource name, as follow :
ruby_block 'attributes' do
only_if {File.exist?('/etc/chef/secret/yp_chefserver')}
block do
dtbg = Chef::EncryptedDataBagItem.load('yp_chefserver','ldap',"IO.read('/etc/chef/secret/yp_chefserver')")
end
end
Leaving this here for those who might need it
EDIT :
Here is final function using the code mentionned above to pull accounts from AD, using encrypted data bags to provide the password and to then pass those results to my node attributes, all during the execution phase :
ruby_block 'attributes' do
extend YpChefserver::LDAP
only_if {File.exist?('/etc/chef/secret/yp_chefserver')}
block do
# Chef::Config[:encrypted_data_bag_secret] = '/etc/chef/secret/yp_chefserver'
dtbg = Chef::EncryptedDataBagItem.load('yp_chefserver','ldap')
node.override['yp_chefserver']['ldap_pw'] = dtbg['ldap_password']
userarray = Array.new
userarray.push("#{node['yp_chefserver']['osAdminUser']}")
get_sam("#{node['yp_chefserver']['ldap_pw']}").each { |i| userarray.push(i[0]) }
node.override['yp_chefserver']['authorized_users'] = userarray
node.override['yp_chefserver']['local_admin_pw'] = dtbg['local_admin_pw']
end
end

Cannot add new value to MongoDB's BSON field using Ruby

I have a document with the field admins and am looking to add new users into this field. The value for these new users is a simple number string.
def modify_admin(identity, doc)
ip_addr = "127.0.0.1:27017"
client = Mongo::Client.new([ip_addr], :database => "camp")
if doc[0] == 'r'
doc = doc[2..-1]
client[:inventory].update_one({"name": doc}, {$push => {"admins" => identity}})
client.close
end
The collection I'm trying to add is in this line: client[:inventory].update_one({"name": doc}, {$push => {"admins" => identity}}),
However I am running into the error NilClass instances are not allowed as keys in a BSON document. (BSON::InvalidKey).
I have tried different syntax for the $push method but nothing seems to work.
My document structure is as follows, I'm using symbols as the field value.
document = {:name => build_array[1], :owner => identity, :admins => identity}
How can I add new values to the :owner field using Ruby?
$push in ruby usually means global variable. So, all you need is to wrap $push operation into parentheses:
- client[:inventory].update_one({"name": doc}, {$push => {"admins" => identity}})
+ client[:inventory].update_one({"name": doc}, {"$push" => {"admins" => identity}})
And you should be fine

Adding element to postgres array field fails while replacing the whole array works

I have an User model which has an array of roles.
From my schema.db:
create_table "users", force: true do |t|
t.string "roles", array: true
My model looks like this:
class User < ActiveRecord::Base
ROLES = %w(superadmin sysadmin secretary)
validate :allowed_roles
after_initialize :initialize_roles, if: :new_record?
private
def allowed_roles
roles.each do |role|
errors.add(:roles, :invalid) unless ROLES.include?(role)
end
end
def initialize_roles
write_attribute(:roles, []) if read_attribute(:roles).blank?
end
Problem is when I try to add another role from console like user.roles << "new_role" then user.save! says true and asking user.roles gives me my wanted output. But when I ask User.find(user_id).roles then I get the previous state without "new_role" in it.
For ex.
user.roles
=> ["superadmin"]
user.roles << "secretary"
=> ["superadmin", "secretary"]
user.save!
=> true
user.roles
=> ["superadmin", "secretary"]
User.find(<user_id>).roles
=> ["superadmin"]
When replacing the whole array, it works as I want:
user.roles
=> ["superadmin"]
user.roles = ["superadmin", "secretary"]
user.save!
=> true
user.roles
=> ["superadmin", "secretary"]
User.find(<user_id>).roles
=> ["superadmin", "secretary"]
I'm using rails 4 and postgresql, roles are for cancancan gem.
Changing other fields like user.name for ex works like expected. I made quite a lot of digging in google, but no help.
Active Record tracks which columns have changed and only saves these to the database. This change tracking works by hooking onto the setter methods - mutating an object inplace isn't detected. For example
user.roles << "superuser"
wouldn't be detected as a change.
There are 2 ways around this. One is never to change any Active Record object attribute in place. In your case this would mean the slight clumsier
user.roles += ["superuser"]
If you can't/won't do this then you must tell Active Record what you have done, for example
user.roles.gsub!(...)
user.roles_will_change!
lets Active Record know that the roles attribute has changed and needs to be updated.
It would be nicer if Active Record dealt better with this - when change tracking came in array columns weren't supported (mysql had the lion's share of the attention at the time)
Yet another approach would be to mark such columns as always needing saving (much like what happens with serialised attributes) but you'd need to monkey patch activerecord for that.
Frederick's answer got me thingking and I wrote a simple gem deep_dirty that provides deep dirty checking by comparing current attribute values to those recast from *_before_type_cast. To automate this on ActiveRecord models, the gem sets up a before_validation callback.
Usage
gem 'deep_dirty'
class User < ActiveRecord::Base
include DeepDirty
end
user.roles << 'secretary'
user.changed? # => false
user.valid? # => true
user.changed? # => true
Also, deep checking can be initiated without validations:
user.changed? # => false
user.deep_changed? # => true
user.changed? # => true
Check out the source code at github: borgand/deep_dirty

Ruby DataMapper::ImmutableError

get '/watch/:id' do |id|
#results = Twitchtvst.all( :fields => [:Twitchtv ],
:conditions => { :user_id => "#{id}" }
)
#p #results.inspect
#results.each do |result|
puts result.id
end
erb :mystream
end
I get this error message immutable resource cannot be lazy loaded. How do I fix this?
The Error message is:
DataMapper::ImmutableError at /watch/1
Immutable resource cannot be lazy loaded
According to the official documentation:
Note that if you don't include the primary key in the selected columns, you will not be able to modify the returned resources because DataMapper cannot know how to persist them. DataMapper will raise DataMapper::ImmutableError if you're trying to do so nevertheless.
I know that you are not modifying anything here but I think that the same rule applies for lazy loading. So I will suggest to try it like that:
#results = Twitchtvst.all( :fields => [:Twitchtv, :id],
:conditions => { :user_id => "#{id}" }
) ode here
Note the id as an additional field.

How to keep Ruby object instance variables hidden from view in irb or logs?

I am making a gem to wrap an API. The service requires a few login parameters so I made a Connection class to initialize by passing in all login values, and storing with instance variables. One of these values, #secret_access_key is secret, obviously. It is not readable within the app. But while testing the gem in irb, I see the secret key displayed along with all other instance variables when the object is returned.
mws = MWS::Connection.new :access_key => '1', :secret_access_key => 'SECRET!!!', :merchant_id => '3', :marketplace_id => '4'
=> #<MWS::Connection:0x007fbd22acef40 #access_key="1", #merchant_id="3", #marketplace_id="4", #secret_access_key="SECRET!!!">
I am paranoid that the secret key will show up in Heroku logs, app error messages, or whatever else.
Should I be worrying? If so, what's the best way to store or hide this information?
Also, I am using httparty gem to manage this, is there something better I can do with that gem?
You could use this workaround:
class MWS::Connection
def inspect
"#<MWS::Connection:#{object_id}>"
end
end
Of course the secret key will still be accessible, but it shouldn't show up in any logs now:
mws = MWS::Connection.new :access_key => '1', :secret_access_key => 'SECRET!!!', :merchant_id => '3', :marketplace_id => '4'
# => #<MWS::Connection:0x007fbd22acef40>
mws.instance_variable_get(:#secret_access_key) # => 'SECRET!!!'
class MWS::Connection
def initalize(opts)
...
#secret_access_key = Cypher.encypher(opts[:secret_access_key]) if opts[:secret_access_key]
end
def secret_access_key
Cypher.decypher #secret_access_key
end
end
class Cypher
def self.encypher(str)
str + 'fancy_encryption_protocol'
end
def self.decypher(str)
str.sub 'fancy_encryption_protocol$', ''
end
end

Resources