Skip to content

Commit 32fc9ff

Browse files
committed
Add ability to map users to multiple companies with FullMultipleCompanySupport
We have a hierarchical set of companies grouped in multiple layers: a) the upper layer must have access to all companies b) the lowest layer must be scoped to only one company c) the middle layer must have access to multiple companies of the lowest layer, but not all of them a) and b) is possible with FullMultipleCompanySupport and super user, but c) is currently not supported. To accomplish this, I propose the concept of "mapped" companies. Users with a mapped company can see and update all resources of the mapped company according to the users permissions, just like the "primary" company. A new mapping table is created and linked to users and companies and the company scoping function is updated accordingly. The mapping table only contains additional mappings, not the "primary" company of the user. In the user edit view a new tab is created to map users to companies. The validation logic must be changed for this. Currently a scoped user can't change the company. Now it must be possible for users with mapped companies to choose between companies. To do this, two changes are needed: - the company selectlist gets scoped to the primary and the mapped companies - a new validator is introduced to validate if the user is allowed to choose a company or if a company is required. This validator handles input through the UI and the API The logic if a user is edited must be changed accordingly: - If a different "primary" company is assigned to a user, the mapping table must be updated. - Bulk editing this must also be supported. - If a user gets cloned, the mappings must be cloned. I implemented this feature with performance and backward compatibility in mind. There is no overhead if FullMultipleCompanySupport is disabled. The feature is fully optional, there is only minimal overhead if FullMultipleCompanySupport is enabled and no mappings are used.
1 parent 12a649e commit 32fc9ff

19 files changed

+231
-37
lines changed

app/Http/Controllers/Api/AssetsController.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -733,9 +733,6 @@ public function update(UpdateAssetRequest $request, Asset $asset): JsonResponse
733733
if ($request->has('model_id')) {
734734
$asset->model()->associate(AssetModel::find($request->validated()['model_id']));
735735
}
736-
if ($request->has('company_id')) {
737-
$asset->company_id = Company::getIdForCurrentUser($request->validated()['company_id']);
738-
}
739736
if ($request->has('rtd_location_id') && !$request->has('location_id')) {
740737
$asset->location_id = $request->validated()['rtd_location_id'];
741738
}

app/Http/Controllers/Api/UsersController.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,11 @@ public function update(SaveUserRequest $request, User $user): JsonResponse
510510

511511
if ($request->filled('company_id')) {
512512
$user->company_id = Company::getIdForCurrentUser($request->input('company_id'));
513+
514+
// Remove the new company from the fmcs mapping table
515+
if (Company::isFullMultipleCompanySupportEnabled() && $user->companies()->get()->contains($request->input('company_id'))) {
516+
$user->companies()->detach($request->input('company_id'));
517+
}
513518
}
514519

515520
if ($user->id == $request->input('manager_id')) {

app/Http/Controllers/Users/BulkUsersController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Models\License;
1010
use App\Models\Actionlog;
1111
use App\Models\Asset;
12+
use App\Models\Company;
1213
use App\Models\Group;
1314
use App\Models\LicenseSeat;
1415
use App\Models\ConsumableAssignment;
@@ -234,6 +235,13 @@ public function update(Request $request)
234235
}
235236
}
236237

238+
// Remove the new company from the fmcs mapping table
239+
if ($request->filled('company_id') && Company::isFullMultipleCompanySupportEnabled()) {
240+
foreach ($users as $user) {
241+
$user->companies()->detach($request->input('company_id'));
242+
}
243+
}
244+
237245
return redirect()->route('users.index')
238246
->with($return_array);
239247
}

app/Http/Controllers/Users/UsersController.php

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,16 @@ public function create(Request $request)
7070

7171
$user = new User;
7272

73-
return view('users/edit', compact('groups', 'userGroups', 'permissions', 'userPermissions'))
73+
$fmcs = Company::isFullMultipleCompanySupportEnabled();
74+
if ($fmcs) {
75+
$companies = Company::pluck('name', 'id');
76+
$companyMappings = $user->company_ids();
77+
} else {
78+
$companies = null;
79+
$companyMappings = null;
80+
}
81+
82+
return view('users/edit', compact('groups', 'userGroups', 'permissions', 'userPermissions', 'fmcs', 'companies', 'companyMappings'))
7483
->with('user', $user);
7584
}
7685

@@ -156,6 +165,15 @@ public function store(SaveUserRequest $request)
156165
$user->groups()->sync([]);
157166
}
158167

