Preventing Update of LastUsed in MetadataBag for specific Controller - symfony

I wrote an event listener for kernel.request to make me able to logout user automatically when he is idle for more than an amount of time.
I use this to calculate idle time:
$idle = time() - $this->session->getMetadataBag()->getLastUsed()
But I have a periodic Ajax request in my pages (for notification counts in pages) and they constantly change the LastUsed field of MetadataBag so Idle limit never reaches.
Is it possible to prevent a specific Controller (that ajax controller) to update session LastUsed ?
If yes, How?
If no, what else can I do to handle this?
Thanks

I don't know how to prevent the update of MetadataBag's lastUsed, but you can manually set the time for the user's last request, in the session and use it.
You can create a listener like below and make it listen to the kernel.request event, and in your other listener, get the data you store using this listener in the session instead of $this->session->getMetadataBag()->getLastUsed().
public function listen(GetResponseEvent $event){
// in your listeners main function
$request = $event->getRequest();
$route = $request->attributes->get('_route');
if($route != 'your_ajax_check_route'){
// update the session and etc.
// retrieve what you store here in your other listener.
}
}

The feature you are talking about (prevent update session lastUsed) can't be done without some Symfony hacking which is unnecesary as you can simple create your own logic for this. For example you can create KernelRequest listener which will update last used session variable for all request except the one you will use to check how much time left too logout:
public function onKernelRequest(GetResponseEvent $event): void
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
//$this->sessionMaxIdleTime is number from config parameter
//representing max idle time in seconds
if ($this->sessionMaxIdleTime > 0) {
$this->session->start();
$time = time();
$route = $event->getRequest()->attributes->get('_route');
//for all routes but one - checking how much time is left
if ('session_check' !== $route) {
//manual set lastUsed time
$this->session->set('manualLastUsedTime', $time);
}
$idleTime = $time - $this->session->get('manualLastUsedTime');
if ($idleTime > $this->sessionMaxIdleTime) {
$this->session->invalidate();
$this->session->getFlashBag()->set('info', 'Logged out due to inactivity.');
if ($event->getRequest()->isXmlHttpRequest()) {
$event->setResponse(new Response('Logged out due to inactivity.', Response::HTTP_FORBIDDEN));
} else {
$event->setResponse(new RedirectResponse($this->router->generate('login')));
}
}
}
Then you can simple create method in some controller, which will be used by some ajax function to check how much time left to logout for example:
/**
* #Route("/session/check", name="session_check", methods={"GET"})
* #param Request $request
* #param SessionInterface $session
* #param int $sessionMaxIdleTime
* #return JsonResponse
*/
public function checkSecondsToExpire(Request $request, SessionInterface $session, $sessionMaxIdleTime = 0): JsonResponse
{
$idleTime = time() - $session->get('manualLastUsedTime');
$secToExp = $sessionMaxIdleTime - $idleTime;
return new JsonResponse(['secToExp' => $secToExp]);
}
Last piece is to make some check mechanism. It can be done as simple as starting some JS function in your base template. The parameter is in twig syntax and is from config (it is the same as sessionMaxIdleTime in kernelRequest listener):
<script>
$(document).ready(function () {
SessionIdler.start({{ session_max_idle_time }});
});
</script>
SessionIdler.start is just a function that runs some other function in specific interval (in this example it will run 1 minute before configured sessionmaxIdleTime):
function start(time) {
checkSessionCounter = setInterval(checkSession, (time - 60) * 1000);
isCheckSessionCounterRunning = true;
// console.debug("checkSession will START in: " + (time - 60) + "s");
}
checkSession function make ajax request to our session_check route and depends on result it shows modal with proper information about too long inactivity. Modal can have a button or action when hiding, that will make another request to session_extend route (which can do nothing - it just need to be captured by kernelRequest listener to overwrite manualLastUsedTime)
This three pieces together creates a mechanism for notify user about too long inactivity without any influence on session metadataBag.

Related

Alexa Skill Lambda returns, but Alexa says Invalid Response

Scenario:
I have a Node.JS 12.x Lambda that is backing an Alexa Skill. The user asks the skill a question, and the search parameter is sent to my Lambda in a slot. I query the SQL DB using the mssql package, then return the result to the user.
Issue:
If I fetch results from the DB, the Lambda executes the query successfully and returns the result, but then the Lambda times out and Alexa seems to have not received the response. If I remove the DB query and just return a string, it all works just fine.
Suspicions:
I think there may be some issue with the async/await stuff in here. I just can't figure out what the issue is though. I have checked my use of async/await several times
If I left anything out just let me know. Thanks in advance!
Code:
/**
* Intent handler for FindSomething intent
*/
const MyHandler = {
/**
* Determine whether this handler is able to process this input
* #param {Object} handlerInput The input object
*/
canHandle(handlerInput) {
// This part works fine
return util.checkIntentMatch(handlerInput, INTENT_NAME);
},
/**
* Handle the input
* #param {Object} handlerInput The input object
*/
async handle(handlerInput) {
// Extract slot values
const [
searchTerm,
] = [
Alexa.getSlotValue(handlerInput.requestEnvelope, 'search_term'),
];
// Fulfill request
const responseText = await getResponseText(searchTerm);
// Respond
return handlerInput.responseBuilder
.speak(responseText)
.getResponse();
},
};
And then getResponseText looks like this:
/**
* Get the response text for the query
* #param {string} searchTerm The search term from the user
*/
async function getResponseText(searchTerm) {
let sectorName = await getSectorForTerm(searchTerm);
console.log(`Inside getResponseText. sectorName: ${sectorName}`);
if (!sectorName) return format(NOT_FOUND_LANGUAGE, { searchTerm });
return format(FOUND_LANGUAGE, { searchTerm, sectorName });
}
/**
* Find the sector for a search term
* #param {string} searchTerm The search term from the user
*/
async function getSectorForTerm(searchTerm) {
// ========================================================================
// If I uncomment this line, it works great -- all the way back to the user
// ========================================================================
//return 'fake result';
// Gather prerequisites in parallel
let [
query,
pool
] = await Promise.all([
fs.readFile(path.join(__dirname, 'queries', 'FindQuery.sql'), 'utf8'),
sql.connect(process.env['connectionString'])
]);
console.log('Pre query');
// Run query
let queryResult = await pool.request()
.input('EntityName', sql.TYPES.VarChar, searchTerm)
.query(query);
console.log('Post query');
// Extract result if any
let result = undefined;
if(queryResult.recordset.length > 0) {
result = queryResult.recordset[0]['SectorName'];
}
console.log(`result of getSectorForTerm: ${result}`);
return result;
}
Edit:
Here is what the log looks like. You can see that the file has loaded, the query has executed, and the return statement is hit within ~500ms. Then several seconds pass before the function times out.
Edit 2:
I have structured my index.js like this example from the AWS docs, so I don't have direct access to context or similar. That can be changed if needed.
You are using 2 time consuming operations in your skill - file read and sql connection. Probably your skill exceeds 8s timeout. Please check CloudWatch logs related to your skill if there is message like
Task timed out after 8.01 seconds
you should work on some enhancements here.
Also make sure that both methods in Promise.all argument return a Promise.

Setting / Modifying record associations with client script

