Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public function update(EnvironmentVariable $environment_variable, Request $reque
validator($data, EnvironmentVariable::rules($environment_variable), EnvironmentVariable::messages())->validate();
$linkedAsset = EnvironmentVariable::validateAssetLinkConsistency($data);

$fields = ['name', 'description', 'asset_type'];
$fields = ['name', 'description', 'asset_type', 'do_not_update'];
if ($linkedAsset) {
// Guarantee value is always the selected asset's ID on this instance.
$data['value'] = (string) $linkedAsset->id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

namespace ProcessMaker\ImportExport\Exporters;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Enums\ExporterMap;
use ProcessMaker\ImportExport\DependentType;
use ProcessMaker\ImportExport\Manifest;
use ProcessMaker\ImportExport\Options;
use ProcessMaker\ImportExport\Psudomodels\Psudomodel;

class EnvironmentVariableExporter extends ExporterBase
{
Expand All @@ -16,6 +20,18 @@ class EnvironmentVariableExporter extends ExporterBase

public $incrementStringSeparator = '_';

public function __construct(
Model|Psudomodel|null $model,
Manifest $manifest,
Options $options,
$ignoreExplicitDiscard
) {
parent::__construct($model, $manifest, $options, $ignoreExplicitDiscard);

// PHP does not allow closures as property defaults.
$this->discard = fn ($envVar) => (bool) ($envVar->do_not_update ?? false);
}

public function export() : void
{
$this->addReference(DependentType::ENVIRONMENT_VARIABLE_VALUE, $this->model->value);
Expand Down
44 changes: 42 additions & 2 deletions ProcessMaker/ImportExport/Exporters/ExporterBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ abstract class ExporterBase implements ExporterInterface

public $hidden = false;

/**
* When true (or a callable returning true), this asset is explicitly discarded.
* Callables receive the model: fn (Model $model): bool
*
* @var bool|callable
*/
public $discard = false;

public static $forceUpdate = false;
Expand Down Expand Up @@ -135,7 +141,7 @@ public function uuid() : string

public function include() : bool
{
if ($this->discard) { // Explicit Discard (not from passed-in options)
if ($this->isExplicitlyDiscarded()) { // Explicit Discard (not from passed-in options)
if (!$this->ignoreExplicitDiscard) {
return false;
}
Expand All @@ -148,6 +154,40 @@ public function include() : bool
return true;
}

/**
* Resolve $discard whether it is a boolean or a callable checked against the model.
*/
public function isExplicitlyDiscarded(): bool
{
if (is_callable($this->discard)) {
return (bool) ($this->discard)($this->model);
}

return (bool) $this->discard;
}

/**
* Import-time check: should an existing target model be discarded (not overwritten)?
*
* Only callables are evaluated here. Boolean `$discard = true` (Users/Groups) is an
* export-time flag handled by include(), not an import-time skip.
* Psudomodels (e.g. Signal) are never discarded by this path.
*/
public static function shouldDiscardExistingModelDuringImport(Model|Psudomodel|null $model): bool
{
if (!$model instanceof Model || !$model->exists) {
return false;
}

$exporter = new static($model, new Manifest(), new Options([]), false);

if (!is_callable($exporter->discard)) {
return false;
}

return (bool) ($exporter->discard)($model);
}

public function addDependent(string $type, Model|Psudomodel $dependentModel, string $exporterClass, $meta = null)
{
$uuid = $dependentModel->uuid;
Expand Down Expand Up @@ -304,7 +344,7 @@ public function toArray()
'hidden' => $this->hidden,
'mode' => $this->mode,
'saveAssetsMode' => $this->saveAssetsMode,
'explicit_discard' => $this->discard,
'explicit_discard' => $this->isExplicitlyDiscarded(),
'dependents' => array_map(fn ($d) => $d->toArray(), $this->dependents),
'name' => $this->getName($this->model),
'description' => $this->getDescription(),
Expand Down
5 changes: 5 additions & 0 deletions ProcessMaker/ImportExport/Manifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ public static function getModel($uuid, $assetInfo, $mode, $exporterClass, $isRoo
$mode = 'discard';
}

// Per-asset explicit discard (e.g. env vars with do_not_update on the target).
if ($model && $exporterClass::shouldDiscardExistingModelDuringImport($model)) {
$mode = 'discard';
}

if ($exporterClass::$forceUpdate) {
$mode = 'update';
}
Expand Down
7 changes: 7 additions & 0 deletions ProcessMaker/Models/EnvironmentVariable.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* @OA\Property(property="description", type="string"),
* @OA\Property(property="value", type="string"),
* @OA\Property(property="asset_type", type="string", nullable=true),
* @OA\Property(property="do_not_update", type="boolean"),
* ),
* @OA\Schema(
* schema="EnvironmentVariable",
Expand All @@ -41,6 +42,11 @@ class EnvironmentVariable extends ProcessMakerModel
'description',
'value',
'asset_type',
'do_not_update',
];

protected $casts = [
'do_not_update' => 'boolean',
];

protected $hidden = [
Expand Down Expand Up @@ -102,6 +108,7 @@ public static function rules($existing = null)
'string',
Rule::in($allowedAssetTypes),
],
'do_not_update' => 'boolean',
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function definition()
'name' => $this->faker->word(),
'description' => $this->faker->sentence(),
'value' => $this->faker->sentence(),
'do_not_update' => false,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->boolean('do_not_update')->default(false)->after('asset_type');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('do_not_update');
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,18 @@
:value.sync="value"
:errors="errors"
/>
<do-not-update-switch v-model="doNotUpdate" />
</modal>
</div>
</template>

<script>
import { FormErrorsMixin, Modal, Required } from "SharedComponents";
import AssetLinkFields from "./AssetLinkFields.vue";
import DoNotUpdateSwitch from "./DoNotUpdateSwitch.vue";

export default {
components: { Modal, Required, AssetLinkFields },
components: { Modal, Required, AssetLinkFields, DoNotUpdateSwitch },
mixins: [ FormErrorsMixin ],
data: function() {
return {
Expand All @@ -59,6 +61,7 @@
description: '',
value: '',
assetType: null,
doNotUpdate: false,
disabled: false,
}
},
Expand All @@ -68,6 +71,7 @@
this.description = '';
this.value = '';
this.assetType = null;
this.doNotUpdate = false;
this.errors = {};
this.disabled = false;
},
Expand All @@ -83,6 +87,7 @@
description: this.description,
value: this.value,
asset_type: this.assetType,
do_not_update: this.doNotUpdate,
})
.then(response => {
ProcessMaker.alert(this.$t('The environment variable was created.'), 'success');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<div class="do-not-update-switch pt-3 mt-2 border-top">
<div class="d-flex align-items-start">
<b-form-checkbox
class="mt-1"
switch
:checked="value"
@input="$emit('input', $event)"
/>
<div class="ml-2">
<div class="font-weight-bold">
<i class="fas fa-lock text-primary mr-1" aria-hidden="true"></i>
{{ $t("Do not overwrite this environment variable on import/DevLink") }}
</div>
<small class="form-text text-muted mb-0">
{{ $t("When enabled, the value of this variable will not be overwritten during imports or DevLink synchronization, even if a variable with the same name exists in the source environment.") }}
</small>
</div>
</div>
</div>
</template>

<script>
export default {
props: {
value: {
type: Boolean,
default: false,
},
},
};
</script>
4 changes: 4 additions & 0 deletions resources/js/processes/environment-variables/edit.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import Vue from "vue";
import AssetLinkFields from "./components/AssetLinkFields.vue";
import DoNotUpdateSwitch from "./components/DoNotUpdateSwitch.vue";

const initial = window.ProcessMaker.EnvironmentVariableEdit || {};

new Vue({
el: "#editEnvironmentVariable",
components: {
AssetLinkFields,
DoNotUpdateSwitch,
},
data() {
return {
Expand All @@ -16,6 +18,7 @@ new Vue({
description: initial.description,
value: initial.value || null,
asset_type: initial.asset_type || null,
do_not_update: !!initial.do_not_update,
},
errors: {
name: null,
Expand Down Expand Up @@ -44,6 +47,7 @@ new Vue({
description: this.formData.description,
asset_type: this.formData.asset_type,
value: this.formData.value,
do_not_update: this.formData.do_not_update,
};
ProcessMaker.apiClient.put(`environment_variables/${this.formData.id}`, payload)
.then(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
:errors="errors"
value-hint="{{ __('For security purposes, this field will always appear empty') }}"
></asset-link-fields>
<do-not-update-switch v-model="formData.do_not_update"></do-not-update-switch>
<br>
<div class="text-right">
{{ html()->button(__('Cancel'), 'button')->class('btn btn-outline-secondary')->attribute('@click', 'onClose') }}
Expand All @@ -58,6 +59,7 @@
asset_type: @json($environmentVariable->asset_type),
// Safe to expose when linked: value is the asset numeric ID, not a secret.
value: @json($environmentVariable->asset_type ? $environmentVariable->value : null),
do_not_update: @json((bool) $environmentVariable->do_not_update),
};
</script>
<script src="{{mix('js/processes/environment-variables/edit.js')}}"></script>
Expand Down
44 changes: 44 additions & 0 deletions tests/Feature/Api/EnvironmentVariablesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,50 @@ public function test_it_should_successfully_update_an_environment_variable()
$this->assertEquals('newvalue', $variable->value);
}

/** @test */
public function test_it_should_create_an_environment_variable_with_do_not_update()
{
$data = [
'name' => 'protectedVar',
'description' => 'protected description',
'value' => 'secret',
'do_not_update' => true,
];

$response = $this->apiCall('POST', self::API_TEST_VARIABLES, $data);

$response->assertStatus(201);
$response->assertJsonFragment(['do_not_update' => true]);
$this->assertDatabaseHas('environment_variables', [
'name' => 'protectedVar',
'do_not_update' => true,
]);
}

/** @test */
public function test_it_should_update_do_not_update_flag()
{
$variable = EnvironmentVariable::factory()->create([
'name' => 'testname',
'value' => 'testvalue',
'do_not_update' => false,
]);

$data = [
'name' => 'testname',
'description' => 'updated description',
'do_not_update' => true,
];
$response = $this->apiCall('PUT', self::API_TEST_VARIABLES . '/' . $variable->id, $data);

$response->assertStatus(200);
$response->assertJsonFragment(['do_not_update' => true]);
$this->assertDatabaseHas('environment_variables', [
'id' => $variable->id,
'do_not_update' => true,
]);
}

/** @test */
public function test_it_should_return_paginated_environment_variables_during_index()
{
Expand Down
Loading
Loading