Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
27 changes: 27 additions & 0 deletions LearningHub.Nhs.WebUI/Controllers/Api/ResourceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,33 @@ public async Task<ActionResult> RecordExternalReferenceUserAgreementAsync([FromB
}
}

/// <summary>
/// The RecordExternalReferenceUserAgreementAsync.
/// </summary>
/// <param name="model">model.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
[HttpPost]
[Route(" CreateResourceVersionValidationResult")]
public async Task<ActionResult> CreateResourceVersionValidationResultAsync([FromBody] ResourceVersionValidationResultViewModel model)
{
await this.resourceService.CreateResourceVersionValidationResultAsync(model);
return this.Ok();
}

/// <summary>
/// The RecordExternalReferenceUserAgreementAsync.
/// </summary>
/// <param name="filename">model.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
[HttpPost]
[Route(" CreateResourceVersionValidationResults")]
public async Task<ActionResult> CreateResourceVersionValidationResultAsync([FromBody] string filename)
{
ResourceVersionValidationResultViewModel model = new ResourceVersionValidationResultViewModel();
await this.resourceService.CreateResourceVersionValidationResultAsync(model);
return this.Ok();
}

/// <summary>
/// The GetHeaderById.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions LearningHub.Nhs.WebUI/Controllers/Api/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,27 @@ public async Task<ActionResult> CheckUserRole()
return this.Ok(isSystemAdmin);
}

/// <summary>
/// to check user password is correct.
/// </summary>
/// <param name="currentPassword">The currentPassword.</param>
/// <returns>The <see cref="Task{ActionResult}"/>.</returns>
[HttpGet]
[Route("ConfirmPassword/{currentPassword}")]
public async Task<ActionResult> ConfirmPassword(string currentPassword)
{
string passwordHash = this.userService.Base64MD5HashDigest(currentPassword);
var userPersonalDetails = await this.userService.GetCurrentUserPersonalDetailsAsync();
if (userPersonalDetails != null && userPersonalDetails.PasswordHash == passwordHash)
{
return this.Ok(true);
}
else
{
return this.Ok(false);
}
}

/// <summary>
/// The GetCurrentUserPersonalDetails.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions LearningHub.Nhs.WebUI/Interfaces/IResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,13 @@ public interface IResourceService
/// <returns>The <see cref="Task{LearningHubValidationResult}"/>.</returns>
Task<LearningHubValidationResult> CreateResourceVersionProviderAsync(ResourceVersionProviderViewModel model);

/// <summary>
/// Creates resource version validation results corresponding to the value in the corresponding input view model.
/// </summary>
/// <param name="validationResultViewModel">Details of the validation results.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
Task CreateResourceVersionValidationResultAsync(ResourceVersionValidationResultViewModel validationResultViewModel);

/// <summary>
/// Delete resource version provider.
/// </summary>
Expand Down
85 changes: 80 additions & 5 deletions LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute/Content.vue
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,50 @@
</transition>
</div>

<div v-if="passwordVerification">
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header text-center">
<h4 class="modal-title"><i class="warningTriangle fa-solid fa-triangle-exclamation"></i> Confirm your password</h4>
</div>
<div class="modal-body">
<div class="model-body-container">
<p>
To continue with the upload please verify your identity by entering your password.
</p>
</div>
<div>
<div class="form-group password-div-width" v-bind:class="{ 'input-validation-error': $v.currentPassword.$error}">
<label for="confirmPassword" class="pt-10">Current password</label>
<div class="error-text" v-if="$v.currentPassword.$invalid && $v.currentPassword.$dirty">
<span class="text-danger">Enter a valid password.</span>
</div>
<input type="password" class="form-control" id="currentPassword" aria-describedby="currentPassword" autocomplete="off" maxlength="1000"
v-model.trim="currentPassword"
placeholder="Current password"
@blur="$v.currentPassword.$touch()"
v-bind:class="{ 'input-validation-error': $v.currentPassword.$error}">
</div>

<div class="row" v-if="showError">
<div class="form-group col-12 text-danger" v-html="errorMessage" />
</div>
</div>
</div>
<div class="modal-footer modal-footer--buttons">
<button type="button" class="nhsuk-button nhsuk-button--secondary" @click="passwordVerification=false">Cancel</button>
<button type="button" class="nhsuk-button" @click="submitPassword">Continue</button>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>

</div>

