mirror of
https://github.com/ansible/awx.git
synced 2026-05-16 13:57:39 -02:30
Fixed various bugs with preventing prompting for credential passwords on schedules and workflow nodes
This commit is contained in:
@@ -33,7 +33,7 @@ function TemplatesStrings (BaseString) {
|
|||||||
NO_INVENTORY_SELECTED: t.s('No inventory selected'),
|
NO_INVENTORY_SELECTED: t.s('No inventory selected'),
|
||||||
REVERT: t.s('REVERT'),
|
REVERT: t.s('REVERT'),
|
||||||
CREDENTIAL_TYPE: t.s('Credential Type'),
|
CREDENTIAL_TYPE: t.s('Credential Type'),
|
||||||
CREDENTIAL_PASSWORD_WARNING: t.s('Credentials that require passwords on launch are not permitted for template schedules and workflow nodes. The following credentials must be removed or replaced to proceed:'),
|
CREDENTIAL_PASSWORD_WARNING: t.s('Credentials that require passwords on launch are not permitted for template schedules and workflow nodes. The following credentials must be replaced to proceed:'),
|
||||||
PASSWORDS_REQUIRED_HELP: t.s('Launching this job requires the passwords listed below. Enter and confirm each password before continuing.'),
|
PASSWORDS_REQUIRED_HELP: t.s('Launching this job requires the passwords listed below. Enter and confirm each password before continuing.'),
|
||||||
PLEASE_ENTER_PASSWORD: t.s('Please enter a password.'),
|
PLEASE_ENTER_PASSWORD: t.s('Please enter a password.'),
|
||||||
credential_passwords: {
|
credential_passwords: {
|
||||||
@@ -93,7 +93,8 @@ function TemplatesStrings (BaseString) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ns.workflows = {
|
ns.workflows = {
|
||||||
INVALID_JOB_TEMPLATE: t.s('This Job Template is missing a default inventory or project. This must be addressed in the Job Template form before this node can be saved.')
|
INVALID_JOB_TEMPLATE: t.s('This Job Template is missing a default inventory or project. This must be addressed in the Job Template form before this node can be saved.'),
|
||||||
|
CREDENTIAL_WITH_PASS: t.s('This Job Template has a credential that requires a password. Credentials requiring passwords on launch are not permitted on workflow nodes.')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,61 +73,65 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel',
|
|||||||
|
|
||||||
vm.promptDataClone.prompts.credentials.passwords = {};
|
vm.promptDataClone.prompts.credentials.passwords = {};
|
||||||
|
|
||||||
if(vm.promptDataClone.launchConf.passwords_needed_to_start) {
|
vm.promptDataClone.prompts.credentials.value.forEach((credential) => {
|
||||||
let machineCredPassObj = null;
|
if(credential.inputs) {
|
||||||
vm.promptDataClone.launchConf.passwords_needed_to_start.forEach((passwordNeeded) => {
|
if(credential.inputs.password && credential.inputs.password === "ASK") {
|
||||||
if (passwordNeeded === "ssh_password" ||
|
vm.promptDataClone.prompts.credentials.passwords.ssh_password = {
|
||||||
passwordNeeded === "become_password" ||
|
id: credential.id,
|
||||||
passwordNeeded === "ssh_key_unlock"
|
name: credential.name
|
||||||
) {
|
};
|
||||||
if (!machineCredPassObj) {
|
}
|
||||||
vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => {
|
if(credential.inputs.become_password && credential.inputs.become_password === "ASK") {
|
||||||
if (defaultCredential.kind && defaultCredential.kind === "ssh") {
|
vm.promptDataClone.prompts.credentials.passwords.become_password = {
|
||||||
machineCredPassObj = {
|
id: credential.id,
|
||||||
id: defaultCredential.id,
|
name: credential.name
|
||||||
name: defaultCredential.name
|
};
|
||||||
};
|
}
|
||||||
} else if (defaultCredential.passwords_needed) {
|
if(credential.inputs.ssh_key_unlock && credential.inputs.ssh_key_unlock === "ASK") {
|
||||||
defaultCredential.passwords_needed.forEach((neededPassword) => {
|
vm.promptDataClone.prompts.credentials.passwords.ssh_key_unlock = {
|
||||||
if (neededPassword === passwordNeeded) {
|
id: credential.id,
|
||||||
machineCredPassObj = {
|
name: credential.name
|
||||||
id: defaultCredential.id,
|
};
|
||||||
name: defaultCredential.name
|
}
|
||||||
};
|
if(credential.inputs.vault_password && credential.inputs.vault_password === "ASK") {
|
||||||
}
|
if(!vm.promptDataClone.prompts.credentials.passwords.vault) {
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
vm.promptDataClone.prompts.credentials.passwords[passwordNeeded] = angular.copy(machineCredPassObj);
|
|
||||||
} else if (passwordNeeded.startsWith("vault_password")) {
|
|
||||||
let vault_id = null;
|
|
||||||
if (passwordNeeded.includes('.')) {
|
|
||||||
vault_id = passwordNeeded.split(/\.(.+)/)[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!vm.promptDataClone.prompts.credentials.passwords.vault) {
|
|
||||||
vm.promptDataClone.prompts.credentials.passwords.vault = [];
|
vm.promptDataClone.prompts.credentials.passwords.vault = [];
|
||||||
}
|
}
|
||||||
|
vm.promptDataClone.prompts.credentials.passwords.vault.push({
|
||||||
// Loop across the default credentials to find the name of the
|
id: credential.id,
|
||||||
// credential that requires a password
|
name: credential.name,
|
||||||
vm.promptDataClone.prompts.credentials.value.forEach((defaultCredential) => {
|
vault_id: credential.inputs.vault_id
|
||||||
if (vm.promptDataClone.prompts.credentials.credentialTypes[defaultCredential.credential_type] === "vault") {
|
|
||||||
let defaultCredVaultId = defaultCredential.vault_id || _.get(defaultCredential, 'inputs.vault_id') || null;
|
|
||||||
if (defaultCredVaultId === vault_id) {
|
|
||||||
vm.promptDataClone.prompts.credentials.passwords.vault.push({
|
|
||||||
id: defaultCredential.id,
|
|
||||||
name: defaultCredential.name,
|
|
||||||
vault_id: defaultCredVaultId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
} else if(credential.passwords_needed && credential.passwords_needed.length > 0) {
|
||||||
}
|
credential.passwords_needed.forEach((passwordNeeded) => {
|
||||||
|
if (passwordNeeded === "ssh_password" ||
|
||||||
|
passwordNeeded === "become_password" ||
|
||||||
|
passwordNeeded === "ssh_key_unlock"
|
||||||
|
) {
|
||||||
|
vm.promptDataClone.prompts.credentials.passwords[passwordNeeded] = {
|
||||||
|
id: credential.id,
|
||||||
|
name: credential.name
|
||||||
|
};
|
||||||
|
} else if (passwordNeeded.startsWith("vault_password")) {
|
||||||
|
let vault_id = null;
|
||||||
|
if (passwordNeeded.includes('.')) {
|
||||||
|
vault_id = passwordNeeded.split(/\.(.+)/)[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vm.promptDataClone.prompts.credentials.passwords.vault) {
|
||||||
|
vm.promptDataClone.prompts.credentials.passwords.vault = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.promptDataClone.prompts.credentials.passwords.vault.push({
|
||||||
|
id: credential.id,
|
||||||
|
name: credential.name,
|
||||||
|
vault_id: vault_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
vm.promptDataClone.credentialTypeMissing = [];
|
vm.promptDataClone.credentialTypeMissing = [];
|
||||||
|
|
||||||
@@ -141,7 +145,13 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel',
|
|||||||
};
|
};
|
||||||
order++;
|
order++;
|
||||||
}
|
}
|
||||||
if(vm.promptDataClone.launchConf.ask_credential_on_launch || (vm.promptDataClone.launchConf.passwords_needed_to_start && vm.promptDataClone.launchConf.passwords_needed_to_start.length > 0)) {
|
if (vm.promptDataClone.launchConf.ask_credential_on_launch ||
|
||||||
|
(_.has(vm, 'promptDataClone.prompts.credentials.passwords.vault') &&
|
||||||
|
vm.promptDataClone.prompts.credentials.passwords.vault.length > 0) ||
|
||||||
|
_.has(vm.promptDataClone.prompts.credentials.passwords.ssh_key_unlock) ||
|
||||||
|
_.has(vm.promptDataClone.prompts.credentials.passwords.become_password) ||
|
||||||
|
_.has(vm.promptDataClone.prompts.credentials.passwords.ssh_password)
|
||||||
|
) {
|
||||||
vm.steps.credential.includeStep = true;
|
vm.steps.credential.includeStep = true;
|
||||||
vm.steps.credential.tab = {
|
vm.steps.credential.tab = {
|
||||||
_active: order === 1 ? true : false,
|
_active: order === 1 ? true : false,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
$state, ProcessErrors, CreateSelect2, $q, JobTemplate,
|
$state, ProcessErrors, CreateSelect2, $q, JobTemplate,
|
||||||
Empty, PromptService, Rest, TemplatesStrings, $timeout) {
|
Empty, PromptService, Rest, TemplatesStrings, $timeout) {
|
||||||
|
|
||||||
let promptWatcher, surveyQuestionWatcher;
|
let promptWatcher, surveyQuestionWatcher, credentialsWatcher;
|
||||||
|
|
||||||
$scope.strings = TemplatesStrings;
|
$scope.strings = TemplatesStrings;
|
||||||
$scope.preventCredsWithPasswords = true;
|
$scope.preventCredsWithPasswords = true;
|
||||||
@@ -341,6 +341,20 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let checkCredentialsForRequiredPasswords = () => {
|
||||||
|
let credentialRequiresPassword = false;
|
||||||
|
$scope.promptData.prompts.credentials.value.forEach((credential) => {
|
||||||
|
if ((credential.passwords_needed &&
|
||||||
|
credential.passwords_needed.length > 0) ||
|
||||||
|
(_.has(credential, 'inputs.vault_password') &&
|
||||||
|
credential.inputs.vault_password === "ASK")
|
||||||
|
) {
|
||||||
|
credentialRequiresPassword = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$scope.credentialRequiresPassword = credentialRequiresPassword;
|
||||||
|
};
|
||||||
|
|
||||||
let watchForPromptChanges = () => {
|
let watchForPromptChanges = () => {
|
||||||
let promptDataToWatch = [
|
let promptDataToWatch = [
|
||||||
'promptData.prompts.inventory.value',
|
'promptData.prompts.inventory.value',
|
||||||
@@ -357,6 +371,12 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
}
|
}
|
||||||
$scope.promptModalMissingReqFields = missingPromptValue;
|
$scope.promptModalMissingReqFields = missingPromptValue;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($scope.promptData.launchConf.ask_credential_on_launch && $scope.credentialRequiresPassword) {
|
||||||
|
credentialsWatcher = $scope.$watch('promptData.prompts.credentials', () => {
|
||||||
|
checkCredentialsForRequiredPasswords();
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.closeWorkflowMaker = function() {
|
$scope.closeWorkflowMaker = function() {
|
||||||
@@ -537,6 +557,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
surveyQuestionWatcher();
|
surveyQuestionWatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (credentialsWatcher) {
|
||||||
|
credentialsWatcher();
|
||||||
|
}
|
||||||
|
|
||||||
$scope.promptData = null;
|
$scope.promptData = null;
|
||||||
|
|
||||||
// Reset the edgeConflict flag
|
// Reset the edgeConflict flag
|
||||||
@@ -564,6 +588,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
surveyQuestionWatcher();
|
surveyQuestionWatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (credentialsWatcher) {
|
||||||
|
credentialsWatcher();
|
||||||
|
}
|
||||||
|
|
||||||
$scope.promptData = null;
|
$scope.promptData = null;
|
||||||
$scope.selectedTemplateInvalid = false;
|
$scope.selectedTemplateInvalid = false;
|
||||||
$scope.showPromptButton = false;
|
$scope.showPromptButton = false;
|
||||||
@@ -671,6 +699,24 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
$scope.selectedTemplateInvalid = false;
|
$scope.selectedTemplateInvalid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let credentialRequiresPassword = false;
|
||||||
|
|
||||||
|
prompts.credentials.value.forEach((credential) => {
|
||||||
|
if(credential.inputs) {
|
||||||
|
if ((credential.inputs.password && credential.inputs.password === "ASK") ||
|
||||||
|
(credential.inputs.become_password && credential.inputs.become_password === "ASK") ||
|
||||||
|
(credential.inputs.ssh_key_unlock && credential.inputs.ssh_key_unlock === "ASK") ||
|
||||||
|
(credential.inputs.vault_password && credential.inputs.vault_password === "ASK")
|
||||||
|
) {
|
||||||
|
credentialRequiresPassword = true;
|
||||||
|
}
|
||||||
|
} else if (credential.passwords_needed && credential.passwords_needed.length > 0) {
|
||||||
|
credentialRequiresPassword = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.credentialRequiresPassword = credentialRequiresPassword;
|
||||||
|
|
||||||
if (!launchConf.survey_enabled &&
|
if (!launchConf.survey_enabled &&
|
||||||
!launchConf.ask_inventory_on_launch &&
|
!launchConf.ask_inventory_on_launch &&
|
||||||
!launchConf.ask_credential_on_launch &&
|
!launchConf.ask_credential_on_launch &&
|
||||||
@@ -682,7 +728,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
!launchConf.ask_diff_mode_on_launch &&
|
!launchConf.ask_diff_mode_on_launch &&
|
||||||
!launchConf.survey_enabled &&
|
!launchConf.survey_enabled &&
|
||||||
!launchConf.credential_needed_to_start &&
|
!launchConf.credential_needed_to_start &&
|
||||||
launchConf.passwords_needed_to_start.length === 0 &&
|
|
||||||
launchConf.variables_needed_to_start.length === 0) {
|
launchConf.variables_needed_to_start.length === 0) {
|
||||||
$scope.showPromptButton = false;
|
$scope.showPromptButton = false;
|
||||||
$scope.promptModalMissingReqFields = false;
|
$scope.promptModalMissingReqFields = false;
|
||||||
@@ -727,6 +772,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
$scope.missingSurveyValue = missingSurveyValue;
|
$scope.missingSurveyValue = missingSurveyValue;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
checkCredentialsForRequiredPasswords();
|
||||||
|
|
||||||
watchForPromptChanges();
|
watchForPromptChanges();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -736,6 +783,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
prompts: prompts,
|
prompts: prompts,
|
||||||
template: $scope.nodeBeingEdited.unifiedJobTemplate.id
|
template: $scope.nodeBeingEdited.unifiedJobTemplate.id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
checkCredentialsForRequiredPasswords();
|
||||||
|
|
||||||
watchForPromptChanges();
|
watchForPromptChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -980,6 +1030,10 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
surveyQuestionWatcher();
|
surveyQuestionWatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (credentialsWatcher) {
|
||||||
|
credentialsWatcher();
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedTemplate.type === "job_template") {
|
if (selectedTemplate.type === "job_template") {
|
||||||
let jobTemplate = new JobTemplate();
|
let jobTemplate = new JobTemplate();
|
||||||
|
|
||||||
@@ -993,6 +1047,12 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
$scope.selectedTemplateInvalid = false;
|
$scope.selectedTemplateInvalid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (launchConf.passwords_needed_to_start && launchConf.passwords_needed_to_start.length > 0) {
|
||||||
|
$scope.credentialRequiresPassword = true;
|
||||||
|
} else {
|
||||||
|
$scope.credentialRequiresPassword = false;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
||||||
|
|
||||||
if (!launchConf.survey_enabled &&
|
if (!launchConf.survey_enabled &&
|
||||||
@@ -1006,7 +1066,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
!launchConf.ask_diff_mode_on_launch &&
|
!launchConf.ask_diff_mode_on_launch &&
|
||||||
!launchConf.survey_enabled &&
|
!launchConf.survey_enabled &&
|
||||||
!launchConf.credential_needed_to_start &&
|
!launchConf.credential_needed_to_start &&
|
||||||
launchConf.passwords_needed_to_start.length === 0 &&
|
|
||||||
launchConf.variables_needed_to_start.length === 0) {
|
launchConf.variables_needed_to_start.length === 0) {
|
||||||
$scope.showPromptButton = false;
|
$scope.showPromptButton = false;
|
||||||
$scope.promptModalMissingReqFields = false;
|
$scope.promptModalMissingReqFields = false;
|
||||||
@@ -1069,7 +1128,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService',
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// TODO - clear out prompt data?
|
|
||||||
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
$scope.selectedTemplate = angular.copy(selectedTemplate);
|
||||||
$scope.selectedTemplateInvalid = false;
|
$scope.selectedTemplateInvalid = false;
|
||||||
$scope.showPromptButton = false;
|
$scope.showPromptButton = false;
|
||||||
|
|||||||
@@ -99,7 +99,13 @@
|
|||||||
<span>{{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }}</span>
|
<span>{{:: strings.get('workflows.INVALID_JOB_TEMPLATE') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-show="selectedTemplate && !selectedTemplateInvalid">
|
<div ng-if="selectedTemplate && credentialRequiresPassword">
|
||||||
|
<div class="WorkflowMaker-invalidJobTemplateWarning">
|
||||||
|
<span class="fa fa-warning"></span>
|
||||||
|
<span>{{:: strings.get('workflows.CREDENTIAL_WITH_PASS') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group Form-formGroup Form-formGroup--singleColumn" ng-show="selectedTemplate && !selectedTemplateInvalid && !(credentialRequiresPassword && !promptData.launchConf.ask_credential_on_launch)">
|
||||||
<label for="verbosity" class="Form-inputLabelContainer">
|
<label for="verbosity" class="Form-inputLabelContainer">
|
||||||
<span class="Form-requiredAsterisk">*</span>
|
<span class="Form-requiredAsterisk">*</span>
|
||||||
<span class="Form-inputLabel">RUN</span>
|
<span class="Form-inputLabel">RUN</span>
|
||||||
@@ -121,7 +127,7 @@
|
|||||||
<button type="button" class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin" id="workflow_maker_prompt_btn" ng-show="showPromptButton" ng-click="openPromptModal()"> Prompt</button>
|
<button type="button" class="btn btn-sm Form-primaryButton Form-primaryButton--noMargin" id="workflow_maker_prompt_btn" ng-show="showPromptButton" ng-click="openPromptModal()"> Prompt</button>
|
||||||
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> Cancel</button>
|
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_cancel_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> Cancel</button>
|
||||||
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_close_btn" ng-show="!(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> Close</button>
|
<button type="button" class="btn btn-sm Form-cancelButton" id="workflow_maker_close_btn" ng-show="!(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)" ng-click="cancelNodeForm()"> Close</button>
|
||||||
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) && !selectedTemplateInvalid" ng-click="confirmNodeForm()" ng-disabled="!selectedTemplate || promptModalMissingReqFields"> Select</button>
|
<button type="button" class="btn btn-sm Form-saveButton" id="workflow_maker_select_btn" ng-show="(workflowJobTemplateObj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) && !selectedTemplateInvalid && !(credentialRequiresPassword && !promptData.launchConf.ask_credential_on_launch)" ng-click="confirmNodeForm()" ng-disabled="!selectedTemplate || promptModalMissingReqFields || credentialRequiresPassword"> Select</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user