Symfony2: allow all unmatched routes to be accessed anonymously - angularjs

I have Symfony2 application separated into 2 bundles: BackendBundle for API and FrontendBundle for AngularJS "client". Everything works under firewall.
BackendBundle has entities, handles API routes; FrontendBundle has Angular views, routing etc. and has only one controller with wildcard:
class AngularController extends Controller {
/**
* #Route("/{route}", name="angular_index_all_unmatched_routes", requirements={"route" = ".*"})
* #Template("FrontendBundle::index.html.twig")
*/
public function angularIndexAction($route) {
return ['route' => $route];
}
}
FrontendBundle routing is defined as last resource in app/config/routing.yml, to be invoked only if any other route was not matched. Thanks to that, it can handle Angular HTML5-mode routes if they're accessed directly (for example copy-paste) - and it works ok.
What I want to do, is define firewall and/or access control in way that all those unmatched routes (handled by AngularController::angularIndexAction()) could be accessible by anonymous user.
Why? I want to open some API routes (via frontend proxy) to be accessible by non-users (for example confirmation URLs sent by email, with some message to user).
I don't want to hardcode access control list for every anonymous "Angular" route, I would like to do it only for API routes. At the end, those unmatched routes should open Angular's index which should know if user is logged in (for displaying full or simplified layout) and should handle Angular routes and display some kind of "Access denied" message if request failed (there is Symfony listener and Angular's $provide interceptor for that).
Any suggestions?
Edit: #Security annotation on AngularController::angularIndexAction() does not work, it still redirects to firewall entry point.
Edit2: Here is fragment of security.yml
firewalls:
unsecured:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
anonymous: true
secured:
pattern: '^.*$'
form_login:
login_path: /our-provider/login
check_path: /our-provider/callback/
anonymous: true
entry_point: our_provider.entry_point
access_control:
- { path: '^/our-provider/(login(/[a-zA-Z]+)?|logout|redirect|callback)', roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: '^/', roles: ROLE_USER }
I know that { path: '^/', roles: ROLE_USER } will redirect all routes to login page if user is not logged in. I assumed it's obvious and did not mentioned it. What I want is force ROLE_USER for matched routes and let IS_AUTHENTICATED_ANONYMOUSLY for those unmatched, without explicitely defining each frontend "proxy-route". In my case there is not 404 Symfony page, because everything goes to angular_index_all_unmatched_routes route and there Angular routing definition decides if there is something to handle or not.

I haven't tried this, and I cannot begin to guess your existing security/route setup in security.yml but I guess you could whitelist the method with IS_AUTHENTICATED_ANONYMOUSLY. From the Symfony docs:
All users (even anonymous ones) have this - this is useful when whitelisting URLs to guarantee access - some details are in How Does the Security access_control Work?.
So, for example, if you were using the #Security annotation you could do something like (not tested):
class AngularController extends Controller {
/**
* #Route("/{route}", name="route", requirements={"route" = ".*"})
* #Template("FrontendBundle::index.html.twig")
* #Security("has_role('IS_AUTHENTICATED_ANONYMOUSLY')")
*/
public function angularIndexAction($route) {
return ['route' => $route];
}
}
More on the #Security annotation here.
Hope this helps :)
Edit
All that said, when you define/restrict your routes under access_control in security.yml, the matching process stops on the first match. I assume that you have some role-restricted paths, which you should define explicitly - and put them first, so if they match the process stops.
Otherwise, you should be able to add a catch-all route, enforced by role IS_AUTHENTICATED_ANONYMOUSLY. Since the path definition of a route is a regex, something like ^/ should catch anything that is not explicitly defined. Just make sure and place it after your restricted route definitions.
You would not need for the #Security annotation in this case.
Edit 2
I tried mocking this out using a clean instance and HTTP BasicAuth but what I was trying to achieve was the following, which I understand as similar to your use case:
Create a backend controller with routes / and /api/ and trigger a HTTP BasicAuth authentication popup
Create a frontend controller with route /{route} that would match everything else and authenticate anonymously.
My firewall and access_control configuration looks like this:
security:
encoders:
# encoder config here
providers:
# provider config here
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured:
anonymous: ~
http_basic: ~
access_control:
- { path: ^/$, roles: ROLE_USER }
- { path: ^/api/, roles: ROLE_USER }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Access control paths are regexes, so ^/$ and ^/ are not the same. The former will only match exactly to route /. The latter will match any route that begins with /; e.g: /home, /products, /contact etc.
Indeed, the latter will match and anonymously authenticate /api, but it will not match /api/, or /api/1 etc. as these are explicitly defined and restricted to ROLE_USER.
So the general idea is to explicitly and (if possible) exactly match the routes you want to restrict, and declare those first. The last declaration ^/ should openly catch any other route that falls through.