<div v-if="!resourceLoading" class="limit-width px-xl-0 mx-xl-0 pb-5">
Expand Down Expand Up @@ -427,6 +471,7 @@
import GenericFileUploader from './GenericFileUploader.vue';
import { UploadResourceType, ResourceType, VersionStatus } from '../constants';
import { resourceData } from '../data/resource';
import { userData } from '../data/user';
import { FileTypeModel } from "../models/contribute/fileTypeModel";
import { FileUploadResult } from "../models/contribute/FileUploadResult";
import { FlagModel } from '../models/flagModel';
Expand Down Expand Up @@ -490,6 +535,8 @@
displayType: '' as string,
commonContentKey: 0,
avUnavailableMessage: false,
passwordVerification: false,
currentPassword: '',
// Some of the Content components have local state
// which isn't in the vuex store.
// This means those fields are validated using an
Expand Down Expand Up @@ -573,7 +620,7 @@
closeAfterSave(): boolean {
return this.$store.state.closeAfterSave;
},
isFileAlreadyUploaded(): boolean {
isFileAlreadyUploaded(): boolean {
switch (this.selectedResourceType) {
case this.resourceType.GENERICFILE:
return this.$store.state.genericFileDetail.file.fileName !== '';
Expand Down Expand Up @@ -739,6 +786,12 @@
this.processPublish();
}
},
submitPassword() {
this.$v.currentPassword.$touch();
if (!this.$v.currentPassword.$invalid) {
this.validatePassword();
}
},
async processPublish() {
this.showError = false;
let publishSuccess = await resourceData.publishResource(this.resourceVersionId, this.publishNotes);
Expand All @@ -758,6 +811,18 @@
this.errorMessage = "An error occurred whilst trying to publish the resource.";
}
},
async validatePassword() {
this.showError = false;
let isValidUser = await userData.IsValidUser(this.currentPassword);
if (isValidUser) {
this.acceptUploadedFile();
this.passwordVerification = false;
}
else {
this.showError = true;
this.errorMessage = "Enter a valid password.";
}
},
deleteResource() {
this.deleteWarning = true;
this.showError = false;
Expand Down Expand Up @@ -822,6 +887,10 @@
this.fileUploadRef.value = null;
(this.$refs.fileUploader as any).uploadResourceFile(this.file);
},
confirmPassword() {
this.currentPassword = '';
this.passwordVerification = true;
},
async fileUploadComplete(uploadResult: FileUploadResult) {
if (!uploadResult.invalid) {
if (uploadResult.resourceType != ResourceType.SCORM) {
Expand Down Expand Up @@ -900,9 +969,9 @@
this.fileErrorType = FileErrorTypeEnum.InvalidScormType;
return;
}
this.acceptUploadedFile();
this.confirmPassword();
} else {
this.acceptUploadedFile();
this.confirmPassword();
}
}
}
Expand Down Expand Up @@ -948,10 +1017,10 @@
this.fileTypeChangeWarning = true;
}
} else {
this.acceptUploadedFile();
this.confirmPassword();
}
} else {
this.acceptUploadedFile();
this.confirmPassword();
}
}
}
Expand Down Expand Up @@ -1062,6 +1131,9 @@
},
publishNotes: {
required
},
currentPassword: {
required
}
},
watch: {
Expand Down Expand Up @@ -1105,4 +1177,7 @@
max-height: 90vh;
overflow-y: auto;
}
.password-div-width{
max-width:70% !important;
}
</style>
15 changes: 14 additions & 1 deletion LearningHub.Nhs.WebUI/Scripts/vuesrc/data/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ const IsSystemAdmin = async function (): Promise<boolean[]> {
});
};

const IsValidUser = async function (currentPassword: string): Promise<boolean[]> {
var IsValidUser = `/api/User/ConfirmPassword/${currentPassword}`;
return await AxiosWrapper.axios.get<boolean[]>(IsValidUser)
.then(response => {
return response.data;
})
.catch(e => {
console.log('IsValidUser:' + e);
throw e;
});
};

