jdickey / crypt_ident Goto Github PK
View Code? Open in Web Editor NEWYet another fairly basic authentication Gem. (Authorisation, and batteries, sold separately.)
License: MIT License
Yet another fairly basic authentication Gem. (Authorisation, and batteries, sold separately.)
License: MIT License
Implement the CryptIdent
module #sign_up
(Registration) method. This will need some means of accessing documented configuration details such as the Repository to use and so on. Implementing a single major API method per feature branch should give us significantly quicker turnaround than waiting to merge back into develop
until everything is done. We've been there, and we'd really rather not go back, thank you very much.
Given that the app this is being developed to support has successfully migrated from Hanami 1.2.0 to Hanami 1.3.0, and given that the Hanami team themselves are encouraging people to migrate existing apps and to start new apps on 1.3.0, is there any benefit served by supporting Hanami 1.2.0? This was prompted by newly bundling (see Commit cc505f7) and running tests, which spit out a deprecation warning:
### The warning below actually was presented on a single line;
### wrapped here and Gem backtrace elided for presentation as issue.
Hanami::Utils::Class.load_from_pattern! is deprecated, please use \
Hanami::Utils::Class.load! instead - called from: \
.../gems/hanami-controller-1.2.0/lib/hanami/controller/configuration.rb:100:in `for'.
# Running tests with run options --seed 19164:
# ... etc ...
Now that we can sign up, the next logical step is implementing #sign_in
and #sign_out
, and updating docs to suit.
Integration tests β where we use live Hanami::Model
classes for UserRepository
, etc β have been proving embarrassingly useful for pointing out places where the mock-everything-outside unit tests have left open questions. Sometimes-careless code hasn't helped.
The latest example: how we access the UserRepository
. Here's a comparison of two code excerpts: one from test/support/unit_test_model_and_repo_classes.rb
which uses the "faked" Repository, and one from test/support/model_and_repo_classes.rb
, which uses a Hanami::Repository
subclass.
# test/support/unit_test_model_and_repo_classes.rb
class UserRepository < Repository
# ...
def find_by_name(name)
select(:name, name)
end
def find_by_token(token)
select(:token, token)
end
# ...
end
where #select
was defined in the FakeRepository
class as
def select(key, value)
@records.values.select { |other| other.to_h[key] == value }
end
The details of @records.values
aren't relevant except to note that it's an Array of Entities, which respond to #to_h
to allow Hash-like selection of values, which is what's going on here. But, because it's calling Array#select
, what's returned is an Array that, for the key
fields used, is either empty or has a single entry. In some ORMs (e.g., ActiveRecord), this is expected behaviour; the thinking apparently goes along the line of "if we always return an Array, then the client app's logic can be more consistent". A perfectly reasonable design decision; does it map to Hanami::Model?
The test/support/model_and_repo_classes.rb
, which uses a real Hanami Repository, is significantly different:
# `test/support/model_and_repo_classes.rb`
class UserRepository < Hanami::Repository
def find_by_name(name)
users.where(name: name).map_to(User).one
end
def find_by_token(token)
users.where(token: token).map_to(User).one
end
# ...
end
Note that we're explicitly limiting the result to a single value, which will either be an Entity or nil
.
Right; so which is correct? Well, let's take a look at the source for Hanami::Repository::Commands#find
, which reads:
# Find by primary key
#
# @return [Hanami::Entity,NilClass] the entity, if found
#
# @raise [Hanami::Model::MissingPrimaryKeyError] if the table doesn't
# define a primary key
#
# @since 0.7.0
#
# @example
# repository = UserRepository.new
# user = repository.create(name: 'Luca')
#
# user = repository.find(user.id)
def find(id)
root.by_pk(id).as(:entity).one
rescue => e
raise Hanami::Model::Error.for(e)
end
This would seem to argue for not using an Array. We'll retrofit our existing code and tests to match.
In live code, when CryptIdent.update_session_expiry
is called passing in a session object (e.g., an instance of Rack::Session::Abstract::SessionHash
, then the current UpdateSessionExpiry#call
method breaks because Abstract::SessionHash
implements many of the methods of the standard Hash class, but #merge
is not among them.
We appear to have two ways we could address this:
session_data
parameter to an actual Hash instance and merge into that before returning it. Doing so would be consistent with our present documentation which states that the method returns a Hash; orRack::Session::Abstract::SessionHash#each
instance method to loop through the data to be merged and insert each item into the session_data
.We prefer Option 1; it's simpler, easier for the reader to follow, and apparently less likely to immediately blow up in our faces if we neglect something.
The pyrotechnics in update_session_expiry
appear to be the only place we exercise an unsupported method on session data, after a brief inspection of the existing code.
There are several XXX: WTAF?!?
and FIXME:
notes in the code and tests. Those should be examined more closely and the notes removed, or at least reworded.
Our Guardfile
and Guard usage are still far from ideal.
Known problems include, but are not necessarily limited to:
minitest
Guard task does not pick up changes to test files.shell
Guard usage lumps together several activities, and does not fire on individual changes. (Only one shell
Guard task may be active in a single Guardfile
apparently.)shell
Guard task does not reliably fire on individual file changes.shell
Guard task apparently eats standard output from tasks that it runs.shell
to run flog
, since guard-flog
is broken due to lack of maintenance.shell
to run flay
, since guard-flay
is broken due to lack of maintenance.shell
to run reek
, since guard-reek
is broken due to lack of maintenance."How can people use Guard", you might reasonably ask, "if maintenance for so many of its plugins died 3-4 years ago?" The answers seems to be that (a) many non-Ruby-development Guard plugins are actively maintained, and (b) Ruby dev usage seems to now largely be limited to the minitest
and rake
Guard plugins, which are reasonably well-maintained as of late 2018.
We've just re-discovered that Rack (the server protocol underlying all reasonably-modern Ruby Web frameworks) doesn't deal nicely with objects stored in session
data (which is, by default, persisted in a cookie). If you assign, say, a Hanami::Entity
-subclass instance to session[:current_user]
(where session
is Hanami's access to Rack::Session
), when you later read from session[:current_user]
, you'll be handed back a Hash
of the Entity's attributes
Entity semantics specify that any two instances of the same Entity class with the sasme attribute values refer to the same value of the Entity, not merely equal values, so converting back to an Entity is harmless. You just need to remember to do it and, as of 0.2.0, we didn't. That must be done throughout the published API.
This causes tests to pass that shouldn't. Specifically, the support classes include code like that of CryptIdent::UpdateSessionExpiry#initialize
:
class UpdateSessionExpiry
def initialize
@config = CryptIdent.cryptid_config
end
# ...
end
Somehow this Works Just Fineβ’ in the test code but fails horribly outside the test environment, since UpdateSessionExpiry
does not include the CryptIdent
module (obviously) and #cryptid_config
is an instance method, not a class method. π
The module which calls the support-class code has a #cryptid_config
getter. That should be passed into the classes that need it.
There is a conflict in the current documentation/spec for the #restart_session_counter
and #session_expired?
methods.
#restart_session_counter
sets the :expires_at?
value in its returned Hash to the Unix epoch (00:00:00 GMT on 1 January 1970, adjusted to local time) if the passed-in session_data[:current_user]
is the Guest User; it declares that any Guest User session is always expired.
#session_expired?
returns false
if the passed-in session_data[:current_user]
is the Guest User; it declares that any Guest User session never expires.
This matters for consistency, obviously; it also matters because how one implements session validation. If the Guest User's session _never_expires, then the first line of your session-validation method would probably look like
return restart_session_counter(session) unless session_expired?(session)
Any further code in that method would safely assume that the Current User is a Registered User who had previously Authenticated and whose session had Expired; the usual practice is to redirect to the Landing Page with a Flash message indicating that the session has Expired.
On the other hand, to make that guard clause work when the Guest User's session is always Expired would have to check the Current User explicitly:
return restart_session_counter(session) unless guest_user?(session) || session_expired?(session)
# ... later on down in the module...
private
def guest_user?(session_data)
return true if session_data[:current_user]&.guest_user?
session_data[:current_user].nil?
end
The answer to the question posed by this issue should likely hinge on whether the second approach (checking for the Guest User explicitly) adds any value over the first (merely comparing timestamps).
Sometime in The Glorious Futureβ’, Freaky/monotime
looks like it could be quite useful, not to mention interesting. In particular, its Instant
class lets you establish a point in time and then take successive readings of the time difference between "now" and when that Instant
was instantiated. Before we understood the difference between the Time
class as defined in Ruby core and the Time
class as defined in the Standard Library, we were considering using Instant for the
password_reset_sent_atattribute required by
CryptIdent. That would make checking for expiration even more trivially easy than straight
Time` arithmetic.
This introduces a change where config.guest_user
works correctly even if no value has been assigned to config.repository
. It does so by explicitly assigning a new UserRepository
instance to config.repository
.
Marked as a bug rather than a feature because the previous setup was nonsensical.
Inadvertently left out-of-date earlier. Oops.
Session data (by default) makes use of cookies via Rack::Session::Cookie
, which requires that its secret values be String instances (or objects which can be implicitly converted to String). That's called serialisation, and it's often (but not necessarily) done using JSON.
What we wind up with when we store, say, a User
Entity instance in session data and then later retrieve that (indirectly) from where it's been stored in a cookie or similar string-only data store, is a Hash of that Entity's attribute values, from which it's trivial (and cheap) to construct a new Entity instance representing the same value as the original.
What our support classes expect presently (as of Commit dcd8df5) for, say, session[:current_user]
is an Entity instance. What they're getting is a Hash. The logical place to make that conversion is at the main CryptIdent
module level, handing explicit instance values to the worker classes rather than expecting them to be able to figure out session data on their own. Because, after all, if some user of this library uses a different store than cookies for session data, we trust the Rack::Session API without having to care about how it's serialised.
This includes updating the documentation for the method, both in the README (use cases) and the source file (API documentation).
The link from the README to the API doc doesn't work. That obviously needs fixing.
Also, the Use Case Workflows descriptive section is really too long for a README; a short summary of the API might be better. The existing workflow documentation should live on as a separate document.
Presently, we require the User Entity and the schema underlying the User Repository to have an attribute or field, respectively, named password_reset_sent_at
. This was copied from the API for another authentication Gem that makes more explicit assumptions about how it's being used, e.g., in sending email messages, than our library does. Given how we're now defining #generate_reset_token
as being that specific and not doing anything else, this name no longer conveys accurate information, since the code that adds that timestamp to a User has no direct role in sending an email or any other form of user communication.
Hence, renaming the field as password_reset_expires_at
makes more sense. It also makes it much simpler to determine whether a Password Reset request has actually Expired, since no processing beyond comparing the current time to the timestamp need be done; the policy is applied on the front rather than back end of that flow.
This would continue to make use of the configuration-specified :reset_expiry
value in computing that value.
Finally, that should probably be a GMT/UTC timestamp rather than a server-local-time timestamp (as Ruby's time.now
returns by default).
This was triggered by reading this blog post and seeing the example snippet that demonstrates how to use Dry::Monads::Result
and Dry::Matcher
to do just this. A single block that, given a result
object, lets you fluently handle success and failure results.
Brilliant, and worth exploring. Or is there anyone who thinks that the mismatch between our#sign_up
method and everything else doesn't suck with a particularly hard self-inflicted vacuum?
There's a note in the crypt_ident.gemspec
file reminding us that
NOTE: During development, we're not specifying Gem versions, because we're bundling locally using
bin/setup
andscripts/build-gem-list.rb
. Before merging back tomaster
, at latest, this must be updated to show minimum Gem versions; e.g.,'>= 3.1.12'
forbcrypt
. It Would Be Very Nice if we had a script to automatically rewrite the Gemspec for us each way. PRs welcome.
So, basically, this utility, let's call it gemspec_versions
, would take one of two command-line parameters:
--lock-down
would rewrite each add_runtime_dependency
and add_development_dependency
specification in the .gemspec to specify a version greater than or equal to the version in the present Gemfile.lock
. For example (as of mid-December, 2018), it would rewrite the line spec.add_development_dependency "minitest"
to
spec.add_development_dependency "minitest", '5.11.3'
--unlock
would rewrite each dependency specification which includes a version specification to omit the version specifier, exactly reversing the --lock
option.Given a locked-down .gemspec and a Gemfile.lock
file, running the commands gemspec_versions --unlock && gemspec_versions --lock
must produce a .gemspec that matches the original.
Marking this as TGF, though having it for the next update cycle would be a Very Good Thing.
You could argue either way: is this a combination of two bugs, or does fixing the bug in the title allow us to also make an enhancement? Regardless, we're about to describe at least one bug.
Currently (as of Commit 8db7906 on develop
), we call SecureRandom.urlsafe_base64
to generate a password if none was specified. This raises two points:
#sign_up
isn't supposed to accept a password in the passed-in attribs
. We're supposed to generate a new user with the specified name, email address, and profile text (if any) and a random password, and then send the new user an email with a link that will let her/him change their password so they can sign in. This is to verify that users both intend to sign up and supply a correct email address. Also, nobody uses good passwords in a new-user form.SecureRandom.urlsafe_base64
generates a random sequence of bytes, its output isn't acceptable to Base64.strict_encode
(and encoding it with Base64.encode
, which is essentially part of what BCrypt::Password.create
does internally, produces a "Base64" string that can't be decoded by Base64.strict_encode
. Why do we care about that, you ask? While TDDing CryptIdent#generate_reset_token
, we looked at decoding the generated token (which is generated in the same way as the sign-up password). Nope; not valid Base64, so tests verifying that it is will fail.To close this issue, the following must happen:
CryptIdent::SignUp
CryptIdent::SignUp#hashed_password
must not accept a password
parameter and must always generate a random Clear-Text Password;SecureRandom.alphanumeric
or equivalent, which generates ASCII-normal characters acceptable as input to Base64.strict_encode
;token_bytes
to specify the length of the Clear-Text Password to generate.CryptIdent::GenerateResetToken
CryptIdent::GenerateResetToken
(forthcoming) must use the same mechanism as in item 2 above (SecureRandom.alphanumeric
or equivalent) and also must use the config-default token_bytes
to specify the length of the token string being generated before being encoded.Being in a hurry always slows you down, eventually.
This has two aspects to it:
README
to include a comprehensive set of simplified use-case explanations that do not simply repeat the API documentation, andThe problem may have come about because our initial Gem setup/YARD plugin setup was cargo-culted from another project somewhere that is now out-of-date due to GitHub infrastructure changes. Project Paper Cuts is awesome, but it clearly isn't the sum total of moving parts in the GitHub documentation infrastructure.
To avoid cluttering up the users
table with fields that are only used in specific use cases, app developers may well prefer to create separate tables to support specific use cases, such as resetting/changing passwords. Configuration should be extended to add a password_reset_table
attribute. Defaulting to null
, it must be set to a table name if that table should be used to access the password_reset_expires_at
and token
attributes.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.