Note: If it's any help, I'm using Pyramid 1.3.2. I know it's a little out of date, I would prefer not to update right away, but I might be able to force an update if the latest version provides better support for this use case.
The Pyramid-based application I'm working on has a strict authorization policy: all calls must be authenticated. Since 1) it's tedious to add this manually on every request handelr; and 2) we don't want anybody to "forget" adding authentication, we enforce this server-wide using a simple Pyramid middleware (tween) that verifies all incoming requests.
Recently, this restriction has been slightly relaxed: occasionally, some resources should support (safe & idempotent) GET without authentication.
It seems this is directly opposed to the usual design ideas behind authentication in most web frameworks (optional authentication), so I can't get it to work quite as expected.
QUESTION: What is the correct approach to implementing an authorization middleware that authenticates & verifies authorization by default, but can be disabled on a view-by-view basis?
So far, I've tried adding a simple decorator like so:
def allows_anonymous_access(f):
f.allows_anonymous_access = True; return f
#allows_anonymous_access
def my_pyramid_view(request):
# ...
In my middleware, I would like to use it like this:
def authorization_middleware(handler, registry):
def verify_authorization(request):
# Identify the user making the request. Make sure we get the
# user's identify if provided, even when the request handler
# allows anonymous access.
try:
request.principal = extract_user(request)
except InvalidCredentials, error:
if getattr(handler, 'allows_anonymous_access', False):
request.principal = AnonymousUser()
else:
raise HTTPUnauthorized(...)
# Invoke the handler.
return handler(request)
# Middleware that will pre/post-process the request.
return authorization_middleware
However, when the middleware executes, handler is not my view. It happens to be a bound method (pyramid.router.Router.handle_request) which does not provide me access to the view callable, meaning I cannot access the flag set by the middleware.
You probably want pyramid.config.set_default_permission(permission). From docs:
Adding a default permission makes it unnecessary to protect each view
configuration with an explicit permission, unless your application
policy requires some exception for a particular view.
If a default permission is in effect, view configurations meant to
create a truly anonymously accessible view (even exception view views)
must use the value of the permission importable as
pyramid.security.NO_PERMISSION_REQUIRED. When this string is used as
the permission for a view configuration, the default permission is
ignored, and the view is registered, making it available to all
callers regardless of their credentials.
Answer provided by raydeo_ on #pyramid freenode IRC channel.
Related
I've just started using loopback4 and I would like to protect the /explorer from being public. The user would initially see a page where username and password must be entered. If successful, the user is redirected to /explorer where he can see all API methods (and execute them). If user is not authenticated, accessing the path /explorer would give a response of "Unauthorized". Is there a way to easily implement this?
There is issue talking about a GLOBAL default strategy is enabled for all routes including explorer in https://github.com/strongloop/loopback-next/issues/5758
The way is to specify a global metadata through the options:
this.configure(AuthenticationBindings.COMPONENT).to({
defaultMetadata: {
strategy: 'JWTStrategy'
}
})
this.component(AuthenticationComponent);
registerAuthenticationStrategy(this, JWTAuthenticationStrategy)
But in terms of enabling a single endpoint added by route.get(), it's not supported yet, see code of how explorer is registered. #loopback/authentication retrieves auth strategy name from a controller class or its members, but if the route is not defined in the controller, it can only fall back to the default options, see implementation
I'm migrating to ActiveStorage an app that is permissions sensitive. I need to make sure files are only accessed by people with permissions, and that those permissions only last for a specified period of time.
With Paperclip, that was as easy as defining a route with a pundit authorization call:
def thumbnail
authorize #record
redirect_to #record.thumbnail.expiring_url(300)
end
Any call to /record/thumbnail would be authenticated by devise, authorized by pundit, and then redirected to S3 with an expiring url. It's a process that worked.
Moving to ActiveStorage, my initial thought was I'd use service_url, but it only works if I'm using S3 -- it doesn't work in testing, it doesn't work if I'm in local development with a disk service. The actual, 'correct' approach is to use url_for, or in the example above redirect_to url_for #record.thumbnail.
The problem is, url_for appears to issue a permanent url -- the url it generates is always valid, for anyone, without authentication or authorization. More importantly, once someone has that url, I can't revoke it, I can't say it's only good for a week. Once someone has that url, they have access to the file, forever. (Or, presumably, at least until the file is updated).
I don't think it's a huge security flaw, but it's definitely a step backward from what was achievable using Paperclip. Am I missing an important detail, or is this really all that's doable with ActiveStorage?
The documentation says:
If you need to enforce access protection beyond the security-through-obscurity factor of the signed blob references, you'll need to implement your own authenticated redirection controller.
Which is not particularly helpful. To do this you create your own controller to serve the blobs:
class BlobsController < ApplicationController
include ActiveStorage::SetBlob
before_action :authorize_blob
def show
expires_in ActiveStorage::Blob.service.url_expires_in
redirect_to #blob.service_url(disposition: params[:disposition])
end
private
def authorize_blob
# Your authorization code goes here
end
end
Then you will need to set up your routing:
get '/blobs/:signed_id/*filename' => "blobs#show", as: "service_blob"
direct :blob do |blob, options|
route_for(:service_blob, blob.signed_id, blob.filename, options)
end
resolve("ActiveStorage::Blob") { |blob, options| route_for(:blob, blob, options) }
Finally, you need to disable the insecure default controller so that a knowledgeable user cannot circumvent your authorization.
If you are in Rails 6.1+, you can do this by setting config.active_storage.draw_routes to false in your application.rb. But you will then need to make sure the rest of the ActiveStorage routes are drawn manually and there are quite a few of them. You can view them on Github.
Otherwise, you will need to add extend the controller to redirect to your new one (or just shut it down entirely). You can add the following to an initializer:
module ActiveStorageRedirect
def self.included(controller)
controller.before_action :redirect_to_authenticated
end
private
def redirect_to_authenticated
redirect_to Rails.application.routes.url_helpers.blob_path(#blob)
end
end
ActiveStorage::Blobs::RedirectController.include(ActiveStorageRedirect) # For Rails >= 6.1
ActiveStorage::Blobs::ProxyController.include(ActiveStorageRedirect) # For Rails >= 6.1
# ActiveStorage::BlobsController.include(ActiveStorageRedirect) # For Rails < 6.1
recently in one of my applications I needed to access currently logged-in user data for saving in another model (something like the author of a book or owner of a book). in my googling, I encountered these references but none of them was useful.
https://github.com/strongloop/loopback/issues/1495
https://docs.strongloop.com/display/public/LB/Using+current+context
...
all of them have this problem about accessing context or req object. after three days I decided to switch to afterRemote remote hook and add Owner or Author on that stage.
but something was wrong with this solution.
in strongloop's documentations (https://docs.strongloop.com/display/public/LB/Remote+hooks) there is a variable as ctx.req.accessToken that saves current logged-in user access token. but in the application this variable is undefined.
instead, I found accessToken in ctx.req.query.access_token and it was currently access_token variable that is sent to the server.
here is my problem:
is this variable (ctx.req.query.access_token) always available or
it's just because loopback-explorer send access_token as GET
variable?
in production mode do applications need to send access_token as
GET variable or it should be sent as Authorization in the header?
why ctx.req.accessToken is undefined?
could these things change over time? cause most of users encounter this problem due to deprecation of app.getCurrentContext()
Is this variable (ctx.req.query.access_token) always available or
it's just because loopback-explorer send access_token as GET
variable?
Well if your application always sends in the querystring, then it'll be always available for you, but it also sent in the header, or cookie or in the request body, but I don't suggest using it because it if the user logged in and the access token is valid and ctx.req.accessToken should be available and you can use it.
In production mode do applications need to send access_token as
GET variable or it should be sent as Authorization in the header?
I believe Authorization header is preferred, as if you send it in a GET variable, well it'll be visible in the logs and someone with the access to the logs can access the session(well unless you trust everyone), other than this it's fine to have it in a GET variable. Though I believe loopback client SDKs(Angular, Android, iOS) all send it via Authorization header by default, so you might have to configure them(maybe not possible).
Why ctx.req.accessToken is undefined?
Sometimes the context is lost thanks to the database drivers connection pooling, or the context req is lost(ctx.req) and they are null.
Assuming ctx.req is defined(because sometimes it's not), then probably that means the user is not logged it, or it's access token wasn't valid(expired or not in database). Also it could be a bug(maybe misconfiguration on your side), which also means for you that you will authentication problems.
Could these things change over time? cause most of users encounter this problem due to deprecation of app.getCurrentContext()
app.getCurrentContext is risky to use and I don't suggest unless you have no other solution. If you use it and it works, it might stop working if the database driver changes or in some corner cases that you haven't tested it, it might not work.
In the updated doc https://loopback.io/doc/en/lb3/Using-current-context.html
add this in your remoting metadata
"accepts": [
{"arg": "options", "type": "object", "http": "optionsFromRequest"}
]
then
MyModel.methodName = function(options) {
const token = options && options.accessToken;
const userId = token.userId
}
but it says
In LoopBack 2.x, this feature is disabled by default for compatibility reasons. To enable, add "injectOptionsFromRemoteContext": true to your model JSON file.
so add "injectOptionsFromRemoteContext": true on your model.json file
I use the security.yml with access_control to secure the API paths based on the user role. This works fine, but how do I secure specific parameters like /api/project/:id?
Different users have access to different project ids. Therefore a database call has to be made to check if this user has access to this project.
I tried to use $this->denyAccessUnlessGranted('GET', $projectId, 'Unauthorized access!'); in the ProjectController, which calls a custom Voter to check the database and therefore the access.
public function getProjectAction(Request $request, $id)
{
$this->denyAccessUnlessGranted('GET', $id, 'Unauthorized access!');
This works, but it seems very unpractical to add this code to 10+ actions in the ProjectController alone and also in many parts of the API.
Therefore my question: What is the best pratice to secure a REST api with symfony2, fosUserBundle and fosRestBundle
I would suggest introducing security voters.
http://symfony.com/doc/current/cookbook/security/voters_data_permission.html
Also create some kind of exception handler / listener, to catch your exceptions and make a specific error response.
http://symfony.com/doc/current/cookbook/service_container/event_listener.html
I am developing a CRUD based database which needs to have some ACL (Access Control Lists) based on users. We want to make it so that there's an Admin access panel as well so we can quickly delete content if needed or otherwise moderate it. I am trying to keep it flexible so that it could be a separate interface for convenience but also not diverge from the general user interface because otherwise it will be harder to maintain.
What would be a good way to handle creating a user interface in the front end, assuming the backend will deal with the ACLs? Is it necessary to create a separate BB.js interface to serve or is it fine to pass a bit of extra code to all users that will be ignored?
Recommendations or warnings would be appreciated too!
I interpreted your question as you want to serve up different interfaces for different access levels of users. Therefore I would:
Put a check for the users access before they hit each route to make sure they aren't accessing a page they shouldn't see
They can still try to circumvent this (since they can change their user model), your backend would still catch any unauthorized requests.
You can also conditionally show and hide page elements based on user access levels.
Heres an example code for my ACL.coffee
acl = {}
acl['admin'] = [
'page1',
'page2',
'page3',
'page4',
'page5'
]
acl['user'] = [
'page1',
'page2',
'page3'
]
hasAccess = (route) ->
# Get User Model
user = window.App.user
# Get Associated ACL
permissions = acl[user.get('role')]
# Check each URL for Access Privileges
# Returns false if route not in array
permissions.some (r) -> ~route.indexOf r
{hasAccess}