mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-23 20:00:33 -04:00
Additional permissions levels when we want to filtered and intersect permissions
This commit is contained in:
parent
daf65587ca
commit
4364b4369e
@ -62,6 +62,13 @@ class BaseController extends Controller
|
|||||||
*/
|
*/
|
||||||
public $forced_index = 'data';
|
public $forced_index = 'data';
|
||||||
|
|
||||||
|
protected $entity_type;
|
||||||
|
|
||||||
|
protected $entity_transformer;
|
||||||
|
|
||||||
|
protected $serializer;
|
||||||
|
|
||||||
|
private array $client_exclusion_fields = ['balance','paid_to_date', 'credit_balance','client_hash'];
|
||||||
/**
|
/**
|
||||||
* Fractal manager.
|
* Fractal manager.
|
||||||
* @var object
|
* @var object
|
||||||
@ -187,7 +194,7 @@ class BaseController extends Controller
|
|||||||
* end user has the correct permissions to
|
* end user has the correct permissions to
|
||||||
* view the includes
|
* view the includes
|
||||||
*
|
*
|
||||||
* @param array $includes The includes for the object
|
* @param string $includes The includes for the object
|
||||||
* @return string The filtered array of includes
|
* @return string The filtered array of includes
|
||||||
*/
|
*/
|
||||||
private function filterIncludes(string $includes): string
|
private function filterIncludes(string $includes): string
|
||||||
@ -286,6 +293,7 @@ class BaseController extends Controller
|
|||||||
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
|
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
'company.company_gateways' => function ($query) use ($user) {
|
'company.company_gateways' => function ($query) use ($user) {
|
||||||
$query->whereNotNull('updated_at')->with('gateway');
|
$query->whereNotNull('updated_at')->with('gateway');
|
||||||
@ -481,21 +489,32 @@ class BaseController extends Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
if ($query instanceof Builder) {
|
if ($query instanceof Builder) {
|
||||||
//27-10-2022 - enforce unsigned integer
|
|
||||||
$limit = $this->resolveQueryLimit();
|
$limit = $this->resolveQueryLimit();
|
||||||
|
|
||||||
$paginator = $query->paginate($limit);
|
$paginator = $query->paginate($limit);
|
||||||
|
|
||||||
$query = $paginator->getCollection();
|
$query = $paginator->getCollection();
|
||||||
|
|
||||||
$resource = new Collection($query, $transformer, $this->entity_type);
|
$resource = new Collection($query, $transformer, $this->entity_type);
|
||||||
|
|
||||||
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
|
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
$resource = new Collection($query, $transformer, $this->entity_type);
|
$resource = new Collection($query, $transformer, $this->entity_type);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->response($this->manager->createData($resource)->toArray());
|
return $this->response($this->manager->createData($resource)->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveQueryLimit()
|
/**
|
||||||
|
* Returns the per page limit for the query.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function resolveQueryLimit(): int
|
||||||
{
|
{
|
||||||
if (request()->has('per_page')) {
|
if (request()->has('per_page')) {
|
||||||
return abs((int)request()->input('per_page', 20));
|
return abs((int)request()->input('per_page', 20));
|
||||||
@ -637,6 +656,11 @@ class BaseController extends Controller
|
|||||||
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
|
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($user->hasIntersectPermissions(['view_client'])){
|
||||||
|
$query->exclude($this->client_exclusion_fields);
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
'company.company_gateways' => function ($query) use ($user) {
|
'company.company_gateways' => function ($query) use ($user) {
|
||||||
$query->whereNotNull('created_at')->with('gateway');
|
$query->whereNotNull('created_at')->with('gateway');
|
||||||
@ -847,23 +871,32 @@ class BaseController extends Controller
|
|||||||
$query->with($includes);
|
$query->with($includes);
|
||||||
|
|
||||||
if (auth()->user() && ! auth()->user()->hasPermission('view_'.Str::snake(class_basename($this->entity_type)))) {
|
if (auth()->user() && ! auth()->user()->hasPermission('view_'.Str::snake(class_basename($this->entity_type)))) {
|
||||||
//06-10-2022 - some entities do not have assigned_user_id - this becomes an issue when we have a large company and low permission users
|
|
||||||
if (in_array($this->entity_type, [User::class])) {
|
if (in_array($this->entity_type, [User::class])) {
|
||||||
|
|
||||||
$query->where('id', auth()->user()->id);
|
$query->where('id', auth()->user()->id);
|
||||||
|
|
||||||
} elseif (in_array($this->entity_type, [BankTransactionRule::class,CompanyGateway::class, TaxRate::class, BankIntegration::class, Scheduler::class, BankTransaction::class, Webhook::class, ExpenseCategory::class])) { //table without assigned_user_id
|
} elseif (in_array($this->entity_type, [BankTransactionRule::class,CompanyGateway::class, TaxRate::class, BankIntegration::class, Scheduler::class, BankTransaction::class, Webhook::class, ExpenseCategory::class])) { //table without assigned_user_id
|
||||||
|
|
||||||
if ($this->entity_type == BankIntegration::class && !auth()->user()->isSuperUser() && auth()->user()->hasIntersectPermissions(['create_bank_transaction','edit_bank_transaction','view_bank_transaction'])) {
|
if ($this->entity_type == BankIntegration::class && !auth()->user()->isSuperUser() && auth()->user()->hasIntersectPermissions(['create_bank_transaction','edit_bank_transaction','view_bank_transaction'])) {
|
||||||
$query->exclude(["balance"]);
|
$query->exclude(["balance"]);
|
||||||
} //allows us to selective display bank integrations back to the user if they can view / create bank transactions but without the bank balance being present in the response
|
} //allows us to selective display bank integrations back to the user if they can view / create bank transactions but without the bank balance being present in the response
|
||||||
else {
|
else {
|
||||||
$query->where('user_id', '=', auth()->user()->id);
|
$query->where('user_id', '=', auth()->user()->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
} elseif (in_array($this->entity_type, [Design::class, GroupSetting::class, PaymentTerm::class, TaskStatus::class])) {
|
} elseif (in_array($this->entity_type, [Design::class, GroupSetting::class, PaymentTerm::class, TaskStatus::class])) {
|
||||||
// nlog($this->entity_type);
|
// nlog($this->entity_type);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
$query->where('user_id', '=', auth()->user()->id)->orWhere('assigned_user_id', auth()->user()->id);
|
$query->where('user_id', '=', auth()->user()->id)->orWhere('assigned_user_id', auth()->user()->id);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// $query->exclude(['balance','credit_balance','paid_to_date']);
|
|
||||||
|
// if(auth()->user()->hasIntersectPermissions(['view_client'])){
|
||||||
|
// $query->exclude($this->client_exclusion_fields);
|
||||||
|
// }
|
||||||
|
|
||||||
if (request()->has('updated_at') && request()->input('updated_at') > 0) {
|
if (request()->has('updated_at') && request()->input('updated_at') > 0) {
|
||||||
$query->where('updated_at', '>=', date('Y-m-d H:i:s', intval(request()->input('updated_at'))));
|
$query->where('updated_at', '>=', date('Y-m-d H:i:s', intval(request()->input('updated_at'))));
|
||||||
|
@ -17,14 +17,10 @@ use App\Jobs\Mail\NinjaMailerObject;
|
|||||||
use App\Libraries\MultiDB;
|
use App\Libraries\MultiDB;
|
||||||
use App\Mail\Admin\EntityFailedSendObject;
|
use App\Mail\Admin\EntityFailedSendObject;
|
||||||
use App\Utils\Traits\Notifications\UserNotifies;
|
use App\Utils\Traits\Notifications\UserNotifies;
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class InvoiceFailedEmailNotification
|
class InvoiceFailedEmailNotification
|
||||||
{
|
{
|
||||||
use UserNotifies, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use UserNotifies;
|
||||||
|
|
||||||
public $delay = 7;
|
public $delay = 7;
|
||||||
|
|
||||||
|
@ -11,14 +11,12 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Traits\Excludable;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
class BankIntegration extends BaseModel
|
class BankIntegration extends BaseModel
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
use Filterable;
|
use Filterable;
|
||||||
use Excludable;
|
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'bank_account_name',
|
'bank_account_name',
|
||||||
|
@ -11,15 +11,16 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\DataMapper\ClientSettings;
|
use Illuminate\Support\Str;
|
||||||
use App\Jobs\Util\WebhookHandler;
|
use Illuminate\Support\Carbon;
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use App\Jobs\Util\WebhookHandler;
|
||||||
|
use App\Models\Traits\Excludable;
|
||||||
|
use App\DataMapper\ClientSettings;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use App\Utils\Traits\UserSessionAttributes;
|
use App\Utils\Traits\UserSessionAttributes;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class BaseModel
|
* Class BaseModel
|
||||||
@ -33,6 +34,7 @@ class BaseModel extends Model
|
|||||||
use MakesHash;
|
use MakesHash;
|
||||||
use UserSessionAttributes;
|
use UserSessionAttributes;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use Excludable;
|
||||||
|
|
||||||
protected $appends = [
|
protected $appends = [
|
||||||
'hashed_id',
|
'hashed_id',
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Utils\Traits\MakesHash;
|
use App\Utils\Traits\MakesHash;
|
||||||
|
use App\Models\Traits\Excludable;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
|
||||||
|
|
||||||
class StaticModel extends Model
|
class StaticModel extends Model
|
||||||
{
|
{
|
||||||
use MakesHash;
|
use MakesHash;
|
||||||
|
use Excludable;
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'updated_at' => 'timestamp',
|
'updated_at' => 'timestamp',
|
||||||
|
@ -410,7 +410,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
* @param string $permission '["view_all"]'
|
* @param string $permission '["view_all"]'
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public function hasExactPermission(string $permission = '___'): bool
|
public function hasExactPermissionAndAll(string $permission = '___'): bool
|
||||||
{
|
{
|
||||||
$parts = explode('_', $permission);
|
$parts = explode('_', $permission);
|
||||||
$all_permission = '__';
|
$all_permission = '__';
|
||||||
@ -436,7 +436,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
public function hasIntersectPermissions(array $permissions = []): bool
|
public function hasIntersectPermissions(array $permissions = []): bool
|
||||||
{
|
{
|
||||||
foreach ($permissions as $permission) {
|
foreach ($permissions as $permission) {
|
||||||
if ($this->hasExactPermission($permission)) {
|
if ($this->hasExactPermissionAndAll($permission)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -444,6 +444,22 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used when we need to match exactly what permission
|
||||||
|
* the user has, and not aggregate owner and admins.
|
||||||
|
*
|
||||||
|
* This method is used when we need to scope down the query
|
||||||
|
* and display a limited subset.
|
||||||
|
*
|
||||||
|
* @param string $permission '["view_all"]'
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function hasExactPermission(string $permission = '___'): bool
|
||||||
|
{
|
||||||
|
return (stripos($this->token()->cu->permissions, $permission) !== false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used when we need to match a range of permissions
|
* Used when we need to match a range of permissions
|
||||||
* the user
|
* the user
|
||||||
@ -461,7 +477,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($permissions as $permission) {
|
foreach ($permissions as $permission) {
|
||||||
if ($this->hasExactPermission($permission)) {
|
if ($this->hasExactPermissionAndAll($permission)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -469,6 +485,43 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used when we need to filter permissions carefully.
|
||||||
|
*
|
||||||
|
* For instance, users that have view_client permissions should not
|
||||||
|
* see the client balance, however if they also have
|
||||||
|
* view_invoice or view_all etc, then they should be able to see the balance.
|
||||||
|
*
|
||||||
|
* First we pass over the excluded permissions and return false if we find a match.
|
||||||
|
*
|
||||||
|
* If those permissions are not hit, then we can iterate through the matched_permissions and search for a hit.
|
||||||
|
*
|
||||||
|
* Note, returning FALSE here means the user does NOT have the permission we want to exclude
|
||||||
|
*
|
||||||
|
* @param array $matched_permission
|
||||||
|
* @param array $excluded_permissions
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasExcludedPermissions(array $matched_permission = [], array $excluded_permissions = []): bool
|
||||||
|
{
|
||||||
|
if ($this->isSuperUser()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($excluded_permissions as $permission) {
|
||||||
|
if ($this->hasExactPermission($permission)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($matched_permission as $permission) {
|
||||||
|
if ($this->hasExactPermission($permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function documents()
|
public function documents()
|
||||||
{
|
{
|
||||||
|
@ -75,6 +75,44 @@ class PermissionsTest extends TestCase
|
|||||||
$company_token->save();
|
$company_token->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHasExcludedPermissions()
|
||||||
|
{
|
||||||
|
$low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first();
|
||||||
|
$low_cu->permissions = '["view_client"]';
|
||||||
|
$low_cu->save();
|
||||||
|
|
||||||
|
$this->assertTrue($this->user->hasExcludedPermissions(["view_client"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasExcludedPermissions2()
|
||||||
|
{
|
||||||
|
$low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first();
|
||||||
|
$low_cu->permissions = '["view_client","edit_all"]';
|
||||||
|
$low_cu->save();
|
||||||
|
|
||||||
|
$this->assertFalse($this->user->hasExcludedPermissions(["view_client"], ['edit_all']));
|
||||||
|
|
||||||
|
$low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first();
|
||||||
|
$low_cu->permissions = 'view_client,edit_all';
|
||||||
|
$low_cu->save();
|
||||||
|
|
||||||
|
$this->assertFalse($this->user->hasExcludedPermissions(["view_client"], ['edit_all']));
|
||||||
|
|
||||||
|
$low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first();
|
||||||
|
$low_cu->permissions = 'view_client,view_all';
|
||||||
|
$low_cu->save();
|
||||||
|
|
||||||
|
$this->assertFalse($this->user->hasExcludedPermissions(["view_client"], ['view_all']));
|
||||||
|
|
||||||
|
|
||||||
|
$low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first();
|
||||||
|
$low_cu->permissions = 'view_client,view_invoice';
|
||||||
|
$low_cu->save();
|
||||||
|
|
||||||
|
$this->assertFalse($this->user->hasExcludedPermissions(["view_client"], ['view_invoice']));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function testIntersectPermissions()
|
public function testIntersectPermissions()
|
||||||
{
|
{
|
||||||
$low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first();
|
$low_cu = CompanyUser::where(['company_id' => $this->company->id, 'user_id' => $this->user->id])->first();
|
||||||
@ -212,8 +250,8 @@ class PermissionsTest extends TestCase
|
|||||||
|
|
||||||
public function testExactPermissions()
|
public function testExactPermissions()
|
||||||
{
|
{
|
||||||
$this->assertTrue($this->user->hasExactPermission("view_client"));
|
$this->assertTrue($this->user->hasExactPermissionAndAll("view_client"));
|
||||||
$this->assertFalse($this->user->hasExactPermission("view_all"));
|
$this->assertFalse($this->user->hasExactPermissionAndAll("view_all"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testMissingPermissions()
|
public function testMissingPermissions()
|
||||||
@ -222,8 +260,8 @@ class PermissionsTest extends TestCase
|
|||||||
$low_cu->permissions = '[""]';
|
$low_cu->permissions = '[""]';
|
||||||
$low_cu->save();
|
$low_cu->save();
|
||||||
|
|
||||||
$this->assertFalse($this->user->hasExactPermission("view_client"));
|
$this->assertFalse($this->user->hasExactPermissionAndAll("view_client"));
|
||||||
$this->assertFalse($this->user->hasExactPermission("view_all"));
|
$this->assertFalse($this->user->hasExactPermissionAndAll("view_all"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testViewAllValidPermissions()
|
public function testViewAllValidPermissions()
|
||||||
@ -232,8 +270,8 @@ class PermissionsTest extends TestCase
|
|||||||
$low_cu->permissions = '["view_all"]';
|
$low_cu->permissions = '["view_all"]';
|
||||||
$low_cu->save();
|
$low_cu->save();
|
||||||
|
|
||||||
$this->assertTrue($this->user->hasExactPermission("view_client"));
|
$this->assertTrue($this->user->hasExactPermissionAndAll("view_client"));
|
||||||
$this->assertTrue($this->user->hasExactPermission("view_all"));
|
$this->assertTrue($this->user->hasExactPermissionAndAll("view_all"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReturnTypesOfStripos()
|
public function testReturnTypesOfStripos()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user