-
Notifications
You must be signed in to change notification settings - Fork 57
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
Enable fine-grained access control for full-stack signals #2835
Comments
FYI, Spring Security is now recommending a declarative approach (the |
What would the three use cases from the ticket description look like as a string inside an annotation like
|
The third case is actually two different things. The security check is about making sure only the owner can write. The balance check is a business rule that has nothing to do with security. I don't think we should try to shoehorn both of them into the same mechanism. As for the first two use cases, applied on individual signal methods, they would probably be doable without any custom SpEL syntax. However, since the annotation is about to be placed on the method that returns the signal, it probably gets more complicated. Spring uses AOP to implement the security checks, and in this case, the security check would be applied to the call to the method that returns the signal, not the signal itself. We may have to introduce our own annotation, or multiple annotations, that mimic @AuthorizeSignalWrite("hasPermission('signal:owner') || (hasPermission('signal:increment') && #incrementBy == 1)")
@AuthorizeSignalRead("hasPermission('signal:read')")
public NumberSignal mySignal() {...} And something like this for the second use case: @AuthorizeSignalWrite("hasPermission('signal:owner')")
@AuthorizeSignalRead("hasPermission('signal:read')")
public NumberSignal mySignal() {...} |
I guess the IDE wouldn't offer the developer much help in correctly writing
Couldn't you also argue that the limitation on allowable increment deltas for the poll is also a business rule rather than a security rule? |
I hope modern IDE:s with Spring support also offer help for SpEL. Personally, I prefer writing Java code to scripting in strings like that since you don't notice if you've made a mistake until runtime (if even then).
Yes, you could. |
Basic SeEL could be supported, but is it supported in a custom annotation like |
I don't know. We have to investigate that. |
My proposal is to allow to extend signals. Suppose that @RolesAllowed(Role.ADMIN) // to restrict "update" and "replace"
public class VoteSignal extends NumberSignal {
/**
* Restricts the vote amount to 1 or -1
*/
@AnonymousAllowed
@Override
public void incrementBy(double amount) {
return super.incrementBy(Math.signum(amount));
}
} In this case, you still get the default latency compensation from the superclass on the client, which works fine as long as the voting amount is correct. @RolesAllowed(Role.ADMIN) // to restrict "update", "replace", and "incrementBy"
public class VoteSignal extends NumberSignal {
/**
* Express the business action of voting
*/
@AnonymousAllowed
public void vote(double amount) {
return incrementBy(1);
}
// ... same for "unvote"
} This one expresses the intention more clearly, but it needs to allow the developer to define a latency compensation action for the generated |
One aspect that isn't directly supported when overriding methods is to intercept transactions in a way that allows you to review all the operations in the transaction as a whole. We can still solve that by also providing a more low-level transaction listener for that purpose while keeping signal subclasses as the main API to use for all of the most common cases. |
A potential future problem with overriding those methods is that there will eventually be a non-trivial return type from each operation rather than only |
What should the TS type be for a service method that returns a
|
That would definitely need some improvements to the generator, as I do expect a |
Given that, in the long run, we can't afford to send the whole updated value each time and we rather need an operation log to apply them incrementally, all things a method can do must go down to a sequence of those basic well-known operations. So, a method would be a simple logical wrapper around a sequence of one or more additions to the log. |
There's one thing we're forgetting: signals can be nested. As a reminder As a simple made-up example, let's say that we have a |
For validation, I think there are (at least) two alternatives here:
|
I propose alternative 3: you can wrap a signal instance to create a new signal with the same type but also using an interceptor callback that will be called for all operations to that signal or any of its child signals. In practice, combining signals does also mean that you always need to have one "write signal" and then somehow copy the values over to a "read signal" with the validated values. This copying is probably more application code than an interceptor and it does also have the drawback that you lose latency compensation unless there's also an API for combining both signals into one. But that combined signal is basically the same as the interceptor wrapper that I'm suggesting. |
Although I'm not sure that Java generics are good enough to allow this. |
Can't overload the existing But there will be another level of complexity with this approach when we get to |
I don't warm up to the idea that you have to extend a signal class in order to do any of this, including adding annotations to it. I would prefer an approach where I can do everything I need by composition. |
One option with composition rather than subclassing could look like this for the max string length option. ListSignal<String> unrestrictedStrings = new ListSignal<>(String.class);
ListSignal<String> restrictedStrings = unrestrictedStrings.withValueValidator(value -> value.length() >= 3); With this approach, restrictions would be applied for all client to which
The same example with a low-level interceptor could look like this: ListSignal<String> restrictedStrings = unrestrictedStrings.withInterceptor(operation -> {
if (operation instanceof ValueOperation valueOp) {
// ValueOperation covers all operations that carry a value, e.g. insert (for list signals), set (for value signals), and put (for map signals) as well as various conditional variants of those.
// Should actually also check for tree signal operations like inserting into a child but not doing that here for brevity.
String value = valueOp.getValue(String.class);
if (value == null || value.length() < 3) {
return SignalOperation.deny();
}
}
// Accept all other operations, e.g. removing or re-ordering entires
return operation;
}); One benefit of this model is that also applies to usage directly through the signal instance's API and not only to Hilla clients. This means that a service could return signal with an interceptor to e.g. some Flow UI logic and this would apply exactly the same restrictions that would be used for operations from a Hilla client. |
That looks better! |
Composition is proving to be a better programming model than inheritance, at least that's where most modern programming languages and libraries are going. But, even with composition, it should be possible to communicate an intent, like "vote", or "withdraw". That restricted string could be an username, for example. |
Communicating intent is a different feature than restricting which values / operations are allowed. The inheritance-based approach focused on the intent that was suggested earlier did still also need annotations to define restrictions. |
Would it be a good constraint to only support predicates at this point? ListSignal<String> readonlyStrings = unrestrictedStrings.withOperationValidator(operation -> false); |
A validator returning We would need these operation types for the initial implementation based on the available operations on the currently implemented signal types.
Note that the The initial implementation can assume that a legitimate client only sends operations that will be accepted so that the server-side validator is there only to protect against tampering. This means that the server can silently ignore operations that do not pass the validator. We will make sure the client remains in sync with the server even after sending an invalid operation only after we have introduced client-side latency compensation based on a stack of unconfirmed operations. Each |
Apart from introducing new methods for the above mentioned operations, would it be OK if we just provide an extra |
If we're going to have any shorthand API on top of the low-level feature, then |
Describe your motivation
There should be a way of only allow specific modification operations for specific signals that are returned to the client.
Use cases
The generic feature is to let server-side application logic decide which operations are allowed in a specific case for a specific user. In a poll application, there could be a
NumberSignal
for each option containing the number of users that have selected that option. In this case, users should only be allowed to submit operations to increment the number by 1 but not to do bigger increments or reset the signal to an arbitrary value. If the user is allowed to change their mind, then "incrementing" by -1 should also be allowed. Another way of implementing the same use case is that there's a list of the choice made by each user which means that users should only be allowed to add an entry to the list if the entry contains the user id of that user and if it's allowed to change their mind then it should only be possible to remove one's own entry.Furthermore, it should be possible to close a poll so that no additional updates are accepted from any user. This means that it needs to be possible to take other parts of the application's overall state into account when deciding whether an operation should be accepted. It's not enough to only use information about the currently logged in user. It should also be possible to configure the access control so that the poll owner can reset the value to any value, even if the poll is closed.
A simpler case is if some signal should be read-only for some users but fully modifiable for other users. A poll application could have a
ValueSignal
containing an object that describes the currently active question. All participants should be able to subscribe to events when the question is changes but only the poll owner can change the question. This seems like a simpler variant of the generic functionality that could work in the same way but potentially have a shorthand API.There's also an even more complex case to take into account in the design even if we don't implement it right away (or ever). If a bank account balance is represented by a
NumberSignal
, then any withdrawal should have the requirement that the withdrawn amount is larger than the current bank balance. Because of potential race conditions in a clustered setup, the access control logic cannot know what the signal value will be when the operation is applied which means that the submitted operation would have to be a "transaction" that conditionally updates the value only if the value at that time is large enough. One way of doing this is that the client must submit a proper conditional transaction and the access control logic verifies that this is the case. But it's redundant that the client even needs to know about this so it would be even better if the client could just submit regularincrementBy(-100)
operation and then the server-side access control logic will inspect that operation and actually submit a corresponding conditional operation to the underlying system.Additional requirements
.result
promise of the operation. If we ever implement support for replacing operations to e.g. include some additional conditions, then this should not lead to an exception from the promise but instead resolving the promise based on the status of the actually applied operation.incrementBy
so that the client can know how to show feedback to the user immediately.Out of scope
Describe the solution you'd like
No response
Describe alternatives you've considered
No response
Additional context
No response
The text was updated successfully, but these errors were encountered: