Fork me on GitHub
#clojure
<
2015-08-30
>
tom04:08:10

Does anyone know off the top of their head if this will work? Can you use compojure contexts like this

(defroutes approutes 
  (context "/a" [] 
    (context "/b" [] (GET "/" [] "B"))
    (context "/c" [] (GET "/" [] "C")))

tom05:08:00

Yes it works.

jrychter12:08:17

This is strange. Is it just me, or has "lein new" stopped working? It breaks for me with "java.io.FileNotFoundException: Could not locate clojure/data/priority_map__init.class or clojure/data/priority_map.clj on classpath."

screamish12:08:22

would it be appropriate to ask for a "critique my newbie code" here?

screamish12:08:46

I'm very very new to clojure

jrychter12:08:08

@screamish: I think #C053PTJE6 is intended to serve that purpose, but I'm not sure, haven't been here in a while.

screamish12:08:45

many thanks simple_smile

reborg14:08:54

Not all collections are sequences but no problems because (seq coll) works almost on any coll. But I was searching a counter-example. Anyone ever stumbled upon a non-trivial (seq something-resembling-a-coll) that throws exception?

bronsa17:08:53

@reborg: RT.seq is defined for java.lang.Iterable, which is a superinterface of java.util.Collection

bronsa17:08:42

meaning all java collections should be properly seqable

bronsa17:08:00

@reborg an exception is those collections whose iterator impl return a mutable object, see http://dev.clojure.org/jira/browse/CLJ-1738

kardan17:08:11

Anyone using yesql (0.5.0 & postgres 9.4 ) with an UPDATE statement? I keep getting #<PSQLException org.postgresql.util.PSQLException: ERROR: syntax error at or near "-" Position: 15> SQL: -- name: use-refresh-token<! -- Consumes a refresh token. UPDATE refresh-tokens SET is_used = 'true' WHERE token_id = :token_id RETURNING token_id, user_id Any hints on what to do?

apviitanen18:08:58

@kardan: The error message is quite accurate: postgres doesn't allow hyphens in the sql identifiers. You could try double quoting the table name in your update statement. However, the best thing you could do would probably be to rename the refresh-token table.

amacdougall18:08:51

I'm trying to get started with Buddy auth. Luminus helpfully set up some basic Ring middleware for me, and I've been reading https://funcool.github.io/buddy-auth/latest/ to understand what's available. Authentication seems straightforward: I handle login/password however I'd like, and set :identity in the session (presumably to a user map of some kind, again, this is left up to me). Authorization is another matter. In the application I'm making, users will be authorized to edit their own content, or that of anyone with a lower privilege level. This means that authorization for a request isn't just a matter of identity. I need to load the target content and check for ownership, which is domain logic. I'm wondering where I should put that logic. For example, GET /admin is easy: is the current user an admin? Done and done. But DELETE /posts/12/comments/10 {:body "blah blah blah"} is harder. If the user isn't an admin, I need to load the post and comment and see if the user is the post author or the comment author, etc...

amacdougall18:08:57

Buddy's docs imply that the authorization should be done at the middleware level, which doesn't seem appropriate for such fine-grained logic. Maybe the middleware should require a set of domain functions from some other namespace? I mean, nothing is stopping me from putting this authorization in the application-level route handlers instead. Maybe coarse stuff, like restricting the entire /admin* family, should be in middleware, while fine stuff, like content ownership, should be in the API request handling logic. But then we're splitting a lot of similar behavior along a somewhat arbitrary line. It seems like many apps would end up with this kind of situation.

kardan18:08:04

@apviitanen: thank you so much. That hyphen was a typo, hence did not get the error when testing in the psql console. - once again thanks a lot. Was totally blind to it

jrychter18:08:57

@amacdougall: I don't think you can do authorization at the middleware level except in the simplest of cases. Who has access to what is something only your application (domain logic) knows.

reborg19:08:20

@bronsa thanks for the pointer.

tcrayford19:08:49

@amacdougall: @jrychter it depends 😉 I have app specific middleware that handles all authorization concerns in Yeller (~ 3 years running now, maybe 5k lines of code in the webapp), and it's worked out well. There are like 3 middlewares that control authorization, and they total maybe 30 lines of code and are in the same file

tcrayford19:08:23

(but then I only have three scopes for authorization in 95% of Yeller. Your project might be more complicated)

amacdougall19:08:38

Mine is pretty simple. User, moderator, admin, superuser. Each user can edit their own content and content of lower privilege levels. But this means that every content edit involves an authorization check like that... I guess it should be in the domain level, i.e. in the functions invoked by the API route handlers. This means that the buddy-auth middleware is ... not going to do a ton for me.

tcrayford19:08:27

that makes sense to me 😉 (I don't know anything about buddy-auth

tcrayford19:08:08

It's up to you: what I do is wrap specific part of my (compojure) routes with different middleware. So admin users can only see admin routes (and only if they have admin access to what they're looking at)

tcrayford19:08:26

for the reference, my auth model: users are either: not logged in [can't access most of the app], logged in and looking at a user specific thing [no permissions except "are they a user"], logged in and looking at a project or some subresources of said project [needs permissions to said project], or logged in and doing some admin thing to said project/subresource [needs admin permissions for said project]

tcrayford19:08:32

(I lied: there are 4 lights)

amacdougall19:08:24

Right. I believe I will at least use the middleware-level authentication to wall off the /admin app, and possibly to wall off all non-`GET` requests from logged-out users. The remainder of the editing operations are going to be inline (i.e. using the normal views, but with UI elements visible only to authorized users), so those make more sense at the domain level. That said, I still feel like authorization should all be in one place. ...based on that gut feeling, I think I'll skip the middleware approach entirely.

tcrayford19:08:53

sure, your app

tcrayford19:08:21

note: folk who are really serious about security put admin in a separate process, only available behind vpn access

amacdougall19:08:29

Your approach seems really reasonable, though. How do you check the user privileges against the desired view from the middleware?

amacdougall19:08:38

And yeah, that's how we do it at my day job. 👍

amacdougall19:08:43

My project is not neglecting security per se, but it's a fun side project, so I don't have to go Fort Knox on it (I hope).

tcrayford19:08:58

(defn wrap-project-admin-only-routes [handler]
  (fn [req]
    (if (= (get-in req [:params :current-user-role]) :permission.role/admin)
      (handler req)
      (do
        (err :out "web"
             :event "error"
             :wrong-permission "not-an-admin"
             :url (get req :uri "nil"))
        nil))))

tcrayford19:08:39

that's a sample middleware. Middleware that throws in current-user-role is further above it in the stack - it looks at the url for the current project being accessed and throws stuff into params

amacdougall19:08:59

Are there multiple project routes available from the same authentication realm? ...ah, I get you. So that's where it's doing the DB lookups or whatever.

tcrayford19:08:21

(defn wrap-project-permission-required [handler]
  (fnk [[:params query] :as req]
       (let [[_ organization-name project-name & _] (string/split (codec/url-decode (safe-get req :uri)) #"/")]
         (if (and organization-name project-name)
           (let [[project permission organization] (find-project-with-permission query (get-in req [:params :current-user]) organization-name project-name)]
             (if (and project permission organization)
               (handler
                (-> req
                    (assoc-in [:params :current-project] project)
                    (assoc-in [:params :current-user-role] (get permission :permission/role))
                    (assoc-in [:params :current-organization] organization))))
             nil)
           nil))))

tcrayford19:08:32

(the string/split is gross, but it does work)

amacdougall19:08:56

I'll dwell on it for a bit. I mostly don't want to get into a situation where my middleware is intercepting an API POST request, hitting the DB once or twice to get model objects, doing logic on them, and approving the request... only for an app route handler to hit the DB again for the exact same objects to actually perform the behavior.

tcrayford19:08:57

yeller's project specific routes all start with /:team/:project/

tcrayford19:08:11

ah yeah. I assoc them directly into params for that reason 😉

amacdougall19:08:21

The middleware could always stash the objects in... jinx

tcrayford19:08:24

P E R F R O M A N C E

amacdougall19:08:48

perf-romance? 💗

tcrayford19:08:07

(actually I'm very happy with this approach for the most part. auth is in one place, auth specific db lookups are in one place, it's super easy to audit and even to change (though it hasn't changed in 3 years))

tcrayford19:08:25

for the record: re /admin: yeller just puts it on a different port and makes that vpn only. Not separate process (just too much effort to run imo), but different port at least saves me from worrying about one of the most common ways of owning webapps: a user getting control of an admin user's app

tcrayford19:08:44

(when I say vpn I really mean ssh tunnel because time pressure, a thing)

jrychter20:08:49

I'll drop in one more note on the middleware: I found that many approaches are unnecessarily hairy. I've spent a lot of time debugging friend, only to find out that it has a magical "/login" link embedded deep within itself, which I did not know about. I currently use buddy, which I think is overall better, but I still think things could be made much simpler.

amacdougall20:08:21

I've been scribbling about this in my notebook for the past hour, and I think that for my particular application, the permissions issue is too fine-grained to be appropriate for middleware. I think middleware functions best as a "set it and forget it" measure. Loading model objects and checking ownership in one place, and then doing app behavior to them in another place, is just too fiddly. @tcrayford's solution makes a lot of sense, but I think it's solving a somewhat different problem. Everyone's app is a special ❄️ ...

amacdougall20:08:47

❄️ = pirate ship helm covered in Swarovsky crystal, obviously

amacdougall20:08:25

Fun topic for future discussion: is there such a thing as overthinking it? 😣

tcrayford20:08:42

haha. Somebody should ask rich that at a conference

amacdougall20:08:39

I'm helping some of my co-workers learn Clojure, and sometimes I ask "so is this simple yet?"

tcrayford20:08:15

I like thinking about things like this in terms of the economics of maintenance more than anything else, like "here are my three alternatives. How much will each cost over 5 years of app maintenance/future development (as best as I can tell). What are the contributing factors to those costs?"

tcrayford20:08:26

(cost is an abstract feeling, not a number or anything, just a way to compare)

tcrayford20:08:05

you can apply said "economic thinking" to the costs of overthinking as well 😉

amacdougall20:08:07

Yeah. This involves a certain amount of guesswork; I can't tell you how many times I've done an ugly hack with TODO: refactor if we touch this again on it, and then... it just sits there for years, working just fine. But it's still better to take the long view.

amacdougall20:08:16

>you can apply said "economic thinking" to the costs of overthinking as well I'm gonna have to give that one some thought.

ordnungswidrig20:08:16

amacdougall: middleware doesn’t need to be applied on a global level. You can also apply wrappers on individual handlers and route to that wrapped handlers.

ordnungswidrig20:08:43

amacdougall: this can help decouple (and declutter) things

amacdougall20:08:47

True! I noticed that Luminus only applies CSRF protection to web views, for instance, not to API endpoints.

ordnungswidrig20:08:42

or, in you case, rely on routing parameters to extract the organization and project from the path and use that in an authentication wrapper

amacdougall20:08:52

In my case, the issue is that if I get an API request to edit a resource, particularly if it's nested, I generally have to load the relevant resources to make a decision. For instance, an unprivileged user can still delete unwanted comments on a post they authored. To decide whether a comment can be deleted, I'd have to load the post and the comment and check their ownership.

amacdougall20:08:11

The necessary information is not contained in the URL itself. (Unlike the usual auth examples, which are about restricting access to a page.)

ordnungswidrig20:08:15

sure, I just want to point out that you can do the loading in a wrapper, that uses from routing parameters set by compojure or whatever, lookup the values in the db, authenticate and store the actual project / organization value in the request and pass over to the wrapper handler.

amacdougall20:08:07

Definitely. I considered that, and I still am. It would work! The question becomes more philosophical at that point.

ordnungswidrig20:08:33

amacdougall: I like that because it separates aspects and makes them testable quite easily.

amacdougall20:08:30

I agree with that. Authorization and execution can quite reasonably be separated. So let's say we know an API request always requires us to load the post and the comment... we can do that with a wrapper that parses the request, identifies the desired resources, loads them, adds them to the request... and then the access rules wrapper can assume the existence of any resource necessary. So can the actual post-authorization domain logic.

amacdougall20:08:10

You might be winning me over, here. On one hand, you've got three kind of parallel sets of route->logic maps. On the other hand, they're separate concerns that take well-defined inputs.

tel21:08:52

is there a way to do something like figwheel-always for clojure.tools.namespace?

tel21:08:23

it doesn’t, rightfully, pick up changes to sql files I’m making when they’re influencing symbol creation via yesql