Merge remote-tracking branch 'origin/develop'

Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	config/version.php
#	public/css/dist/skins/skin-black-dark.css
#	public/css/dist/skins/skin-black-dark.min.css
#	public/css/dist/skins/skin-blue-dark.css
#	public/css/dist/skins/skin-blue-dark.min.css
#	public/css/dist/skins/skin-green-dark.css
#	public/css/dist/skins/skin-green-dark.min.css
#	public/css/dist/skins/skin-orange-dark.css
#	public/css/dist/skins/skin-orange-dark.min.css
#	public/css/dist/skins/skin-purple-dark.css
#	public/css/dist/skins/skin-purple-dark.min.css
#	public/css/dist/skins/skin-red-dark.css
#	public/css/dist/skins/skin-red-dark.min.css
#	public/css/dist/skins/skin-yellow-dark.css
#	public/css/dist/skins/skin-yellow-dark.min.css
#	public/mix-manifest.json
This commit is contained in:
snipe 2024-04-23 10:28:50 +01:00
commit 56cb9a0f4e
35 changed files with 204 additions and 35 deletions

View file

@ -665,25 +665,26 @@ class AssetsController extends Controller
$model = AssetModel::find($asset->model_id);
// Update custom fields
$problems_updating_encrypted_custom_fields = false;
if (($model) && (isset($model->fieldset))) {
foreach ($model->fieldset->fields as $field) {
$field_val = $request->input($field->db_column, null);
if ($request->has($field->db_column)) {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
$asset->{$field->db_column} = Crypt::encrypt($field_val);
}
}
if ($field->element == 'checkbox') {
if(is_array($field_val)) {
$field_val = implode(',', $field_val);
$asset->{$field->db_column} = $field_val;
}
}
else {
$asset->{$field->db_column} = $field_val;
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
$field_val = Crypt::encrypt($field_val);
} else {
$problems_updating_encrypted_custom_fields = true;
continue;
}
}
$asset->{$field->db_column} = $field_val;
}
}
}
@ -709,8 +710,11 @@ class AssetsController extends Controller
$asset->image = $asset->getImageUrl();
}
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.success')));
if ($problems_updating_encrypted_custom_fields) {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning')));
} else {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
}
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);

View file

@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v6.3.4',
'full_app_version' => 'v6.3.4 - build 13139-g6f9ba6ede',
'build_version' => '13139',
'full_app_version' => 'v6.3.4 - build 13226-g5229dd65c',
'build_version' => '13226',
'prerelease_version' => '',
'hash_version' => 'g6f9ba6ede',
'full_hash' => 'v6.3.4-234-g6f9ba6ede',
'hash_version' => 'g5229dd65c',
'full_hash' => 'v6.3.4-85-g5229dd65c',
'branch' => 'master',
);

View file

