diff --git a/README.md b/README.md index 8410979d9a4493d27e0df7cd15de1b81e022b71e..8157c32586d6b28aeeda3744d7af0c3ea56ad564 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,4 @@ and then trust the generated certificate ```dotnet dev-certs https --trust``` ## Testing the new server -The server ist now available under: https://localhost:6000/ (port may be different!) \ No newline at end of file +The server ist now available under: https://localhost:6000/ (port may be different!) diff --git a/src/Project.sln b/src/Project.sln index 9ca995957837dec82b0f967591d4a59dc55ab48f..fd1c0cab4653b543e9482c36f00e8ecd06eb68bb 100644 --- a/src/Project.sln +++ b/src/Project.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28803.156 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project", "Project\Project.csproj", "{16C4EBA5-BA87-45EC-AE1A-E8569A897959}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Project", "Project\Project.csproj", "{16C4EBA5-BA87-45EC-AE1A-E8569A897959}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project.Tests", "Project.Tests\Project.Tests.csproj", "{EEE96892-A211-44EE-B2B8-11FAB31F2E26}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Project.Tests", "Project.Tests\Project.Tests.csproj", "{EEE96892-A211-44EE-B2B8-11FAB31F2E26}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Project/Controllers/ProjectController.cs b/src/Project/Controllers/ProjectController.cs index aa92bccaca91faf284bcd3e843f5ca974c643759..d557f6cf43494e46c35b18fd8aad1625122f1052 100644 --- a/src/Project/Controllers/ProjectController.cs +++ b/src/Project/Controllers/ProjectController.cs @@ -1,5 +1,6 @@ using Coscine.Action; using Coscine.Action.EventArgs; +using Coscine.Action.Utils; using Coscine.Api.Project.ParameterObjects; using Coscine.Api.Project.ReturnObjects; using Coscine.ApiCommons; @@ -15,6 +16,7 @@ using Coscine.ResourceLoader; 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; @@ -39,7 +41,13 @@ namespace Coscine.Api.Project.Controllers private readonly ResourceModel _resourceModel; private readonly CoscineLogger _coscineLogger; private readonly VisibilityModel _visibilityModel; + private readonly InvitationModel _invitationModel; + private readonly RoleModel _roleModel; + private readonly UserModel _userModel; private readonly int _maxAvailable = 100; + private readonly string _userUrlPrefix = "https://purl.org/coscine/users"; + private readonly Uri _orgPrefixUrl = new Uri("http://www.w3.org/ns/org#"); + private readonly RdfStoreConnector _rdfStoreConnector; /// <summary> /// ProjectController constructor @@ -58,6 +66,10 @@ namespace Coscine.Api.Project.Controllers _projectQuotaModel = new ProjectQuotaModel(); _coscineLogger = new CoscineLogger(logger); _visibilityModel = new VisibilityModel(); + _rdfStoreConnector = new RdfStoreConnector(Program.Configuration.GetString("coscine/local/virtuoso/additional/url")); + _invitationModel = new InvitationModel(); + _roleModel = new RoleModel(); + _userModel = new UserModel(); } /// <summary> @@ -506,6 +518,11 @@ namespace Coscine.Api.Project.Controllers _projectQuotaModel.Delete(projectQuota); } + foreach (var invitation in _invitationModel.GetAllWhere((x) => x.Project == project.Id)) + { + _invitationModel.Delete(invitation); + } + _activatedFeaturesModel.DeactivateAllFeatures(project); if (propegateAction) @@ -532,7 +549,6 @@ namespace Coscine.Api.Project.Controllers public IActionResult Store() { var user = _authenticator.GetUser(); - var isRWTHMember = IsRWTHMember(user); var projectObject = ObjectFactory<ProjectObject>.DeserializeFromStream(Request.Body); if (projectObject?.ParentId != new Guid() @@ -541,7 +557,7 @@ namespace Coscine.Api.Project.Controllers return Unauthorized("User is not allowed to create SubProjects."); } - var project = _projectModel.StoreFromObject(projectObject, user, isRWTHMember); + var project = _projectModel.StoreFromObject(projectObject, user, _rdfStoreConnector.GetQuotaDefault(user.Id.ToString())); if (projectObject.ParentId != new Guid() // for now, only an owner can add subprojects to projects @@ -563,26 +579,199 @@ namespace Coscine.Api.Project.Controllers } /// <summary> - /// Checks if the given user is a member of the RWTH + /// List all invitations of a project. /// </summary> - /// <param name="user">User object</param> - /// <returns>True, if member of RWTH or false, if not a member of RWTH</returns> - private bool IsRWTHMember(User user) + /// <param name="projectId">Project id of the project</param> + /// <returns>List of invitations</returns> + [HttpGet("[controller]/invitation/list/{projectId}")] + public ActionResult<IEnumerable<InvitationReturnObject>> ListInvitations(Guid projectId) { - var externalIds = new ExternalIdModel().GetAllWhere((externalId) => externalId.UserId == user.Id); - if (!externalIds.Any()) + var project = _projectModel.GetById(projectId); + + if (project == null) { - return false; + return NotFound($@"The project ""{projectId}"" was not found."); + } + + var user = _authenticator.GetUser(); + + if (!_projectModel.HasAccess(user, project, UserRoles.Owner)) + { + return Unauthorized($"You are not an owner of the project."); + } + + var invitations = _invitationModel.GetAllWhere(x => x.Project == projectId && x.Expiration > DateTime.UtcNow) + .Select(x => new InvitationReturnObject + { + Id = x.Id, + Expiration = x.Expiration, + Issuer = x.Issuer, + ProjectId = x.Project, + RoleId = x.Role, + UserMail = x.InviteeEmail + }); + + return new ActionResult<IEnumerable<InvitationReturnObject>>(invitations); + } + + /// <summary> + /// Create and send an invitation to specified mail. + /// </summary> + /// <param name="sendInvitationObject">Informations for sending an invitation</param> + /// <returns>NoContent</returns> + [HttpPost("[controller]/invitation")] + public IActionResult SendInvitation(SendInvitationObject sendInvitationObject) + { + var user = _authenticator.GetUser(); + + if (!IsValidEmail(sendInvitationObject.Mail)) + { + return BadRequest($@"The email ""{sendInvitationObject.Mail}"" is invalid."); + } + + var project = _projectModel.GetById(sendInvitationObject.Project); + + if (project == null) + { + return NotFound($@"The project ""{sendInvitationObject.Project}"" was not found."); + } + + if (_roleModel.GetById(sendInvitationObject.Role) == null) + { + return NotFound($@"The role ""{sendInvitationObject.Role}"" was not found."); + } + + if (!_projectModel.HasAccess(user, project, UserRoles.Owner)) + { + return Unauthorized($"You are not an owner of the project."); + } + + var invitations = _invitationModel.GetAllWhere( + x => x.Project == sendInvitationObject.Project && + x.InviteeEmail == sendInvitationObject.Mail && + x.Expiration > DateTime.UtcNow + ); + + if (invitations != null && invitations.Any()) + { + return BadRequest("This invitee already has a valid invitation to this project."); + } + + var token = _invitationModel.CreateInvitation(sendInvitationObject.Project, user.Id, sendInvitationObject.Role, sendInvitationObject.Mail); + + var body = new JObject + { + ["Args"] = new JObject() + { + ["placeholder"] = new JObject() + { + ["confirmation_link"] = $@"{_configuration.GetString("coscine/local/api/additional/url")}/invitation?token={token}" + } + } + }; + + NotificationBusUtil.Send(Program.Configuration, "user_invitation", NotificationBusUtil.GetUserList(new User { EmailAddress = sendInvitationObject.Mail }), sendInvitationObject.Project.ToString(), body); + + return NoContent(); + } + + /// <summary> + /// Deletes an invitation. + /// </summary> + /// <param name="invitationId">Id of a invitation</param> + /// <returns>NoContent</returns> + [HttpDelete("[controller]/invitation/{invitationId}")] + public IActionResult DeleteInvitation(Guid invitationId) + { + var invitation = _invitationModel.GetById(invitationId); + + if(invitation == null) + { + return NotFound("Invitation was not found."); } - var externalIdList = new List<string>(); - foreach (var externalId in externalIds) + var user = _authenticator.GetUser(); + + if (!_projectModel.HasAccess(user, _projectModel.GetById(invitation.Project), UserRoles.Owner)) { - externalIdList.Add(externalId.ExternalId1); + return Unauthorized($"You are not an owner of this project."); } - return new RdfStoreConnector(Program.Configuration.GetStringAndWait("coscine/local/virtuoso/additional/url")).GetTriples(new Uri("https://ror.org/04xfq0f34"), null, null, 1, externalIdList).Any(); + + _invitationModel.Delete(invitation); + + return NoContent(); } + /// <summary> + /// Resolve an invitation for the current user. + /// </summary> + /// <param name="token">Token of a invitation</param> + /// <returns>NoContent</returns> + [HttpGet("[controller]/invitation/resolve/{token}")] + public IActionResult ResolveInvitation(Guid token) + { + var user = _authenticator.GetUser(); + + var invitation = _invitationModel.GetByToken(token); + + if(invitation == null) + { + return NotFound("Invitation was not found."); + } + + if (invitation.Expiration < DateTime.UtcNow) + { + return BadRequest("The invitation has expired"); + } + + var project = _projectModel.GetById(invitation.Project); + + if (!_projectModel.HasAccess(_userModel.GetById(invitation.Issuer), project, UserRoles.Owner)) + { + return Unauthorized($"The issuer is not an owner of the project."); + } + + if (_projectRoleModel.GetAllWhere(x => x.ProjectId == invitation.Project && x.UserId == user.Id).Any()) + { + return BadRequest($"The invitee is already part of the project."); + } + + var role = _roleModel.GetById(invitation.Role); + + _emitter.EmitUserAdd(new UserEventArgs(_configuration) + { + Project = project, + Role = role, + User = user, + }); + + var projectRole = new ProjectRole() + { + RelationId = Guid.NewGuid(), + ProjectId = invitation.Project, + UserId = user.Id, + RoleId = invitation.Role + }; + + _projectRoleModel.Insert(projectRole); + + _invitationModel.Delete(invitation); + + return Ok($"User {user.Id} is now {role.DisplayName} of project {project.Id}."); + } + + private static bool IsValidEmail(string email) + { + try + { + return new System.Net.Mail.MailAddress(email).Address == email; + } + catch + { + return false; + } + } + private void LogAnalyticsViewHome(List<string> projectIds) { _coscineLogger.AnalyticsLog( diff --git a/src/Project/ParameterObjects/SendInvitationObject.cs b/src/Project/ParameterObjects/SendInvitationObject.cs new file mode 100644 index 0000000000000000000000000000000000000000..04fc270a95df192d272697b2b4dd743ae06cbf23 --- /dev/null +++ b/src/Project/ParameterObjects/SendInvitationObject.cs @@ -0,0 +1,23 @@ +using System; + +namespace Coscine.Api.Project.ParameterObjects +{ + /// <summary> + /// Parameter object containing the invitation informations. + /// </summary> + public class SendInvitationObject + { + /// <summary> + /// Id of the project + /// </summary> + public Guid Project { get; set; } + /// <summary> + /// Id of the target role + /// </summary> + public Guid Role { get; set; } + /// <summary> + /// Email of the target user + /// </summary> + public string Mail { get; set; } + } +} diff --git a/src/Project/ReturnObjects/InvitationReturnObject.cs b/src/Project/ReturnObjects/InvitationReturnObject.cs new file mode 100644 index 0000000000000000000000000000000000000000..4241c6e3957ebecb99cd8df74d01870644a5e637 --- /dev/null +++ b/src/Project/ReturnObjects/InvitationReturnObject.cs @@ -0,0 +1,35 @@ +using System; + +namespace Coscine.Api.Project.ReturnObjects +{ + /// <summary> + /// Return object for an invitation. + /// </summary> + public class InvitationReturnObject + { + /// <summary> + /// The invitation id. + /// </summary> + public Guid Id { get; set; } + /// <summary> + /// When the invite will expire. + /// </summary> + public DateTime Expiration { get; set; } + /// <summary> + /// Email of the invitee. + /// </summary> + public string UserMail { get; set; } + /// <summary> + /// Id of the issuer. + /// </summary> + public Guid Issuer { get; set; } + /// <summary> + /// Id of the project. + /// </summary> + public Guid ProjectId { get; set; } + /// <summary> + /// Id of the target Role. + /// </summary> + public Guid RoleId { get; set; } + } +}