How to login with user but still stay admin? - perl

I want to implement feature when operator/admin may login as user. Do something under user's credentials and then return back and continue as operator/admin
I try to mount whole application under /as_user/:user_id route. So when request come I adjust session to :user_id.
I try detour
$app->routes->route( '/as_user/:app_user' )->detour( app => $app );
But in this case when GET /as_user/17/packages request come the application fall into infinite loop
Also I think to append ?U=17 query parameter. But I do not know how and where rewrite code in such way: All link should be rendered with ?U=17 appended.
Please advice how to login with another user but still stay admin.

Seems I found the answer:
$r->under( '/as_user/:user_id', sub{
# FIX THE SESSION HERE. Just like:
# $_[0]->session->{ user_id } = _[0]->match->stack->[-1]->{ user_id };
return 1; # Required to not break the dispatch chain
})->route('/')->detour( 'App' );
Instead of application instance you should pass application class and Mojolicious will instantiate it itself.
PS. Infinite loop maybe because of cyclic refs. (But Mojolicious check refs here)
UPD
Infinite loop because of bug

Related

How to change Plack Session's name?

I have two applications on the same domain, but they are both creating a plack_session every time the user logs in. It happens because application A overwrites application B's plack session.
It's a complex process to remove one of them and make them use one that is created by a central application, but for now, how can I change one of those 'plack_session' names to something like 'plack_session2' so they don't see each other?
I don't even know if it is possible.
Here is the document for Plack Session, but I can't see anything that can help me here.
As shown in the documentation you link to, the Plack session middleware is enabled with code like this:
builder {
enable 'Session',
state => Plack::Session::State->new;
$app;
};
Later in the same document, you'll find the documentation for the new() method:
new ( %params )
The %params can include session_key, sid_generator and sid_checker however in both cases a default will be provided for you.
session_key
This is the name of the session key, it defaults to 'plack_session'.
...
Putting all this together, I'd guess (and I haven't ever done this) that you can do what you want with code like this:
builder {
enable 'Session',
state => Plack::Session::State->new(
session_key => 'my_session_key',
);
$app;
};

Testing stateful Mojolicious apps

I want to test hiding and unhiding of an entry. I conduct the following tests in Mojolicious t/basic.t:
my $t = Test::Mojo->new('AdminApi');
$t->get_ok('/publications/hide/1');
$t->get_ok('/read/publications/meta')->content_unlike(qr/Paper with id 1:/i);
$t->get_ok('/read/publications/meta/1')->content_like(qr/Cannot find entry id: 1/i);
$t->get_ok('/publications/unhide/1');
$t->get_ok('/read/publications/meta')->content_like(qr/Paper with id 1: <a href/i);
$t->get_ok('/read/publications/meta/1')->content_unlike(qr/Cannot find entry id: 1/i);
My problem is that the two lines '/publications/hide/1' and '/publications/unhide/1' do not hide and unhide the entry. The state of the entry remains untouched.
If I repeat the steps manually in the browser everything works well. For the obvious reasons I want to have it automated with the tests. How to do this?
EDIT: The calls '/publications/hide/1' and '/publications/unhide/1' change the state of the database - just a simple UPDATE query. The change applies to the whole application - for all users. But one needs to be logged in as a user to hide/unhide. Question: how do I emulate a logged user during the test?
Contents generated by '/read/publications/meta' and '/read/publications/meta/1' can be read without login.
Bitbucket Repo
File with test code: basic.t
As you have already said, you need to be logged in to perform the hide and unhide action.
my $t = Test::Mojo->new('AdminApi');
You are creating a new UserAgent here. The Test::Mojo class inherits from Mojo::UserAgent. It has a cookie_jar and thus keeps a session alive. You need that to perform this action:
$t->get_ok('/publications/hide/1');
But right now you are not logged in. What you need to do is log in the user. Looking at the code in your repository, you actually assert that you are not logged in.
$t->get_ok('/')->status_is(200)->content_like(qr/Please login or register/i);
Before you perform the hide, you need to log in the user. After digging a bit in your code I found the action and the template to do that, so I know what the request needs to look like.
$t->post_ok(
'/do_login' => { Accept => '*/*' },
form => { user => 'admin', pass => 'hunter2' }
);
Now your $t UserAgent should be logged in and you can do the hide. Note that get_ok only checks if there was no transport error. So in fact it would make sense to now check if in fact you are now logged in.
You could do that by introspecting the session in the application, by checking the logfile (you are writing "Login success" there) or by checking the page for some string that says that the user is logged in. In templates/display/start.html.ep there is a text that welcomes the logged-in user, so you can use that.
$t->post_ok(
'/do_login' => { Accept => '*/*' },
form => { user => 'admin', pass => 'hunter2' }
)->text_like(qr/Nice to see you here admin/i);
Because text_like uses the text-nodes, the <em> around the username is not relevant in the test.
Right, now we know you are logged in. Time to switch the thing on and off.
$t->get_ok('/publications/hide/1');
Because there is no obvious error thrown for that as far as I can tell, I don't see how to test the success of that. Status code is one way, but there might be something in the content as well that you could test.
To verify the state of the application, you would now call the publication.
$t->get_ok('/read/publications/meta')->content_unlike(qr/Paper with id 1:/i);
$t->get_ok('/read/publications/meta/1')->content_like(qr/Cannot find entry id: 1/i);
Right. But remember, our $t is still logged in. Maybe the logged-in user is allowed to see hidden stuff as well as unhidden stuff. Maybe they are not.
It's probably safer to make a second UserAgent that's not logged in, and check with that one as well.
# check with an unauthorized user
my $t_not_logged_in = Test::Mojo->new('AdminApi');
$t_not_logged_in
->get_ok('/read/publications/meta')
->content_unlike(qr/Paper with id 1:/i);
$t_not_logged_in
->get_ok('/read/publications/meta/1')
->content_like(qr/Cannot find entry id: 1/i);
Now basically you repeat the same thing by unhiding your content and testing again. Rinse and repeat.
Keep in mind that unless you are using an explicit testing database (which you seem not to do), you cannot be sure that there even is an entry 1. Or what the name of that is. You should use fixtures for the tests. You could, for example, create a fresh instance of the DB using sqlite and work with that.

