I just recently migrated from easyadmin 2 to easyadmin 3 and in my lists i can now see all the data from the entitiy (e.g. Company).
In easyadmin 2 it automatically (at least that´s what i guess) limited the output belonging to the logged-in user.
I have read that i can set custom functions for each Controller like
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
if (!in_array("ROLE_ADMIN",$this->getUser()->getRoles())) {
$qb = $this->get(EntityRepository::class)->createQueryBuilder($searchDto, $entityDto, $fields, $filters);
$qb->andWhere('entity.creator = :user');
$qb->setParameter('user', $this->getUser());
return $qb;
}
}
but that can´t be the solution i guess since somehow it worked in easyadmin 2 as well without having to write custom indexQueryBuilder functions.
Any help very much appreciated.
I have roles ADMIN, MANAGER, COURIER, so my code:
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
use EasyCorp\Bundle\EasyAdminBundle\Orm\EntityRepository;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
use Doctrine\ORM\QueryBuilder;
class LuggageManagerCrudController extends AbstractCrudController
{
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters): QueryBuilder
{
$qb = $this->get(EntityRepository::class)->createQueryBuilder($searchDto, $entityDto, $fields, $filters);
if (in_array('ROLE_MANAGER', $this->getUser()->getRoles())) {
$qb->andWhere('entity.manager = :user');
} else {
$qb->andWhere('entity.courier = :user');
}
$qb->setParameter('user', $this->getUser()->getEmail());
return $qb;
}
}
And I have another AdminCrudController for ADMIN's roles
Related
I am trying to get the new AutoPopulate attribute to work but I am having some difficulty understanding the new AutoQuery functionality.
To test it out I am aiming to replace this service that is a standard AutoQuery endpoint but it also filters by the logged in users ID. I want to replace it so it works completely with just the model definition.
public class DevExtremeService : ServiceBase
{
public IAutoQueryDb AutoQuery { get; set; }
public QueryResponse<DeWatchedUrlResponse> Any(WatchedUrlDevExRequest request)
{
var q = AutoQuery.CreateDevXQuery(request, Request.GetRequestParams(), Request);
q.Where(x => x.UserAuthCustomId == GetUserId());
var response = AutoQuery.Execute(request, q, base.Request);
return response;
}
}
[Route("/de/watched-urls")]
public class WatchedUrlDevExRequest : QueryDb<WatchedUrlRecord, DeWatchedUrlResponse>
{
}
So I deleted the service and updated model to:
[ValidateIsAuthenticated]
[AutoPopulate(nameof(WatchedUrlDevExRequest.UserAuthCustomId), Eval = "userAuthId")]
[Route("/de/watched-urls")]
public class WatchedUrlDevExRequest : QueryDb<WatchedUrlRecord, DeWatchedUrlResponse>
{
public long UserAuthCustomId { get; set; }
}
My understanding from reading the release notes is that userAuthId is a variable declared in the AutoQuery #script context that is added by default.
I have tried a few different variations and I cannot get the property to populate. The docs seem focused on audit history and multitenancy but really I am just looking for a quick way to make endpoints.
I have 2 main questions:
Why is the auto populate not working on this property?
Where can I see the default #script definition so I can see how things like userAuthId are defined and better get an understanding how to add my own?
edit
I re-read docs and I gues this only works when writing data to db. I really like the concept of being able to apply #script to a request model via attribute. Is that possible?
AutoQuery CRUD's [AutoPopulate] attribute initially only populated AutoQuery CRUD's Data Model when performing CRUD operations, e.g. Inserting, Updating or Deleting entities.
For ensuring a query only returns a users records, it's recommended to use an AutoFilter instead, which behaves as expected ensuring the query is always applied to the Data Model, e.g:
[ValidateIsAuthenticated]
[Route("/de/watched-urls")]
[AutoFilter(QueryTerm.Ensure, nameof(WatchedUrlRecord.UserAuthCustomId),
Eval = "userAuthId")]
public class WatchedUrlDevExRequest : QueryDb<WatchedUrlRecord, DeWatchedUrlResponse>
{
}
However as I can see it's a useful feature I've also just added support for [AutoPopulate] & [AutoMap] attributes on Query DTOs in this commit where your AutoQuery DTO would work as expected where it populates the Request DTO property:
[ValidateIsAuthenticated]
[AutoPopulate(nameof(WatchedUrlDevExRequest.UserAuthCustomId), Eval = "userAuthId")]
[Route("/de/watched-urls")]
public class WatchedUrlDevExRequest : QueryDb<WatchedUrlRecord, DeWatchedUrlResponse>
{
public long UserAuthCustomId { get; set; }
}
This change is available from v5.10.3 that's now available on MyGet.
An alternative approach to populate AutoQuery's Request DTO you could have a custom AutoQuery implementation like you have, an Extensible Query Filter or custom base class or I'd personally go with a Global Request Filter that updates all Request DTOs with a shared interface, e.g:
GlobalRequestFilters.Add((req, res, dto) => {
if (dto is IHasUserAuthCustomId authDto)
{
var session = req.GetSession();
if (session.IsAuthenticated)
authDto.UserAuthCustomId = session.UserAuthId;
}
});
Or you could wrap this logic in a Request Filter Attribute and apply the behavior to Request DTOs that way.
Note: userAuthId is a ServiceStack #Script method that returns the currently authenticated User Id.
I have extended the built-in User ContentType with a Content Picker Field that can be used to select multiple Video ContentItems. This gives me a video multi-picker control on the Edit page of each User.
I love how Orchard CMS makes this so elegantly simple to setup.
Now that I can associate multiple Videos with a User, I'd like to create a Query that will display just the Videos that the currently logged in User has been granted access.
I was hoping to be able to setup a Query using the Projector module, in what I thought was the obvious way (see below), but this returns no results.
This is how I configured the second filter:
Clicked on the + Add a new Filter link on the Edit Query screen
Chose Videos:Ids from the User Content Fields section, like this:
Configured the new filter like this:
What am I doing wrong, or what is the simplest way of diagnosing this issue?
This is how the Content Picker field is defined:
I have spotted my error - it was due to me not having a proper understand of how the filters worked. The Videos:Ids filter in the User Content Fields section does not give access to the current user's list of videos, as I assumed. Instead, it is offering the field to be used in the filter, which would be useful if I were to write a query to produce a list of Users that had access to a specific Video.
It was wishful thinking that it worked the way I wanted, but it's obvious in retrospect how it actually works.
Update: in the hope it's useful for others, here's the custom filter I developed:
public interface IFilterProvider : IEventHandler
{
void Describe(dynamic describe);
}
public class CurrentUserVideosFilter : IFilterProvider
{
private readonly IWorkContextAccessor _workContextAccessor;
public CurrentUserVideosFilter(IWorkContextAccessor workContextAccessor)
{
_workContextAccessor = workContextAccessor;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(dynamic describe)
{
describe.For("My Filter Category", T("My Filter Category"), T("My Filter Category"))
.Element("Current User's Videos", T("Current User's Videos"), T("Current User's Videos"),
(Action<dynamic>)ApplyFilter,
(Func<dynamic, LocalizedString>)DisplayFilter,
null
);
}
public void ApplyFilter(dynamic context)
{
var query = (IHqlQuery)context.Query;
context.Query = query.ForType("Video")
.Where(x => x.ContentPartRecord<IdentityPartRecord>(), x => x.InG("Id", GetVideoIdsForCurrentUser()));
}
private IList<int> GetVideoIdsForCurrentUser()
{
var currentUser = _workContextAccessor.GetContext().CurrentUser;
if (currentUser == null) return new int[0];
dynamic item = currentUser.ContentItem;
var videoContentItems = (IEnumerable<ContentItem>)item.User.Videos.ContentItems;
return videoContentItems.Select(i => i.Id).ToList();
}
public LocalizedString DisplayFilter(dynamic context)
{
return T("Videos that have been assigned to the currently logged in user");
}
}
I created this class in a new Orchard module, which contains all my customisations for the site I'm building. Once I installed the module, the filter was immediately available. I assume Orchard uses reflection to seek out all types that implement the IFilterProvider interface.
This is how the filter appears on the Add a Filter screen:
Clicking on the filter shows this screen:
Once the filter has been saved, the query works exactly how I'd like - it shows all videos that have been assigned to the currently logged in user.
Using Grails (or hibernate), I was wanting to know if there is a specific design pattern or method we should be using when implementing a SEARCH of our domain.
For example, on my website, I want to be able to filter(or search) by multiple properties in the domain.
EG: For I have a page which displays a list of HOTELS. When I submit a search form, or if a user clicks "filter by name='blah'", when I enter the controller I get the following:
Domain
String name
String location
Controller
if(params.name && params.reference) {
// Find name/reference
} else if(params.name) {
// Find name
} else if(params.reference) {
// Find reference
} else {
// Find all
}
As you can understand, if there are more properties in the domain to search/filter, the longer the controller gets.
Any help. Please note, I do not want to use the 'searchable' plugin, as this is too complex for my needs.
I would embed these in a named query in the Domain class itself. For example:
Class Hotel {
String name
String city
String country
boolean isNice
static namedQueries = {
customSearch { p ->
if (p?.name) eq('name', p.name)
if (p?.city) eq('name', p.city)
if (p?.country) eq('name', p.country)
if (p?.isNice != null) eq('isNice', p.isNice)
}
}
}
Then later in a controller somewhere ...
def results = Hotel.customSearch(params)
Of course this is a very simple example, but you can expand on it using the same named query or even adding others and chaining them together.
I have a business requirement to only send permissioned properties in our response payload. For instance, our response DTO may have several properties, and one of them is SSN. If the user doesn't have permissions to view the SSN then I would never want it to be in the Json response. The second requirement is that we send null values if the client has permissions to view or change the property. Because of the second requirement setting the properties that the user cannot view to null will not work. I have to still return null values.
I have a solution that will work. I create an expandoObject by reflecting through my DTO and add only the properties that I need. This is working in my tests.
I have looked at implementing ITextSerializer. I could use that and wrap my response DTO in another object that would have a list of properties to skip. Then I could roll my own SerializeToString() and SerializeToStream(). I don't really see any other ways at this point. I can't use the JsConfig and make a SerializeFn because the properties to skip would change with each request.
So I think that implementing ITextSerializer is a good option. Are there any good examples of this getting implemented? I would really like to use all the hard work that was already done in the serializer and take advantage of the great performance. I think that in an ideal world I would just need to add a check in the WriteType.WriteProperties() to look and the property is one to write, but that is internal and really, most of them are so I can't really take advantage of them.
If someone has some insight please let me know! Maybe I am making the implementation of ITextSerialzer a lot harder that it really is?
Thanks!
Pull request #359 added the property "ExcludePropertyReference" to the JsConfig and the JsConfigScope. You can now exclude references in scope like I needed to.
I would be hesitant to write my own Serializer. I would try to find solutions that you can plug in into the existing ServiceStack code. That way you will have to worry less about updating dlls and breaking changes.
One potential solution would be decorating your properties with a Custom Attributes that you could reflect upon and obscure the property values. This could be done in the Service before Serialization even happens. This would still include values that they user does not have permission to see but I would argue that if you null those properties out they won't even be serialized by JSON anyways. If you keep all the properties the same they you will keep the benefits of strong typed DTOs.
Here is some hacky code I quickly came up with to demonstrate this. I would move this into a plugin and make the reflection faster with some sort of property caching but I think you will get the idea.
Hit the url twice using the following routes to see it in action.
/test?role
/test?role=Admin (hack to pretend to be an authenticated request)
[System.AttributeUsage(System.AttributeTargets.Property)]
public class SecureProperty : System.Attribute
{
public string Role {get;set;}
public SecureProperty(string role)
{
Role = role;
}
}
[Route("/test")]
public class Test : IReturn
{
public string Name { get; set; }
[SecureProperty("Admin")]
public string SSN { get; set; }
public string SSN2 { get; set; }
public string Role {get;set;}
}
public class TestService : Service
{
public object Get(Test request)
{
// hack to demo roles.
var usersCurrentRole = request.Role;
var props = typeof(Test).GetProperties()
.Where(
prop => ((SecureProperty[])prop
.GetCustomAttributes(typeof(SecureProperty), false))
.Any(att => att.Role != usersCurrentRole)
);
var t = new Test() {
Name = "Joe",
SSN = "123-45-6789",
SSN2 = "123-45-6789" };
foreach(var p in props) {
p.SetValue(t, "xxx-xx-xxxx", null);
}
return t;
}
}
Require().StartHost("http://localhost:8080/",
configurationBuilder: host => { });
I create this demo in ScriptCS. Check it out.
I have been struggling with what I thought would be simple.
I have a content type called Supplier. This supplier has contact information containing two addresses, one for Correspondence Address and one for Visiting Address. The supplier has also several locations, like location north and location south. A location is also an address. So basically I have a content item Supplier with a lot of addresses and all of them with their own type.
Migration:
public int Create() {
//Creating the Location contentrecord, contentpart and contenttype
SchemaBuilder.CreateTable("LocationPartRecord", table => table
.ContentPartRecord()
.Column<int>("LocationsPartRecord_id")
);
ContentDefinitionManager.AlterPartDefinition("LocationPart", part => part
.Attachable(false)
.WithField("LocationName", f => f.OfType("TextField"))
.WithField("AddressLine1", f => f.OfType("TextField"))
.WithField("AddressLine2", f => f.OfType("TextField"))
.WithField("Zipcode", f => f.OfType("TextField"))
.WithField("City", f => f.OfType("TextField"))
.WithField("Country", f => f.OfType("TextField")));
ContentDefinitionManager.AlterTypeDefinition("Location",
cfg => cfg
.WithPart("CommonPart")
.WithPart("LocationPart")
);
//Creating the Locations 'container' contentpart
SchemaBuilder.CreateTable("LocationsPartRecord", table => table
.ContentPartRecord()
);
ContentDefinitionManager.AlterPartDefinition("LocationsPart", builder => builder.Attachable());
//Creating the supplier. Specific supplier contentfields can be added later. Doing records, so I can add
//datafields later that are not contentfields
SchemaBuilder.CreateTable("SupplierPartRecord", table => table
.ContentPartRecord());
ContentDefinitionManager.AlterPartDefinition("SupplierPart", part => part
.Attachable(false)
);
ContentDefinitionManager.AlterTypeDefinition("Supplier", builder => builder
.Creatable()
.Draftable()
.WithPart("CommonPart")
.WithPart("TitlePart")
.WithPart("BodyPart")
.WithPart("AutoroutePart", partBuilder =>
partBuilder.WithSetting("AutorouteSettings.AllowCustomPattern", "true")
.WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'Supplier', Pattern: 'aanbieders/{Content.Slug}', Description: 'aanbieders/supplier-name'}]")
.WithSetting("AutorouteSettings.DefaultPatternIndex", "0"))
.WithPart("SupplierPart")
.WithPart("LocationsPart"));
return 1;
}
Models:
*LocationPartRecord and LocationPart *
public class LocationPartRecord:ContentPartRecord {
public virtual LocationsPartRecord LocationsPartRecord { get; set; }
}
public class LocationPart:ContentPart<LocationPartRecord> {
LocationsPartRecord LocationsPartRecord {
get { return Record.LocationsPartRecord; }
set { Record.LocationsPartRecord = value; }
}
}
LocationsPartRecord and LocationsPart (container)
public class LocationsPartRecord:ContentPartRecord {
public LocationsPartRecord()
{
Locations = new List<LocationPartRecord>();
}
[CascadeAllDeleteOrphan]
public virtual IList<LocationPartRecord> Locations { get; set; }
}
public class LocationsPart:ContentPart<LocationsPartRecord> {
public LocationsPart() {
Locations = new List<LocationPart>();
}
public readonly LazyField<IList<LocationPart>> _locations = new LazyField<IList<LocationPart>>();
public IList<LocationPart> Locations {
get { return _locations.Value; }
set { _locations.Value = value; }
}
}
From here I am stuck. I would like to see when Creating a new supplier, I get a screen containing all the content item fields for supplier and a list of locations, with the ability to create, delete or update a location.
I don't need the code to be spelled out, but a direction would suffice. Which drivers, controllers and views should I create. This is only for admin console. For frontend the locations need to be displayed and not edited.
I don't think there will be any way to get the functionality you're after without custom coding. As you have suggested, the comments module could be a good example to copy. The Controllers in the comments module are only to manage all of the comments in their own admin pages, separate to the content items they belong to. The edit / display of the comments is still provided through the drivers and handlers.
Using the Comments module analogy:
CommentsPart = AddressesPart - This would be added to your Supplier content type
CommentPart = AddressPart - This would be added to your Address content type
You could strip out a lot of the extra functionality that is included for managing comments and just copy the drivers, handlers, views and models for these two parts.
I have seen some gallery modules that may allow you to build these relationships through the admin interface, however I haven't used it myself:
http://gallery.orchardproject.net/List/Modules/Orchard.Module.Downplay.Mechanics
Address shouldn't be a part, it should be a field. This way, you can have more than one, and each can be named.
Don't know if this would be helpful (and the site appears to be down - but Google has a cached version if you are patient for it to load), but there is a good blog about exactly your situation. It's Skywalkers excellent Web Shop series. I believe Part 8 contains the code related to multiple addresses (uses Address and Addresses). This seems to involve your problem, and the code may be what you need.
In case you have trouble getting to the site, there is also a CodePlex repository for the code. Additionally, Bertrand's Nwazet Commerce module might have similar code.