Related

Is there a way to configure Apache to auto-recognize URLs for Django vs React without hard-coding each endpoint?

We're using Apache 2.4 with React 17 and a Django 3.2 (Python 3.9) application. Curious about a better way to set up our Apache configs to route requests to the React and Django apps. Right now, our Apache virtual hosts file hard-codes which routes need to be handled by the React app vs which need to be handled by Django ...
AliasMatch ^/(?!people)(?!states/)(?!countries/)(?!predefined_types/)(?!coop_types/)(?!coops/)(?!data)(?!save_to_sheet_from_form).* /var/www/html/client/build/$0
<Directory "/var/www/html/client/build/">
Options Indexes FollowSymLinks
AllowOverride all
</Directory>
WSGIDaemonProcess ssl_directory home=/var/www/html/web python-home=/var/www/html/web/venv
WSGIProcessGroup ssl_directory
WSGIScriptAlias /coops /var/www/html/web/directory/wsgi.py/coops process-group=ssl_directory
WSGIScriptAlias /data /var/www/html/web/directory/wsgi.py/data process-group=ssl_directory
WSGIScriptAlias /countries /var/www/html/web/directory/wsgi.py/countries process-group=ssl_directory
WSGIScriptAlias /states /var/www/html/web/directory/wsgi.py/states process-group=ssl_directory
WSGIScriptAlias /predefined_types /var/www/html/web/directory/wsgi.py/predefined_types process-group=ssl_directory
WSGIScriptAlias /coop_types /var/www/html/web/directory/wsgi.py/coop_types process-group=ssl_directory
WSGIScriptAlias /people /var/www/html/web/directory/wsgi.py/people process-group=ssl_directory
WSGIScriptAlias /save_to_sheet_from_form /var/www/html/web/directory/wsgi.py/save_to_sheet_from_form process-group=ssl_directory
The Django app, for its part, defines urls in the standard way (in our urls.py file) ...
...
urlpatterns = [
path('data', views.data, name='data'),
path('coops/no_coords', views.coops_wo_coordinates, name='coops_wo_coordinates'),
path('coops/unapproved', views.unapproved_coops, name='unapproved_coops'),
path('coops/', views.CoopList.as_view()),
path('coops/<int:pk>/', views.CoopDetail.as_view()),
path('people/', views.PersonList.as_view()),
path('people/<int:pk>/', views.PersonDetail.as_view()),
path('users/', views.CreateUserView.as_view()),
path('predefined_types/', views.CoopTypeList.as_view()),
path('coop_types/', views.CoopTypeList.as_view()),
path('countries/', views.CountryList.as_view()),
path('states/<country_code>', views.StateList.as_view()),
path('login', views.signin),
path(settings.LOGOUT_PATH, views.signout),
path('user_info', views.user_info),
]
urlpatterns = format_suffix_patterns(urlpatterns)
Is there a more automated way we can get Apache to know what routes shoudl go to Django vs React? Whenever we add a new Django endpoint, we have to add a hard-coded exception in our Apache configs.
Edit: Here is an example how I make a React call to the API ...
const { REACT_APP_PROXY } = process.env;
class PersonService {
getById(id, callback) {
fetch(REACT_APP_PROXY + "/people/" + id)
.then((response) => {
return response.json();
})
.then((data) => {
const person = data;
person.contact_methods.map((contact_method) => {
if (contact_method.type == "PHONE") {
person.phone = contact_method.phone.substring(2);
} else if (contact_method.type == "EMAIL") {
person.email = contact_method.email;
}
});
if (callback) callback(person);
});
}
}
Concerning python there are two wsgi-options that could help you to reduce the required lines and steps.
Using the WSGIScriptAlias directive to map to a directory containing
any number of WSGI applications:
WSGIScriptAlias /wsgi/ /usr/local/wsgi/scripts/
When this is used, the next part of the URL after the URL prefix is
used to identify which WSGI application script file within the target
directory should be used. Both the mount point and the directory path
must have a trailing slash.
Another option is intended primarily to suppress the extension in the
Frontend but could be used too to redirect based on different aspects:
WSGIScriptAliasMatch ^/wsgi/([^/]+) /usr/local/wsgi/scripts/$1.wsgi
In this case, any path information appearing after the URL prefix, will be mapped to a corresponding WSGI script file in the directory, but with a ‘.wsgi’ extension. The extension would though not need to be included in the URL.
The options can be configured with common Apache directives too, and there are additional features like directory index can be configured like that, it's useful to read this page about all the options: https://modwsgi.readthedocs.io/en/master/user-guides/configuration-guidelines.html
Furthermore you've the option to use the powerful mod_rewrite. Certainly there are many more options, most important is that to distinguish between script-types you've to have some aspect in the url or path to decide where to redirect. It can be a suffix and / or usage of WSGIScriptAlias which both would be most simple but could be based on mod_mime perhaps too.
So actually the question in return is how far you're willing to use suffixes or URLs that make a distinction possible and easy enough to be handled by the offered options.
One aspect to consider is that a python app can reference js-files too, so simply a suffix might not be enough but if full paths are provided that's perhaps not too difficult to handle.
With mod_rewrite you have full control over redirection and remapping in Apache
Redirecting and Remapping with mod_rewrite
You should implement something like this:
RedirectMatch "^/docs/(.*)" "http://new.example.com/docs/$1"
or
RewriteRule ^images/([^/]+)$ /assets/images/public/$1 [L]
That redirects each part of application documents to its corresponding handler.
RedirectMatch "^/react-docs/(.*)" "http://react-handler.example.com/react-docs/$1"
As you see you need something in your URL that RedirectMatch or RewriteRule uses Regex on it for separating and sending each document to corresponding handler.
Q&As on mod_rewrite