const getCurrentUserBasicDetails = async function (): Promise<UserBasicModel> {
return await AxiosWrapper.axios.get<UserBasicModel>('/api/User/GetCurrentUserBasicDetails')
.then(response => {
Expand Down Expand Up @@ -173,5 +185,6 @@ export const userData = {
keepUserSessionAlive,
getkeepUserSessionAliveInterval,
isGeneralUser,
IsSystemAdmin
IsSystemAdmin,
IsValidUser
}
2 changes: 1 addition & 1 deletion LearningHub.Nhs.WebUI/Scripts/vuesrc/helpers/fileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const MAX_FILE_SIZE = 10 * 1000 * 1000 * 1000; // 10GB

// This list should correspond to the disallowed extensions contained in the FileType table
const BLOCKED_FILE_EXTENSIONS = ['.app', '.asp', '.aspx', '.dll', '.dmg', '.exe', '.flv', '.f4v', '.js', '.jsp',
'.php', '.shtm', '.shtml', '.swf','.webm'];
'.php', '.shtm', '.shtml', '.swf', '.webm', '.bat', '.cmd', '.vbs', '.msi', '.pif', '.sh', '.tar', '.gz', '.7z', '.rar', '.sys', '.bak', '.iso', '.torrent'];

const IMAGE_FILE_EXTENSIONS = ['.apng', '.avif', '.bmp', '.cur', '.gif', '.ico', '.jfif', '.jpeg', '.jpg', '.pjp',
'.pjpeg', '.png', '.psd', '.svg', '.tif', '.tiff', '.webp'];
Expand Down
29 changes: 28 additions & 1 deletion LearningHub.Nhs.WebUI/Services/ContributeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
using LearningHub.Nhs.WebUI.Models.Contribute;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic;
using MK.IO.Models;
using Newtonsoft.Json;

/// <summary>
Expand All @@ -38,7 +40,7 @@ public class ContributeService : BaseService<ContributeService>, IContributeServ
/// <param name="mediaService">MKIO media service.</param>
/// <param name="learningHubHttpClient">Learning hub http client.</param>
/// <param name="logger">Logger.</param>
public ContributeService(IFileService fileService, IResourceService resourceService, IAzureMediaService azureMediaService, ILearningHubHttpClient learningHubHttpClient, ILogger<ContributeService> logger, IAzureMediaService mediaService)
public ContributeService(IFileService fileService, IResourceService resourceService, IAzureMediaService azureMediaService, ILearningHubHttpClient learningHubHttpClient, ILogger<ContributeService> logger, IAzureMediaService mediaService)
: base(learningHubHttpClient, logger)
{
this.fileService = fileService;
Expand Down Expand Up @@ -502,6 +504,31 @@ public async Task<FileUploadResult> ProcessResourceFileAsync(int resourceVersion

if ((fileType == null) || (fileType != null && fileType.NotAllowed) || file.Length <= 0)
{
// Define dangerous file extensions
string[] dangerousExtensions = { ".exe", ".dll", ".bat", ".js", ".vbs", ".sh", ".ps1" };
if (dangerousExtensions.Any(ext => file.FileName.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
{
var error = $"A potentially harmful file has been detected and blocked: {file.FileName}.";
var validationDetail = new ResourceVersionValidationResultViewModel
{
ResourceVersionId = resourceVersionId,
Success = false,
Details = string.Empty,
AmendUserId = currentUserId,
ResourceVersionValidationRuleResultViewModels = new[]
{
new ResourceVersionValidationRuleResultViewModel
{
ResourceTypeValidationRuleEnum = ResourceTypeValidationRuleEnum.HtmlResource_RootIndexPresent,
Success = false,
Details = error,
},
}.ToList(),
};

await this.resourceService.CreateResourceVersionValidationResultAsync(validationDetail);
}

return new FileUploadResult()
{
FileName = file.FileName,
Expand Down
32 changes: 32 additions & 0 deletions LearningHub.Nhs.WebUI/Services/ResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,38 @@ public async Task<LearningHubValidationResult> CreateResourceVersionProviderAsyn
return apiResponse.ValidationResult;
}

/// <summary>
/// Creates resource version validation results corresponding to the value in the corresponding input view model.
/// </summary>
/// <param name="validationResultViewModel">Details of the validation results.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
public async Task CreateResourceVersionValidationResultAsync(ResourceVersionValidationResultViewModel validationResultViewModel)
{
ApiResponse apiResponse = null;
var json = JsonConvert.SerializeObject(validationResultViewModel);
var stringContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json");

var client = await this.LearningHubHttpClient.GetClientAsync();

var request = $"Resource/CreateResourceVersionValidationResult";
var response = await client.PostAsync(request, stringContent).ConfigureAwait(false);

if (response.IsSuccessStatusCode)
{
var result = response.Content.ReadAsStringAsync().Result;
apiResponse = JsonConvert.DeserializeObject<ApiResponse>(result);

if (!apiResponse.Success)
{
throw new Exception("save failed!");
}
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
throw new Exception("AccessDenied");
}
}

/// <summary>
/// The delete resource version provider.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@
<None Include="Scripts\Post-Deploy\Scripts\UpdateFileTypes.sql" />
<None Include="Scripts\Post-Deploy\Scripts\AddDigitalLearningSolutionsExternalSystem.sql" />
<Build Include="Stored Procedures\Activity\GetAssessmentActivityCompletionPercentage.sql" />
<None Include="Scripts\LH Database Scripts\CreateResourceTypeValidationRule.sql" />
</ItemGroup>
<ItemGroup>
<None Include="Scripts\Pre-Deploy\Scripts\Card5766_AuthorTableChanges.PreDeployment.sql" />
Expand Down
Loading
Loading