I'm building a document management application with the following Data Models:
Doc_Metadata
- Approval_Requests
- WorkflowStage
- Approver
- Comment
I am trying to use the Document Approval Workflow template as a starting point, and associating the Doc_Metadata parent to the "Requests" model, such that each approval request is associated to (owned by) a parent Metadata record.
I have gotten it to work from start to finish without throwing any errors, HOWEVER, no matter what I do I cannot get the Metadata - Request relation to save.
I've posted my client scripts for the Add Request page below, and also attached the zip of my application in case someone wants to look in more detail.
Any and all suggestions are incredibly appreciated, I love the idea of appmaker but have been struggling to understand relations versus how they are traditionally handled in SQL.
/**
* #fileoverview Client script functions for AddRequest page.
*/
/**
* Navigates user to the add request page and sets page URL parameters.
* #param {string=} metadataKey - optional metadata with this key will be used
* as default for the new approval request.
*/
function gotoAddRequestPage(metadataKey) {
var params = {
metadataKey: metadataKey
};
// DEBUG
console.log(params.metadataKey);
console.log(metadataKey);
gotoPage(app.pages.AddRequest, params);
}
/**
* Creates a new request and redirects user to the edit screen afterwards.
* #param {Widget} submitButton - button that triggers the action.
*/
function createRequest(submitButton) {
var addRequestPage = submitButton.root;
if (addRequestPage.validate()) {
submitButton.enabled = false;
submitButton.datasource.saveChanges({
success: function() {
submitButton.enabled = true;
//DEBUG
console.log("requestId:" + submitButton.datasource.item._key);
goToRequestDetailsPage(submitButton.datasource.item._key);
},
failure: function(error) {
submitButton.enabled = true;
}
});
}
}
/**
* Creates a new request and redirects user to the edit screen afterwards.
* #param {Widget} cancelButton - button that triggers the action.
*/
function cancelCreateRequest(cancelButton) {
cancelButton.datasource.clearChanges();
app.showPage(app.pages.Main);
}
function onRequestCreate () {
google.script.url.getLocation(function(location) {
var metadataKey = location.parameter.metadataKey;
var props = {
metadataKey: metadataKey
};
var allMetadataDs = app.datasources.AllMetadata;
var metadataDs = allMetadataDs.item;
var requestDs = app.datasources.RequestsByKey;
//DERBUG//
console.log("metadataKey: " + metadataKey);
var newRequest = requestDs.createItem();
newRequest.Metadata = metadataDs;
var requests = metadataDs.Request;
requests.push(newRequest);
});
}
struggling to understand relations versus how they are traditionally handled in SQL
You can configure your app to use Cloud SQL database: https://developers.google.com/appmaker/models/cloudsql
I cannot get the Metadata - Request relation to save
Here is a snippet that should work(assuming that you are using datasource in autosave mode).
var allMetadataDs = app.datasources.AllMetadata;
// metadata record that you need should be selected at this point of time
var metadata = allMetadataDs.item;
var requestDs = app.datasources.RequestsByKey.modes.create;
var requestDraft = requestDs.item;
// This line should create relation between draft request record and
// existing Metadata record.
requestDraft.Metadata = metadata;
// send your draft to server to save
requestDs.createItem(function(newRecord) {
// log persisted request record asynchronously
console.log(newRecord);
});
By the way, your life will become way easier, if you add a drop down with metadata items to the request creation form.

Symfony2 - redirect logged in users when entering anonymous areas

