I want to use isGranted('EDIT', $userObject) for allow edit given user data by all administrators and managers and that one user.
Should I use ACL for control edit $userObject?
I have written extra Voter which check if logged user and given object are the same or user is manager or admin.
In acl I must add ACE for userObject for all administrators, managers and that one user.
Wchich way is recommended?
I am new in Symfony..
below is voter's code:
function vote(TokenInterface $token, $object, array $attributes)
{
$intersect=array_intersect(array('EDIT','VIEW' ), $attributes);
if (!empty($intersect))
{
//intersect is not empty, it seems to edit or view are in $attributes
//voter grants privileges for [user->granted object]
//manager->every customer, child-manager
//admin->every customer and manager
if ($token->getUser()->isAdmin())
{
return VoterInterface::ACCESS_GRANTED;
}
elseif ($token->getUser()->isCustomer())
{
//voter not want to think about customer grants, because customer grants currently are held in ACL
return VoterInterface::ACCESS_ABSTAIN;
}
/* #var $object \PSB\StoreBundle\Entity\Customer */
if (is_a($object, '\PSB\StoreBundle\Entity\Customer'))
{
if ($token->getUser()->isManager())
{
//managers also edit customers
return VoterInterface::ACCESS_GRANTED;
}
}
elseif (is_a($object, '\PSB\StoreBundle\Entity\Manager'))
{
/* #var $object \PSB\StoreBundle\Entity\Manager */
if ($token->getUser()->isManager())
{
//manager can edit own children
if ($token->getUser() == $object->getParent())
{
return VoterInterface::ACCESS_GRANTED;
}
}
}
}
return VoterInterface::ACCESS_ABSTAIN;
}
When your model already stores the data required to know if an action should be granted or not, it's really annoying to keep the ACL in sync with your real data.
So you should obviously implement your own voters for this.
PS: You should use $object instanceof Class instead of is_a($object, 'Class')
Related
I'm using BreezeJS and have a question regarding how data is saved. Here's my code and comments
[Authorize]
/*
* I want to point out the security hole here. Any Authorized user is able to pass to this method
* a saveBundle which will be saved to the DB. This saveBundle can contain anything, for any user,
* or any table.
*
* This cannot be stopped at the client level as this method can be called from Postman, curl, or whatever.
*
* The only way I can see to subvert this attack would be to examine the saveBundle and verify
* no data is being impacted that is not owned or related directly to the calling user.
*
* Brute force could be applied here because SaveResult contains Errors and impacted Entities.
*
*/
[HttpPost]
public SaveResult SaveChanges(JObject saveBundle)
{
return _efContext.SaveChanges(saveBundle);
}
To limit access to a callers ability to retrieve data I first extract from the access_token the user_id and limit all my queries to include this in a where clause, making it somewhat impossible for a user to retrieve another users data.
But that would not stop a rogue user who had a valid access_token from calling SaveChanges() in a brute force loop with incremental object ids.
Am I way off on this one? Maybe I'm missing something.
Thanks for any help.
Mike
The JObject saveBundle that the client passes to the SaveChanges method is opaque and hard to use. The Breeze ContextProvider converts that to a map of entities and passes it to the BeforeSaveEntities method. BeforeSaveEntities is a method you would implement on your ContextProvider subclass, or in a delegate that you attach to the ContextProvider, e.g.:
var cp = new MyContextProvider();
cp.BeforeSaveEntitiesDelegate += MySaveValidator;
In your BeforeSaveEntities or delegate method, you would check to see if the entities can be saved by the current user. If you find an entity that shouldn't be saved, you can either remove it from the change set, or throw an error and abort the save:
protected override Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(
Dictionary<Type, List<EntityInfo>> saveMap)
{
var user = GetCurrentUser();
var entityErrors = new List<EFEntityError>();
foreach (Type type in saveMap.Keys)
{
foreach (EntityInfo entityInfo in saveMap[type])
{
if (!UserCanSave(entityInfo, user))
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Forbidden)
{ ReasonPhrase = "Not authorized to make these changes" });
}
}
}
return saveMap;
}
You will need to determine whether the user should be allowed to save a particular entity. This could be based on the role of the user and/or some other attribute, e.g. users in the Sales role can only save Client records that belong to their own SalesRegion.
I am attempting to implement security to restrict access to specific blogs by using USER_ROLE for each user. Each blog page has 1 owner that can post to it.
This is built on what I've learned from the fantastic tutorials from KNP University.
http://knpuniversity.com/screencast/symfony2-ep2
I've set this up using access_control in security.yml to limit access to each user based on their USER_ROLE. (user1 has access to /job1/new and /job1/create in order to create/edit/delete posts on the blog page---only 1 user has access to each blog page)
access_control:
- { path: ^/job1/new, roles: [ROLE_USER1, ROLE_ADMIN] }
- { path: ^/job2/new, roles: [ROLE_USER2, ROLE_ADMIN] }
- { path: ^/job3/new, roles: [ROLE_USER3, ROLE_ADMIN] }
- { path: ^/job1/create, roles: [ROLE_USER1, ROLE_ADMIN] }
- { path: ^/job2/create, roles: [ROLE_USER2, ROLE_ADMIN] }
- { path: ^/job3/create, roles: [ROLE_USER3, ROLE_ADMIN] }
Each /job1, /job2 etc. are separate blog pages. And I am using an if statement in Twig to determine which user has access to create/edit/delete posts.
{% if is_granted('ROLE_USER1') %}
<li>Add New Post</li>
{% endif %}
The problem is, as I add in more blog pages I will need to create more paths in the access control (e.g., /job4, /job5 etc.) which isn't an ideal solution although it does work.
I have detailed the code out here in the link below, as it was recommended to use security in the controller based on a Disqus conversation 'joe joe' with Ryan Weaver here --- http://knpuniversity.com/screencast/symfony2-ep2
My questions are:
1) Now that I have created a ManyToMany relationship with User and Category how do I setup security in the controller to prevent other users from accessing the create/edit/delete actions that they don't have the roles for?
2) How do I hide the option in Twig for create/edit/delete using this method / also what do I add in the access_control using this approach?
It is unrealistic to grow access control such that it needs to explicitly include each new user. Rather, one can use ROLE_USER to only allow access to editing/creating any entity (e.g., a blog) to an authenticated user. Once the user has authenticated, a controller can provide access to that user's blog entities.
This requires a one-to-many relationship between user and blogs. In the controller, it then becomes a simple matter of something like this:
...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/**
* ...
* #Security("has_role('ROLE_USER')")
*/
class BlogifyController extends Controller
{
public function blogAction
{
$user = $this->getUser();
$blogs = $user->getBlogs();
// do your form thing, etc.
...
}
}
I think you should use builtin Voter from Security Bundle. So, you create a Blog entity with 1:1 relationship to User Entity, then create a Voter service, build you logic in vote method, use that voter in controller, that's it. Here is sample example:
class SomeVoter implements VoterInterface
{
const CREATE = 'create';
const EDIT = 'edit';
const DELETE = 'delete';
/**
* #param string $attribute
* #return bool
*/
public function supportsAttribute($attribute)
{
return in_array($attribute, array(
self::CREATE,
self::EDIT,
self::DELETE
));
}
/**
* #param string $class
* #return bool
*/
public function supportsClass($class)
{
$supportedClass = 'Acme\DemoBundle\Entity\Blog';
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
* #param TokenInterface $token
* #param object $blog
* #param array $attributes
* #return int
*/
public function vote(TokenInterface $token, $blog, array $attributes)
{
....
$attribute = $attributes[0];
$user = $token->getUser();
switch($attribute) {
case 'edit':
if ($user->getId() === $blog->getUser()->getId()) {
return VoterInterface::ACCESS_GRANTED;
}
break;
....
}
...
}
}
controller action:
public function editAction($id)
{
$blog = ...;
if (false === $this->get('security.context')->isGranted('edit', $blog)) {
throw new AccessDeniedException('Unauthorised access!');
}
...
}
First off, I'm not using FOSUserBundle and I can't because I'm porting a legacy system which has its own Model layer (no Doctrine/Mongo/whatsoever here) and other very custom behavior.
I'm trying to connect my legacy role system with Symfony's so I can use native symfony security in controllers and views.
My first attempt was to load and return all of the user's roles in the getRoles() method from the Symfony\Component\Security\Core\User\UserInterface. At first, it looked like that worked. But after taking a deeper look, I noticed that these roles are only refreshed when the user logs in. This means that if I grant or revoke roles from a user, he will have to log out and back in for the changes to take effect. However, if I revoke security roles from a user, I want that to be applied immediately, so that behavior isn't acceptable to me.
What I want Symfony to do is to reload a user's roles on every request to make sure they're up-to-date. I have implemented a custom user provider and its refreshUser(UserInterface $user) method is being called on every request but the roles somehow aren't being refreshed.
The code to load / refresh the user in my UserProvider looks something like this:
public function loadUserByUsername($username) {
$user = UserModel::loadByUsername($username); // Loads a fresh user object including roles!
if (!$user) {
throw new UsernameNotFoundException("User not found");
}
return $user;
}
(refreshUser looks similar)
Is there a way to make Symfony refresh user roles on each request?
So after a couple of days trying to find a viable solution and contributing to the Symfony2 user mailing list, I finally found it. The following has been derived from the discussion at https://groups.google.com/d/topic/symfony2/NDBb4JN3mNc/discussion
It turns out that there's an interface Symfony\Component\Security\Core\User\EquatableInterface that is not intended for comparing object identity but precisely to
test if two objects are equal in security and re-authentication context
Implement that interface in your user class (the one already implementing UserInterface). Implement the only required method isEqualTo(UserInterface $user) so that it returns false if the current user's roles differ from those of the passed user.
Note: The User object is serialized in the session. Because of the way serialization works, make sure to store the roles in a field of your user object, and do not retrieve them directly in the getRoles() Method, otherwise all of that won't work!
Here's an example of how the specific methods might look like:
protected $roles = null;
public function getRoles() {
if ($this->roles == null) {
$this->roles = ...; // Retrieve the fresh list of roles
// from wherever they are stored here
}
return $this->roles;
}
public function isEqualTo(UserInterface $user) {
if ($user instanceof YourUserClass) {
// Check that the roles are the same, in any order
$isEqual = count($this->getRoles()) == count($user->getRoles());
if ($isEqual) {
foreach($this->getRoles() as $role) {
$isEqual = $isEqual && in_array($role, $user->getRoles());
}
}
return $isEqual;
}
return false;
}
Also, note that when the roles actually change and you reload the page, the profiler toolbar might tell you that your user is not authenticated. Plus, looking into the profiler, you might find that the roles didn't actually get refreshed.
I found out that the role refreshing actually does work. It's just that if no authorization constraints are hit (no #Secure annotations, no required roles in the firewall etc.), the refreshing is not actually done and the user is kept in the "unauthenticated" state.
As soon as you hit a page that performs any kind of authorization check, the user roles are being refreshed and the profiler toolbar displays the user with a green dot and "Authenticated: yes" again.
That's an acceptable behavior for me - hope it was helpful :)
In your security.yml (or the alternatives):
security:
always_authenticate_before_granting: true
Easiest game of my life.
From a Controller, after adding roles to a user, and saving to the database, simply call:
// Force refresh of user roles
$token = $this->get('security.context')->getToken()->setAuthenticated(false);
Take a look here, set always_authenticate_before_granting to true at security.yml.
I achieve this behaviour by implementing my own EntityUserProvider and overriding loadByUsername($username) method :
/**
* Load an user from its username
* #param string $username
* #return UserInterface
*/
public function loadUserByUsername($username)
{
$user = $this->repository->findOneByEmailJoinedToCustomerAccount($username);
if (null === $user)
{
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
//Custom function to definassigned roles to an user
$roles = $this->loadRolesForUser($user);
//Set roles to the user entity
$user->setRoles($roles);
return $user;
}
The trick is to call setRoles each time you call loadByUsername ... Hope it helps
Solution is to hang a subscriber on a Doctrine postUpdate event. If updated entity is User, same user as logged, then I do authenticate using AuthenticationManager service. You have to inject service container (or related services) to subscriber, of course. I prefer to inject whole container to prevent a circular references issue.
public function postUpdate(LifecycleEventArgs $ev) {
$entity = $ev->getEntity();
if ($entity instanceof User) {
$sc = $this->container->get('security.context');
$user = $sc->getToken()->getUser();
if ($user === $entity) {
$token = $this->container->get('security.authentication.manager')->authenticate($sc->getToken());
if ($token instanceof TokenInterface) {
$sc->setToken($token);
}
}
}
}
Sorry i cant reply in comment so i replay to question. If someone new in symfony security try to get role refresh work in Custom Password Authentication then inside function authenticateToken :
if(count($token->getRoles()) > 0 ){
if ($token->getUser() == $user ){
$passwordValid=true;
}
}
And do not check for passwords from DB/LDAP or anywhere. If user come in system then in $token are just username and had no roles.
I've been battling this for Symfony4, and I think I've finally settled down to a solution.
The thing is that in my case, the roles depend on the "company" the user is working with. It may be a CEO in one company, but an operator in another one, and the menus, permissions, etc. depend on the company. When switching companies, the user must not re-login.
Finally I've done the following:
Set the firewall to stateless.
In the FormAuthentication class, I set an attribute in the session explicitely, with the username.
I set up another Guard, which essentially take this attribute and loads the user for it from the database, for every single request.
class FormAuthenticator extends AbstractFormLoginAuthenticator
{
/** Constructor omitted */
public function supports(Request $request)
{
return 'app_login' === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'nomusuari' => $request->request->get('nomusuari'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['nomusuari']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $userProvider->loadUserByUsername($credentials['nomusuari']);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Invalid user/password');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
$valid = $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
return $valid;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$request->getSession()->set("user_username",$token->getUsername());
return new RedirectResponse(
$this->urlGenerator->generate("main")
);
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
The SessionAuthenticator (returns JSON, you may have to adapt it):
class SessionAuthenticator extends AbstractGuardAuthenticator
{
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*/
public function supports(Request $request)
{
return $request->getSession()->has("user_username");
}
/**
* Called on every request. Return whatever credentials you want to
* be passed to getUser() as $credentials.
*/
public function getCredentials(Request $request)
{
return $request->getSession()->get("user_username","");
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if (null === $credentials) {
// The token header was empty, authentication fails with HTTP Status
// Code 401 "Unauthorized"
return null;
}
// if a User is returned, checkCredentials() is called
/*return $this->em->getRepository(User::class)
->findOneBy(['apiToken' => $credentials])
;*/
return $userProvider->loadUserByUsername($credentials);
}
public function checkCredentials($credentials, UserInterface $user)
{
// Check credentials - e.g. make sure the password is valid.
// In case of an API token, no credential check is needed.
// Return `true` to cause authentication success
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = [
// you may want to customize or obfuscate the message first
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
// you might translate this message
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Finally, my security.yaml:
main:
anonymous:
stateless: true
guard:
entry_point: App\Security\FormAuthenticator
authenticators:
- App\Security\SessionAuthenticator
- App\Security\FormAuthenticator
Working fine. I can see the changes in the toolbar, and the Roles are refreshed.
HTH,
Esteve
I have a code in which I have to check if a user is a part of a certain group (lets say "GroupA").
I have the user details stored in the Sharepoint variable SPUser. Now I need to check if this user is a part of GroupA and then take some action.
How can I achieve this?
Source : How to check if a user exists in a group
you can use following extension method, like this:
public static bool InGroup(this SPUser User, string GroupName)
{
return User.Groups.Cast<SPGroup>().Any(g => g.Name.ToLower() == GroupName.ToLower());
}
Then call it like this:
bool inGroup = spuser.InGroup("GroupName");
If you want to check the current user then another approach can be like this:
From: Check user already exist in specified SharePoint Group
SPWeb web = SPContext.Current.Web;
SPGroupCollection webGroups = web.Groups;
foreach (SPGroup group in webGroups)
{
//Checking the group
if (group.ContainsCurrentUser)
{
// perform action
}
else
{
//perform action
}
}
For More Reference:
Tell if user exists in SharePoint Group through web service
I need to retrieve all SPUser's from a SPGroup. Unfortunately, the group may contain Active Directory groups, so a simple SPGroup.Users is not enough (I'd just get a single SPUser for the AD group, with the IsDomainGroup property set to true).
Does anyone have a good idea how can I obtain a list of all SPUser's, descending into any Active Directory groups contained in a SPGroup? Is there an alternative to SPGroup.ContainsCurrentUser that takes a SPUser parameter?
Based on a blog post I found, I have written the following code:
private static List<SPUser> ListUsers(SPWeb web, SPPrincipal group)
{
try
{
web.Site.CatchAccessDeniedException = false;
var users = new List<SPUser>();
foreach(SPUser user in web.SiteUsers)
{
using(var userContextSite = new SPSite(web.Site.ID, user.UserToken))
{
try
{
using (var userContextWeb = userContextSite.OpenWeb(web.ID))
{
try
{
if (userContextWeb.SiteGroups[group.Name]
.ContainsCurrentUser)
users.Add(user);
}
catch (SPException)
{
// group not found, continue
}
}
}
catch(UnauthorizedAccessException)
{
// user does not have right to open this web, continue
}
}
}
return users;
}
finally
{
web.Site.CatchAccessDeniedException = true;
}
}
I don't like the fact that I have to impersonate every single user, and this code will only find AD users that have already been imported into SharePoint (so an SPUser exists for them), but that's good enough for me.
Unfortunately, it may be the case that not every member of the AD group has a corresponding SPUser object in the site (yet).
In this scenario, I'd enumerate all the members of the active directory group, and force them into the site with the SPWeb's EnsureUser() method, which returns an SPUser, and creates a new one if it doesn't already exist in the site.
For guidance on enumerating active directory members, see Get List of Users From Active Directory In A Given AD Group.