using Coscine.Action; using Coscine.Action.EventArgs; using Coscine.ApiCommons; using Coscine.Configuration; using Coscine.Database.DataModel; using Coscine.Database.Models; using Coscine.Database.ReturnObjects; using Coscine.Database.Util; using Coscine.Logging; using Coscine.ResourceTypes; using Coscine.ResourceTypes.ResourceTypeConfigs; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Coscine.Api.Resources.Controllers { /// <summary> /// This controller represents the actions which can be taken with a resource object. /// </summary> [Authorize] public class ResourceController : Controller { private readonly Authenticator _authenticator; private readonly ResourceModel _resourceModel; private readonly IConfiguration _configuration; private readonly Emitter _emitter; private readonly ProjectRoleModel _projectRoleModel; private readonly ProjectResourceModel _projectResourceModel; private readonly CoscineLogger _coscineLogger; private readonly ResourceTypeModel _resourceTypeModel; private readonly VisibilityModel _visibilityModel; private readonly LicenseModel _licenseModel; /// <summary> /// ResourceController constructor specifying a ResourceModel. /// </summary> public ResourceController(ILogger<ResourceController> logger) { _authenticator = new Authenticator(this, Program.Configuration); _configuration = Program.Configuration; _resourceModel = new ResourceModel(); _emitter = new Emitter(_configuration); _projectRoleModel = new ProjectRoleModel(); _projectResourceModel = new ProjectResourceModel(); _coscineLogger = new CoscineLogger(logger); _resourceTypeModel = new ResourceTypeModel(); _visibilityModel = new VisibilityModel(); _licenseModel = new LicenseModel(); } /// <summary> /// Returns a list of all resources the current user has access to. /// </summary> /// <returns>List of Resources</returns> [Route("[controller]")] public IActionResult Index() { var user = _authenticator.GetUser(); return Json(_resourceModel.GetAllWhere((resource) => (from projectResource in resource.ProjectResources where (from projectRole in projectResource.Project.ProjectRoles where projectRole.User == user && (projectRole.Role.DisplayName == "Owner" || projectRole.Role.DisplayName == "Member") && !projectRole.Project.Deleted select projectRole).Any() select projectResource).Any() ).Select((resource) => Helpers.CreateResourceReturnObject(resource))); } /// <summary> /// Returns the resource with a specified id. /// </summary> /// <param name="id">The resource id.</param> /// <returns>ResourceObject if OK, 401 if not authorized</returns> [HttpGet("[controller]/{id}")] public IActionResult Get(Guid id) { var resource = _resourceModel.GetById(id); var user = _authenticator.GetUser(); // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md) // - Resource: View Resource (RCV, Metadatamanager) if (_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member, UserRoles.Guest)) { _resourceModel.SetType(resource); var returnObject = Helpers.CreateResourceReturnObject(resource); if (_resourceModel.HasAccess(user, resource, UserRoles.Guest)) { returnObject.ResourceTypeOption = null; } return Json(returnObject); } else { return Unauthorized("User does not own this resource!"); } } /// <summary> /// Returns whether or not the current user is creator of a specified resource. /// </summary> /// <param name="id">The resource id.</param> /// <returns>JSON object.</returns> [HttpGet("[controller]/{id}/isCreator")] public IActionResult IsUserResourceCreator(Guid id) { var resource = _resourceModel.GetById(id); var user = _authenticator.GetUser(); return Json(new JObject { ["isResourceCreator"] = resource.Creator.Equals(user.Id) }); } /// <summary> /// Updates a resource. /// </summary> /// <param name="id">The resource id.</param> /// <param name="resourceObject">Entry representing the user</param> /// <returns>JSON object.</returns> [HttpPost("[controller]/{id}")] public IActionResult Update(Guid id, [FromBody] ResourceObject resourceObject) { var resource = _resourceModel.GetById(id); var resourceTypeModel = new ResourceTypeModel(); var resourceType = resourceTypeModel.GetById(resource.TypeId); if (resource.Archived == "0") { // TODO: Consider making all those checks automatically as annotations over the Object<ResourceObject> if (!(string.IsNullOrWhiteSpace(resourceObject.DisplayName) || string.IsNullOrWhiteSpace(resourceObject.ResourceName) || string.IsNullOrWhiteSpace(resourceObject.Description) || resourceObject.Disciplines?.Any() != true || resourceObject.Visibility == null)) { var user = _authenticator.GetUser(); // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md) // - Resource: Change Resource Settings if (_resourceModel.HasAccess(user, resource, UserRoles.Owner) || (_resourceModel.HasAccess(user, resource, UserRoles.Member) && resource.Creator.Equals(user.Id))) { LogAnalyticsEditResource(resource, _resourceModel.GetMetadataCompleteness(resourceObject), resourceObject.Disciplines, user); var result = SetResourceTypeOptions(resource, resourceType, resourceObject); if (result != null) { return result; } var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource); if (resourceTypeDefinition.GetResourceTypeInformation().Result.CanUpdateResource) { resourceTypeDefinition.UpdateResource(id.ToString()); } return Json(_resourceModel.UpdateByObject(resource, resourceObject)); } else { return Unauthorized("The user is not authorized to perform an update on the selected resource!"); } } else { return BadRequest("Mandatory information of the resource is missing!"); } } else { return BadRequest("The resource is archived and cannot be modified!"); } } /// <summary> /// Sets a read only status of a given resource. /// </summary> /// <param name="id">A GUID as a string that identifies the resource.</param> /// <param name="status">A boolean value that specifies if the resource is archived.</param> /// <returns>JSON object.</returns> [HttpPost("[controller]/{id}/setReadonly")] public IActionResult SetResourceReadonly(Guid id, bool status) { var resource = _resourceModel.GetById(id); var user = _authenticator.GetUser(); // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md) // - Resource: Change Resource Settings if (_resourceModel.HasAccess(user, resource, UserRoles.Owner) || (_resourceModel.HasAccess(user, resource, UserRoles.Member) && resource.Creator.Equals(user.Id))) { var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource); if (resourceTypeDefinition.GetResourceTypeInformation().Result.CanSetResourceReadonly) { resourceTypeDefinition.SetResourceReadonly(id.ToString(), status); } // update archived status of the resource resource.Archived = status ? "1" : "0"; _resourceModel.Update(resource); var returnObject = Helpers.CreateResourceReturnObject(resource); if (Request.Query != null && Request.Query["noanalyticslog"] != "true") { if (status) { LogAnalyticsArchiveResource(resource, user); } else { LogAnalyticsUnarchiveResource(resource, user); } } return Json(returnObject); } else { return Unauthorized("The user is not authorized to perform an update on the selected resource!"); } } /// <summary> /// Deletes a resource. /// </summary> /// <param name="id">A GUID as a string that identifies the resource.</param> /// <returns>Deleted ResourceObject if OK, 401 if not</returns> [HttpDelete("[controller]/{id}")] public IActionResult Delete(Guid id) { var resource = _resourceModel.GetById(id); var user = _authenticator.GetUser(); // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md) // - Resource: Change Resource Settings if (_resourceModel.HasAccess(user, resource, UserRoles.Owner) || (_resourceModel.HasAccess(user, resource, UserRoles.Member) && resource.Creator.Equals(user.Id))) { var resourceObject = Helpers.CreateResourceReturnObject(resource); LogAnalyticsDeleteResource(resource, _resourceModel.GetMetadataCompleteness(resourceObject), resourceObject.Disciplines, user); _emitter.EmitResourceDelete(new ResourceEventArgs(_configuration) { Resource = resource }); _resourceModel.DeleteResource(resource); return Json(resourceObject); } else { return Unauthorized("The user is not authorized to perform an update on the selected resource!"); } } /// <summary> /// Stores the provided resource object in a specified project. /// </summary> /// <param name="projectId">A GUID as a string that identifies the resource.</param> /// <param name="resourceObject">Entry representing the user</param> /// <returns>JSON object.</returns> [HttpPost("[controller]/project/{projectId}")] public async Task<IActionResult> StoreToProject(Guid projectId, [FromBody] ResourceObject resourceObject) { var projectModel = new ProjectModel(); var resourceTypeModel = new ResourceTypeModel(); var resourceType = resourceTypeModel.GetById(resourceObject.Type.Id); var project = projectModel.GetById(projectId); var user = _authenticator.GetUser(); if (resourceType.Type == "gitlab") { var parseTosAccepted = bool.TryParse(resourceObject.ResourceTypeOption["TosAccepted"]?.ToString(), out var gitlabTosAccepted); if (!parseTosAccepted) { return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""TosAccepted""."); } if (!gitlabTosAccepted) { return BadRequest("Gitlab Terms of Service not accepted!"); } } if (string.IsNullOrWhiteSpace(user.EmailAddress)) { return Unauthorized("Access denied!"); } // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md) // - Resource: Create Resource if (projectModel.HasAccess(user, project, UserRoles.Owner, UserRoles.Member)) { var orgs = Util.OrganizationsHelper.GetOrganization(user); if (orgs?.Any() != true) { orgs = new List<string> { "*" }; } if (!Util.ResourceTypeHelper.IsResourceTypeUsable(resourceType, projectId, orgs)) { return Unauthorized("The user is not authorized to add a new resource of this type to the selected project!"); } var totalReservedQuota = Helpers.CalculateTotalReservedQuota(resourceType, projectId); var maximumQuota = Helpers.GetMaximumQuota(projectId, resourceType.Id); var success = long.TryParse(resourceObject.ResourceTypeOption["Size"]?.ToString(), out long desiredResourceQuota); var freeQuota = new QuotaDimObject { Value = Helpers.ConvertCapacityUnits(maximumQuota, QuotaUnit.GibiBYTE) - Helpers.ConvertCapacityUnits(totalReservedQuota, QuotaUnit.GibiBYTE), Unit = QuotaUnit.GibiBYTE }; if (success && desiredResourceQuota > freeQuota.Value) { return BadRequest($"You can not create a resource with a quota value of {desiredResourceQuota} GB. You have already reserved {Helpers.ConvertCapacityUnits(totalReservedQuota, QuotaUnit.GibiBYTE)} GB for {resourceObject.Type.DisplayName} resources and have only {freeQuota.Value} GB free."); } var resource = _resourceModel.StoreFromObject(resourceObject, user); var result = SetResourceTypeOptions(resource, resourceType, resourceObject); if (result != null) { return result; } var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource); await resourceTypeDefinition.CreateResource(resource.Id.ToString(), desiredResourceQuota); projectModel.AddResource(project, resource); _emitter.EmitResourceCreate(new ResourceEventArgs(_configuration) { Resource = resource }); var resourceReturnObject = Helpers.CreateResourceReturnObject(resource); LogAnalyticsAddResource(resource, _resourceModel.GetMetadataCompleteness(resourceReturnObject), resourceReturnObject.Disciplines, user); return Json(resourceReturnObject); } else { return Unauthorized("The user is not authorized to add a new resource to the selected project!"); } } private IActionResult SetResourceTypeOptions(Database.DataModel.Resource resource, Database.DataModel.ResourceType resourceType, ResourceObject resourceObject) { GetResourceTypeConfigOptions getConfig = null; if (resourceType.Type == "rdss3" || resourceType.Type == "rdss3worm") { getConfig = new GetRdsResourceTypeConfigOptions { Bucketname = resource.Id.ToString() }; } else if (resourceType.Type == "gitlab") { var accessToken = resourceObject.ResourceTypeOption["AccessToken"]?.ToString(); if (string.IsNullOrWhiteSpace(accessToken)) { return BadRequest(@"""ResourceTypeOption"" does not contain an valid ""AccessToken""."); } var parseResult = int.TryParse(resourceObject.ResourceTypeOption["ProjectId"]?.ToString(), out var gitlabProjectId); if (!parseResult) { return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""ProjectId""."); } var repoUrl = resourceObject.ResourceTypeOption["RepoUrl"]?.ToString(); if (string.IsNullOrWhiteSpace(repoUrl)) { return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""RepoUrl""."); } var branch = resourceObject.ResourceTypeOption["Branch"]?.ToString(); if (string.IsNullOrWhiteSpace(branch)) { return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""Branch""."); } var parseTosAccepted = bool.TryParse(resourceObject.ResourceTypeOption["TosAccepted"]?.ToString(), out var gitlabTosAccepted); if (!parseTosAccepted) { return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""TosAccepted""."); } getConfig = new GetGitLabResourceTypeConfigOptions { AccessToken = accessToken, ProjectId = gitlabProjectId, RepoUrl = repoUrl, Branch = branch, TosAccepted = gitlabTosAccepted }; } var config = ResourceTypeFactory.Instance.GetResourceTypeConfig(resourceType.Type, resourceType.SpecificType, getConfig); ResourceTypeFactory.Instance.SaveResourceTypeToDatabase(resourceType.Type, config, resource); return null; } private async Task LogAnalyticsEditResource(Database.DataModel.Resource resource, string metadataCompleteness, IEnumerable<DisciplineObject> disciplines, User user) { await Task.Run(() => { var resourceTypeDisplayName = _resourceTypeModel.GetById(resource.TypeId).DisplayName; var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value; var reserved = GetReservedResourceQuota(resource); var maximum = GetMaximumResourceQuota(resource, projectId); var analyticsLogObject = new AnalyticsLogObject { Operation = "Edit Resource", Type = "Action", RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(), ProjectId = projectId.ToString(), QuotaSize = new List<string> { $"{resourceTypeDisplayName}: {reserved}/{maximum}" }, ResourceId = resource.Id.ToString(), ApplicationsProfile = resource.ApplicationProfile, MetadataCompleteness = metadataCompleteness, License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null, Disciplines = disciplines.Select(x => x.DisplayNameEn).ToList(), Visibility = resource.VisibilityId.HasValue ? _visibilityModel.GetById(resource.VisibilityId.Value)?.DisplayName : null, }; _coscineLogger.AnalyticsLog(analyticsLogObject); }); } private async Task LogAnalyticsAddResource(Database.DataModel.Resource resource, string metadataCompleteness, IEnumerable<DisciplineObject> disciplines, User user) { await Task.Run(() => { var resourceTypeDisplayName = _resourceTypeModel.GetById(resource.TypeId).DisplayName; var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value; var reserved = GetReservedResourceQuota(resource); var maximum = GetMaximumResourceQuota(resource, projectId); var analyticsLogObject = new AnalyticsLogObject { Operation = "Add Resource", Type = "Action", RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(), ProjectId = projectId.ToString(), QuotaSize = new List<string> { $"{resourceTypeDisplayName}: {reserved}/{maximum}" }, ResourceId = resource.Id.ToString(), ApplicationsProfile = resource.ApplicationProfile, MetadataCompleteness = metadataCompleteness, License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null, Disciplines = disciplines.Select(x => x.DisplayNameEn).ToList(), Visibility = resource.VisibilityId.HasValue ? _visibilityModel.GetById(resource.VisibilityId.Value)?.DisplayName : null, }; _coscineLogger.AnalyticsLog(analyticsLogObject); }); } private async Task LogAnalyticsDeleteResource(Database.DataModel.Resource resource, string metadataCompleteness, IEnumerable<DisciplineObject> disciplines, User user) { await Task.Run(() => { var resourceTypeDisplayName = _resourceTypeModel.GetById(resource.TypeId).DisplayName; var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value; var reserved = GetReservedResourceQuota(resource); var maximum = GetMaximumResourceQuota(resource, projectId); var analyticsLogObject = new AnalyticsLogObject { Operation = "Delete Resource", Type = "Action", RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(), ProjectId = projectId.ToString(), QuotaSize = new List<string> { $"{resourceTypeDisplayName}: {reserved}/{maximum}" }, ResourceId = resource.Id.ToString(), ApplicationsProfile = resource.ApplicationProfile, MetadataCompleteness = metadataCompleteness, License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null, Disciplines = disciplines.Select(x => x.DisplayNameEn).ToList(), Visibility = resource.VisibilityId.HasValue ? _visibilityModel.GetById(resource.VisibilityId.Value)?.DisplayName : null, }; _coscineLogger.AnalyticsLog(analyticsLogObject); }); } private async Task LogAnalyticsArchiveResource(Database.DataModel.Resource resource, User user) { await Task.Run(() => { var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value; var analyticsLogObject = new AnalyticsLogObject { Operation = "Archive Resource", Type = "Action", RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(), ProjectId = projectId.ToString(), ResourceId = resource.Id.ToString(), ApplicationsProfile = resource.ApplicationProfile, License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null }; _coscineLogger.AnalyticsLog(analyticsLogObject); }); } private async Task LogAnalyticsUnarchiveResource(Database.DataModel.Resource resource, User user) { await Task.Run(() => { var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value; var analyticsLogObject = new AnalyticsLogObject { Operation = "Unarchive Resource", Type = "Action", RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(), ProjectId = projectId.ToString(), ResourceId = resource.Id.ToString(), ApplicationsProfile = resource.ApplicationProfile, License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null }; _coscineLogger.AnalyticsLog(analyticsLogObject); }); } private static int GetReservedResourceQuota(Database.DataModel.Resource resource) { var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource); if (resourceTypeDefinition.GetResourceTypeInformation().Result.IsQuotaAvailable) { try { return (int)resourceTypeDefinition.GetResourceQuotaAvailable(resource.Id.ToString()).Result; } catch (Exception) { // Error communicating with the resource } } return 0; } private static int GetMaximumResourceQuota(Database.DataModel.Resource resource, Guid parentProjectId) { var projectQuotaModel = new ProjectQuotaModel(); var projectQuota = projectQuotaModel.GetWhere((x) => x.ProjectId == parentProjectId && x.ResourceTypeId == resource.TypeId); var reservedForCurrent = GetReservedResourceQuota(resource); var resourceTypeModel = new ResourceTypeModel(); // Get the resource type var resourceType = resourceTypeModel.GetById(resource.TypeId); if (resourceType == null) { throw new ArgumentNullException($"Could not find resourceType with id: {resource.TypeId}"); } var totalReserved = Helpers.CalculateTotalReservedQuota(resourceType, parentProjectId); return (int)(projectQuota.Quota - (totalReserved.Value - reservedForCurrent)); } } }