@ -4,6 +4,7 @@ namespace Database\Factories;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\Location;
use App\Models\Statuslabel;
use App\Models\Supplier;
@ -353,6 +354,16 @@ class AssetFactory extends Factory
return $this->state(['requestable' => false]);
}
public function hasEncryptedCustomField(CustomField $field = null)
{
return $this->state(function () use ($field) {
return [
'model_id' => AssetModel::factory()->hasEncryptedCustomField($field),
];
});
}
/**
* This allows bypassing model level validation if you want to purposefully
* create an asset in an invalid state. Validation is turned back on

View file

@ -3,6 +3,7 @@
namespace Database\Factories;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\CustomFieldset;
use App\Models\Depreciation;
use App\Models\Manufacturer;
@ -429,4 +430,13 @@ class AssetModelFactory extends Factory
];
});
}
public function hasEncryptedCustomField(CustomField $field = null)
{
return $this->state(function () use ($field) {
return [
'fieldset_id' => CustomFieldset::factory()->hasEncryptedCustomField($field),
];
});
}
}

View file

@ -3,6 +3,7 @@
namespace Database\Factories;
use App\Models\CustomFieldset;
use App\Models\CustomField;
use Illuminate\Database\Eloquent\Factories\Factory;
class CustomFieldsetFactory extends Factory
@ -43,4 +44,13 @@ class CustomFieldsetFactory extends Factory
];
});
}
public function hasEncryptedCustomField(CustomField $field = null)
{
return $this->afterCreating(function (CustomFieldset $fieldset) use ($field) {
$field = $field ?? CustomField::factory()->testEncrypted()->create();
$fieldset->fields()->attach($field, ['order' => '1', 'required' => false]);
});
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -5,16 +5,16 @@
"/css/build/app.css": "/css/build/app.css?id=4c2cb86bf76bcd58e9e362a356bc9182",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=f25c77ed07053646a42e9c19923d24fa",
"/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=268041e902b019730c23ee3875838005",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=9a474f38641582dc07bc004b44e2849e",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=9f4a71e8812af08857dad5879b4855c8",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=c98bdc8f8d3560d732c4985f76cee8d5",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=03075904b967308132b810bc0205ab1c",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=47ab28abd019c2b1f9aae60a3d44824a",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=af8c7daf7e9a2c784eafb76f65c418f7",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=fc7adb943668ac69fe4b646625a7571f",
"/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=ac9929237d506367832e8fee426ff396",
"/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=7fb8cf2421ad272b41393fdf5844559f",
"/css/dist/skins/skin-purple.css": "/css/dist/skins/skin-purple.css?id=cf6c8c340420724b02d6e787ef9bded5",
"/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=6559832aaaa3a4c8ef7540daa24127f5",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=5852d0e007f9907d196f7b04f945edb2",
"/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=122d3df19d2c0552d7ef388e69f7d71f",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=5414c37b1403f41e051ad7b3aac112b4",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=293aac2b6a8b56419c4ca3bfeffcbf0d",
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=bd61fefb56b30ed6d8c946f02bc956fb",
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=f0fbbb0ac729ea092578fb05ca615460",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
@ -35,18 +35,18 @@
"/js/dist/all.js": "/js/dist/all.js?id=fca6ea9956fd827d9790c08e0e982b22",
"/js/dist/all-defer.js": "/js/dist/all-defer.js?id=19ccc62a8f1ea103dede4808837384d4",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=293aac2b6a8b56419c4ca3bfeffcbf0d",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=bd61fefb56b30ed6d8c946f02bc956fb",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=5852d0e007f9907d196f7b04f945edb2",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=5414c37b1403f41e051ad7b3aac112b4",
"/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=392cc93cfc0be0349bab9697669dd091",
"/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=9f4a71e8812af08857dad5879b4855c8",
"/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=47ab28abd019c2b1f9aae60a3d44824a",
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=fc7adb943668ac69fe4b646625a7571f",
"/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=c98bdc8f8d3560d732c4985f76cee8d5",
"/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=af8c7daf7e9a2c784eafb76f65c418f7",
"/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
"/css/dist/skins/skin-red-dark.min.css": "/css/dist/skins/skin-red-dark.min.css?id=6559832aaaa3a4c8ef7540daa24127f5",
"/css/dist/skins/skin-red-dark.min.css": "/css/dist/skins/skin-red-dark.min.css?id=122d3df19d2c0552d7ef388e69f7d71f",
"/css/dist/skins/skin-purple.min.css": "/css/dist/skins/skin-purple.min.css?id=cf6c8c340420724b02d6e787ef9bded5",
"/css/dist/skins/skin-purple-dark.min.css": "/css/dist/skins/skin-purple-dark.min.css?id=ac9929237d506367832e8fee426ff396",
"/css/dist/skins/skin-purple-dark.min.css": "/css/dist/skins/skin-purple-dark.min.css?id=7fb8cf2421ad272b41393fdf5844559f",
"/css/dist/skins/skin-orange.min.css": "/css/dist/skins/skin-orange.min.css?id=268041e902b019730c23ee3875838005",
"/css/dist/skins/skin-orange-dark.min.css": "/css/dist/skins/skin-orange-dark.min.css?id=9a474f38641582dc07bc004b44e2849e",
"/css/dist/skins/skin-orange-dark.min.css": "/css/dist/skins/skin-orange-dark.min.css?id=03075904b967308132b810bc0205ab1c",
"/css/dist/skins/skin-contrast.min.css": "/css/dist/skins/skin-contrast.min.css?id=f0fbbb0ac729ea092578fb05ca615460"
}

View file

@ -70,7 +70,7 @@
&.btn-primary, .btn-primary:link {
background-color: darken(@black, 10%);
border-color: darken(@black, 20%);
border-color: #FFF;
color: #fff;
}

View file

@ -245,7 +245,6 @@ body {
}
.btn-primary:hover {
background-color: var(--button-primary);
color: var(--link)!important;
}
#componentsTable>tbody>tr>td>nobr>a>i.fa {
color: var(--text-main);

View file

@ -236,7 +236,6 @@ body {
}
.btn-primary:hover {
background-color: var(--button-primary);
color: var(--link)!important;
}
#componentsTable>tbody>tr>td>nobr>a>i.fa {
color: var(--text-main);

View file

@ -243,7 +243,6 @@ a:link.btn-default{
}
.btn-primary:hover {
background-color: var(--button-primary);
color: var(--link)!important;
}
#componentsTable>tbody>tr>td>nobr>a>i.fa {
color: var(--text-main);

View file

@ -246,7 +246,6 @@ body {
}
.btn-primary:hover {
background-color: var(--button-primary);
color: var(--link)!important;
}
#componentsTable>tbody>tr>td>nobr>a>i.fa {
color: var(--text-main);

View file

@ -247,7 +247,6 @@ body {
}
.btn-primary:hover {
background-color: var(--button-primary);
color: var(--link)!important;
}
#componentsTable>tbody>tr>td>nobr>a>i.fa {
color: var(--text-main);

View file

@ -55,7 +55,7 @@
&.btn-primary, .btn-primary:link {
background-color: var(--button-default);
border-color: var(--button-default);
border-color: #000000;
color: #545454;
}

View file

@ -17,6 +17,7 @@ return [
'update' => [
'error' => 'Asset was not updated, please try again',
'success' => 'Asset updated successfully.',
'encrypted_warning' => 'Asset updated successfully, but encrypted custom fields were not due to permissions',
'nothing_updated' => 'No fields were selected, so nothing was updated.',
'no_assets_selected' => 'No assets were selected, so nothing was updated.',
'assets_do_not_exist_or_are_invalid' => 'Selected assets cannot be updated.',

View file

@ -245,6 +245,7 @@ return [
'select_all' => 'Select All',
'search' => 'Search',
'select_category' => 'Select a Category',
'select_datasource' => 'Select a Datasource',
'select_department' => 'Select a Department',
'select_depreciation' => 'Select a Depreciation Type',
'select_location' => 'Select a Location',

View file

@ -306,6 +306,7 @@
<label style="grid-area: source-title">DataSource</label>
<select style="grid-area: source-field" x-model="option.datasource">
<optgroup label="Asset">
<option value="" disabled>{{ trans('general.select_datasource') }}</option>
<option value="asset_tag">{{trans('admin/hardware/table.asset_tag')}}</option>
<option value="name">{{trans('admin/hardware/form.name')}}</option>
<option value="serial">{{trans('admin/hardware/table.serial')}}</option>

View file

@ -5,10 +5,12 @@ namespace Tests\Feature\Api\Assets;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Company;
use App\Models\CustomField;
use App\Models\Location;
use App\Models\Statuslabel;
use App\Models\Supplier;
use App\Models\User;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\TestCase;
@ -479,4 +481,55 @@ class AssetStoreTest extends TestCase
$json->has('messages.company_id')->etc();
});
}
public function testEncryptedCustomFieldCanBeStored()
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL');
$status = Statuslabel::factory()->create();
$field = CustomField::factory()->testEncrypted()->create();
$superuser = User::factory()->superuser()->create();
$assetData = Asset::factory()->hasEncryptedCustomField($field)->make();
$response = $this->actingAsForApi($superuser)
->postJson(route('api.assets.store'), [
$field->db_column_name() => 'This is encrypted field',
'model_id' => $assetData->model->id,
'status_id' => $status->id,
'asset_tag' => '1234',
])
->assertStatusMessageIs('success')
->assertOk()
->json();
$asset = Asset::findOrFail($response['payload']['id']);
$this->assertEquals('This is encrypted field', Crypt::decrypt($asset->{$field->db_column_name()}));
}
public function testPermissionNeededToStoreEncryptedField()
{
// @todo:
$this->markTestIncomplete();
$status = Statuslabel::factory()->create();
$field = CustomField::factory()->testEncrypted()->create();
$normal_user = User::factory()->editAssets()->create();
$assetData = Asset::factory()->hasEncryptedCustomField($field)->make();
$response = $this->actingAsForApi($normal_user)
->postJson(route('api.assets.store'), [
$field->db_column_name() => 'Some Other Value Entirely!',
'model_id' => $assetData->model->id,
'status_id' => $status->id,
'asset_tag' => '1234',
])
// @todo: this is 403 unauthorized
->assertStatusMessageIs('success')
->assertOk()
->assertMessagesAre('Asset updated successfully, but encrypted custom fields were not due to permissions')
->json();
$asset = Asset::findOrFail($response['payload']['id']);
$this->assertEquals('This is encrypted field', Crypt::decrypt($asset->{$field->db_column_name()}));
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Tests\Feature\Api\Assets;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\User;
use Illuminate\Support\Facades\Crypt;
use Tests\TestCase;
class AssetUpdateTest extends TestCase
{
public function testEncryptedCustomFieldCanBeUpdated()
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL');
$field = CustomField::factory()->testEncrypted()->create();
$asset = Asset::factory()->hasEncryptedCustomField($field)->create();
$superuser = User::factory()->superuser()->create();
$this->actingAsForApi($superuser)
->patchJson(route('api.assets.update', $asset->id), [
$field->db_column_name() => 'This is encrypted field'
])
->assertStatusMessageIs('success')
->assertOk();
$asset->refresh();
$this->assertEquals('This is encrypted field', Crypt::decrypt($asset->{$field->db_column_name()}));
}
public function testPermissionNeededToUpdateEncryptedField()
{
$this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL');
$field = CustomField::factory()->testEncrypted()->create();
$asset = Asset::factory()->hasEncryptedCustomField($field)->create();
$normal_user = User::factory()->editAssets()->create();
$asset->{$field->db_column_name()} = Crypt::encrypt("encrypted value should not change");
$asset->save();
// test that a 'normal' user *cannot* change the encrypted custom field
$this->actingAsForApi($normal_user)
->patchJson(route('api.assets.update', $asset->id), [
$field->db_column_name() => 'Some Other Value Entirely!'
])
->assertStatusMessageIs('success')
->assertOk()
->assertMessagesAre('Asset updated successfully, but encrypted custom fields were not due to permissions');
$asset->refresh();
$this->assertEquals("encrypted value should not change", Crypt::decrypt($asset->{$field->db_column_name()}));
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Tests\Support;
trait CanSkipTests
{
public function markIncompleteIfMySQL($message = 'Test skipped due to database driver being MySQL.')
{
if (config('database.default') === 'mysql') {
$this->markTestIncomplete($message);
}
}
}

View file

@ -87,5 +87,18 @@ trait CustomTestMacros
return $this;
}
);
TestResponse::macro(
'assertMessagesAre',
function (string $message) {
Assert::assertEquals(
$message,
$this['messages'],
"Response messages was not {$message}"
);
return $this;
}
);
}
}

View file

@ -7,6 +7,7 @@ use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use RuntimeException;
use Tests\Support\AssertsAgainstSlackNotifications;
use Tests\Support\CanSkipTests;
use Tests\Support\CustomTestMacros;
use Tests\Support\InteractsWithAuthentication;
use Tests\Support\InitializesSettings;
@ -14,6 +15,7 @@ use Tests\Support\InitializesSettings;
abstract class TestCase extends BaseTestCase
{
use AssertsAgainstSlackNotifications;
use CanSkipTests;
use CreatesApplication;
use CustomTestMacros;
use InteractsWithAuthentication;