I created an action that handles redirection to respected areas depending on user's type and ROLE (trainee, company or university let's say). If user is not logged in, it redirects to homepage (anonymous area), and if logged in - to their profile pages. I use it in homepage and many other cases, for example, as sign up and login success redirection.
public function authorizationAction(Request $request)
{
$user = $this->getUser();
$authorizationChecker = $this->get('security.authorization_checker');
$request->cookies->remove('action');
if ($user) {
if ($user->getType() == 'company' && $authorizationChecker->isGranted('ROLE_COMPANY_GUEST')) {
/** #var Company $company */
$company = $user->getCompany();
if ($user->getState() == 'active' && $company->getState() == 'active') {
$response = $this->redirectToRoute('company');
} else {
$response = $this->redirectToRoute('company_profile');
}
} elseif ($user->getType() == 'university' && $authorizationChecker->isGranted('ROLE_UNIVERSITY_GUEST')) {
/** #var University $university */
$university = $user->getUniversity();
if ($user->getState() == 'active' && $university->getState() == 'active') {
$response = $this->redirectToRoute('university');
} else {
$response = $this->redirectToRoute('university_profile');
}
} elseif ($user->getType() == 'trainee' && $authorizationChecker->isGranted('ROLE_TRAINEE')) {
/** #var Trainee $trainee */
$trainee = $user->getTrainee();
if ($user->getState() == 'active' && $trainee->getState() == 'active') {
$response = $this->redirectToRoute('trainee');
} else {
$response = $this->redirectToRoute('trainee_profile');
}
} else {
$response = $this->redirectToRoute('homepage');
}
} else {
$response = $this->redirectToRoute('homepage');
}
return $response;
}
I have seen some examples recommending using symfony events (kernel.request) to handle it minimizing controller code. But in this case I will not be able to use this action as sign up and login success path.
I am not using FOS, because of lack of user customization. I prefer handling User my self.
Is my approach wrong and something to be worried about?
Some things that I am concerned:
Redirection count:
For example. I am logged in user and I go to homepage and am redirected to my action where I am checked whether I am logged in or not and depending on user type I am redirected to respected page.
public function indexAction(Request $request)
{
if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
return $this->redirectToRoute('authorization');
}
// ...
}
Slowing website:
In the future I will be using this action in more pages and website will definatelly slow down everytime executing same code on each page.
I am not using FOS, because of lack of user customization. I prefer handling User my self.
Blockquote
You really shouldn't reinvent the wheel here but even if you want to your assessment that extending FOS is hard is wrong, take a look at this gist how many of us Symfony Devs extend FOS to enable social login via HWIOauthBundle. Extending it for other purposes is equally trivial.
Update Since OP's Comments Below
....
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use AppBundle\Form\Type\LoginType;
....
....
$login_form = $this->createForm(LoginType::class);
$login_form->handleRequest($request);
if (!$login_form->isValid()){
//throw error
}
$email = strip_tags($login_form->get('email')->getData());
$password = $login_form->get('password')->getData();
// Call the User Manager
$userManager = $this->get('fos_user.user_manager');
$user = $userManager->findUserByUsernameOrEmail($email);
if(!$user){
//throw error : User With Submitted Credentials Does Not Exist
}
$user_password = $user->getPassword();
$encoded_password = $this->get('security.password_encoder')->encodePassword($user, $password);
if($encoded_password != $user_password){
//throw error : Wrong Password, Please Check Your Details & Re-submit
}
// Successful query of username/email and password now we log in user
// Create new token
$token = new UsernamePasswordToken($user, $user->getPassword(), 'main', $user->getRoles());
// Login New User
$tokenStorage = $this->get('security.token_storage');
$tokenStorage->setToken($token);
// User now logged in
$user_id = $user->getId();
....
Redirection count:
In your case each redirection would cause at least 1 database query to verify user session, so yes many them could lead to a bad use and server experience. If you use FOSUserBundle with Doctrine's Second Level Cache, you can avoid this query everytime you call $this->getUser();
Slowing website:
This very small bit of code with hardly be you bottleneck at scale but for arguments sake let's assume it is. I would solve this problem by introducing client side sessions. A framework like Angular or my personal favorite Ember would allow you to store user sessions on the client so as not to even bother going back to your server all the time for such a menial task as checking authentication or roles. I would still advise you to keep some server side code for those cheeky users who want to poke holes in your code.
The Access Control section in the Symfony documentation might offer easier solutions to restrict access. In my time using Symfony I have always been able to use it for redirection and access control.

fosuserbundle ldap configuration for strange use case

