I've just started out using MongoDB and, in particular, Mongoid.
naturally I'd like to ensure my User's passwords are kept nice and secure, and previously I'd have done this with ActiveRecord and used bcrypt. I'm looking for a nice, clean, secure, simple way to implement the same sort of thing using Mongoid.
I've taken a look at mongoid-encryptor but I've not quite got my head around how to use it.
Assume my simplified User looks like this, as per the example in mongoid-encryptor's Readme file.
class User
include Mongoid::Document
include Mongoid::Encryptor
field :name
field :password
encrypts :password
end
And in my WebApp (using Sinatra in this case) I'd define a helper such as
def login (name, cleartxtpass)
return User.where(name: name, password: cleartxtpass).first
end
How do I get it to use bcrypt?
Is there any pre-processing I need to do with cleartxtpass or will Mongoid::Encryptor just handle that? It's not clear from the docs.
Okay well after some digging I decided to not bother using Mongoid::Encryptor but to stick to the tried and tested way I used to do these things when using ActiveRecord.
So now my User looks like
class User
include Mongoid::Document
field :name, type: String
field :password_hash, type: String
index({name: 1}, {unique: true, name: 'user_name_index'})
include BCrypt
def password
#password ||= Password.new(password_hash)
end
def password=(new_password)
#password = Password.create(new_password)
self.password_hash = #password
end
end
and my authenticate helper method looks like
def auth_user(username, password)
user = User.where(name: username).first
return user if user && user.password == password
return nil
end
That works a treat.
The simply way to do it is:
class User
include Mongoid::Document
include ActiveModel::SecurePassword
field :name, type: String
field :password_digest, type: String
has_secure_password
end
Related
I have a User class as follows:
class User
attr_reader :username, :password
def inintialize(username, password)
#username = username
#password = password
end
end
And I want to store many of these User classes in an array as follows:
users = []
def initialize_users
user = User.new("user0", "password0")
users.push(user)
user = User.new("user1", "password1")
users.push(user)
end
And now I want to implement a searching function where I can search via username, or password. I know I can iterate over the classes comparing each field like so:
def search(username)
users.each do |user|
if user.username == username then
puts "Found"
break
end
end
end
But is there any other way to do this without the iterating or is this simply the easiest/cleanest way?
I was thinking maybe there is a way to do something like:
users["username_to_find"]
=> true
Although I am not sure how to implement that. I would believe I would have to rewrite the User class to have a built in list, but from there I am lost. I guess even if I do implement this feature there is still a iteration that has to happen.
Also I would like to access that users data from within that notation such as
users["username_to_find"].password
=> "password111"
Anyone have any ideas?
PS: User class is reduced to relevant code, it actually holds many more data members which are specific to each user such as sockets, and methods for sending data to a specific users sockets.
You really have no choice but to iterate over every element of the array and perform your matching test. Enumberable#detect is what you would want to use:
def search(username)
# return the first matching result
users.detect { |user| user.username == username }
end
This would return the first User with a matching username or nil. If you want to allow multiple results to be returned (e.g. more of what the word "search" denotes) than you would want to use Enumberable#select which returns all matching blocks:
def search(username)
# return ALL matching results
users.select { |user| user.username == username }
end
If you need to potentially match on multiple criteria (e.g. search on username and first name, etc) than you will need to take this approach. If you are only searching on username, than the solution given by #dax above is perfect.
If it doesn't need to be an Array, why not use a Hash? It provides exactly the functionality you want.
users = {}
user = User.new("user0", "password0")
users[user.username] = user
Access it like so
users['tim_the_toolman'] # => nil, there is no user by that name
users['user0'] # => returns user0
users['user0'].password # => 'password0'
use Array find and select methods. Assume you are searching by name
find returns first object which satisfies your condition
array.find { |user| user.name == searched_name } # will be either one User or nil, if neither has name eq to searched_name
select returns all objects which satisfy your condition
array.select { |user| user.name == searched_name } #will be either array of Users which have name eq searched_name or an empty array when none has this name
So there doesn't appear to be any clean way to generically allow Hash field with strong parameters. This may of course be a strong parameters issue but I'm curious if there is a workaround. I have a model with some fields...
field :name, type: String
field :email, type: String
field :other_stuff, type: Hash, default: {}
Now I could just permit everything:
params.require(:registration).permit!
But that isn't really a great idea and what I'd like to do is something like...
params.require(:registration).permit(:name, :email, { other_stuff: {} })
However this doesn't seem to be possible with strong parameters, it isn't possible to just whitelist a hash as a property (yay for SQL centric ActiveRecord APIs!). Any ideas how this might be done, or is my best bet to submit a Rails patch to allow for this scenario.
Ok, after researching this, I found an elegant solution that I will start using too:
params.require(:registration).permit(:name).tap do |whitelisted|
whitelisted[:other_stuff] = params[:registration][:other_stuff]
end
source: https://github.com/rails/rails/issues/9454#issuecomment-14167664
If necessary nested attributes can also be permitted as follows:
def create_params
params[:book]["chapter"].permit(:content)
end
For a field that allows nested hashes, I use the following solution:
def permit_recursive_params(params)
params.map do |key, value|
if value.is_a?(Array)
{ key => [ permit_recursive_params(value.first) ] }
elsif value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
{ key => permit_recursive_params(value) }
else
key
end
end
end
To apply it to for example the values param, you can use it like this:
def item_params
params.require(:item).permit(values: permit_recursive_params(params[:item][:values]))
end
I am new to ndb and gae and have a problem coming up with a good solution setting indexes.
Let say we have a user model like this:
class User(ndb.Model):
name = ndb.StringProperty()
email = ndb.StringProperty(required = True)
fb_id = ndb.StringProperty()
Upon login if I was going to check against the email address with a query, I believe this would be quite slow and inefficient. Possibly it has to do a full table scan.
q = User.query(User.email == EMAIL)
user = q.fetch(1)
I believe it would be much faster, if User models were saved with the email as their key.
user = user(id=EMAIL)
user.put()
That way I could retrieve them like this a lot faster (so I believe)
key = ndb.Key('User', EMAIL)
user = key.get()
So far if I am wrong please correct me. But after implementing this I realized there is a chance that facebook users would change their email address, that way upon a new oauth2.0 connection their new email can't be recognized in the system and they will be created as a new user. Hence maybe I should use a different approach:
Using the social-media-provider-id (unique for all provider users)
and
provider-name (in rare case that two twitter and facebook users share
the same provider-id)
However in order to achieve this, I needed to set two indexes, which I believe is not possible.
So what could I do? Shall I concatenate both fields as a single key and index on that?
e.g. the new idea would be:
class User(ndb.Model):
name = ndb.StringProperty()
email = ndb.StringProperty(required = True)
provider_id = ndb.StringProperty()
provider_type = ndb.StringProperty()
saving:
provider_id = 1234
provider_type = fb
user = user(id=provider_id + provider_type)
user.put()
retrieval:
provider_id = 1234
provider_type = fb
key = ndb.Key('User', provider_id + provider_type)
user = key.get()
This way we don't care any more if the user changes the email address on his social media.
Is this idea sound?
Thanks,
UPDATE
Tim's solution sounded so far the cleanest and likely also the fastest to me. But I came across a problem.
class AuthProvider(polymodel.PolyModel):
user_key = ndb.KeyProperty(kind=User)
active = ndb.BooleanProperty(default=True)
date_created = ndb.DateTimeProperty(auto_now_add=True)
#property
def user(self):
return self.user_key.get()
class FacebookLogin(AuthProvider):
pass
View.py: Within facebook_callback method
provider = ndb.Key('FacebookLogin', fb_id).get()
# Problem is right here. provider is always None. Only if I used the PolyModel like this:
# ndb.Key('AuthProvider', fb_id).get()
#But this defeats the whole purpose of having different sub classes as different providers.
#Maybe I am using the key handeling wrong?
if provider:
user = provider.user
else:
provider = FacebookLogin(id=fb_id)
if not user:
user = User()
user_key = user.put()
provider.user_key = user_key
provider.put()
return user
One slight variation on your approach which could allow a more flexible model will be to create a separate entity for the provider_id, provider_type, as the key or any other auth scheme you come up with
This entity then holds a reference (key) of the actual user details.
You can then
do a direct get() for the auth details, then get() the actual user details.
The auth details can be changed without actually rewriting/rekeying the user details
You can support multiple auth schemes for a single user.
I use this approach for an application that has > 2000 users, most use a custom auth scheme (app specific userid/passwd) or google account.
e.g
class AuthLogin(ndb.Polymodel):
user_key = ndb.KeyProperty(kind=User)
status = ndb.StringProperty() # maybe you need to disable a particular login with out deleting it.
date_created = ndb.DatetimeProperty(auto_now_add=True)
#property
def user(self):
return self.user_key.get()
class FacebookLogin(AuthLogin):
# some additional facebook properties
class TwitterLogin(AuthLogin):
# Some additional twitter specific properties
etc...
By using PolyModel as the base class you can do a AuthLogin.query().filter(AuthLogin.user_key == user.key) and get all auth types defined for that user as they all share the same base class AuthLogin. You need this otherwise you would have to query in turn for each supported auth type, as you can not do a kindless query without an ancestor, and in this case we can't use the User as the ancestor becuase then we couldn't do a simple get() to from the login id.
However some things to note, all subclasses of AuthLogin will share the same kind in the key "AuthLogin" so you still need to concatenate the auth_provider and auth_type for the keys id so that you can ensure you have unique keys. E.g.
dev~fish-and-lily> from google.appengine.ext.ndb.polymodel import PolyModel
dev~fish-and-lily> class X(PolyModel):
... pass
...
dev~fish-and-lily> class Y(X):
... pass
...
dev~fish-and-lily> class Z(X):
... pass
...
dev~fish-and-lily> y = Y(id="abc")
dev~fish-and-lily> y.put()
Key('X', 'abc')
dev~fish-and-lily> z = Z(id="abc")
dev~fish-and-lily> z.put()
Key('X', 'abc')
dev~fish-and-lily> y.key.get()
Z(key=Key('X', 'abc'), class_=[u'X', u'Z'])
dev~fish-and-lily> z.key.get()
Z(key=Key('X', 'abc'), class_=[u'X', u'Z'])
This is the problem you ran into. By adding the provider type as part of the key you now get distinct keys.
dev~fish-and-lily> z = Z(id="Zabc")
dev~fish-and-lily> z.put()
Key('X', 'Zabc')
dev~fish-and-lily> y = Y(id="Yabc")
dev~fish-and-lily> y.put()
Key('X', 'Yabc')
dev~fish-and-lily> y.key.get()
Y(key=Key('X', 'Yabc'), class_=[u'X', u'Y'])
dev~fish-and-lily> z.key.get()
Z(key=Key('X', 'Zabc'), class_=[u'X', u'Z'])
dev~fish-and-lily>
I don't believe this is any less convenient a model for you.
Does all that make sense ;-)
While #Greg's answer seems OK, I think it's actually a bad idea to associate an external type/id as a key for your entity, because this solution doesn't scale very well.
What if you would like to implement your own username/password at one point?
What if the user going to delete their Facebook account?
What if the same user wants to sign in with a Twitter account as well?
What if the user has more than one Facebook accounts?
So the idea of having the type/id as key looks weak. A better solution would be to have a field for every type to store only the id. For example facebook_id, twitter_id, google_id etc, then query on these fields to retrieve the actual user. This will happen during sign-in and signup process so it's not that often. Of course you will have to add some logic to add another provider for an already existed user or merge users if the same user signed in with a different provider.
Still the last solution won't work if you want to support multiple sign-ins from the same provider. In order to achieve that you will have to create another model that will store only the external providers/ids and associate them with your user model.
As an example of the second solution you could check my gae-init project where I'm storing the 3 different providers in the User model and working on them in the auth.py module. Again this solution doesn't not scale very well with more providers and doesn't support multiple IDs from the same provider.
Concatenating the user-type with their ID is sensible.
You can save on your read and write costs by not duplicating the type and ID as properties though - when you need to use them, just split the ID back up. (Doing this will be simpler if you include a separator between the parts, '%s|%s' % (provider_type, provider_id) for example)
If you want to use a single model, you can do something like:
class User(ndb.Model):
name = ndb.StringProperty()
email = ndb.StringProperty(required = True)
providers = ndb.KeyProperty(repeated=True)
auser = User(id="auser", name="A user", email="auser#example.com")
auser.providers = [
ndb.Key("ProviderName", "fb", "ProviderId", 123),
ndb.Key("ProviderName", "tw", "ProviderId", 123)
]
auser.put()
To query for a specific FB login, you simple do:
fbkey = ndb.Key("ProviderName", "fb", "ProviderId", 123)
for entry in User.query(User.providers==fbkey):
# Do something with the entry
As ndb does not provide an easy way to create a unique constraint, you could use the _pre_put_hook to ensure that providers is unique.
Recently updated to Mongoid 3.1 from 3.0.3 and this resulted in some broken code and confusion on my side.
Say you have a pair of classes with a belongs_to/has_many relationship, like so:
class Band
include Mongoid::Document
field :name, type: String
has_many :members, :autosave => true
end
class Member
include Mongoid::Document
field :name, type: String
belongs_to :band
end
Saving all this to the database like so:
b = Band.new
b.name = "Sonny and Cher"
b.members << Member.new(name: "Sonny")
b.members << Member.new(name: "Cher")
b.save
I would in my API, be able to return a 'member' object like so:
m = Member.where(name: "Sonny").first
m.to_json
which yields the following, as expected:
{"_id":"<removed>","band_id":"5151d89f5dd99dd9ec000002","name":"Sonny"}
My client can request the full band object with a subsequent call if it wants to. However, in some cases I DO want to include the referenced item directly. With 3.0.3, I would just do the following:
m = Member.where(name: "Sonny").first
m[:band] = m.band
m.to_json
and this would add a new field with the full band information to it. With 3.1, however (it may have started in earlier versions, but I didn't test), I now get this:
{"_id":"<removed>","band_id":{"_id":"5151dc025dd99d579e000002","name":"Sonny and Cher"},"name":"Sonny"}
So, it looks like the band info has been eager-loaded into the field? Why is it stored under the key ':band_id' and not ':band'? I guess ':band' is protected, but I still don't think the data should be stored under the ':band_id' key. I suspect I am missing something here. Any ideas?
You can specify an :include option for to_json like so:
m.to_json(include: :band)
The JSON will then have a key band with the Band object converted to JSON and band_id will still be present.
I have a Profile model:
class Profile(db.Model):
user = db.UserProperty(auto_current_user=True)
bio = db.StringProperty()
I'd like to display the user's existing bio in this view. If the user has no profile yet, I'd like to create it. Here's what I have so far, which doesn't work yet:
class BioPage(webapp2.RequestHandler):
def get(self):
user = users.get_current_user()
if user:
profile = Profile.get_or_insert(user=user) #This line is wrong
profile.bio = "No bio entered yet."
profile.save()
self.response.headers['Content-Type'] = 'text/plain'
self.response.out.write('Hello, ' + user.nickname() + '<br/>Bio: ' + profile.bio)
else:
self.redirect(users.create_login_url(self.request.uri))
How do I fix the incorrect line above? I know that get_or_insert() should take a key name, but I can't figure out what that would be.
(Should the user field in Profile even be a db.UserProperty?)
You have to pass the key_name to get_or_insert(), in this case, like so:
profile = Profile.get_or_insert(key_name=user.email())
Note that since the user property is auto-populated because of the auto_current_user=True you don't need to pass it to the get_or_insert() call. In your case you don't need to pass anything but the key name.
You probably don't want to use db.UserProperty, for reasons explained here. In summary, if a user changes his/her email address, your (old) stored 'User' will not compare equal to the currently-logged-in (new) 'User'.
Instead, store the user.user_id() as either a StringProperty on your Profile model (as shown on the page I referenced above), or as the key (key_name) of your Profile model. An example of the latter is here.