I make a call from my frontend to the userPrivateProfile controller.The route is /api/user/private/:id so let's say I make a call at /api/user/private/65. Before I excecute the controller the request is intecepted by SecurityAuthAction where I make sure that the request headers have the token and if that's the case I want to change the :id to something different.
Controller.java
#With(SecurityAuthAction.class)
public Result userPrivateProfile(Long id) {
//LOGIC
}
SecurityAuthAction.java
public Promise<SimpleResult> call(Http.Context ctx) throws Throwable {
String[] authTokenHeaderValues = ctx.request().headers()
.get(AUTH_TOKEN_HEADER);
if ((authTokenHeaderValues != null) && (authTokenHeaderValues.length == 1) && (authTokenHeaderValues[0] != null)) {
Long userId = sessionService
.findUserByToken(authTokenHeaderValues[0]);
ctx.args.put("id",userId.toString());
return delegate.call(ctx);
}
My problems are
that I cannot retrieve the :id specified from the original call using ctx
Since I cannot find where the request parameter is I cannot change it as well
I tried iterating through the ctx.args Map but I didn't find something there.The output is:
ROUTE_VERB ROUTE_
ACTION_METHOD
ROUTE_CONTROLLER
ROUTE_COMMENTS
ROUTE_PATTERN
GET
userPrivateProfile
controllers.Controller
/api/user/private/$id<[^/]+>
Thanx for your help :)
Unfortunately the Play Framework (certainly in version 2.1) does not give you easy access to URL query parameters when performing action composition. This discussion on the Play Google group may be of interest to you. One workaround mentioned there is to parse the URL in SecurityAuthAction to get the value of the id query parameter. However this is a little messy, and doesn't help you with the next part of your problem, which is changing the id before it gets to the downstream action.
Changing the details of the request as it's being handled by the server seems uncommon and wrong to me. Typically if you wanted to change what a client is requesting, you'd issue a HTTP 303 response redirecting them to the URL you want them to go to. But this doesn't feel like a situation for redirection. What I reckon you should do is just push your call to sessionService down to your main controller class:
SecurityAuthAction.java
public Promise<SimpleResult> call(Http.Context ctx) throws Throwable {
if (authorisation token header is present in request) {
return delegate.call(ctx);
}
return unauthorised();
}
Controller.java
#With(SecurityAuthAction.class)
public Result userPrivateProfile(Long id) {
// We've already established that an auth token header is present in the request
final String authToken = ctx.request().headers().get(AUTH_TOKEN_HEADER)[0];
final Long userId = sessionService.findUserByToken(authToken);
// TODO: Proceed...
}
If userId is something that you need all over your application, then it might be a candidate for inclusion in your application's cookie.
Related
Day 1: Added below rest endpoint for delete operation.
#Path("/company/v1/department")
#Component
public class ManageResource {
#DELETE
#Path("/{identifier}/{identifier_value}/employee")
public void delete(#PathParam("identifier") String identifier,
#PathParam("identifier_value") final String identifierValue,
#QueryParam("age") final String age) {
//delete operation
}
}
I was able to invoke DELETE endpoint using postman with below request:
DELETE: http://localhost:8080/company/v1/department/name/baner/employee?age=50
Day 2: Added below rest endpoint for the update operation in the same resource.
#Path("/company/v1/department")
#Component
public class ManageResource {
#DELETE
#Path("/{identifier}/{identifier_value}/employee")
public void delete(#PathParam("identifier") String identifier,
#PathParam("identifier_value") final String identifierValue,
#QueryParam("age") final String age) {
//delete operation
}
#PUT
#Path("/empid/{value}/employee")
#Consumes(MediaType.APPLICATION_JSON)
public void update(#PathParam("value") final String identifierValue,
#RequestBody final EmployeeUpdateRequest request) {
//update operation
}
}
After adding this new endpoint, I am able to invoke PUT using postman with below request:
PUT: http://localhost:8080/company/v1/department/empid/epid-123/employee
{
//Json request body
}
But when I try to invoke Delete endpoint it is giving me 405 (Method Not Allowed) error.
If I comment my new Put method, then the Delete method works fine.
Also, if I replace Path for Put method to "/{identifier}/{identifier_value}/employee" then both Delete and Put method works fine.
I am using Jersey 1.19 with tomcat.
Can someone help me with this?
Your Paths are in conflict with each other. Let me try to explain:
DELETE = /{identifier}/{identifier_value}/employee
PUT = /empid/{value}/employee
That means when we evaluate the path from left to right, we can either have
{identifier} which is anything or
"empid" which is a fixed string
Jersey always tries to find the "most perfect" match for a REST endpoint. It does so by evaluating the path from left to right.
Fixed strings always take precedence before random variables!
Basically that means when you want to call a DELETE, you cannot have the value "empid" for the variable "{identifier}" because then you are already out-of-scope
So the DELETE call to
http://localhost:8080/company/v1/department/empid/empid-123/employee
will not work as Jersey had to make a decision whether "empid" in the request matches "{identifier}" (DELETE) or "empid" (PUT). And as i tried to explain above, fixed strings take a higher priority.
In contrast, any other DELETE request where
http://localhost:8080/company/v1/department/{identifier}/empid-123/employee
and
{identifier} != "empid"
works.
Possible solution:
make your rest endpoints resource-oriented
DELETE:
/employee/{employee-id}
PUT:
/employee/{employee-id}
Notice how the endpoints are identical, since other than the ID in most systems, no information is needed to identify an entity.
I have implemented by project using Spring-Data-Rest. I am trying to do an update on an existing record in a table. But when I try to send only a few fields instead of all the fields(present in Entity class) through my request, Spring-Data-Rest thinking I am sending null/empty values. Finally when I go and see the database the fields which I am not sending through my request are overridden with null/empty values. So my understanding is that even though I am not sending these values, spring data rest sees them in the Entity class and sending these values as null/empty. My question here is, is there a way to disable the fields when doing UPDATE that I am not sending through the request. Appreciate you are any help.
Update: I was using PUT method. After reading the comments, I changed it to PATCH and its working perfectly now. Appreciate all the help
Before update, load object from database, using jpa method findById return object call target.
Then copy all fields that not null/empty from object-want-to-update to target, finally save the target object.
This is code example:
public void update(Object objectWantToUpdate) {
Object target = repository.findById(objectWantToUpdate.getId());
copyNonNullProperties(objectWantToUpdate, target);
repository.save(target);
}
public void copyNonNullProperties(Object source, Object target) {
BeanUtils.copyProperties(source, target, getNullPropertyNames(source));
}
public String[] getNullPropertyNames (Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] propDesList = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<String>();
for(PropertyDescriptor propDesc : propDesList) {
Object srcValue = src.getPropertyValue(propDesc.getName());
if (srcValue == null) {
emptyNames.add(propDesc.getName());
}
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}
You can write custom update query which updates only particular fields:
#Override
public void saveManager(Manager manager) {
Query query = sessionFactory.getCurrentSession().createQuery("update Manager set username = :username, password = :password where id = :id");
query.setParameter("username", manager.getUsername());
query.setParameter("password", manager.getPassword());
query.setParameter("id", manager.getId());
query.executeUpdate();
}
As some of the comments pointed out using PATCH instead of PUT resolved the issue. Appreciate all the inputs. The following is from Spring Data Rest Documentation:
"The PUT method replaces the state of the target resource with the supplied request body.
The PATCH method is similar to the PUT method but partially updates the resources state."
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#customizing-sdr.hiding-repository-crud-methods
Also, I like #Tran Quoc Vu answer but not implementing it for now since I dont have to use custom controller. If there is some logic(ex: validation) involved when updating the entity, I am in favor of using the custom controller.
I think in terms of REST, the ID should be placed into the URL, something like:
https://example.com/module/[ID]
and then I call GET, PUT, DELETE on that URL. That's kind of clear I think. In Spring MVC controllers, I'd get the ID with #PathVariable. Works.
Now, my practical problem with Spring MVC is, that if I do this, I have to NOT include the ID as part of the form (as well), Spring emits warnings of type
Skipping URI variable 'id' since the request contains a bind value with the same name.
otherwise. And it also makes kind of sense to only send it once, right? What would you do if they don't match??
That would be fine, but I do have a custom validator for my form backing bean, that needs to know the ID! (It needs to check if a certain unique name is already being used for a different entity instance, but cannot without knowing the ID of the submitted form).
I haven't found a good way to tell the validator that ID from #PathVariable, since the validation happens even before code in my controller method is executed.
How would you solve this dilemma?
This is my Controller (modified):
#Controller
#RequestMapping("/channels")
#RoleRestricted(resource = RoleResource.CHANNEL_ADMIN)
public class ChannelAdminController
{
protected ChannelService channelService;
protected ChannelEditFormValidator formValidator;
#Autowired
public ChannelAdminController(ChannelService channelService, ChannelEditFormValidator formValidator)
{
this.channelService = channelService;
this.formValidator = formValidator;
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET)
public String editChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model)
{
if (channelId > 0)
{
// Populate from persistent entity
}
else
{
// Prepare form with default values
}
return "channel/admin/channel-edit";
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
try
{
// Has to validate in controller if the name is already used by another channel, since in the validator, we don't know the channelId
Long nameChannelId = channelService.getChannelIdByName(channelEditForm.getName());
if (nameChannelId != null && !nameChannelId.equals(channelId))
result.rejectValue("name", "channel:admin.f1.error.name");
}
catch (EmptyResultDataAccessException e)
{
// That's fine, new valid unique name (not so fine using an exception for this, but you know...)
}
if (result.hasErrors())
{
return "channel/admin/channel-edit";
}
// Copy properties from form to ChannelEditRequest DTO
// ...
// Save
// ...
redirectAttributes.addFlashAttribute("successMessage", new SuccessMessage.Builder("channel:admin.f1.success", "Success!").build());
// POST-REDIRECT-GET
return "redirect:/channels/" + channelId + "/admin";
}
#InitBinder("channelForm")
protected void initBinder(WebDataBinder binder)
{
binder.setValidator(formValidator);
}
}
I think I finally found the solution.
As it turns out Spring binds path variables to form beans, too! I haven't found this documented anywhere, and wouldn't have expected it, but when trying to rename the path variable, like #DavidW suggested (which I would have expected to only have a local effect in my controller method), I realized that some things got broken, because of the before-mentioned.
So, basically, the solution is to have the ID property on the form-backing object, too, BUT not including a hidden input field in the HTML form. This way Spring will use the path variable and populate it on the form. The local #PathVariable parameter in the controller method can even be skipped.
The cleanest way to solve this, I think, is to let the database handle the duplicates: Add a unique constraint to the database column. (or JPA by adding a #UniqueConstraint)
But you still have to catch the database exception and transform it to a user friendly message.
This way you can keep the spring MVC validator simple: only validate fields, without needing to query the database.
Could you not simply disambiguate the 2 (URI template variables vs. parameters) by using a different name for your URI template variable?
#RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long chanId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
[...]
What ever you said is correct the correct way to design rest api is to mention the resource id in path variable if you look at some examples from the swagger now as open api you could find similar examples there
for you the correct solution would be to use a custom validator like this
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile){
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}
This would make sure all the fields are validated and you dont need to pass the id in the form.
So I am new with restlet. I am creating a Android application that can communicate with a GAE server (with objectify DB)
I Did this very good tutorial to learn:
http://www.tutos-android.com/webservice-rest-android-appengine-restlet-objectify
It's working very well but do very little.
Onely 2 methods:
public interface UserControllerInterface {
#Put
void create(User user);
#Get
Container getAllUsers();
}
For my application its more complicated so I add many more methods:
public interface UserControllerInterface {
#Put
public void createUser(ObagooUser user);
#Put
public void createMessage(ObagooUser user, String message);
#Put
public void updateMessage(ObagooMessage message);
#Get
public List<ObagooUser> getAllUser();
#Get
public ObagooUser getUserById(String id);
#Get
public List<ObagooMessage> getAllMessage();
#Get
public List<ObagooMessage> getAllMessageFromSender(ObagooUser sender);
#Get
public ObagooMessage getFreeMessage(ObagooUser user);
}
Each of these mothds working server side (I tested with Junit).
Now I am coding the android part and I am having problems.
When I do a simple call to getAllMessage() I get an error:
java.lang.IllegalArgumentException: id cannot be zero
at com.google.appengine.api.datastore.KeyFactory.createKey(KeyFactory.java:44)
at com.googlecode.objectify.ObjectifyFactory.typedKeyToRawKey(ObjectifyFactory.java:269)
at com.googlecode.objectify.impl.ObjectifyImpl.find(ObjectifyImpl.java:159)
at com.googlecode.objectify.impl.ObjectifyImpl.find(ObjectifyImpl.java:183)
at com.obagoo.dao.ObagooUserDAO.getUserById(ObagooUserDAO.java:43)
at com.obagoo.controller.ObagooController.getUserById(ObagooController.java:47)
It's going in the wrong method (getUserById).
I put a break point in my getAllMessage and it's going in, but it is also going in other methods.
If I test many times, sometimes it's calling, createUser or another random method.
Do you see what I am doind wrong?
Adding the getAllMessage code:
public List<ObagooMessage> getAllMessage() {
// logger.debug("Getting all Obagoo Messages");
List<ObagooMessage> msg = new ArrayList<ObagooMessage>();
Objectify ofy = ObjectifyService.begin();
Query<ObagooMessage> q = ofy.query(ObagooMessage.class);
for (ObagooMessage u : q) {
msg.add(u);
}
return msg;
}
In the examples that I've seen, its always shown that you should separate the controller/resource handling the URI for the list resource from the single item (id/name based) resource. So you would have something like:
router.attach("/users", UsersController.class);
router.attach("/users/{id}", UserController.class
router.attach("/messages", MessagesController.class);
Notice the plural naming on the first class: UsersController, and singular naming on the the second class: UserController. The first class would handle cases where no id was being provided, such as a get of all users. Also, note when the id is provided in the URI, it can be automatically mapped into an id field on the class. So the Get method has no parameters on the method call.
As for handling a subset, then for messages from a specific user, that could be handled with query parameters. For instance when calling via a URI with /messages?sender=id, the MessagesController.class would use the following in the method handling the Get:
Form queryParams = getRequest().getResourceRef().getQueryAsForm();
String id = queryParams.getFirstValue("sender");
Hope that helps. I'm no expert, so anyone feel free to correct me.
As error says: you are creating a key with zero Id.
My gues is that your ObagoMessage Id field is long? You should make it Long. Primitive long Id values are not autogenerated, while object type Long are. See the docs.
I want to login my users automatically from our application. I know liferay has an auto login feature, but I don't know how to use it. I didn't find much valuable information on the web.
What do I need to do to make autologin work?
I want to login a user automaticaly when he clicks a link, without him having to enter name and password. The name and password is saved on our application database.
I believe the OP has no use for an answer now. Nonetheless, this deserves a comprehensive answer. In fact, I am surprised that it does not have one yet.
First of all, this is a bad idea: such an arrangement as the one proposed by the OP is really too insecure. Nevertheless, a solution to the described problem can be a good prototype for someone creating an autologin for Liferay.
Now, let us say you want to automatically log in any user whose screen name is sent in a query string parameter. For example, if one access http://localhost:8080/web/guest/home?insecurely_login_user=juju then the Liferay in the juju user should be logged in. How to do that? Follow the steps below:
Create the autologin class
Firstly, create a hook plugin. In its docroot/WEB-INF/src directory, creates a class implementing the com.liferay.portal.security.auth.AutoLogin interface. In my example, I will call it br.brandizzi.adam.liferay.insecure.InsecureAutoLogin.
The AutoLogin interface has only one method, called login(), which expects two parameters (an HttpServletRequest and an HttpServletResponse instances) and returns an array of strings. So, my class will look like this without implementation:
public class InsecureAutoLogin implements AutoLogin {
#Override
public String[] login(HttpServletRequest request,
HttpServletResponse response) throws AutoLoginException {
// TODO Auto-generated method stub
return null;
}
}
The AutoLogin.login() method will try to retrieve the information necessary to the authentication from many sources, mainly the request object. If it decides that the user should be logged in, it returns an array with relevant data for authentication; if it decides to not log the user in, it can just return null.
In our case, we try to get the name of the user from the the insecurely_login_user parameter from the request. If there is such parameter, we will proceed with the login; if there is no such parameter, it just returns null:
String screenName = request.getParameter("insecurely_login_user");
if (screenName == null || screenName.isEmpty()) {
return null;
}
So we have the screen name. What to do now? Let us get a user from the database with the same screen name.
long companyId = PortalUtil.getCompanyId(request);
User user = UserLocalServiceUtil.getUserByScreenName(companyId,
screenName);
If a user wich such a screen name exists, it will be retrieved and attributed to the user variable. In this case, the authentication should be successful and the autologin class should return an array of three strings - the credentials. Those are the values to be returned as credentials, in the order they should appear in the array:
the user id as a string
the password of the user, which can be encrypted or not;
a boolean value, cast to string, indicating if the password is encrypted.
So here is the line:
return new String[] {
String.valueOf(user.getUserId()),
user.getPassword(),
String.valueOf(user.isPasswordEncrypted())
};
If a user is not found, however, an exception will be thrown. So, we have to surround the code above with a try/catch construction. If an exception is thrown, just return null:
try {
long companyId = PortalUtil.getCompanyId(request);
User user = UserLocalServiceUtil.getUserByScreenName(companyId,
screenName);
return new String[] { String.valueOf(user.getUserId()),
user.getPassword(),
String.valueOf(user.isPasswordEncrypted()) };
} catch (Exception e) {
return null;
}
In the end, this is my InsecureAutoLogin class:
public class InsecureAutoLogin implements AutoLogin {
public String[] login(HttpServletRequest request,
HttpServletResponse response) throws AutoLoginException {
String screenName = request.getParameter("insecurely_login_user");
if (screenName == null || screenName.isEmpty())
return null;
try {
long companyId = PortalUtil.getCompanyId(request);
User user = UserLocalServiceUtil.getUserByScreenName(companyId,
screenName);
return new String[] { String.valueOf(user.getUserId()),
user.getPassword(),
String.valueOf(user.isPasswordEncrypted()) };
} catch (Exception e) {
return null;
}
}
}
Registering the autologin class
Now our hook should register this class as an autologin processor. That is really easy.
First, edit the file docroot/WEB-INF/liferay-hook.xml adding a portal-properties element with the value portal.properties:
<?xml version="1.0"?>
<!DOCTYPE hook PUBLIC "-//Liferay//DTD Hook 6.1.0//EN" "http://www.liferay.com/dtd/liferay-hook_6_1_0.dtd">
<hook>
<portal-properties>portal.properties</portal-properties>
</hook>
Now, create a file named portal.properties at docroot/WEB-INF/src. It should contain a property named auto.login.hooks whose value should be the name of our class:
auto.login.hooks=br.brandizzi.adam.liferay.insecure.InsecureAutoLogin
And that is it. Deploy this hook and your autologin will work.
Conclusion
As I have said, you should not use such an unsafe "authentication" method. It is too easy to bypass it, getting even administration permissions! However, if you follow these steps, you have a skeleton to create a better autologin feature. Also, I know some people really want to do something like this insecure "authentication" method and sometimes we have to suspend our judgments and just help one to shoot one's feet...
The source code of this project can be found here and you can download the WAR here.
Step 1: Create a class CustomLoginFilter and implements from AutoLogin interface.Override login method. Code as follows.
public String[] login(HttpServletRequest req, HttpServletResponse response)throws AutoLoginException {
//Get the login parameter
String loginEmailId = ParamUtil.getString(req, “_58_login”);
String password = req.getParameter(“_58_password”);
String[] credentials = new String[3];
credentials[0] = userId
credentials[1] = loginEmailId;
credentials[2] = password;
//Write your business logic Here and return String[].
}
Step 2: Write below code in portal-ext.properties
// you get full control from this custom class.
auto.login.hooks=com.bolog.portal.login.security.CustomLoginFilter
//Override Liferay Authentication pipeline
auth.pipeline.enable.liferay.check=false
auth.pipeline.pre=com.bolog.portal.login.security.CustomLoginAuthenticator
Step 3: Create class CustomLoginAuthenticator and implements from Authenticator.
Override authentication methods.
public int authenticateByEmailAddress(long arg0, String arg1, String arg2,
Map<String, String[]> arg3, Map<String, String[]> arg4)
throws AuthException {
//Write Your business login here and if authentication success then return 1 otherwise return 0;
return 0;
}
public int authenticateByScreenName(long arg0, String arg1, String arg2,
Map<String, String[]> arg3, Map<String, String[]> arg4)
throws AuthException {
//Write Your business login here and if authentication success then return 1 otherwise return 0;
return 0;
}
public int authenticateByUserId(long arg0, long arg1, String arg2,
Map<String, String[]> arg3, Map<String, String[]> arg4)
throws AuthException {
//Write Your business login here and if authentication success then return 1 otherwise return 0;
return 0;
}
Step 4: If authentication fail then you can also redirect any page by following code
if(Validator.isNull(credentials) || credentials[0]==null){
req.setAttribute(AutoLogin.AUTO_LOGIN_REDIRECT, “Your Login page path”);
}
What exactly do you mean by "autologin"? If you want Liferay to check wheter the user is already authenticated by some external entity (like a single sign-on server as CAS), you can just enable that in the portal.properties. There it's already preconfigured for the liferay supported authentication mechanisms. Otherwise you might need to implement your own autologin hook (as indicated in this post for example
Well found it.
Step 1: Click on add iframe and let the configuration view pop up.
Step 2: Provide the url and if there are any variables like (www.mysite.com/Action=Login&User . . . .), add the Action=Login in hidden variables text field.
Step 3: Click authenticate and select form based authentication. In this, make usre that the user field name and password field name are given correctly, and the values will be '#screen_name#','#password#'.
For example, suppose the url is something like www.mysite.com/Action=Login?User=aj&Password=aj.
User Name (field)=User
Password (field)=Password
User Name (Value)=aj
Password (Value)=aj
Hidden variables(field)=Action=Login
Now, whenever any user logs into the liferay applicaiton, if his/her account exists in the site specified(in the url), it will log into that site automatically(acts like a single sign on).
This is working !!!
-Aj