I'm trying to create a fosuserbundle for a quite strange use case, which is mandatory requirement, so no space to diplomacy.
Use case is as follow:
users in a mongo db table populated by jms messages -no registration form
users log in by ldap
user record not created by ldap, after a successful login username is checked against mongodb document
Considering that ldap could successfully log in people that exhist in ldap but cannot access site (but login is still successful), what could be the best way to perform such authentication chain?
I was thinking about some possible options:
listen on interactive login event, but imho there's no way to modify an onSuccess event
create a custom AuthenticationListener to do another check inside onSuccess method
chain authentication using scheb two-factor bundle
any hint?
I've used Fr3DLdapBundle which can be incorporate with FOSUserBundle quite easily (I'm using the 2.0.x version, I have no idea if the previous ones will do the same or be as easy to set up).
In the LdapManager (by default) it creates a new user if one is not already on the database which is not what I wanted (and doesn't seem to be what you want) so I have added my own manager that checks for the presence of the user in the database and then deals with the accordingly.
use FR3D\LdapBundle\Ldap\LdapManager as BaseLdapManager;
.. Other use stuff ..
class LdapManager extends BaseLdapManager
{
protected $userRepository;
protected $usernameCanonicalizer;
public function __construct(
LdapDriverInterface $driver,
$userManager,
array $params,
ObjectRepository $userRepository,
CanonicalizerInterface $usernameCanonicalizer
) {
parent::__construct($driver, $userManager, $params);
$this->userRepository = $userRepository;
$this->usernameCanonicalizer = $usernameCanonicalizer;
}
/**
* {#inheritDoc}
*/
public function findUserBy(array $criteria)
{
$filter = $this->buildFilter($criteria);
$entries = $this->driver->search(
$this->params['baseDn'], $filter, $this->ldapAttributes
);
if ($entries['count'] > 1) {
throw new \Exception('This search can only return a single user');
}
if ($entries['count'] == 0) {
return false;
}
$uid = $entries[0]['uid'][0];
$usernameCanonical = $this->usernameCanonicalizer->canonicalize($uid);
$user = $this->userRepository->findOneBy(
array('usernameCanonical' => $usernameCanonical)
);
if (null === $user) {
throw new \Exception('Your account has yet to be set up. See Admin.');
}
return $user;
}

Symfony2 - Redirect response from request EventListener in dev mode while ignoring built in request events

I am building my own user management system in Symfony2 (not using FOSUserBundle) and want to be able to force users to change their password.
I have setup an EventListener to listen to the kernal.request event, then I perform some logic in the listener to determine if the user needs to change their password; if they do, then they are redirected to a "Change Password" route.
I add the service to my config.yml to listen on the kernal.request:
password_change_listener:
class: Acme\AdminBundle\EventListener\PasswordChangeListener
arguments: [ #service_container ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onMustChangepasswordEvent }
And then the listener:
public function onMustChangepasswordEvent(GetResponseEvent $event) {
$securityContext = $this->container->get('security.context');
// if not logged in, no need to change password
if ( !$securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED') )
return;
// If already on the change_password page, no need to change password
$changePasswordRoute = 'change_password';
$_route = $event->getRequest()->get('_route');
if ($changePasswordRoute == $_route)
return;
// Check the user object to see if user needs to change password
$user = $this->getUser();
if (!$user->getMustChangePassword())
return;
// If still here, redirect to the change password page
$url = $this->container->get('router')->generate($changePasswordRoute);
$response = new RedirectResponse($url);
$event->setResponse($response);
}
The problem I am having is that in dev mode, my listener is also redirecting the profiler bar and assetic request events. It works when I dump assets and clear cache and view the site in production mode.
Is there a way I can ignore the events from assetic/profiler bar/any other internal controllers? Or a better way to redirect a user to the change_password page (not only on login success)?
Going crazy thinking up wild hack solutions, but surely there is a way to handle this elegantly in Symfony2?
This is the very hack solution I am using for now:
Determine if in dev environment
If so, get an array of all the routes
Filter the route array so that only the routes I have added remain
Compare the current route to the array of routes
If a match is found, this means that the event is not an in-built controller, but must be one that I have added, so perform the redirect.
And this is the madness that makes that work:
// determine if in dev environment
if (($this->container->getParameter('kernel.environment') == 'dev'))
{
// Get array of all routes that are not built in
// (i.e You have added them yourself in a routing.yml file).
// Then get the current route, and check if it exists in the array
$myAppName = 'Acme';
$routes = $this->getAllNonInternalRoutes($myAppName);
$currentRoute = $event->getRequest()->get('_route');
if(!in_array($currentRoute, $routes))
return;
}
// If still here, success, you have ignored the assetic and
// web profiler actions, and any other actions that you did not add
// yourself in a routing.yml file! Go ahead and redirect!
$url = $this->container->get('router')->generate('change_password_route');
$response = new RedirectResponse($url);
$event->setResponse($response);
And the crazy hack function getAllNonInternalRoutes() that makes it work (which is a modification of code I found here by Qoop:
private function getAllNonInternalRoutes($app_name) {
$router = $this->container->get('router');
$collection = $router->getRouteCollection();
$allRoutes = $collection->all();
$routes = array();
foreach ($allRoutes as $route => $params)
{
$defaults = $params->getDefaults();
if (isset($defaults['_controller']))
{
$controllerAction = explode(':', $defaults['_controller']);
$controller = $controllerAction[0];
if ((strpos($controller, $app_name) === 0))
$routes[]= $route;
}
}
return $routes;
}

Resources