168+
// Update the company mappings after removing the own company from the input, only super user are allowed to change the mappings
169+
if (Company::isFullMultipleCompanySupportEnabled() && auth()->user()->isSuperUser()) {
170+
$company_mappings = $request->input('companyMappings');
171+
if ($company_mappings && ($key = array_search($user->company_id, $company_mappings)) !== false) {
172+
unset($company_mappings[$key]);
173+
}
174+
$user->companies()->sync($company_mappings);
175+
}
176+
159177
return Helper::getRedirectOption($request, $user->id, 'Users')
160178
->with('success', trans('admin/users/message.success.create'));
161179
}
@@ -206,7 +224,16 @@ public function edit(User $user)
206224
$userPermissions = Helper::selectedPermissionsArray($permissions, $user->permissions);
207225
$permissions = $this->filterDisplayable($permissions);
208226

209-
return view('users/edit', compact('user', 'groups', 'userGroups', 'permissions', 'userPermissions'))->with('item', $user);
227+
$fmcs = Company::isFullMultipleCompanySupportEnabled();
228+
if ($fmcs) {
229+
$companies = Company::pluck('name', 'id');
230+
$companyMappings = $user->company_ids();
231+
} else {
232+
$companies = null;
233+
$companyMappings = null;
234+
}
235+
236+
return view('users/edit', compact('user', 'groups', 'userGroups', 'permissions', 'userPermissions', 'fmcs', 'companies', 'companyMappings'))->with('item', $user);
210237
}
211238

212239
}
@@ -327,6 +354,15 @@ public function update(SaveUserRequest $request, User $user)
327354
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
328355
session()->put(['redirect_option' => $request->get('redirect_option')]);
329356

357+
// Update the company mappings after removing the own company from the input, only super user are allowed to change the mappings
358+
if (Company::isFullMultipleCompanySupportEnabled() && auth()->user()->isSuperUser()) {
359+
$company_mappings = $request->input('companyMappings');
360+
if ($company_mappings && ($key = array_search($user->company_id, $company_mappings)) !== false) {
361+
unset($company_mappings[$key]);
362+
}
363+
$user->companies()->sync($company_mappings);
364+
}
365+
330366
if ($user->save()) {
331367
// Redirect to the user page
332368
return Helper::getRedirectOption($request, $user->id, 'Users')
@@ -482,8 +518,17 @@ public function getClone(Request $request, User $user)
482518

483519
$userPermissions = Helper::selectedPermissionsArray($permissions, $clonedPermissions);
484520

521+
$fmcs = Company::isFullMultipleCompanySupportEnabled();
522+
if ($fmcs) {
523+
$companies = Company::pluck('name', 'id');
524+
$companyMappings = $user_to_clone->company_ids();
525+
} else {
526+
$companies = null;
527+
$companyMappings = null;
528+
}
529+
485530
// Show the page
486-
return view('users/edit', compact('permissions', 'userPermissions'))
531+
return view('users/edit', compact('permissions', 'userPermissions', 'fmcs', 'companies', 'companyMappings'))
487532
->with('user', $user)
488533
->with('groups', Group::pluck('name', 'id'))
489534
->with('userGroups', $userGroups)

app/Http/Requests/UpdateAssetRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Http\Requests\Traits\MayContainCustomFields;
66
use App\Models\Asset;
7+
use App\Models\Company;
78
use App\Models\Setting;
89
use Illuminate\Support\Facades\Gate;
910
use Illuminate\Validation\Rule;
@@ -21,6 +22,13 @@ public function authorize()
2122
return Gate::allows('update', $this->asset);
2223
}
2324

25+
public function prepareForValidation(): void
26+
{
27+
$this->merge([
28+
'company_id' => Company::getIdForCurrentUser($this->company_id),
29+
]);
30+
}
31+
2432
/**
2533
* Get the validation rules that apply to the request.
2634
*

app/Models/Accessory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class Accessory extends SnipeModel
6363
'name' => 'required|min:3|max:255',
6464
'qty' => 'required|integer|min:1',
6565
'category_id' => 'required|integer|exists:categories,id',
66-
'company_id' => 'integer|nullable',
66+
'company_id' => 'integer|nullable|fmcs_validator',
6767
'location_id' => 'exists:locations,id|nullable|fmcs_location',
6868
'min_amt' => 'integer|min:0|nullable',
6969
'purchase_cost' => 'numeric|nullable|gte:0|max:9999999999999',

app/Models/Asset.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public function declinedCheckout(User $declinedBy, $signature)
103103
'status_id' => ['required', 'integer', 'exists:status_labels,id'],
104104
'asset_tag' => ['required', 'min:1', 'max:255', 'unique_undeleted:assets,asset_tag', 'not_array'],
105105
'name' => ['nullable', 'max:255'],
106-
'company_id' => ['nullable', 'integer', 'exists:companies,id'],
106+
'company_id' => ['nullable', 'integer', 'exists:companies,id', 'fmcs_validator'],
107107
'warranty_months' => ['nullable', 'numeric', 'digits_between:0,240'],
108108
'last_checkout' => ['nullable', 'date_format:Y-m-d H:i:s'],
109109
'last_checkin' => ['nullable', 'date_format:Y-m-d H:i:s'],

app/Models/Company.php

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ final class Company extends SnipeModel
7575
'notes',
7676
];
7777

78-
private static function isFullMultipleCompanySupportEnabled()
78+
public static function isFullMultipleCompanySupportEnabled()
7979
{
8080
$settings = Setting::getSettings();
8181

@@ -109,21 +109,12 @@ public static function getIdFromInput($unescaped_input)
109109
*/
110110
public static function getIdForCurrentUser($unescaped_input)
111111
{
112-
if (! static::isFullMultipleCompanySupportEnabled()) {
113-
return static::getIdFromInput($unescaped_input);
112+
$current_user = auth()->user();
113+
$input = static::getIdFromInput($unescaped_input);
114+
if (static::canManageUsersCompanies()) {
115+
return $input;
114116
} else {
115-
$current_user = auth()->user();
116-
117-
// Super users should be able to set a company to whatever they need
118-
if ($current_user->isSuperUser()) {
119-
return static::getIdFromInput($unescaped_input);
120-
} else {
121-
if ($current_user->company_id != null) {
122-
return $current_user->company_id;
123-
} else {
124-
return null;
125-
}
126-
}
117+
return $current_user->company_id;
127118
}
128119
}
129120

@@ -165,14 +156,17 @@ public static function isCurrentUserHasAccess($companyable)
165156

166157
if (auth()->user()) {
167158
// Log::warning('Companyable is '.$companyable);
168-
$current_user_company_id = auth()->user()->company_id;
159+
$current_user = auth()->user();
169160
$companyable_company_id = $companyable->company_id;
170161

171162
// Set this to check companyable on company
172163
if ($companyable instanceof Company) {
173164
$companyable_company_id = $companyable->id;
174165
}
175-
return ($current_user_company_id == null) || ($current_user_company_id == $companyable_company_id) || auth()->user()->isSuperUser();
166+
167+
return ($current_user->company_id == null
168+
|| $current_user->company_ids()->contains($companyable_company_id)
169+
|| $current_user->isSuperUser());
176170
}
177171

178172
return false;
@@ -186,8 +180,11 @@ public static function isCurrentUserAuthorized()
186180

187181
public static function canManageUsersCompanies()
188182
{
189-
return ! static::isFullMultipleCompanySupportEnabled() || auth()->user()->isSuperUser() ||
190-
auth()->user()->company_id == null;
183+
$current_user = auth()->user();
184+
return (!static::isFullMultipleCompanySupportEnabled()
185+
|| $current_user->isSuperUser()
186+
|| $current_user->company_id == null
187+
|| $current_user->company_ids()->count() > 1);
191188
}
192189

193190
/**
@@ -300,7 +297,11 @@ private static function scopeCompanyablesDirectly($query, $column = 'company_id'
300297

301298
// If we are scoping the companies table itself, look for the company.id
302299
if ($query->getModel()->getTable() == 'companies') {
303-
return $query->where('companies.id', '=', $company_id);
300+
if ($company_id != null) {
301+
return $query->whereIn('companies.id', auth()->user()->company_ids());
302+
} else {
303+
return $query->where('companies.id', '=', null);
304+
}
304305
}
305306

306307

@@ -310,7 +311,11 @@ private static function scopeCompanyablesDirectly($query, $column = 'company_id'
310311
// Dynamically get the table name if it's not passed in, based on the model we're querying against
311312
$table = ($table_name) ? $table_name."." : $query->getModel()->getTable().".";
312313

313-
return $query->where($table.$column, '=', $company_id);
314+
if ($company_id != null) {
315+
return $query->whereIn($table.$column, auth()->user()->company_ids());
316+
} else {
317+
return $query->where($table.$column, '=', null);
318+
}
314319
}
315320

316321

app/Models/Component.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class Component extends SnipeModel
3939
'qty' => 'required|integer|min:1',
4040
'category_id' => 'required|integer|exists:categories,id',
4141
'supplier_id' => 'nullable|integer|exists:suppliers,id',
42-
'company_id' => 'integer|nullable|exists:companies,id',
42+
'company_id' => 'integer|nullable|exists:companies,id|fmcs_validator',
4343
'location_id' => 'exists:locations,id|nullable|fmcs_location',
4444
'min_amt' => 'integer|min:0|nullable',
4545
'purchase_date' => 'date_format:Y-m-d|nullable',

app/Models/Department.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Department extends SnipeModel
3333
protected $rules = [
3434
'name' => 'required|max:255|is_unique_department',
3535
'location_id' => 'numeric|nullable',
36-
'company_id' => 'numeric|nullable',
36+
'company_id' => 'numeric|nullable|fmcs_validator',
3737
'manager_id' => 'numeric|nullable',
3838
];
3939

0 commit comments

Comments
 (0)