Session Management (Zend Framework specific)

I'm trying to get the rememberMe() function to remember users and retain sessions for months at a time.
I've read that if you pass a value through rememberMe() it will not work if the session has already been started. From the session_set_cookie_params() documentation in the PHP manual, "you need to call session_set_cookie_params() for every request and before session_start() is called."
By I am calling Zend_session::start() in my bootstrap as i thought I was supposed to. My problem is that rememberMe() doesn't seem to be working.
When I call session_get_cookie_params(); I get:
Array([lifetime] => 0 [path] => / [domain] => [secure] => httponly] =>)
Any thoughts?
I've solved the problem. sessions were being erased by another website on the same server which expires sessions every 24 minutes. To fix this I set the session.save_path to a new folder. I also set session.gc_maxlifetime and session.cookie_lifetime to be very large numbers.
problem solved!
Don't use the start() method. It should work fine if you are using MVC. The session_start must be called before any output is send and that's right before sending response (because of outputbuffering). The session is started automatically upon first Zend_Session_namespace usage.

How to use Zend_Form_Element_Hash?

Then I'm trying to use Zend_Form_Element_Hash it regenerates a hash every request.
In my code:
// form
$this->addElement('hash', 'hihacker', array('salt' => 'thesal'));
Then I dumping $_SESSION I see a new value each page reload.
Then I send a form it reports an error "The token '28a5e0e2a50a3d4afaa654468fd29420' does not match the given token 'a64407cc11376dac1916d2101de90d29'", each time - new pair of tokens
$form = new Form();
$form->addElement('hash', 'hihacker',
array('salt' => 'YOUR TOO MUCH SALTY TEXT !!##'));
if ($this->_request->isPost() && $form->isValid($this->_request->getPost())) {
// Valid ! you are safe do what ever you want .
} else if (count($form->getErrors('request_token')) > 0) {
///get him to the error controller
$this->_forward('csrf-forbidden', 'error');
return;
}
its working very well for me but double check your session setting
"
Internally, the element stores a unique identifier using Zend_Session_Namespace, and checks for it at submission (checking that the TTL has not expired). The 'Identical' validator is then used to ensure the submitted hash matches the stored hash.
The 'formHidden' view helper is used to render the element in the form.
"
form ZF docs
Zend_Form_Element_Hash is supposed to regenerate every request. What you're describing is your tokens going out of synch. This generally happens with multiple forms or with redirects/forwards.
If you're using ajax somewhere on the page you can put this in the controller action (near the end)
$form->hash->initCsrfToken();
$this->view->hash = $form->hash->getValue();
Then when you do the ajax call, just pull the token and replace the token on the form using a selector and .replaceWith(). This is how you deal with multiple forms as well
Otherwise you're probably either redirecting something or loading something twice and you should change the hop in the Zend library. The hop is how many times a token can be requested before it expires
Check that there is not a hidden redirect or forward somewhere in your script... the hash has a hop count of 1 so any redirect will make it expire.
FWIW i think there was a subtle bug in the hash a few versions of ZF ago. I got stuck on exactly the same problem, and hacked the code to make the hop count = 2. When I upgraded ZF this problem went away.

How do I use and debug WWW::Mechanize?

