I'm having trouble with Yii2 Role Based Access Control. In the usual set-up, the authentication rule takes place when the identity of the current user. Like written in the docs. Authorization
In my case, how can I set up the authorization (aside from the basic feature) using another set of models.? Here is my set up.
Table auth_assignment [item_name, user_id] from rbac migration,
user [id] from the yii2 migration.
I created a new table assignment [user_id related to user, rec_id related to recognition of an organization].
This is the scenario. I have the roles admin, organization-head, member. How can I check if the organization-head, or member belongs to their own Recognition module; not the other modules from other organization-heads?
I used also the context access control filter by peixoto.
Here is my code for checking. RecognitionRule checks if there is a user user_id equal to the identity of the user; and account_id equal to rec_id. The second condition tells if he is belong to the organization
/**
* Checks if ID matches user passed via params
*/
class RecognitionRule extends Rule
{
public $name = 'isRecognition';
/**
* #param string|integer $user the user ID.
* #param Item $item the role or permission that this rule is associated with
* #param array $params parameters passed to ManagerInterface::checkAccess().
* #return boolean a value indicating whether the rule permits the role or permission it is associated with.
*/
public function execute($user, $item, $params)
{
if(isset($params['recognition'])){ //Directly specify the model you plan to use via param
$model = $params['recognition'];
}else{ //Use the controller findModel method to get the model - this is what executes via the behaviour/rules
$id = Yii::$app->request->get('id'); //Note, this is an assumption on your url structure.
$model = Yii::$app->controller->findModel($id); //Note, this only works if you change findModel to be a public function within the controller.
}
return \common\models\Assignment::find()->where(['rec_id' => $model->id, 'user_id' => $user])->exists();
}
}
Still, I am not allowed to perform the action. Any clues?
I got the answers. I based my answer on AccessRule behavior and rbac\Rule $params
snippet of the RecognitionRule
/**
* #param string|integer $user the user ID.
* #param Item $item the role or permission that this rule is associated with
* #param array $params parameters passed to ManagerInterface::checkAccess().
* #return boolean a value indicating whether the rule permits the role or permission it is associated with.
*/
public function execute($user, $item, $params)
{
if(isset($params['recognition'])){ //Directly specify the model you plan to use via param
$model = $params['recognition'];
} else{ //Use the controller findModel method to get the model - this is what executes via the behaviour/rules
$id = Yii::$app->request->get('id'); //Note, this is an assumption on your url structure.
}
return \common\models\Assignment::find()->where(['rec_id' => $id, 'user_id' => $user])->exists();
}
}
?>
RecognitionController
[
'class' => 'common\rbac\ContextAccessRule',
'modelClass' => 'frontend\models\recognition',
'allow' => true,
'actions' => ['view','update'],
'roles' => ['viewOwnRecognition', 'updateOwnRecognition'],
],
],
],
];
Related
I am using spatie/laravel-activitylog to log the activities and jenssegers/laravel-mongodb connect to a Mongo database. In user Model, the user data is storing into mysql and log data need to be stored in mongodb. I am getting "Call to a member function prepare() on null"
config/activitylog.php
` /*
* This is the database connection that will be used by the migration and
* the Activity model shipped with this package. In case it's not set
* Laravel's database.default will be used instead.
*/
'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'),`
database.php
`'mongodb' => [
'driver' => 'mongodb',
'host' => '127.0.0.1',
'port' => '27017',
'database' => 'test_log',
'username' => '',
'password' => '',
'options' => [
'database' => 'admin'
]
],`
.env
`ACTIVITY_LOGGER_DB_CONNECTION=mongodb`
App\Models\User.php
`<?php
namespace App\Models;
// use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Jenssegers\Mongodb\Auth\User as Authenticatable;
class User extends Authenticatable implements MustVerifyEmail
{
use LogsActivity;
/**
* The attributes that are mass assignable.
*
* #var array<int, string>
*/
protected $fillable = [];
/**
* The attributes that should be hidden for serialization.
*
* #var array<int, string>
*/
/**
* The attributes that should be cast.
*
* #var array<string, string>
*/
/**
* The attributes that should be mutated to dates.
*
* #var array
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults();
}
}
`
In user Model, the user data is storing into mysql and log data need to be stored in mongodb.
The error message "Call to a member function prepare() on null" usually occurs when the database connection is not properly configured or the database connection does not exist.
In this case, you have set the database connection for the activity logs in the config/activitylog.php and .env files. However, it seems that the database connection is not being used properly in the User model.
To resolve this issue, you need to ensure that the database connection defined in 'config/activitylog.php' is being passed to the User model when logging activities.
You can add the following code to your User model to explicitly set the database connection:
use Illuminate\Support\Facades\Config;
protected static $logConnection = 'mongodb';
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()->using(Config::get('activitylog.database_connection') ?: static::$logConnection);
}
This will make sure that the activity logs are stored in the MongoDB database defined in the mongodb connection in config/database.php.
Fixed the issue with the help of this tutorial:: https://nickdevs.wordpress.com/2022/05/17/laravel-laravel-activitylog-inegrate-activity-log-with-mongodb/
The issue was with Spatie-activity-log and Jenssegers MongoDB. Spatie-activity-log uses default Model. We need to overwrite the Spatie Activity Model for Jenssegers MongoDB.
Create Activity.php in App/Models. copy the existing code from Spatie\Activitylog\Models\Activity.php from vendor/spatie/laravel-activitylog/src/Models/Activity.php. Replace the lines listed below.
remove:
namespace Spatie\Activitylog\Models;
use Illuminate\Database\Eloquent\Model;
class Activity extends Model implements ActivityContract
add:
namespace App\Models;
use Jenssegers\Mongodb\Eloquent\Model as MongoDBModel;
class Activity extends MongoDBModel implements ActivityContract
I want to be able to verify comfirm row with specific hashcode exists in a table before a user bearing the given hash code can fill a registration form.
I know the hash code exists because it's in the database. But the application redirect to the page it should if the hash code is non-existent and outputs a message that the hash code doesn't exist.
This used to work in Laravel 5 but it seems the implementation has changed since Laravel 8.
Here's my code:
use App\Http\Controllers\Site\Guest\newMembership\RegistrationForm;
...
Route::get('registration/{hashCode}', [RegistrationForm::class, 'registrationForm'])->name('registration');
Route::post('registration', [RegistrationForm::class, 'registrationProcessing'])->name('registration_data');
My controller, RegistrationForm.php has the code:
namespace App\Http\Controllers\Site\Guest\newMembership;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class RegistrationForm extends Controller
{
public function registrationForm($hashCode)
{
// Check if this $hashCode exists at all, otherwise redirect to referral request home page.
$hash_exists = (new \App\Http\Controllers\Site\Guest\newMembership\Form)->hashExists($hashCode);
// In the event the number of times it occurs is zero
if($hash_exists < 1)
{
// Tell user the referral link is non-existent.
session()->flash('membershipInfo', 'Your code does not exist.');
// Take this user to the referral request welcome page
return redirect()->route('membership.index');
}
// Assign new variable name to hash code
$referralCode = $hashCode;
return view('membership.registration', [
'referralCode' => $referralCode,
]);
}
The Form contoller has the code:
// Import database model
use App\Models\Site\Guest\Referralrequestapplication;
...
/**
* Count number of entries with given ID
*/
public function hashExists($hashCode)
{
//DB::table('referralrequestapplications')
Referralrequestapplication::where(['blocked', 0], ['image_reference', $hashCode])->count();
}
The model code:
namespace App\Models\Site\Guest;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Referralrequestapplication extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'blocked', 'review', 'reviewer', 'firstname', 'surname', 'phone', 'email', 'nin', 'facebook', 'twitter', 'instagram', 'state', 'city', 'neighbourhood', 'address', 'image_reference', 'created_at', 'updated_at',
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $table = 'referralrequestapplications';
}
The migration schema has the code:
Schema::create('referralrequestapplications', function (Blueprint $table) {
$table->bigIncrements('id');
$table->boolean('blocked')->default(0);
$table->boolean('reviewed')->default(0);
$table->string('reviewer')->nullable();
$table->string('firstname', 30)->nullable();
$table->string('surname', 30)->nullable();
$table->string('phone', 30)->nullable();
$table->string('email', 70)->nullable();
$table->string('nin', 20)->nullable();
$table->string('facebook', 70)->nullable();
$table->string('twitter', 70)->nullable();
$table->string('instagram', 70)->nullable();
$table->string('state', 40)->nullable();
$table->string('city', 40)->nullable();
$table->string('neighbourhood', 40)->nullable();
$table->string('address', 200)->nullable();
$table->string('image_reference', 200)->nullable();
$table->timestamps();
});
I fixed this issue by changing the code in Form controller from:
Referralrequestapplication::where(['blocked', 0], ['image_reference', $hashCode])->count();
To:
Referralrequestapplication::where('blocked', 0)->where('mage_reference', $hashCode)->exists();
Also I changed the following in RegistrationForm controller:
if($hash_exists < 1)
To:
if($hash_exists = 0)
In which case is it better to use the Validate model? Example: I have two model
AgreementForm
Agreement
When I create data - I use
$model = new AgreementForm();
if ( $model->load( \Yii::$app->request->post() ) && $model->save() ) {
....
}
Code of AgreemtnForm
AgreementForm extemds Model {
......
public function save() {
if(!$this->validate()) {
return null;
}
$model = new Agreement();
$model->content = $this->content;
if( $model->save() ) {
return true;
}
}
But when I update data, I use only that code:
public function actionUpdate( $id ) {
$model = Agreement::findOne( $id );
if( $model->load( \Yii::$app->request->post() ) && $model->save() ){
$this->refresh();
}
else {
return $this->render('update', [
'model' => $model,
]);
}
}
What is the validation model in this case, if I duplicate the validation in another model? Can I only use 1 model?
I will try to explain why you should almost always use a Form model to validate your data, providing some cases:
1) Doing
if( $model->load( \Yii::$app->request->post() ) && $model->save() )
could be dangerous, the user can send post data that he was not supposed to.
eg. Agreement has a userid column that gets id of the user that created it. If a user, sends this field in the post request, he could potentially change its value.
Your form model should define the properties that you expect to be sent.
2) You want to define additional validation for your model based on eg. the user role, the time the Agreement is valid etc.
Lets say you have 2 user roles:
Retailer
Merchant
Retailer can create an Agreement that is 300 characters long and max price 1000, opposed to 700 and 10000 for the merchant.
How do you cope with that?
You create 2 different forms:
$user = Yii::$app->user;
if ($user->can('retailer')) {
$model = new RetailerAgreementForm();
}
else {
$model = new MerchantAgreementForm();
}
if ( $model->load( \Yii::$app->request->post() ) && $model->save() ) {
....
}
In the form models, you can add the additional validation for your fields:
public function rules()
{
return [
['body', 'string', 'max' => 300],
['price', 'integer', 'max'=> 1000],
];
}
Using different forms for this I believe is the best option.
3) Your form model fields do not correspond to Database model columns 1 to 1.
Consider this example:
Your want to save the address of the agreement, street, state and city. You have a list of cities, states and streets.
The only thing you want to do with the address is save it and load it, eg. nobody is going to search per city.
So you just define a column address(type text) in your table and save the data as a JSON.
Your AgreementForm defines the address as separate fields and validates them accordingly and your Agreement model just validates address to be a string.
Do not duplicate your validation between AgreementForm and Agreement models. They should define different validation rules.
Note: Even if you do not render the form in a view and just post some data, it is good to use a form model to get exactly the fields you want to change and if needed, validate them with additional rules. The only case that I could think to just use the DB model directly is when you want to provide just some basic crud operations for your model.
I'm using different scenario for validation purpose.
The field email is not required in a scenario but if i pass email, it is not getting saved in DB.
Rules in Model:
[['firstname','email'], 'string', 'max' => 256],
Scenario function
public function scenarios() {
$scenarios = parent::scenarios();
$scenarios['insert2'] = ['firstname', 'status'];
return $scenarios;
}
In controller:
$model = new User();
$model->scenario = "insert2";
$model->load($data);
print_r($model);
Print_r returns email with empty
From Yii 2 load() docs:
Note, that the data being populated is subject to the safety check by setAttributes().
Now, setAttributes() signature:
public void setAttributes ( $values, $safeOnly = true )
where $safeOnly set to true means the assignments should only be done to the safe attributes. A safe attribute is one that is associated with a validation rule in the current $scenario.
So email must be included in the scenario.
In Yii2, when you want the rule to be applied on certain scenarios, you can specify the on property of a rule, like the following
public function rules()
{
return [
//rule applied only in scenario 'insert2'
[['first_name','status'], 'required','on' => 'insert2'],
//rule applied in all scenarios
['email','safe'],
//rule applied only in scenario 'insert3'
[['first_name','status','email'], 'required','on' => 'insert3'],
];
}
Now if you specify $model->scenario = "insert2" in controller then first_name and status are required and email if you give any value will get saved because ['email','safe'] rule also applied here.
If you specify $model->scenario = "insert3" in controller then first_name,status and email are now required fields and rule ['email','safe'] also get applied.
Please note you may not use public function scenarios() {.. here
From http://www.yiiframework.com/doc-2.0/guide-structure-models.html#validation-rules
The validation on Kohana ORM is done using rules
function rules()
{
return array(
'username' => array(
array('not_empty'),
array(array($this, 'availability')),
)
);
}
I'm struggling to validate a JSON encoded column using $_serialize_columns.
class Model_Admin extends ORM {
protected $_belongs_to = array();
protected $_has_many = array(
'plans' => array(),
'groups' => array(),
'transactions' => array(),
'logins' => array()
);
protected $_serialize_columns = array('data');
/**
* #param array $data
* #param Validation $validation
*
* #return bool
*/
public function data($data, $validation)
{
return
Validation::factory(json_decode($data, TRUE))
// ... rules ...
->check();
}
public function rules()
{
return array(
'data' => array(
array(array($this, 'data'), array(':value',':validation')
)
);
}
}
the array that gets encoded is:
array(
'name' => '',
'address' => '',
'phone' => '',
'postalcode' => ''
);
the data method receives the json encoded data, because the ORM runs the filters before doing the validation, so I need to convert it back to an associative array, then create a new validation object to check specifically for the content of that array. Because I can't merge Validation rules from another Validation instance
Updated Answer
The use of a second validation object is necessary since save() causes the internal model validation object to be checked. This means that rules added to the validation object being checked from a validation rule will be ignored (Validation->check() imports the rules into local scope before looping).
Since the data itself is technically another object (in the sense of object relationships, it has its own dataset that needs validation) the ideal solution would be to find a way to create a real model that saves the data.
There are numerous other benefits to saving data with proper database column definitions, not least if you need to perform data property lookups, make in-situ changes etc. (which would otherwise require unserializing the data column, potetnailly in all rows).
There are some alternatives, but they feel like kludges to me:
Create a model that represents the data object and add rules to it, using check() to validate the data (problem: will require a lot of maintenance, no real-world table means columns must be manually defined).
Set the data as real columns in the Admin model, and use a filter that will convert it into the data column on set (problem: again, must manually define the columns and exclude the additional columns from the save operation).
I hope this is of some use.
Original Answer
The Kohana ORM save() method permits the inclusion of an "extra" validation object, which is merged into the main ORM validation object namespace.
This is documented briefly here.
If I have understood correctly, I think you are looking to do something like this:
// another script, e.g., a controller
// Create the model
$admin = ORM::factory('Admin');
// $data = the data as an array, before serialization ...
$extra_validation = Validation::factory($data)
// add ->rule() calls here, but DO NOT chain ->check()
;
// Set $data in the model if it is going to be saved, e.g., $admin->data = $data;
// Set other data... e.g., $admin->foo = 'bar';
// Save the model
try {
$admin->save($extra_validation);
}
catch (ORM_Validation_Exception $e)
{
// Manipulate the exception result
}
While in this example you must still create another validation object, you are now able to catch all exceptions in a single block. I would recommend using var_dump() or similar on $e->errors() to check the namespace if you are using i18n messages to provide a human-readable error message. You should find that a namespace called "_external" has been created in the response.