diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-create/project-form-create.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-create/project-form-create.component.html index b3273d65..c3183d84 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-create/project-form-create.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-create/project-form-create.component.html @@ -8,7 +8,7 @@
- +
@@ -47,4 +47,3 @@ - diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-create/project-form-create.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-create/project-form-create.component.ts index 52471dab..0b02cd59 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-create/project-form-create.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-create/project-form-create/project-form-create.component.ts @@ -46,7 +46,7 @@ export class ProjectFormCreateComponent implements OnInit { // Initialize Form Controls this.name = new FormControl('', [Validators.required, Validators.minLength(5), Validators.maxLength(20), this.noSpaceSpecial()]) this.description = new FormControl('') - this.visibility = new FormControl('') + this.visibility = new FormControl('public', [Validators.required]) // Initialize FormGroup using FormBuilder this.projectForm = this.formBuilder.group({ diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-form-edit/project-form-edit.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-form-edit/project-form-edit.component.html index 5b5b6e04..2044d197 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-form-edit/project-form-edit.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-edit/project-form-edit/project-form-edit.component.html @@ -9,7 +9,7 @@

Edit Project

- +
@@ -47,4 +47,3 @@

Edit Project

- diff --git a/src/Analysim.Web/Controllers/AccountController.cs b/src/Analysim.Web/Controllers/AccountController.cs index ec063331..b0146a34 100644 --- a/src/Analysim.Web/Controllers/AccountController.cs +++ b/src/Analysim.Web/Controllers/AccountController.cs @@ -1365,7 +1365,7 @@ public IActionResult GetProjects([FromRoute] int userID) //var user = _dbContext.Users.SingleOrDefault(x => x.Id == userID); //if (user == null) return NotFound(new { message = "User Not Found" }); - var userProjects = _dbContext.ProjectUsers + var userProjectsQuery = _dbContext.ProjectUsers .Include(pu => pu.Project) .ThenInclude(p => p.BlobFiles) .Include(pu => pu.Project) @@ -1373,8 +1373,28 @@ public IActionResult GetProjects([FromRoute] int userID) .Include(pu => pu.Project) .ThenInclude(p => p.ProjectTags) .ThenInclude(pt => pt.Tag) - .Where(pu => pu.UserID == userID) - .AsEnumerable(); + .Where(pu => pu.UserID == userID); + + if (!IsCurrentUserAdmin()) + { + var currentUserIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (int.TryParse(currentUserIdClaim, out var currentUserId)) + { + userProjectsQuery = userProjectsQuery.Where(pu => + (pu.Project.Visibility != null && pu.Project.Visibility.ToLower() == "public") || + pu.Project.ProjectUsers.Any(projectUser => + projectUser.UserID == currentUserId && + projectUser.UserRole != null && + projectUser.UserRole.ToLower() != "follower")); + } + else + { + userProjectsQuery = userProjectsQuery.Where(pu => + pu.Project.Visibility != null && pu.Project.Visibility.ToLower() == "public"); + } + } + + var userProjects = userProjectsQuery.ToList(); //if (userProjects.Count() == 0) return NoContent(); diff --git a/src/Analysim.Web/Controllers/ProjectController.cs b/src/Analysim.Web/Controllers/ProjectController.cs index 010e350c..0faf6ec3 100644 --- a/src/Analysim.Web/Controllers/ProjectController.cs +++ b/src/Analysim.Web/Controllers/ProjectController.cs @@ -42,6 +42,8 @@ public class ProjectController : ControllerBase private readonly ApplicationDbContext _dbContext; private readonly IConfiguration _configuration; private readonly IMailNetService _mailNetService; + private const string PublicVisibility = "public"; + private const string FollowerRole = "follower"; public ProjectController( ApplicationDbContext dbContext, @@ -54,6 +56,70 @@ IMailNetService mailNetService _mailNetService = mailNetService; } + private bool TryGetCurrentUserId(out int userId) + { + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + return int.TryParse(userIdClaim, out userId); + } + + private bool IsCurrentUserAdmin() + { + var currentUsername = User.Identity?.Name ?? User.FindFirstValue(ClaimTypes.Name); + if (string.IsNullOrWhiteSpace(currentUsername)) return false; + + var admins = _configuration.GetSection("AdminUsers").Get>() ?? new List(); + return admins.Any(u => string.Equals(u, currentUsername, StringComparison.OrdinalIgnoreCase)); + } + + private static bool IsPublicProject(Project project) + { + return string.Equals(project.Visibility, PublicVisibility, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsCollaborator(ProjectUser projectUser) + { + return !string.Equals(projectUser.UserRole, FollowerRole, StringComparison.OrdinalIgnoreCase); + } + + private bool CanCurrentUserViewProject(Project project) + { + if (IsPublicProject(project)) return true; + if (IsCurrentUserAdmin()) return true; + if (!TryGetCurrentUserId(out var userId)) return false; + + return project.ProjectUsers.Any(pu => pu.UserID == userId && IsCollaborator(pu)); + } + + private IQueryable ApplyVisibleProjectScope(IQueryable projects) + { + if (IsCurrentUserAdmin()) return projects; + + if (TryGetCurrentUserId(out var userId)) + { + return projects.Where(p => + (p.Visibility != null && p.Visibility.ToLower() == PublicVisibility) || + p.ProjectUsers.Any(pu => pu.UserID == userId && pu.UserRole != null && pu.UserRole.ToLower() != FollowerRole)); + } + + return projects.Where(p => p.Visibility != null && p.Visibility.ToLower() == PublicVisibility); + } + + private async Task CanCurrentUserViewProjectAsync(int projectId) + { + return await ApplyVisibleProjectScope(_dbContext.Projects) + .AnyAsync(p => p.ProjectID == projectId); + } + + private async Task CanCurrentUserViewProjectResourceAsync(int? projectId) + { + return !projectId.HasValue || await CanCurrentUserViewProjectAsync(projectId.Value); + } + + private NotFoundObjectResult ProjectNotFound() + { + return NotFound(new { message = "Project Not Found" }); + } + // Image Helpers private static string BuildImageDataUrl(byte[] imageBytes, string extension) { @@ -90,17 +156,17 @@ private static string GetImageContentType(string extension) * Description: Get Project */ [HttpGet("[action]/{projectID}")] - public IActionResult GetProjectByID([FromRoute] int projectID) + public async Task GetProjectByID([FromRoute] int projectID) { // Find Project // Include To Many List - var project = _dbContext.Projects + var project = await _dbContext.Projects .Include(p => p.BlobFiles) .Include(p => p.ProjectUsers) .Include(p => p.Notebooks) .Include(p => p.ProjectTags).ThenInclude(pt => pt.Tag) - .SingleOrDefault(p => p.ProjectID == projectID); - if (project == null) return NotFound(new { message = "Project Not Found" }); + .SingleOrDefaultAsync(p => p.ProjectID == projectID); + if (project == null || !CanCurrentUserViewProject(project)) return ProjectNotFound(); // Return Ok Request return Ok(new @@ -117,16 +183,16 @@ public IActionResult GetProjectByID([FromRoute] int projectID) * Description: Get Project */ [HttpGet("[action]/{owner}/{projectname}")] - public IActionResult GetProjectByRoute([FromRoute] string owner, [FromRoute] string projectname) + public async Task GetProjectByRoute([FromRoute] string owner, [FromRoute] string projectname) { // Find Project - var project = _dbContext.Projects + var project = await _dbContext.Projects .Include(p => p.BlobFiles) .Include(p => p.Notebooks) .Include(p => p.ProjectUsers).ThenInclude(pu => pu.User) .Include(p => p.ProjectTags).ThenInclude(pt => pt.Tag) - .SingleOrDefault(p => p.Route.ToLower() == owner.ToLower() + "/" + projectname.ToLower()); - if (project == null) return NotFound(new { message = "Project Not Found" }); + .SingleOrDefaultAsync(p => p.Route.ToLower() == owner.ToLower() + "/" + projectname.ToLower()); + if (project == null || !CanCurrentUserViewProject(project)) return ProjectNotFound(); // Return Ok Request return Ok(new @@ -142,15 +208,15 @@ public IActionResult GetProjectByRoute([FromRoute] string owner, [FromRoute] str * Description: Get Project Range */ [HttpGet("[action]")] - public IActionResult GetProjectRange([FromQuery(Name = "id")] List idList) + public async Task GetProjectRange([FromQuery(Name = "id")] List idList) { // Find Project - var projects = _dbContext.Projects + var projects = await ApplyVisibleProjectScope(_dbContext.Projects) .Include(p => p.BlobFiles) .Include(p => p.ProjectUsers) .Include(p => p.ProjectTags).ThenInclude(pt => pt.Tag) .Where(p => idList.Contains(p.ProjectID)) - .ToList(); + .ToListAsync(); // Return Ok Request return Ok(new @@ -166,15 +232,15 @@ public IActionResult GetProjectRange([FromQuery(Name = "id")] List idList) * Description: Get Project List */ [HttpGet("[action]")] - public IActionResult GetProjectList() + public async Task GetProjectList() { // Get All Project And Include To Many List - var projects = _dbContext.Projects + var projects = await ApplyVisibleProjectScope(_dbContext.Projects) .Include(p => p.BlobFiles) .Include(p => p.ProjectUsers) .Include(p => p.ProjectTags).ThenInclude(pt => pt.Tag) - .ToList(); + .ToListAsync(); // Return Ok Request return Ok(new @@ -190,18 +256,21 @@ public IActionResult GetProjectList() * Description: Filter Project Using Search Term */ [HttpGet("[action]")] - public IActionResult Search([FromQuery(Name = "term")] List searchTerms) + public async Task Search([FromQuery(Name = "term")] List searchTerms) { - var matchedTag = _dbContext.Tag - .ToList() + var tags = await _dbContext.Tag.ToListAsync(); + var matchedTag = tags .Where(t => searchTerms.Any(st => t.Name.ToLower().Contains(st.ToLower()))); - var matchedProject = _dbContext.Projects + var matchedProject = await ApplyVisibleProjectScope(_dbContext.Projects) .Include(p => p.BlobFiles) .Include(p => p.ProjectUsers) .Include(p => p.ProjectTags).ThenInclude(pt => pt.Tag) - .ToList() - .Where(p => matchedTag.Any(mt => p.ProjectTags.Any(pt => pt.Tag.Name.ToLower() == mt.Name.ToLower()))); + .ToListAsync(); + + matchedProject = matchedProject + .Where(p => matchedTag.Any(mt => p.ProjectTags.Any(pt => pt.Tag.Name.ToLower() == mt.Name.ToLower()))) + .ToList(); if (matchedProject.Count() == 0) return NoContent(); return Ok(new @@ -226,6 +295,7 @@ public async Task DownloadFile([FromRoute] int fileID) // Find blobfile var blobFile = await _dbContext.BlobFiles.FindAsync(fileID); if (blobFile == null) return NotFound(); + if (!await CanCurrentUserViewProjectResourceAsync(blobFile.ProjectID)) return NotFound(); //find blobfile content var blobFileContent = await _dbContext.BlobFileContent.FindAsync(fileID); @@ -260,6 +330,7 @@ public async Task Download([FromRoute] string username, [FromRout .FirstOrDefaultAsync(b => b.User.UserName == username && b.Container == projectname && b.Directory + b.Name + b.Extension == filepath); if (blobFile == null) return NotFound(); + if (!await CanCurrentUserViewProjectResourceAsync(blobFile.ProjectID)) return NotFound(); //find blobfile content var blobFileContent = await _dbContext.BlobFileContent.FindAsync(blobFile.BlobFileID); @@ -290,6 +361,7 @@ public async Task DownloadNotebook([FromRoute] int notebookID, [F var notebook = await _dbContext.Notebook.FindAsync(notebookID); if (notebook == null) return NotFound(); + if (!await CanCurrentUserViewProjectResourceAsync(notebook.ProjectID)) return NotFound(); //BlobDownloadInfo data = await _blobService.GetNotebookAsync(notebook); @@ -310,6 +382,8 @@ public async Task GetNotebook([FromRoute] int notebookID) { var notebook = await _dbContext.Notebook.Include(notebook => notebook. observableNotebookDatasets).FirstOrDefaultAsync(notebook => notebook.NotebookID == notebookID); + if (notebook == null) return NotFound(); + if (!await CanCurrentUserViewProjectResourceAsync(notebook.ProjectID)) return NotFound(); return Ok(new { message = "Notebook Retrieved", @@ -326,8 +400,12 @@ public async Task GetNotebook([FromRoute] int notebookID) [HttpGet("[action]")] public IActionResult GetAllNotebooks() { + var visibleProjectIds = ApplyVisibleProjectScope(_dbContext.Projects) + .Select(p => p.ProjectID); + var notebooks = _dbContext.Notebook .Include(n => n.observableNotebookDatasets) + .Where(n => !n.ProjectID.HasValue || visibleProjectIds.Contains(n.ProjectID.Value)) .ToList(); return Ok(new @@ -348,6 +426,10 @@ public async Task GetNotebookVersions([FromRoute] int notebookID) .Select(n => n.Version) .ToListAsync(); + var notebook = await _dbContext.Notebook.FindAsync(notebookID); + if (notebook == null) return NotFound(); + if (!await CanCurrentUserViewProjectResourceAsync(notebook.ProjectID)) return NotFound(); + return Ok(new { message = "versions for the notebook retrieved", @@ -371,6 +453,7 @@ public async Task GetProjectComments([FromRoute] int projectId) { if (projectId <= 0) return BadRequest("Invalid project id."); + if (!await CanCurrentUserViewProjectAsync(projectId)) return ProjectNotFound(); var flatComments = await _dbContext.ProjectComments .AsNoTracking() @@ -442,6 +525,13 @@ public async Task GetProjectLogComments([FromRoute] int projectLo if (projectLogId <= 0) return BadRequest("Invalid project log id."); + var projectId = await _dbContext.ProjectLogs + .Where(l => l.LogID == projectLogId) + .Select(l => l.ProjectID) + .SingleOrDefaultAsync(); + if (projectId == 0) return NotFound(new { message = "Project Log Not Found" }); + if (!await CanCurrentUserViewProjectAsync(projectId)) return ProjectNotFound(); + var flatComments = await _dbContext.ProjectComments .AsNoTracking() .Where(c => c.ProjectLogID == projectLogId) @@ -583,6 +673,7 @@ public async Task GetProjectLogs([FromRoute] int projectId) { if (projectId <= 0) return BadRequest("Invalid project id."); + if (!await CanCurrentUserViewProjectAsync(projectId)) return ProjectNotFound(); var now = DateTime.UtcNow; @@ -732,6 +823,7 @@ public async Task GetPublications([FromRoute] int projectId) { if (projectId <= 0) return BadRequest("Invalid project id."); + if (!await CanCurrentUserViewProjectAsync(projectId)) return ProjectNotFound(); var publications = await _dbContext.Publications .AsNoTracking() @@ -771,6 +863,7 @@ public async Task GetProjectNotebookReferences([FromRoute] int pr { if (projectId <= 0) return BadRequest("Invalid project id."); + if (!await CanCurrentUserViewProjectAsync(projectId)) return ProjectNotFound(); var notebooks = await _dbContext.Notebook .AsNoTracking() @@ -1438,6 +1531,7 @@ public async Task ForkProject([FromForm] ProjectForkVM formdata) // Find Project var project = await _dbContext.Projects.FindAsync(formdata.ProjectID); if (project == null) return NotFound(new { message = "Project Not Found" }); + if (!await CanCurrentUserViewProjectAsync(project.ProjectID)) return ProjectNotFound(); // Check if the project already exists bool projectExists = await _dbContext.Projects @@ -1642,6 +1736,7 @@ public async Task ForkProjectWithoutBlob([FromForm] ProjectForkVM // Find Project var project = await _dbContext.Projects.FindAsync(formdata.ProjectID); if (project == null) return NotFound(new { message = "Project Not Found" }); + if (!await CanCurrentUserViewProjectAsync(project.ProjectID)) return ProjectNotFound(); // Check if the project already exists bool projectExists = await _dbContext.Projects @@ -2030,6 +2125,7 @@ public async Task FollowProject([FromRoute] int projectID) var project = await _dbContext.Projects.FindAsync(projectID); if (project == null) return NotFound(new { message = "Project Not Found" }); + if (!await CanCurrentUserViewProjectAsync(project.ProjectID)) return ProjectNotFound(); var projectUser = await _dbContext.ProjectUsers.FindAsync(user.Id, projectID); @@ -4361,8 +4457,10 @@ public async Task DeleteNotebook([FromRoute] int notebookID, [Fro * Description: Get User List Of Project */ [HttpGet("[action]/{projectID}")] - public IActionResult GetUserList([FromRoute] int projectID) + public async Task GetUserList([FromRoute] int projectID) { + if (!await CanCurrentUserViewProjectAsync(projectID)) return ProjectNotFound(); + // Find Project var users = _dbContext.ProjectUsers .Include(pu => pu.User).ThenInclude(u => u.BlobFiles) @@ -4387,8 +4485,10 @@ public IActionResult GetUserList([FromRoute] int projectID) * Description: Get list of file from project */ [HttpGet("[action]/{projectID}")] - public IActionResult GetFileList([FromRoute] int projectID) + public async Task GetFileList([FromRoute] int projectID) { + if (!await CanCurrentUserViewProjectAsync(projectID)) return ProjectNotFound(); + var files = _dbContext.BlobFiles .ToList() .Where(p => p.ProjectID == projectID); @@ -4404,8 +4504,12 @@ public IActionResult GetFileList([FromRoute] int projectID) [HttpGet("[action]")] public IActionResult GetAllDatasets() { + var visibleProjectIds = ApplyVisibleProjectScope(_dbContext.Projects) + .Select(p => p.ProjectID); + var files = _dbContext.BlobFiles .Where(b => b.Extension == ".csv") + .Where(b => !b.ProjectID.HasValue || visibleProjectIds.Contains(b.ProjectID.Value)) .ToList(); // Return Ok Status @@ -4417,8 +4521,10 @@ public IActionResult GetAllDatasets() } [HttpGet("[action]/{projectID}/{directory}")] - public IActionResult GetNotebooks([FromRoute] int projectID, [FromRoute] string directory) + public async Task GetNotebooks([FromRoute] int projectID, [FromRoute] string directory) { + if (!await CanCurrentUserViewProjectAsync(projectID)) return ProjectNotFound(); + string decodedDirectory = HttpUtility.UrlDecode(directory); var notebooks = _dbContext.Notebook .Include(n => n.observableNotebookDatasets) @@ -4443,14 +4549,16 @@ public IActionResult GetNotebooks([FromRoute] int projectID, [FromRoute] string * Description: Get list of tag from project */ [HttpGet("[action]/{projectID}")] - public IActionResult GetTagList([FromRoute] int projectID) + public async Task GetTagList([FromRoute] int projectID) { + if (!await CanCurrentUserViewProjectAsync(projectID)) return ProjectNotFound(); + // Find Project var project = _dbContext.Projects.Where(p => p.ProjectID == projectID); if (!project.Any()) return NotFound(new { message = "Project Not Found" }); var query = project - .SelectMany(p => _dbContext.ProjectTags) + .SelectMany(p => p.ProjectTags) .Select(pt => pt.Tag); // Return Ok Status