Change Camel's basic /camel url

Issue:
I would need to change the basic /camel url which camel uses by default, but when i try to change it in application.yml nothing happens to it.
Would like to keep other systems intact without changing their urls, from what they already have (would require quiet a bit of work in back-end systems)
Current URL: http://localhost:8080/camel/hello
Desired URL: http://localhost:8080/service/hello
Checked links which are NOT working for me:
Link1
Link2
Link3
EG: application.yml
camel:
springboot:
name: CamelRestContext
component:
servlet:
mapping:
enabled: true
context-path: /service
So apparently this way works:
camel:
springboot:
name: RestDSLContext
servlet:
mapping:
context-path: /service/*
rest:
context-path: /service

Configure the sw-precache WebPack plugin to load a server rendered page as the navigateFallback route

consider the following scenario:
My express server dynamically generates HTML for the "/" route of my single page application.
I would like to re-serve this same generated HTML as the service worker navigateFallback when the user is offline.
I'm using https://www.npmjs.com/package/sw-precache-webpack-plugin in my webpack configuration.
If I generate an index.html via html-webpack-plugin, say, and set index.html as my navigateFallback file, that generated file gets served correctly by the service worker.
However, I can see no way to cause the on-the-fly rendered index html (what the live server returns for the "/" path) to be cached and used as the offline html.
Use dynamicUrlToDependencies option of Service Worker Precache to cache your route url and its dependencies. Then set navigateFallback to '/' and navigateFallbackWhitelist to a regex matching your sublinks logic.
Take this configuration : (Add const glob = require('glob') atop of your webpack config)
new SWPrecacheWebpackPlugin({
cacheId: 'my-project',
filename: 'offline.js',
maximumFileSizeToCacheInBytes: 4194304,
dynamicUrlToDependencies: {
'/': [
...glob.sync(`[name].js`),
...glob.sync(`[name].css`)
]
},
navigateFallback: '/',
navigateFallbackWhitelist: [/^\/page\//],
staticFileGlobsIgnorePatterns: [/\.map$/],
minify: false, //set to "true" when going on production
runtimeCaching: [{
urlPattern: /^http:\/\/localhost:2000\/api/,
// Use network first and cache as a fallback
handler: 'networkFirst'
}],
})
That use case should be supported. I have an example of something similar using the underlying sw-precache library, and I believe the syntax should be equivalent when using the Webpack wrapper.
In this case, /shell is the URL used for dynamically generated content from the server, constituting the App Shell, but it sounds like your use case is similar, with / instead of /shell.
{
// Define the dependencies for the server-rendered /shell URL,
// so that it's kept up to date.
dynamicUrlToDependencies: {
'/shell': [
...glob.sync(`${BUILD_DIR}/rev/js/**/*.js`),
...glob.sync(`${BUILD_DIR}/rev/styles/all*.css`),
`${SRC_DIR}/views/index.handlebars`
]
},
// Brute force server worker routing:
// Tell the service worker to use /shell for all navigations.
// E.g. A request for /guides/12345 will be fulfilled with /shell
navigateFallback: '/shell',
// Other config goes here...
}

Symfony FOS user bundle - Switch User failed: "Username "*" does not exist."

It really is what the headline states - I can't figure out why user switching isn't working.
In my security.yml I have:
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
form_login:
provider: fos_userbundle # using FOSUserBundle for user authentication
check_path: fos_user_security_check
login_path: fos_user_security_login
csrf_token_generator: security.csrf.token_manager # CSRF token - can be changed?
default_target_path: default_logged_in_target # default route to go to after login
# default_target_path: fos_user_profile_show
always_use_default_target_path: true # ignore the requested url and allways go to default route after login
logout:
path: fos_user_security_logout
target: default_loged_out_target
logout: true
anonymous: true
switch_user: true
Calling the URL
[ProjectPath]/account?_switch_user=testuser
results in the error
Switch User failed: "Username "testuser" does not exist."
"testuser" however is a perfectly valid user and I can log in normaly with that user when logging in the standard way.
The user I am logged in with when calling the URL with "_switch_user" does have the role "ROLE_ALLOWED_TO_SWITCH" - however this doesn't seem to be the problem.
I am stuck here.
Any hints are highly appreciated.
EDIT:
The stacktrace shows that in "SwitchUserListener.php" the call to
$this->tokenStorage->setToken($this->attemptSwitchUser($request));
fails and is catched resulting in the given error.
I could get it to work adding the fos_userbundle user provider to security.yml like this:
firewalls:
..
main:
...
switch_user:
provider: fos_userbundle
Ok, I made a stupid mistake, but perhaps someone else did as well. I use email for authenticating, but used the username for switching the user. Make sure you use the same property. So in my case I needed to use the email:
?_switch_user=example#email.com

How do you escape a . (full stop) or / so it doesn't change URL's meaning?

I have a Web API 2.0 service which defines a particular route:
/api/someEntityGroup/{entityName}
I'm calling this enpoint using Angular $resource service.
The problem is when user wants to provide an entity name with characters that have a specific meaning in URL:
404 Not found - . (full stop), /, +
400 Bad request - ?, :, &, %, *, <, >
And these are the ones I've encountered. There may be others that may be problematic as well and I'm not even aware of them (yet).
If I use window.escape() function these still don't work, but I mainly get 404 back (the only exception being * which still returns 400 Bad request).
My code
Angular resource creation:
.factory("entityResource", ["$resource", function() {
return $resource("/api/entities/:id", null, {
search: {
method: "GET",
url: "/api/entities/:name",
isArray: true
}
});
}]);
How I call it in my code:
entityResource.search({ query: scope.name }, function(data) {
...
});
My Api controller action:
[RoutePrefix("/api/entities")]
public class EntitiesController: ApiController
{
[Route("{searchQuery}")]
public IEnumerable<Interest> Get(string searchQuery)
{
return this.interestService.Search(searchQuery);
}
...
}
I can shed some light on your 404 Not found issue when using ., /, + characters.
The issue isn't with Angular but rather with Web API and the way it resolves routes. Urls that Web API interprets as being managed resources (e.g. static content, pages etc.) it will try to resolve independently.
Set the following in your web.config to disable this behavior and force WebAPI to run all requests through your modules:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
Just a warning - If your Web API is hosted together with something like MVC or a static website, the above is not recommended as it will force all managed resources (pages, MVC routes, content[css,js,images]) through your API modules and there will be a performance impact. However, if all the API is doing is serving resource routes I would recommend enabling the above.

Resources