-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Reduce the perceived complexity of the security component #21029
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
Comments
i think that is an interesting idea to have a (delegated) security service. only the thing with login with using the Session might be a mit more complicated but i think that should be easy doable too. |
One problem I see coming back, is a god-class with its dependencies. People will want to inject the I would like to to add some remarks: $user = $this->get('security.token_storage')->getToken()->getUser(); By doing this, you might get issues depending one when you call this. I see people do this in constructors and get wonky bugs that are hard to reproduce. I see people do this in classes registered as service, which then get called when no user is available. Actually, getting the user is already easy. In your controller you can add it as argument for your method or call $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN'); I think it's a better idea stop hinting towards People think everything they add is a role but is actually an attribute, see: https://stovepipe.systems/post/symfony-security-roles-vs-voters
Role hierarchy should be flattened when creating the token: #20748 (comment). However, this could result in behavioural BC breaks when the hierarchy is updated when changed and the user is already logged in (?) $this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_ANONYMOUSLY');
$this->get('security')->isAnonymous(); There's a subtle difference here. The first is to check whether the authorization level is at least anonymous and the latter check if the user is an anonymous user. This feature is currently not available within symfony with isGranted but can be done in another way: // not available
$this->get('security.authorization_checker')->isGranted('ROLE_ANONYMOUS');
// available
$this->get('security.token_storage')->getToken() instanceof AnonymousToken; Regarding the Common tasks verbosity those are really nice when done in controllers (though you shouldn't). This could be something to add in the |
@iltar thanks for your remarks. As always, very interesting. I just want to add something: we're not talking here about a god-class (like, for example, RubyOnRails' ActiveRecord). We're talking about a small utility class with 10 to 15 methods. In fact, it's the same idea such as our Framework Base Controller or Console SymfonyStyle. They provide nice shortcuts for common operations and hide the low-level details. |
@javiereguiluz it can work but only if that specific class has only lazy dependencies (get run-time from the container). Otherwise, if people decide to start injecting this service (which IMO should be avoided at all costs), they will start hitting issues like #11690 again due to the |
@iltar you are right. This is very important. For now I'm confident because the work made by @nicolas-grekas and others to make everything lazy in Symfony will help us a lot. |
Basically this is Facade pattern 😃 (not like the Facade's used in Laravel). When this is accepted the php-doc and end-user documentation should warn against the storing of method results in the constructor. I believe this is already done for some cases, but I can't remember where. |
If using such class is a dependency hell, I think we should go for controller traits and/or using the base IMO, security in the controller is the best option. |
I think indeed adding a security god object will do more bad then good. It will only get bigger. With the base controller you'd already get this utilization. Without base controller, you probably know what your'e doing already. That would make problem 1&3 more or less a non issue. Problem 3 actually still requires Now, i'd be all up for utilization or shortcut methods within a certain API, if truly convenient. Ie. Same for making things more intuitive, ie roles and attributes, or programmatically doing certain tasks like |
👍 with that anyway. But I don't know if it should be done in the Token class and / or in the TokenStorage class. |
Practically it's the most logical imo. But it doesnt work out when typehinting against interfaces. However i dont have a real problem typehinting against concrete classes in controllers (without a base class). Making this work for everbody.
|
We should not forget that most of the stuff for new developers takes place in the controller from what I can tell, but I can only base this on personal experience from IRC. @javiereguiluz Regarding the following: // After:
@Security("is_granted('ROLE_ADMIN')")
@Security("is_granted('EDIT_PRODUCT')")
// Alternative
@Security("is_granted('ROLE_ADMIN') and is_granted('EDIT_PRODUCT')") This should simply be: @Security("is_granted('EDIT_PRODUCT')") If editing a product requires the user to be an admin, this should be integrated in the voter, otherwise you get inconsistent authorization rules. "Admin" is nothing more than a detail of the identification process on which you can base your authorization, but should imo not be used directly. If you were to strip the @Security("is_granted('ADMIN')") This does not define its intention at all. AlternativeWhat about a rule definition using the expression engine? This would be the easy way of writing simple voters. Instead of having to write a voter manually, this could generate them for the developer, making authorization slightly easier as the concept of voters can be hidden for now. security:
access_rules:
EDIT_PRODUCT: 'identify_as("admin") or owned_by(user)'
DELETE_PRODUCT: 'identify_as("admin")' Syntax and options just as example to illustrate the idea. The original ideaI would like to give some short feedback (again) on each subject after sleeping a bit on it. Problem 1 - Low level callsAuthorization checker & Token Storage: I'm still of opinion that it's a good idea to get rid of the Current features already supported:
Problem 2 - Unintuitive behaviorRole hierarchy: This should simply be flattened and inserted into the Token. Authentication level: e.g.
Removing the security:
access_control:
# anonymous: true is the default value
- { path: ^/login }
- { path: ^/dashboard, anonymous: false }
# same as @Security("is_granted('ACCESS_ADMIN_CP')") on the controller
# having configured the permissions option could imply that the default value
# of anonymous becomes false instead of true for this item
- { path: ^/admin, permissions: [ACCESS_ADMIN_CP] }
# can be anonymous, but still trigger a voter, weird edge case that _could_ be supported
- { path: ^/preview, permissions: [PREVIEW_DRAFT], anonymous: true }
# no longer need the following item, reducing complexity and issues:
#- { path: ^/, roles: IS_AUTHENTICATED_FULLY } There would be 1 problem still, that would be inside a template, granting permissions based on (e.g.) Problem 3 - Common tasks verbosityEncoding (hashing! rename?) a password / login a user manually: These methods could be added to the Controller and this is probably the only location where I think it's useful. |
I like the way of defining "access_rules" in the security.yml file. When your application grows, if you pretty much have to write a voter for each action (because each action requires a certain access authorization), it's over killing and the security logic is split in several classes. So to simplify, you end-up checking roles because it's faster. With the configuration, you centralize security definition in one single file: maintenance get easier and no need to create voters, we can have a single voter that handle all the definition. However, we need a way to check that the subject is of certain type. Maybe a simple
would be enough? I would prefix thing by "subject_" or "user_" to distinguish on what the control is. |
The security:
access_rules:
VIEW_PRODUCT_LIST: identified_as('admin')
EDIT_PRODUCT:
supports: subject_type_is('App\Entity\Product')
expression: identified_as('admin') or user_owns(subject) |
So where does request matching comes in? Should it be a firewall only thingy? edit: wait we have |
It doesn't have request matching, it's merely a definition of an access rule. This would be the same as writing a voter for it. |
@ro0NL I don't think you should match request here. You define authorizations, and then you have to add the @Security annotation in front of your actions. |
Speaking about; i dont think it's a good idea to make this a dependency if you want specific controller/action matching. We shouldnt force annotations for that.. The feature is priceless and should (imo.) be fixed by the framework, hence #20272 |
@ro0NL nobody forces the annotation. In your controller you can still use the voter and in the security config you can still use the access_control. Given my previous example: /** @Security("is_granted('EDIT_PRODUCT', product)") */
public function editProduct(Product $product);
$authorizationChecker->isGranted('EDIT_PRODUCT', $product); # with my previous proposal
security:
access_control:
# no subject, already the case in 3.2
- { path: ^/foo, permissions: [VIEW_PRODUCT_LIST] } |
Understand. And with simple - { route: foo, permission: [X, Y] } Priceless ;-) |
It's the same as in the current situations, so nothing would change: security:
access_control:
- { path: ^/foo, roles: [VIEW_PRODUCT_LIST] } But lets return to the actual topic 😉 |
I really'd like a shortcut for the "login a user" part. I use this code pretty much in each project (after user registration or password change). I think we should also bring a shorcut in the controller for that. |
I think only chortcut login method make sense. I don't sure it will be good put login method in base controller. Mostly I need use login in test and particularly in Behat tests. |
A manual login might be nice for cases after registration, where previously no authentication was known, such as a registration. However, I prefer to have a token based login with a redirect, but this is quite more complex than doing a simple login. For @Vinorcola's case, the user is logged out because the user has changed. If changing a password should not log out the user, change that particular login in your user (implement the |
Please note that the manual login has been requested before (in the DX-hype period). However, in a PR implementing this it was concluded that it is impossible to make such a feature working in all cases. |
For the proposal about a configuration-based voter, I suggest people interested in it to start experimenting in outside the core, to see how useful it is in real projects (rather than just in theory), as was done for the Guard component. |
@stof Security is complex to teach, and teaching about voters to newcomers make them look at the security component as the most complex to use, and most of the time they have problems in understanding how voters work. This |
@Pierstoval I'm aware that a single voter would implement it. My point is that you should start implementing this voter in a bundle to try the proposal and see whether the configuration-based voter is flexible enough or whether you end up writing custom voters again in your own real-world projects |
not 100% related to this issue but to this topic when beginning a new Symfony project and thinking about security, what would be helpful in the symfony docs where some comparation or list that shows when to use the one thing and when the other. (or both) in that special case i did use a simple_preauth and did the logic there. (auth was connected to an external service to check if some ticket is still valid) |
@stof What do you think of this POC I just made? https://github.com/Orbitale/PermissionsBundle One single voter, default variables as helpers, and an ExpressionFunctionProvider that can be customized to fit needs in terms of security (and could be replaced by core provider instead if needed, but for now I wanted to use a custom one just for the PoC in case of). |
Based on this discussion I came-up with the following concept, note that interfaces are not suffixed and more decoupled then usually done the Symfony code base. But I'm open to suggestions 😉 Permission systemInspired on suggestions from @Pierstoval (Alex Rock Ancelet). Legend
ExecutionThe system works with Permissions, a Permission answers a simple yes/no A Permission can be a marker like The A Permission can implement one of the following interfaces, all which derive
Special interfaces (advanced features):
|
Lets get rid of roles completely. You have a token. The token identifies the user. The token can have groups it belongs to (maybe similar to oauth2 scopes?).
Attributes is extremely ambiguous. I would love to see a better name for this.
It's actually 'can Token execute Permission'. I'm also missing voters in your system, how will multiple parts of an application vote on a permission? I don't really think the suggested permission system will work. I think that the "Role" concept and "User" object should be removed from the current system, leaving us with:
The current Authorization (permission) system can be confusing because of some limitations, such has having only 1 subject to vote on. Sometimes it's good to know if you can do a specific action for N subjects (same object). You can always create data wrappers, but this is more complex and won't work easily with I think a lot of power lies in annotations (but should be optional): class BarController
{
/**
* @isGranted()
* same as
* @isGranted("BarController::fooAction", {request, object, entity})
*
* @isGranted("VIEW", {object, entity})
* same as
* @isGranted("VIEW", entity)
* @isGranted("VIEW", object)
*/
public function fooAction(Request $request, SomeObject $object, SomeEntity $entity);
} // would require a small adaption in voters
- public function voteOnAttribute($attribute, $subject, TokenInterface $token)
+ public function voteOnAttribute($attribute, array $subjects, TokenInterface $token) The security annotation is quite powerful, but writing the On top of that, I often see people needing to preview permissions for other users. This is impossible with the current system. If we only have a Token and no more User object, this could be done easily with a new kinda of method: $token = // ... createTokenFor($identifier), security method
public function isGrantedFor($token, $attribute(?), $subject); |
👍 Token, I couldn't come-up with a better name :) ROLE is whats used by PostgreSQL to define a User or group of users ;) This is more a replacement for the ACL and current ROLE/attribute system, the current voting system is something we shouldn't discard as it's the most powerful.
This concept started out differently, as an DomainAction with attributes. So better a naming/concept is needed for this. |
Groups? No, I don't think so. Attributes is the best option to me, because attributes are the stuff that validates an action via voters. Roles are just attributes checked by a specific voter (that uses the role hierarchy), actually. Getting rid of roles is a seducing idea, but it can actually simplify a lot of common actions if you have a system where you can manipulate users' roles.
Totally agree, a Role should just continue to be an attribute. Actually, the idea I have is that maybe roles & attributes could be merged in a specific In the current system, it would simply return the full list of user's roles (using role hierarchy) and special attributes (like This would make things much easier for voters when implementing the
I also agree on @iltar's point about voters. @sstok: your proposal does not seem to keep consistency with current roles and tokens, as well as voters. I like the idea of a This would lead in a class hierarchy a bit cooler (I'm writing a diagram for this idea) |
I took your idea, and extended a little upon it 👍 but I don't like having to define expressions, I would rather use a class with actual PHP. Ask If Token was granted the Permission, they have access. A Permission can be an Attribute (IS_AUTHENTICATED_). Or something a little more advanced; Say I want to create a FtpUser for the WebhostingAccount. I need to be either the owner of the WebhostingAccount (can be determined by a Voter) or be granted permission, currently I have no way to determine if I have permission because Roles don't have know about objects or and ACL only allows to reference existing objects. So I can't grant FtpUser::create because there is no object-id or way to determine there relationship. By using a Permission I can grant |
What you are talking about is a simple voter, that's all 🤣 |
@Pierstoval that naming is not correct. They are not permission attributes, they are an extend to identification. I mentioned groups or scopes, because they help identify. If you call them PermissionAttributes, it would imply that if I gave |
@Pierstoval And it only took me 1 year to make it this simple... 🙂 Edit. Sorry for wrong mention 😇 |
Yes, I'm probably misexplaining, but actually, token attributes would be the ones that would correspond to what we define as a permission. If you take the example of roles, you have the app definition: role hierarchy, and the token definition: Token::getRoles(). I was more narrowing down the concept of role to a concept of permission 🙂 |
Well the Token is only responsible for identification, nothing else. It's just extremely confusing to have both "Attributes" - which is very ambiguous - and "Roles", which imply some sort of groups but are abused to give access to when the token has this role. However, this does not work for the role hierarchy because it's calculated runtime. Regarding attributes, I think a rename is required, but not sure into what. Can be Security Attribute, Permission Attribute or even an Action Identifier (because often tightly coupled to an action in your application). |
That's why merging attributes and role is necessary so roles could be just "special" attributes, managed by the Role voter 🙂 |
I think they should actually be completely separated 🤣 |
When you are doing IMO, roles are just "special attributes" managed by a voter. In the case of my PermissionsBundle, permissions are just related to ExpressionLanguage, that's all. Still, attributes are static properties related to a token/user, and the permission system relies on voters that are based on three things:
As said, roles are just "special attributes", nothing more |
The hard thing in this discussion is that there are 2 different concepts named
I think you are not all talking about the same attributes (or maybe even mixing the concepts in the same reply). Permission attributes are not something you want to enforce storing inside a token. |
I'd like to revive this discussion as 4.0 is nearing fast and that will be the end for another 2 years for invasive breaking changes. In my opinion the current authentication layer is broken and stuck in 2010 paradigms, see also #23081 and #21998. The security component as a whole isn't terribly complex in itself, dropping ACLs was a good idea, and right now I think the access control and authorization layers are fine for the next few years. It's mainly the authentication itself, where Guard has already improved some things for custom authentication mechanisms, but the |
It's a good start but I personally have more issues with |
Perhaps you could introduce a very simplistic interface with only |
Yes, I think that would the ideal goal in the end. Something like an |
Closing because the scope of this issue is too broad and not actionable. Moreover, @iltar and others are making some improvements related to security lately, so we're already solving these problems. Thanks! |
I'm sure that the Security component is great ... but most Symfony newcomers think that it's an abhorrent monstrosity. Could we please discuss about solutions to reduce its perceived complexity?
Problem 1 - Low level calls
I think this is the worst problem for newcomers. Symfony forces you to make "low level" calls for all its operations.
Example: getting the current user (what's a token storage?)
instead of:
Example: checking permissions (who's the authorization checker?)
instead of:
Problem 2 - Unintuitive behavior
Example: defining a role hierarchy simplifies the assignment of roles for users, but complicates the code a lot. You need to deal with the
getReachableRoles()
method, etc.Example: the management of the user types is so confusing. Checking if a user is anonymous is usually done like this:
First, it's incredibly verbose. Second, it returns
true
also when the user is NOT strictly anonymous (for example when the user is logged in).Why not add something like this that returns
true
ONLY when the user is really anonymous (not logged in, not remembered, etc.):The anonymous handling in general is very confusing. For example, having to add the
anonymous
option to firewalls is always a big "WTF?" in workshops for Symfony newcomers.Problem 3 - Common tasks verbosity
Example: encoding a password for the currently logged user
Instead of:
Example: login a user.
instead of:
Problem 4 - Roles, attributes
IS_AUTHENTICATED_ANONYMOUSLY
a role or not?We could make everything "security permissions" and forget about roles.
Maintaining the
ROLE_
prefix would be a good idea for most apps, but if they were permissions, people could remove it if needed. Example: "does the user have theADMIN
permission?" instead of "does the user has theROLE_ADMIN
role?".Final remarks
I'm not proposing to remove the low level security calls. I'm not asking to hide Security features. I'm not asking to remove developer's control of Security component. I'm just asking to:
References
A while ago I published a proof-of-concept bundle with some of these ideas: EasySecurityBundle. Don't use it in real applications! I don't know if it's safe and it's not optimized for performance. It was just a proof of concept to see if this idea is doable.
The text was updated successfully, but these errors were encountered: