Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip] authn+authz #32

Merged
merged 51 commits into from
Nov 17, 2017
Merged

[wip] authn+authz #32

merged 51 commits into from
Nov 17, 2017

Conversation

issa-tseng
Copy link
Member

No description provided.

issa-tseng and others added 30 commits November 2, 2017 17:50
* so now rather than supplying every single dependency every time you
  can call injector.withDefaults(db, …) which supplies all the defaults.
* the second parameter still takes the same definition hash and
  overrides the defaults piecemeal.
* use it in server.js.
* Ramda omit, it turns out, reifies prototype properties.
* eg: actor.can(‘create’, Actee.species(‘form’));
* is this actually a good idea? or should we exclusively rely on fromX
  static methods?
* we have some special actor classes that we want to keep track of; we
  already mark them as type: system, so we know which ones not to touch,
  but that doesn’t help us actually locate each particular system actor.
* modifies an existing migration, but i doubt anybody ran it already.
* naming is hard. up for debate.
* before, they took precisely one parameter each and tossed them into
  .where(). this meant only obj-style where syntax was possible.
* we want to limit direct calls on db, and use simply where possible, so
  ideally we make this more flexible somehow.
* now you can pass in objects, 3-tuple conditions, or an array
  containing many of either.
* a small perf hit. debatable whether overloading or exposing more calls
  is better. but if you want a mix of condition types, you need to do
  the dynamic type check no matter what.
* new Session instance and query module.
  * Session.decorate(expressHandler) will insert a Session instance onto
    the request object. it contains an Option[Actor].
  * if credentials are provided but invalid, it will fail early with a
    401 authentication failed.
  * debatable whether we want to simply make this an express prefilter
    rather than wrap it when we need it (which is probably most of the
    time).
* Actor and Session both get new method impls:
  * can(verb, actee) returns Promise[Bool].
  * ifCan(verb, actee).then(action) runs the action only if the actor
    (or the actor implied by the session) meets the criteria; otherwise
    it rejects with a 403 insufficient rights.
  * Actor#can and Actor#ifCan are not actually used yet.
  * both depend on the new Grant::can.
* grants.getByTriple upgraded:
  * defensively takes either Actor or Option[Actor].
  * accounts for Some(Actor) or None when resolving implied actors.
* all of this is put to use for POST /users, which is exemplary of the
  intended interface.
* need to wrap "in (?)" with parens.
* .then() needs a function.
* upgrade body-parser to actually be able to detect its failure signature.
  expressjs/body-parser#122
* also change sendError signature to match the standard one more.
* not sure when the rowCount property disappeared from knex. sometime
  after the prototype was written, perhaps.
* some friction with our attempt at composable query blocks; if a native
  Promise is injected into the middle of the query chain, then because
  .then conflates map/flatMap it’ll just gobble up the FutureQuery and
  sit on it forever, not knowing that .end() needs to be called.
* the mid-term solution is to create a new ExplicitPromise that can wrap
  a native Promise, and which exposes the same .end() method (now
  renamed to .point() to be a bit more generic - perhaps it’s a bad
  name as it is not /precisely/ an algebraic point operation), and will
  handle any FutureQueries it receives appropriately.
* the basic resolve/reject have been move over to use ExplicitPromise
  (though actually reject is native as the chain won’t matter in a
  rejection case).
* will have to engineer a better solution or be careful with libraries
  that return native Promises in the future (perhaps an .explicify()
  much like bluebird .promisify()?).
* at least for now, the problem can be solved elsewhere more cleanly, as
  session construction is pretty rigidly controlled so we can just make
  guarantees and assumptions upon it.
* since we might serialize for many different purposes, and we are not
  actually performing the obj-to-bytes serialization.
* instead, what we are expressing is that this is the modification to
  the data that makes it appropriate for API consumption.
* also changes user serialization format to merge actor data inline with
  the user object, rather than have user contain actor.
* #1: i’m dumb.
  * #1a: the simplification fix the issue, but i now know the underlying
    problem is that ramda compose operates RtL.
* #2: i’m dumb.
* both return ExplicitPromises.
* set bcrypt up to run on 12-round salt generation; seems like the new
  normal.
* don’t catch the same conditions twice; just immediately defer to
  finalize.
* eg:
  User.getById(params.id)
    .then(getOrNotFound) // returns 404 if Option.none.
    .then((user) => /* guaranteed user */)
* used in subsequent commits.
* as users and actors will always be updated together, transactionally.
* checks authz.
* updates only appropriate fields.
* saves both actor and user records.
* PUT body json { "old": "old", "new": "new" } to change the password.
* verifies the old password before changing to the new.
* finally grew enough overlapping functionality that this made sense.
* added a .transacting() method to upgrade a query tree to transacting.
* matches the syntax of query blocks, except:
* it’s a call instead of a getter. style question to be resolved around
  whether to make them all getters or make them all calls. i am open to
  either, but they should certainly be consistent.
* apart from the lack of tests, this PR is ready for review.
@issa-tseng
Copy link
Member Author

Okay, please to review @lognaturel @matthew-white @ggalmazor. Commit messages are highly descriptive.

Explicitly open questions:

  1. There aren't yet tests; I have ideas on approaches here (test the raw db queries more explicitly; test the higher-order queries logically; do integration tests at the HTTP level (but what to do about the database?)), but I'd like to hear suggestions now that the code is more concrete.
  2. Some of the linting things are maybe a bit insane (no-use-before-define).
  3. .transacting vs .transacting() are currently not consistent; would like to hear which is preferred and I will push them all in that direction.

Downriver questions that don't prevent this PR from merging:

  1. How exactly we want to expose and machine the rudimentary perms settings we've discussed for MVP (eg "anybody can register an account" vs "admins must create an account"; "any account holder can create a new form" vs "admins must create a new form"; etc).
    a. Option A: the backend has no idea about them and the frontend simply looks for and creates/deletes the appropriate grants records explicitly, or
    b. Option B: the backend understands and explicitly exposes a simplified /settings endpoint that provides some of these concepts as higher-level booleans.
    c. The answer here should keep in mind the down-the-road state of things, where these rudimentary options work alongside a more flexible permission assignment UI.
  2. How to do session refresh; right now all sessions last for exactly 24 hours with no refresh.
    a. Option A: implicitly kick expiry out to 24 hours from each use. (pros: simple; cons: GETs trigger database writes).
    b. Option B: use OAuth2's refresh token mechanism. (pros: explicit, standardized, we'll need it anyway; cons: more homework up front (n.b. I don't mind doing this homework)).
    c. Option C: ignore the problem for now.
  3. We'll need HTTPS to protect plaintext passwords in transport.
  4. The mechanism whereby the frontend is informed of the current sessionholder's rights on various things, so that it can appropriately provide the correct verbs/grey out inaccessible operations, etc.
    a. Option A: GETted objects contain a grants key containing an array of available verbs.
    b. Option B: A separate API endpoint may be queried to retrieve said array of verbs. (We will likely need this endpoint anyway, but the question stands.)

More options exist for each of these; these are just the obvious paths in my mind.

* as discussed on slack; seems people like the more-permissive route.
* it was pretty much done already.
* as expected.
* the POST actor must have a grant to 'create' on 'form'.
* :id is by the xmlFormId.
@issa-tseng
Copy link
Member Author

Okay, now basic Form POST/GET are in there too since they were incredibly easy to add.

.canOrReject('create', Actee.species('form'))
.then(() => Form.fromXml(body))
.then((form) => form.create())
.then((savedForm) => (session.actor.isDefined()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lacking pattern-matching, one wonders whether this can be made smoother.


static getByBearerToken(token) {
return sessions.getByBearerToken(token)
// TODO: awkward commuting homework.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to go from Option[T] to Promise[Option[T]], which is awkward with the current machinery.

Copy link
Member

@lognaturel lognaturel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot here and I know it was a lot of work, @cxlt! The highest-level comment which I also wrote inline is that creating new endpoints that ensures proper authentication and authorization is straightforward and should be comfortable to most people. 👍

Some of the machinery is hard for me to follow. I know some of what's at play is that I'm not too familiar with some of the syntax (e.g. ({ db }) => {...} as @ggalmazor explained in Slack) and of course, that there's some complexity inherent in what is being modeled.

The commit comments are really helpful and I would like to see a little bit of that become comments and/or dev docs. I know that @cxlt you were reluctant to get started with dev docs because understandably you expect things will change but I think just a little bit of high-level narrative now would help. A concrete way to start would be to take Instance.with(ActeeTrait)(({ Grant, actors, all, grants, simply }) => class and upack it a little bit. Link out to a good narrative on traits, describe that package.js is the dependency injection container, a paragraph on Option, things like that. I think a few minutes on that would help a lot with giving someone an idea of where to start. It doesn't have to go in this PR.

Small thing but I would personally like to see words like "easy", "simple", "just" removed from the comments in a later edit pass. They can be a bit demoralizing for people who don't find the code they refer to all that simple (e.g. me). Textbooks do this a lot and that is often why they get thrown out the window.


// Instances are simple databags representing model records, with convenience
// methods sprinkled in to ease the readability of common tasks.
//
// InstanceBase is the only common superclass to them all.
class InstanceBase {
forCreate() { return this; }
forSerialize() { return this; }
forApi() { return this; }

// Returns a new copy of this instance with merged properties.
with(data) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ES5 removed a lot of limitations with reserved keywords; they are only illegal in cases where the lexer could genuinely be confused. If you have another verb to suggest that is as descriptive as with I am happy to substitute, though.

service.get('/users/:id', endpoint(({ params }) =>
User.getByActorId(params.id).then((user) => user.withActor())));
// TODO: probably hide email addresses unless admin/self?
service.get('/users/:id', endpoint(({ params }) => User.getByActorId(params.id)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer getting any info about a specific user be protected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay; sounds like future work though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#33

// TODO: probably hide email addresses unless admin/self?
service.get('/users/:id', endpoint(({ params }) => User.getByActorId(params.id)));

service.post('/users', endpoint(({ body, session }) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems the biggest thing about this PR is making sure that the code here is approachable and useful. I think it is.

@issa-tseng
Copy link
Member Author

I hear what you're saying on "easy"/"just"/etc, but without such words it is difficult to express the justification for punting certain things down the road.

@issa-tseng
Copy link
Member Author

I'm going to merge this just to simplify the cross-repository stuff and start working on subbranches but please don't let that stop anybody from continuing to review and comment.

@issa-tseng issa-tseng merged commit 684a519 into master Nov 17, 2017
@issa-tseng issa-tseng deleted the cxlt/auth2 branch November 17, 2017 04:14
@ggalmazor
Copy link
Contributor

Hi everyone!

I've reviewed the commit messages and I've also downloaded your branch and checked the code and I can't see any important stuff that we should have to take care of right now. I feel that with this PR, Jubilant is taking a good momentum. Huge step forward here, congratulations! :)

@ggalmazor
Copy link
Contributor

Regarding the tests, I can't really add much to what I said in #29 at this moment. Maybe after you develop your ideas I can be more helpful. As I mentioned in Slack, I'll be focusing on Aggregate and Briefcase but I can still help with specific problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants