Just looked through it some more. I would say:
1. The env has things like identity/session/ring request (if you add it), so that should be leveraged to do permissions/identity.
2. The input
of the resolver/mutation tells you the “what” of the access
So, the main feedback is essentially what I already gave you: I’d use a (fn [env input] )
, where input
for a read is assumed to be one of the ao/identities
or possibly a sequence if you’re supporting batch.
and input
is mutation params if it is a write…that perhaps makes it a bit tough. Forms can be composed in an arbitrary way. I agree with you that a partial auth failure on save should prob be treated as an attack and denied, but the fact remains that individual fields will have granular permissions, so for writes you really need to analyze things.
For example, my write protections in my systems put an ownership link on every major entity (e.g. :item/owner _ref-to-acct-entity
)_ which allows for the most important and common write permission check: “do you own it (or have write access to that other owner) already”? A write of something with a tempid needs the ownership added, and a write with a real ID need to be verified. This makes something like save-form!
easy to augment, because that creates an easy-to-verify pattern in general
So, my form save auth middleware is pretty trivial, and general.
I made a blog video on the new work around hooks and easier APIs. Have a look and see what you think: https://youtu.be/3_HsamkcZu4
do you think that would be possible to use completely without react? so that the "definition" parts all stay fulcro only and then there iis just the choice of either using react hooks to hook into the DOM lifecycle or using my shadow-grove stuff instead. it looks like that should work right?
(def User (fulcro/something [:user/id :user/name ...]))
(defc ui-user []
(bind data
(fulcro-grove/use User {...}))
(render
(<< [:div (:user/id data)])))
currently its all coupled to react hooks but it doesn't have to be right?
probably not a goal for now and thats fine but it would be neat if at some point I could just leverage all the work you've done on fulcro and just use it without react 🙂
this looks really cool, and seems like it's trending with some of the experiments I've been doing myself outside of Fulcro with my EQL lib https://github.com/lilactown/autonormal and React wrapper https://github.com/lilactown/helix. However, I still find the Fulcro API surface overwhelming. There's still a ton of "Fulcro take the wheel" in what I'm seeing in this video, which makes it incredibly daunting to adopt e.g. at work where I would be much more inclined to use something that has been production tested, rather than my hare-brained experiments 🙂 Sort of in line with thheller, I would like to see Fulcro continue to be boiled down to less and less UI stuff: create an entity, instantiate an entity, fetch an entity, listen to changes. As someone who's very very familiar with React and building UIs, but not super familiar with Fulcro, there's so much Fulcro on the screen that it's difficult for me to figure out what's essential to what I want to do - basically fetch data from Pathom and store it in a normalized db - and what's useful for Fulcro apps that are bought into Fulcro's way of organizing a project. I hope this is useful feedback. 🙏:skin-tone-2:
@thheller Yes, this decouples it from React. I’m using hooks as a way to get to the side-effects, but if you look at the implementation, there’s no React required. The “render listener” happens after transactions. I’d be happy to finish disconnecting it cleanly from React if we find any other hard dependencies…really just the mount and initial root render are tied to react.
oh that is good to hear. I'll check the raw-components stuff you have and see if I can port it.
@lilactown Fulcro does almost nothing with UI and React. There’s a DOM ns and render plugins. The vast majority of the work is around the data model management, network interaction, transaction processing/ordering, remote interaction. Yeah, Fulcro takes the wheel on all that stuff. That’s the design. I saw your experiments. More power to you if you see a way to distill something useful out what is here. The real complexity in real distributed apps is non-trivial, and therefore not small. That said, Much of Fulcro (UISM, Dynamic Routing, DOM, Form State) should be viewed as optional things you can use, not things you must use. I don’t have time to manage all of those bits as a separate project/lib/repo/doc…so they are combined together…but they truly are pretty separate.
@thheller yeah, you just need some way of dealing with “lifecycle”
and getting the render update to the right place on the screen
but the render listener approach just basically gets you an event you can use to pull data from the db after transaction steps (and network results). So, there’s no need to even embed it specifically in the UI layer. Fulcro has basically been this way for most of the 3.x lifespan, BTW. Most of the internals are usable in a completely headless way, and the rendering has been pluggable as well. I just haven’t done much documentation around how you get truly away from React, but React really isn’t coupled in very many places. application.cljc
does not require it, though components
does. but that is due to defsc
making react components. So, at the moment you’d get some React “bloat” in your build, but you can completely avoid calling any functions.
The add-hooks-options!
actually has no real ties to React, and in fact just sets up a function so it responds properly to Fulcro requests for things like get-query
and such…so, it could even be renamed add-pure-function-options!
or something.
You could redirect the React requires to a stub file and avoid the bloat, and I think you probably know how to do that 😄
yeah I glanced over the code and I can easily replicate what the react use-state hook does as well as the use-lifecycle
I'll get back to you when I have questions
excellent. looking forward to what you come up with
Thanks for the response.
As for 1 above, my ad hoc strategy was to have the developer https://gist.github.com/cjsauer/9ab075ca995d7ee855040271c766db27#file-authorization-clj-L14 when creating the pathom plugin. In reality tho this is just a convenience.
As for 2, here’s my thinking on the interface being (fn [env entity-ident user-ident])
hung on defattr
. I want an interface that lets me surgically select an edge from the graph and answer the question “can this specific user perform c|r|u|d on this exact edge leading from this exact entity?“. The “this exact entity” part is what is difficult (for me at least) to generalize. With reads, it’s easy to just follow pathom’s parse, because it keeps track of the current entity for you. For form saves, it’s also easy, because the diff is in the perfect shape to make this trivial (https://gist.github.com/cjsauer/9ab075ca995d7ee855040271c766db27#file-authorization-clj-L60 over the [k v]
tuples of that delta, calling the auth
function on every key that is attempting to be modified.
But for arbitrary mutations, just being handed an input
alone is a bit tough like you said…I can’t quite see how I’d bend that to obtain entity-ident
for every attribute in that input.
===
Here’s a thought that might help us ground this discussion: do you think that the ideal authorization interface would be (fn [env user e a v]) -> #{:c :r :u :d}
? In other words, the most general access control would check every single datom that is attempting to be read, or that would result from a successful mutation. This is what’s been guiding my experiments. Can I reason about access at the fact level?
As a complex example, you could imagine an access control scheme that depends on all three e a v
. Imagine a system where admins can schedule patients at any time of day, but “schedulers” are only allowed to schedule appointments between the hours of A and B. This would be access control that involves
• e
(which schedule we’re affecting)
• a
(the :appointment/time
)
• v
(must be between hours A and B)
Remember that saves happen thorugh form diffs, which are normalized. There’s an ident, and then there are before/after values. Often for permissions you need all of the input/params to make your decision. I would not go for an EAV concept at all.
The UI layer might just need a different option key, because those decisions are likely to be UI-centric, and less narrowly focused on data security.
The server layer is what I’m speaking about, and at that layer you want the general env
(which needs to be customizable) and then you always need every bit of context that is needed for the resolution of the request: The input (for resolver) or params (for mutation).
IMO, that is the most general way to go about it for the I/O layer at least…but the UI layer may in fact need a different mechanism.
for you example, imagine that the security of your scheduling system also depends on which doctor you’re seeing, and which insurance you have.
EAV is simply insufficient
> Often for permissions you need all of the input/params to make your decision. I would not go for an EAV concept at all. Hm yea, that’s a good point. EAV is too local, and not able to interact with other tuples. Very good point.
Back pedaling a bit, have you attempted to use your custom defresolver/mutation
macros with RAD at all? How would they interact with the auto-resolvers for example?
Thinking more, EAV seems general enough to cover access control of reads, as I can’t think of interdependencies between the viewing of facts. Things like “you can only read X if you also read Y” just don’t really happen.
However they fail when it comes to writes, which often have invariants that need to be upheld.
Ignoring my flawed save/delete middleware, the wrap-read
pathom plug-in solution I shared might still be viable.
I have a post-load mutation. The load! retrieves a list of people from the server. In the post I want to set the first person in the db as a selected-person in a different component. I’ve tried a few different options and none seem to work.
(defmutation post-load [_]
(action [{:keys [app state]}]
(log/spy @state)
(let [person (->> @state ::person/id vals first)
id (-> person ::person/id str)]
(swap! state merge/merge-component person-ui/PersonRoot person :target [:component/id ::person-ui/root :ui/selected-person]))))
;(swap! state targeting/integrate-ident* state [::person-model/id id] :target [:component/id ::person-ui/root :ui/selected-person])
;(swap! state merge/merge* [:component/id ::person-ui/root :ui/selected-person] (log/spy person)))))
;(routing/route-to! app tabs/ProfileTab {::person/id id}))))
Batching N+1 queries. Reads need a generalize input that is probably always a sequence so that you can resolve N things. Pathom started out with that morphing, but always making it a sequence turns out to be the right thing.
I’ve also tried
(swap! state merge/merge-component person-model/Person person :target [:component/id ::person-ui/root :ui/selected-person])
which is the component that matches the Person query in the root component.Merge doesn't make sense to me since you do not want to insert data into the DB, only add an "edge" / pointer to existing data. How does the targeting call not work for you? Also notice swap is just a map. You can change it manually: (assoc-in state [:component/id :person-ui/root :ui/selected-person] [:person/id <the ID value>)
@holyjak is there an idiomatic way to add an edge?
Btw I’ve been using Fulcro troubleshooting and it’s great.
The targeting namespace but I saw you already tried that... Just wanted to point out it is just data, no magic
@holyjak targeting ns worked in the end.
but there can be more than one fact required per input. A natural key, for example, might be two incoming facts to resolve one
this is why pathom inputs are sets
And the swap! that I would have expected to work.
Looks like the named option is :replace not :target with merge/merge-component.
I’m trying to use a component that joins with a union component and hitting this error https://github.com/fulcrologic/fulcro/blob/develop/src/main/com/fulcrologic/fulcro/components.cljc#L1010 I’m attempting to do something like
(comp/get-query (add-extra-params ns/OtherComponent))
I’ve also tried manually building the query and encounter the same error. Do all union joins need to be backed by their own component?Having a look at the code this seems to use dynamic queries which I haven’t touched at all so I’m leaving this for now and just lumping all the query params on the ‘base’ union component
I’m aiming for a sort of ‘higher order’ union component. I’ll leave this until I’ve internalised dynamic queries
yes union queries need their own component.
Thanks 👍