In my CakePHP application I have multi-tenancy which is provided through isolated databases (each tenant has their own, tenant-specific database).
There is also a 'global' database which contains users and tenancy information. The 'tenants' table contains the name of which database the particular tenant occupies. Each user contains a single tenant_id.
Structure:
global_db:
users (contains tenant_id foreign key)
tenants (contains tenant-specific database name, ie: 'isolated_tenant1_db')
isolated_tenant1_db:
orders
jobs
customers
isolated_tenant2_db:
orders
jobs
customers
This system works correctly when the user is logged in via forms / sessions. When they login through /Users/login their tenancy is verified, stored in Session, and database parameters are loaded so their own 'isolated' models can use this dynamic connection.
However, issues arise when the user tries to login via Basic Auth, and directly request the controller function they want to access. For example /Orders/view/1.xml.
In this case, CakePHP attempts to construct the 'Order' Model before the user has been logged in, and therefore before any tenancy information is available - which means it has no idea what database to connect to in order to access orders.
From putting debug() statements around the place I can see that the order in which models / controllers / auth are constructed / executed is as follows (when executing /Orders/view/1.xml):
Model __construct: User
Controller __construct: OrdersController
Model __construct: Permission
Model __construct: Order
function: OrdersController/beforeFilter
AuthComponent __startup
Model __construct: Models related to Order
My problem is that AuthComponent::_startup is executed after Order Model has been constructed. I need to attempt to login the user (and get their database information) before this 'Order' model is constructed.
Questions:
What causes the User model to be constructed before anything else? (I also have the default CakePHP ACL enabled)
Where in the App can I put a call to Auth->login() to attempt login if the request contains BasicAuth headers, that will be executed prior to trying to load tenant-specific models? I assume putting this inside User __construct is a very bad idea.
== UPDATE 01/05/2014 ==
Inserting code samples.
bootstrap.php:
Checks whether the request is being made to api. subdomain:
// Determine whether the request is coming from the api.* subdomain, and if so set the API_REQUEST define to true.
if (preg_match('/^api\./i',$_SERVER['HTTP_HOST']))
{
define('API_REQUEST',true);
// Any links generated (in emails etc), will contain the full base url. If a cron job logged in via the API is generating
// those e-mails, then users will receive links to api.mydomain, instead of just mydomain.
$full_base_url = Router::fullBaseUrl();
$new_full_base_url = preg_replace('/\/\/api\./i', '//', $full_base_url);
Router::fullBaseUrl($new_full_base_url);
CakeLog::write('auth_base_url_debug', 'modified fullbaseurl from ' . $full_base_url . ' to ' . $new_full_base_url);
}
else
{
define('API_REQUEST',false);
}
AppController.php:
public $components = array(
'Security',
'Session',
'Acl',
'Auth' => array(
'className' => 'ExtendedAuth',
'authenticate' => array(
'FormAlias',
),
'authorize' => array(
'Actions' => array('actionPath' => 'controllers')
),
'loginRedirect' => array('controller' => 'Consignments', 'action' => 'index'),
'logoutRedirect' => array('controller' => 'Users', 'action' => 'login'),
),
//'Users.RememberMe',
);
function beforeFilter()
{
// Reroute all requests to API subdomain (ie: api.mydomain) to api_ prefixed actions.
// Also, enable Basic Authentication if the user is accessing via api.*
// If login fails, return 401 error instead of 302 redirect to login page.
if(API_REQUEST == true)
{
$this->params['action'] = 'api_'.$this->params['action']; // prefix the actions with api_
$this->Auth->authenticate = array('BasicAlias'); // Switch to using Basic Authentication
if($this->Auth->login() == false) // Attempt Basic Auth Login
{ // Login failed
CakeLog::write('auth_api', 'Unauthorized API request to: ' . $this->params['action']);
header("HTTP/1.0 401 Unauthorized"); // Force returning an Unauthorized header (401)
exit; // MUST BE CALLED TO PREVENT 302 BEING SENT!
}
}
}
It is important to note that BasicAlias Auth Component is not included in the $components within AppController, but used dynamically if the request is to the api.* subdomain. However, the order in which classes are constructed has no effect whether BasicAlias AuthComponent is included in $components, or used dynamically as shown above.
AppModel:
function __construct($id = false, $table = null, $ds = null)
{
if(($ds == null) && ($this->use_tenant_database == true))
{
// Create a connection to the tenants database and configure model to use this connection.
$Tenant = ClassRegistry::init('Tenant');
$db_name = $Tenant->checkAndCreateTenantDatabaseConnectionForCurrentUser();
if($db_name == false)
{
header("HTTP/1.0 500 Server Error"); // Force returning a Server Error Header (500)
debug('AppModel::$db_name = false, unable to proceed');
CakeLog::write('tenant_error', 'db_name = false, unable to connect.');
exit; // MUST BE CALLED TO PREVENT 302 BEING SENT!
}
// Point model to the tenant database connection:
$this->useDbConfig = $db_name;
}
parent::__construct($id, $table, $ds);
}
And then within any models which use a specific tenant database:
class Order extends AppModel
{
var $use_tenant_database = true;
...
}
Tenant.php:
/**
* Check whether a connection to the current users tenant database has already been created and if so, return its name.
* Otherwise, create the connection and return its name.
*
* #return boolean|Ambigous <mixed, multitype:, NULL, array, boolean>
*/
public function checkAndCreateTenantDatabaseConnectionForCurrentUser()
{
// Check whether we have the tenants database connection information available in the Configure variable:
if(Configure::check('Tenant.db_name') == true)
{ // the db_config is available in configure, use it!
$db_name = Configure::read('Tenant.db_name');
}
else
{ // The tenants db_name has not been set in the configure variable, we need to create a database connection and then
// set the configure variable.
$tenant_id = $this->getCurrentUserTenantId();
if($tenant_id == null)
{ // Unable to resolve the tenant_id, instead, connect to the default database.
debug('TRIED TO CONSTRUCT MODEL WITHOUT KNOWING TENANT DATABASE!!');
exit;
}
$db_name = $this->TenantDatabase->createConnection($tenant_id);
if($db_name == false)
{ // The database connection could not be created.
CakeLog::write('tenant_error', 'unable to find the database name for tenant_id: ' . $tenant_id);
return false;
}
Configure::write('Tenant.db_name', $db_name);
}
return $db_name;
}
So, if the user requests a URL for example:
http://api.mydomain.com/Orders/getAllPendingOrders
Where they have supplied BASIC auth credentials along with the request, then what happens is that classes are constructed / executed in the following order:
Model __construct: User
Controller __construct: OrdersController
Model __construct: Permission
Model __construct: Order
Model __construct: Tenant
Model __construct: TenantDatabase
function: OrdersController/beforeFilter
AuthComponent __startup --> This then performs the login.
Model __construct: other models.
The problem is: Order.php is being constructed the user has been logged in, which means when the code in AppModel.php is executed:
$db_name = $Tenant->checkAndCreateTenantDatabaseConnectionForCurrentUser();
It is unable to determine the users current tenancy.
I need to find out a workaround for this, either by somehow performing the login BEFORE Order.php is constructed, or hacking it so that if you attempt to construct a model which has $use_tenant_database = true, and the user is not logged in, then BasicAuth is performed at this point to try and login the user.. however this feels wrong to me.
You might want to have a look at Authorization (who’s allowed to access what) portion in Cake's documentation. Specifically look at the isAuthorized function and how it works.
You might need something like this in your Orders controller:
// app/Controller/OrdersController.php
public function isAuthorized($user) {
// All registered users can add posts
if ($this->action === 'add') {
return true;
}
// The owner of an order can edit and delete it
if (in_array($this->action, array('edit', 'delete'))) {
$orderId = (int) $this->request->params['pass'][0];
if ($this->Order->isOwnedBy($orderId, $user['id'])) {
return true;
}
}
return parent::isAuthorized($user);
}
Implement your logic in before filter Request Life-cycle callback in the app controller.
Controller::beforeFilter() :
This function is executed before every action in the controller. It’s a handy place to check for an active session or inspect user permissions.
http://book.cakephp.org/2.0/en/controllers.html
It turns out these models were being constructed by the 'Search.Prg' plugin, a CakeDC plugin for handling search / filtering of results. The initialize() function within the component was being executed and causing the model to be constructed prior to the user being logged in.
The way in which this was solved was to move the Basic Auth check / login process from AppController beforeFilter to ExtendedAuthComponent (my own custom authenciation component) initialize function.
The end code was this:
ExtendedAuthComponent.php
public function initialize(Controller $controller)
{
parent::initialize($controller); // Call parent initialization first, this sets up request and response variables.
$this->controller = $controller;
// Reroute all requests to API subdomain (ie: api.rms.roving.net.au) to api_ prefixed actions.
// Also, enable Basic Authentication if the user is accessing via api.*
// If login fails, return 401 error instead of 302 redirect to login page.
if(API_REQUEST == true)
{
$controller->params['action'] = 'api_'.$controller->params['action']; // prefix the actions with api_
if($this->loggedIn() == false) // Attempt Basic Auth Login
{ // Login failed
$this->authenticate = array('BasicAlias'); // Switch to using Basic Authentication
if($this->login() == false)
{
CakeLog::write('auth_api', 'Unauthorized API request to: ' . $this->params['action']);
header("HTTP/1.0 401 Unauthorized"); // Force returning an Unauthorized header (401)
exit; // MUST BE CALLED TO PREVENT 302 BEING SENT!
}
}
}
}
This causes the user to be logged in via Basic Auth before the Search.Prg components initialize() function is run, which means the users tenancy is determined before the model(s) are constructed, solving the problem.
Related
I'm building an SPA that a user can upload some files. The front-end is written in AngularJS while the back-end is using an API in Laravel 5.7. The authentication is implemented using the jwt-auth library.
So far I have implemented this for registered users where each user has a personal directory on the server where he/she uploads the files. The difference between the registered and the anonymous users is that the files of the anonymous will be deleted after a while.
Now, I want to do the same for anonymous/guest users (if the press the button continue as a guest). So what I tried first in the authContrroller.php side is to use something like this:
public function authentication(Request $rrequest) {
$credentials = $request->only('email', 'password');
// Guest authentication
if( $credentials['email'] === 'guest' && $credentials['password'] === 'guest' )
{
$payload = auth()->factory()->claims(['sub' => $this->createRandomDir()])->make();
$token = auth()->manager()->encode($payload);
// OR
$factory = JWTFactory::customClaims([
'sub' => $this->createRandomDir(),
]);
$payload = $factory->make();
$token = JWTAuth::encode($payload);
}
// Registered user authentication authentication
else
{
if (! $token = auth()->setTTL(60)->attempt($credentials))
return response()->json(['error' => 'invalid_credentials'], 400);
}
return response()->json(compact('token'));
}
The idea was to create a random directory and enclose it inside the payload and use it on the next requests.
But in the case of the guest, the server returns as a token an empty object. Possibly because there wasn't a user in the DB.
Another idea that I'm thinking of is to create a random user (add it to the DB) and assign to it a random directory each time a user needs to use the app as guest/anonymous. The only thing that I'm afraid on that approach is that if there are thousands of guests then thousands random users should be created on the DB.
So what do you think? Is there any other and more efficient way to handle this?
Any idea is welcomed.
Iam writing an application with cakephp where i will have admin and agents where they can login to the system. Admin will have different layout from the agents. I have already create the the users table where i added a role field (admin,agent) ,i added the prefixes in core.php
Configure::write('Routing.prefixes', array('admin','agent'));
I managed to create the login and the logout for admin, but still iam confused how i should proceed with the rest. For Example i dont understand how beforeFilter() and isAuthorized() functions works. How i can check if user has access to that function or not. Also the redirections if a someone try to access this page domain.com/admin to be redirected to admin/login page .
Thanks.
Use the beforeFilter() to control access to each action, the below example will only allow access to the view and index action - any other action will be blocked :
$this->Auth->allow('view', 'index');
if you want to allow access to all the actions in your controller , try this in your before filter:
$this->Auth->allow();
To control who has access to what you could use a simple function in your app controller like so:
protected function _isAuthorized($role_required) {
if ($this->Auth->user('role') != $role_required) {
$this->Session->setFlash("your message here...");
$this->redirect("wherever you want the user to go to...");
}
}
In your controller action, eg. admin_delete on the first line you would do the following:
$this->_isAuthorized('admin');
Finally the redirect works like so:
$this->redirect(array('controller' => 'home', 'action' => 'dashboard'));
if you are redirecting within the same controller simply do the following:
$this->redirect('dashboard');
Hope this helps.
What i usually do is extend my App controller into an AdminAppController and SiteController , in the AdminAppController I have the following code in my beforeFilter:
$controller = strtolower($this->params["controller"]);
$action = strtolower($this->params["action"]);
$crole = $this->Auth->user("role");
$allowed = false;
$roles = array(
"all"=>array("user#login","user#register","user#forgot"),
"admin"=>array("pages#index","pages#view")
);
if(in_array($controller."#".$action,$roles["all"])){
$allowed = true;
}else{
if(in_array($controller."#".$action,$roles[$crole])){
$allowed = true;
}
}
if($allowed==false){
$this->setFlash("Access denied message...");
$this->redirect("...");
}
Don't know if this is the best practice but it works just fine. I normally hate CakePHP's built in Authorization system.
To check for allowance per role, I think it's best to use the Auth->allow([...]) in a per controller basis.
I find it best to check in Controller::beforeFilter() with a:
switch ($role) {
case 'admin':
$this->Auth->allow(...); //Allow delete
//notice no break; statement, so next case will execute too if admin
case 'manager':
$this->Auth->allow(...); //Allow edit
case default:
$this->Auth->allow(...); //Allow index
}
While you can check in AppController, I don't want to remember to change two files when I edit just one.
In my design, an action has a rank, 0-10 required to access it (0 being a guest [not logged in], 10 being an admin). Each user has a rank, 0-10. If you have the rank or higher, you can access the action, otherwise you can't. Nice and simple, it's all I need.
The problem is that CakePHP wants me to treat actions with two separate concepts. I have to mark them Auth->allow/deny to determine if the auth system even bothers with them, and then I control access to them with isAuthorized().
isAuthorized works great for my needs... except that any action I want to have access of rank 0 to has to be Auth->allow()... which then ignores my isAuthorized method completely. If I deny all the pages, the login triggers on the pages that should be rank 0, before it checks isAuthorized, so even if I grant authorization through it, the person has to log in first.
Is there any way to merge the two systems together, or a simple way to replace it? Most of the auth system is great, and takes care of business for me without me having to mess with it... but this is just awkward and is going to cause problems when I don't notice a mixed up allow/deny or something.
Thanks!
As far as I see it, the only way to do this is to create a guest user. This is because the Auth component checks the existence of a user before ever getting to isAuthorized() like you explained.
You can do this by writing directly to the session. This will tell the Auth component that someone is logged in, so your isAuthorized() method will be called.
AppController
public function beforeFilter() {
// if no one is logged in, log in a guest
if (!$this->Auth->user()) {
$this->Session->write(AuthComponent::$sessionKey, array(
'User' => array(
'id' => 0
)
));
}
}
public function isAuthorized($user) {
$authorized = false;
if ($this->Auth->user('id') == 0) {
// public guest user access
}
// other logic
return $authorized;
}
A possibly better way to do this is to use a custom authentication object, which basically tells Cake to use that class to help authenticate. This splits the logic into a separate class, making it easier to test and even disable.
app/Controller/Component/Auth/GuestAuthenticate.php
App::uses('BaseAuthenticate', 'Controller/Component/Auth');
class GuestAuthenticate extends BaseAuthenticate {
public function authenticate(CakeRequest $request, CakeResponse $response) {
// no real authentication logic, just return a guest user
return array('User' => array('id' => 0));
}
}
AppController
public $components = array(
'Auth' => array(
'authenticate' => array(
'Form',
'Guest' // tell Cake to try Form authentication then Guest authentication
)
)
);
public function beforeFilter() {
if (!$this->Auth->user()) {
// no user? log in a guest (this will fail form authentication
// then try guest authentication)
$this->Auth->login();
}
}
public function isAuthorized($user) {
$authorized = false;
if ($this->Auth->user('id') == 0) {
// public guest user access
}
// other logic
return $authorized;
}
You can find more information on custom authentication objects here: http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html
Maybe this has already been dealt with satisfactorily but I have a solution.
In my AppController I have the following:
public function isAuthorized($user = null) {
if (in_array($this->action, $this->Auth->allowedActions)) {
return true;
}
return false;
}
As you've found, if you don't explicitly authorize actions, they are denied to authenticated users, even if they are allowed to the public. This snippet of code just makes the isAuthorized() method honour the settings in the Auth->allow() list.
It seems to work for me, so I hope it helps.
Please bear with me, as I'm a programming/cakephp noob, but I do not know how to restrict the user from access to other user data. When a user logs in, they get a dashboard of their listings (which come from several models....restaurants, hotels, golf courses, images for each listing, location information, coupons, etc..).
Depending on the model/controller, I could have user '3' (who has hotel listings) type into the browser bar /restaurants/edit/1 and edit the restaurant information of user '17' who has a restaurant with ID='1'. Worse, they can even access /users/dashboard/17. How do I limit a user to only access their own data? I was hoping there was some sort of 'beforeAllow()' part of the AuthComponent I could use in the AppController that checks user id beforehand and kicks them back out to their dashboard if they try to perform a CRUD action on other users' data.
Even if I was using ACL (I know I should but it's frankly a little too over my head at this stage of learning), I'd still have to know the right code to limit user access, correct?
Below is my AppController:
class AppController extends Controller {
public $components = array(
'Session',
'Auth' => array(
'loginRedirect' => array('controller' => 'users', 'action' => 'view'),
'logoutRedirect' => array('controller' => 'docs', 'action' => 'index'),
'authError' => 'Sorry, you are not authorized to view this page.'
)
);
function beforeFilter() {
$this->Auth->userModel = 'User';
$this->Auth->allow('join_now','debug','index', 'condos', 'houses', 'hotels_and_motels', 'print_all_coupons', 'print_coupon', 'search', 'golf', 'charters', 'events', 'nightlife', 'shopping', 'visitors_info', 'contact_us', 'view', 'results');
}
}
and here is a sample edit function (the edit function from my UnitsController):
function edit($id) {
$this->set('title', 'Edit your property');
$this->Unit->id = $id;
if (empty($this->request->data)) {
$this->request->data = $this->Unit->read();
} else {
if ($this->Unit->saveAll($this->request->data)) {
$this->Session->setFlash('Your property has been updated.', 'success');
}
}
}
I will say that every one of my db tables has a user_id field so the logged in user can be matched with the user_id of each model.
I thought that this SO question was what I was looking for but they ended up getting off on a tangent in it and never answered the original question the user asked.
If you where using cakes acl and auth, then no, you wouldn't have to write a bunch of code, checking the user ids for each action, but you'd have to write the code to tie together the acl's. You'd tell the Component that your controller and actions require acl privelages. And it doesnt the look ups in the aros and acos tables to make sure that your object requesting the content has the proper permissions.
I HIGHLY recommend you take a look at the tutorial and figure out how to get it to work
If you don't go that route, then you will have to add in the checking to every action that loads dependent content. Basically you'll, when a action is requested, you'll feature the object, then get the user associated to that object and check to see if the id of the user the same as the id as the requesting the object.
if ( $this->Unit->User->uid != $this->Session->User->uid ) {
throw new NotFoundException('Could not find that Unit');
} else {
...
}
The other thing you can do, for pages that are the same, but customized, is not use the url /user/dashboard/17 and instead just use /user/dashboard then in the dashboard action, pull the user id from the session data and load the profile for the user that is authenticated
I think you will need to use CRUD authorization for adding EditOwn actions. The answer described in CakePHP ACL Database Setup: ARO / ACO structure? handles much of the logic that you need.
Please note that the solution is still not complete and you will get access to actions that can render other users' data. Example: scaffold 'index' methods. To restrict access to other users' data here, you can modify the query to add filters based on User ID that you get from Session.
======================================== EDIT ====================================
Per charles suggestion, I accomplished the Offline/Online feature using the following code, based on Charles code:
<?php
Class AppController extends Controller{
// prevents unauthorized access
public $components = array('Auth');
// the name of the model storing site_offline boolean
public $uses = array('Configuration');
// callback invoked before every controller action
public function beforeFilter() {
// returns your site_offline status assuming 0 is offline
if ($this->Configuration->get_site_status() == 1) {
$this->Auth->allow('*');
}else {
if(($this->Configuration->get_site_status() == 0) and (!$this->Auth->user() == null)){
// I set it up like this for now to allow access to any authenticated user,
//but later will change it to only allow admins access thru a login form
$this->Auth->allow('*');
}else{
//If site is offline and user is not authenticated, sent them to
// the a screen using the OFFLINE layout and provide a screen for login.
$this->layout = 'offline';
$this->setFlash('Maintenance Mode. Check back shortly.');
$this->Auth->deny('*');
}
}
}
}
?>
Then I used jQuery to hide my login form. An admin clicks on the message to show the login form. This is an attempt to prevent any login tryouts.
============================ END EDIT ==========================================
I would like to know what is the best way to create a "site offline/online" feature in CakePHP. Basically, I would like to allow an administrator to turn off access to the site to everyone registered or not. The offline page should have a login access thru which only admins can login.
The idea I have is to create some kind of dashboard controller, where as soon as the administrator is logged in he/she will be redirected to this dashboard from where he can access the other controller actions (admin_edit, etc). This dashboard and all admin actions (admin_delete, etc) should use the admin layout.
Is this a good approach? For the offline/online feature should I create a settings table with a site_offline field that can be turned on or off? Where in app_controller and what code should I use to check for it before allowing or not access to the site?
Thanks a lot for your help,
first add a config in the core.config
/*
* This is the site maintenance
* The built in defaults are:
*
* - '1' - Site works
* - '0' - site down for maintenance.
*/
Configure::write('Site.status', 1);
in the AppController you'll check it in the beforeRender function
if (Configure::read('Site.status') == 0) {
$this->layout = 'maintenance';
$this->set('title_for_layout', __('Site_down_for_maintenance_title'));
} else {
// do something
}
i'm here load a separate layout form the maintenance to let me add whatever layout i want
If you were going to save site_offline boolean value in a database table you should be able to easily do this with a callback in AppController and the Auth component.
<?php
AppController extends Object {
// prevents unauthorized access
public $components = array('Auth');
// the name of the model storing site_offline boolean
public $uses = array('NameOfModel');
// callback invoked before every controller action
public function beforeFilter() {
// returns your site_offline status assuming 0 is offline
if ($this->NameOfModel->get_status() === 0) {
$this->Auth->deny('*');
} else {
$this->Auth->allow('*');
}
}
}
I've always liked the idea of the DashboardsController for admin functions. That's actually the exact name of the class I use and the same general idea.