I am very new to Perl and i am learning on the fly while i try to automate some projects for work. So far its has been a lot of fun.
I am working on generating a report for a customer. I can get this report from a web page i can access.
First i will need to fill a form with my user name, password and choose a server from a drop down list, and log in.
Second i need to click a link for the report section.
Third a need to fill a form to create the report.
Here is what i wrote so far:
my $mech = WWW::Mechanize->new();
my $url = 'http://X.X.X.X/Console/login/login.aspx';
$mech->get( $url );
$mech->submit_form(
form_number => 1,
fields =>{
'ctl00$ctl00$cphVeriCentre$cphLogin$txtUser' => 'someone',
'ctl00$ctl00$cphVeriCentre$cphLogin$txtPW' => '12345',
'ctl00$ctl00$cphVeriCentre$cphLogin$ddlServers' => 'Live',
button => 'Sign-In'
},
);
die unless ($mech->success);
$mech->dump_forms();
I dont understand why, but, after this i look at the what dump outputs and i see the code for the first login page, while i belive i should have reached the next page after my successful login.
Could there be something with a cookie that can effect me and the login attempt?
Anythings else i am doing wrong?
Appreciate you help,
Yaniv
This is several months after the fact, but I resolved the same issue based on a similar questions I asked. See Is it possible to automate postback from the client side? for more info.
I used Python's Mechanize instead or Perl, but the same principle applies.
Summarizing my earlier response:
ASP.NET pages need a hidden parameter called __EVENTTARGET in the form, which won't exist when you use mechanize normally.
When visited by a normal user, there is a __doPostBack('foo') function on these pages that gives the relevant value to __EVENTTARGET via a javascript onclick event on each of the links, but since mechanize doesn't use javascript you'll need to set these values yourself.
The python solution is below, but it shouldn't be too tough to adapt it to perl.
def add_event_target(form, target):
#Creates a new __EVENTTARGET control and adds the value specified
#.NET doesn't generate this in mechanize for some reason -- suspect maybe is
#normally generated by javascript or some useragent thing?
form.new_control('hidden','__EVENTTARGET',attrs = dict(name='__EVENTTARGET'))
form.set_all_readonly(False)
form["__EVENTTARGET"] = target
You can only mechanize stuff that you know. Before you write any more code, I suggest you use a tool like Firebug and inspect what is happening in your browser when you do this manually.
Of course there might be cookies that are used. Or maybe your forgot a hidden form parameter? Only you can tell.
EDIT:
WWW::Mechanize should take care of cookies without any further intervention.
You should always check whether the methods you called were successful. Does the first get() work?
It might be useful to take a look at the server logs to see what is actually requested and what HTTP status code is sent as a response.
If you are on Windows, use Fiddler to see what data is being sent when you perform this process manually, and then use Fiddler to compare it to the data captured when performed by your script.
In my experience, a web debugging proxy like Fiddler is more useful than Firebug when inspecting form posts.
I have found it very helpful to use Wireshark utility when writing web automation with WWW::Mechanize. It will help you in few ways:
Enable you realize whether your HTTP request was successful or not.
See the reason of failure on HTTP level.
Trace the exact data which you pass to the server and see what you receive back.
Just set an HTTP filter for the network traffic and start your Perl script.
The very short gist of aspx pages it that they hold all of the local session information within a couple of variables prefixed by "__" in the general aspxform. Usually this is a top level form and all form elements will be part of it, but I guess that can vary by implementation.
For the particular implementation I was dealing with I needed to worry about 2 of these state variables, specifically:
__VIEWSTATE
__EVENTVALIDATION.
Your goal is to make sure that these variables are submitted into the form you are submitting, since they might be part of that main form aspxform that I mentioned above, and you are probably submitting a different form than that.
When a browser loads up an aspx page a piece of javascript passes this session information along within the asp server/client interaction, but of course we don't have that luxury with perl mechanize, so you will need to manually post these yourself by adding the elements to the current form using mechanize.
In the case that I just solved I basically did this:
my $browser = WWW::Mechanize->new( );
# fetch the login page to get the initial session variables
my $login_page = 'http://www.example.com/login.aspx';
$response = $browser->get( $login_page);
# very short way to find the fields so you can add them to your post
$viewstate = ($browser->find_all_inputs( type => 'hidden', name => '__VIEWSTATE' ))[0]->value;
$validation = ($browser->find_all_inputs( type => 'hidden', name => '__EVENTVALIDATION' ))[0]->value;
# post back the formdata you need along with the session variables
$browser->post( $login_page, [ username => 'user', password => 'password, __VIEWSTATE => $viewstate, __EVENTVALIDATION => $validation ]);
# finally get back the content and make sure it looks right
print $response->content();