diff --git a/csharp/App/Backend/Controllers/Controller.cs b/csharp/App/Backend/Controllers/Controller.cs index 322242871..d6db3ec68 100644 --- a/csharp/App/Backend/Controllers/Controller.cs +++ b/csharp/App/Backend/Controllers/Controller.cs @@ -4,7 +4,6 @@ using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.Relations; using Microsoft.AspNetCore.Mvc; using static System.Net.HttpStatusCode; -using static System.String; using Folder = InnovEnergy.App.Backend.DataTypes.Folder; using Installation = InnovEnergy.App.Backend.DataTypes.Installation; using Object = System.Object; @@ -47,58 +46,58 @@ public class Controller } - // [Returns] - // [Returns(HttpStatusCode.Unauthorized)] - // [HttpGet($"{nameof(GetUserById)}")] - // public Object GetUserById(Int64 id) - // { - // var caller = GetCaller(); - // if (caller is null) - // return new HttpResponseMessage(HttpStatusCode.Unauthorized); - // - // var user = Db.GetUserById(id); - // - // if (user is null || !caller.HasAccessTo(user)) - // return new HttpResponseMessage(HttpStatusCode.Unauthorized); - // - // return user; - // } - - // - // [Returns] - // [Returns(HttpStatusCode.Unauthorized)] - // [HttpGet($"{nameof(GetInstallationById)}")] - // public Object GetInstallationById(Int64 id) - // { - // var caller = GetCaller(); - // if (caller == null) - // return new HttpResponseMessage(HttpStatusCode.Unauthorized); - // - // var installation = Db.GetInstallationById(id); - // - // if (installation is null || !caller.HasAccessTo(installation)) - // return new HttpResponseMessage(HttpStatusCode.Unauthorized); - // - // return installation; - // } + [Returns] + [Returns(Unauthorized)] + [HttpGet($"{nameof(GetUserById)}")] + public Object GetUserById(Int64 id) + { + var caller = GetSession()?.User; + if (caller == null) + return _Unauthorized; + + var user = Db.GetUserById(id); + + if (user is null || !caller.HasAccessTo(user)) + return _Unauthorized; + + return user; + } - // [Returns] - // [Returns(HttpStatusCode.Unauthorized)] - // [HttpGet($"{nameof(GetFolderById)}")] - // public Object GetFolderById(Int64 id) - // { - // var caller = GetCaller(); - // if (caller == null) - // return new HttpResponseMessage(HttpStatusCode.Unauthorized); - // - // var folder = Db.GetFolderById(id); - // - // if (folder is null || !caller.HasAccessTo(folder)) - // return new HttpResponseMessage(HttpStatusCode.Unauthorized); - // - // return folder; - // } + [Returns] + [Returns(Unauthorized)] + [HttpGet($"{nameof(GetInstallationById)}")] + public Object GetInstallationById(Int64 id) + { + var user = GetSession()?.User; + if (user == null) + return _Unauthorized; + + var installation = Db.GetInstallationById(id); + + if (installation is null || !user.HasAccessTo(installation)) + return _Unauthorized; + + return installation; + } + + + [Returns] + [Returns(Unauthorized)] + [HttpGet($"{nameof(GetFolderById)}")] + public Object GetFolderById(Int64 id) + { + var user = GetSession()?.User; + if (user == null) + return _Unauthorized; + + var folder = Db.GetFolderById(id); + + if (folder is null || !user.HasAccessTo(folder)) + return _Unauthorized; + + return folder; + } [Returns] // assuming swagger knows about arrays but not lists (JSON) diff --git a/csharp/App/Backend/DataTypes/Installation.cs b/csharp/App/Backend/DataTypes/Installation.cs index c3f60e2e2..ebc9244c7 100644 --- a/csharp/App/Backend/DataTypes/Installation.cs +++ b/csharp/App/Backend/DataTypes/Installation.cs @@ -14,6 +14,6 @@ public class Installation : TreeNode public Double Long { get; set; } public String S3Bucket { get; set; } = ""; - public String S3Key { get; set; } = ""; + public String S3Url { get; set; } = ""; } \ No newline at end of file diff --git a/csharp/App/Backend/DataTypes/Methods/Credentials.cs b/csharp/App/Backend/DataTypes/Methods/Credentials.cs index 06db0d78c..1a8e24703 100644 --- a/csharp/App/Backend/DataTypes/Methods/Credentials.cs +++ b/csharp/App/Backend/DataTypes/Methods/Credentials.cs @@ -8,7 +8,7 @@ public static class CredentialsMethods { public static Session? Login(this Credentials credentials) { - if (credentials.Username.IsNull() || credentials.Password.IsNull()) + if (credentials.Username.IsNullOrEmpty() || credentials.Password.IsNullOrEmpty()) return null; var user = Db.GetUserByEmail(credentials.Username); diff --git a/csharp/App/Backend/DataTypes/Methods/Folder.cs b/csharp/App/Backend/DataTypes/Methods/Folder.cs index 274574a4f..321aca9eb 100644 --- a/csharp/App/Backend/DataTypes/Methods/Folder.cs +++ b/csharp/App/Backend/DataTypes/Methods/Folder.cs @@ -33,7 +33,9 @@ public static class FolderMethods public static IEnumerable Ancestors(this Folder folder) { - return folder.Unfold(Parent); + return folder + .Unfold(Parent) + .Skip(1); // skip self } public static Folder? Parent(this Folder folder) diff --git a/csharp/App/Backend/DataTypes/Methods/Installation.cs b/csharp/App/Backend/DataTypes/Methods/Installation.cs index 795e8a1dc..f3488b08d 100644 --- a/csharp/App/Backend/DataTypes/Methods/Installation.cs +++ b/csharp/App/Backend/DataTypes/Methods/Installation.cs @@ -1,3 +1,5 @@ +using CliWrap; +using CliWrap.Buffered; using InnovEnergy.App.Backend.Database; namespace InnovEnergy.App.Backend.DataTypes.Methods; @@ -5,13 +7,39 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods; public static class InstallationMethods { + public static async Task RenewS3BucketUrl(this Installation installation) + { + await RenewS3BucketUrl(installation, TimeSpan.FromDays(1)); + } + + public static async Task RenewS3BucketUrl(this Installation installation, TimeSpan validity) + { + //secret 55MAqyO_FqUmh7O64VIO0egq50ERn_WIAWuc2QC44QU + const String apiKey = "EXO44d2979c8e570eae81ead564"; + const String salt = "3e5b3069-214a-43ee-8d85-57d72000c19d"; + + var cmd = Cli + .Wrap("s3cmd") + .WithArguments(new[] { "signurl",$"s3://{installation.Id}-{salt}", validity.TotalSeconds.ToString(), "--access_key", apiKey}); + + var x = await cmd.ExecuteBufferedAsync(); + installation.S3Url = x.StandardOutput.Replace("\n", "").Replace(" ", ""); + + Console.WriteLine(installation.S3Url); + + Db.Update(installation); + } + public static IEnumerable Ancestors(this Installation installation) { var parentFolder = Parent(installation); - return parentFolder is null - ? Enumerable.Empty() - : parentFolder.Ancestors(); + if (parentFolder is null) + return Enumerable.Empty(); + + return parentFolder + .Ancestors() + .Prepend(parentFolder); } public static Folder? Parent(this Installation installation) diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index fc66f90d1..86960dab2 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -110,6 +110,34 @@ public static class SessionMethods && Db.Delete(userToDelete); } + + public static Boolean GrantUserAccessTo(this Session? session, User? user, Installation? installation) + { + var sessionUser = session?.User; + + return sessionUser is not null + && user is not null + && installation is not null + && user.IsDescendantOf(sessionUser) + && sessionUser.HasAccessTo(installation) + && !user.HasAccessTo(installation) + && Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id }); + + } + + public static Boolean GrantUserAccessTo(this Session? session, User? user, Folder? folder) + { + var sessionUser = session?.User; + + return sessionUser is not null + && user is not null + && folder is not null + && user.IsDescendantOf(sessionUser) + && sessionUser.HasAccessTo(folder) + && !user.HasAccessTo(folder) + && Db.Create(new FolderAccess { UserId = user.Id, FolderId = folder.Id }); + } + public static Boolean Logout(this Session? session) { return session is not null diff --git a/csharp/App/Backend/DataTypes/Methods/User.cs b/csharp/App/Backend/DataTypes/Methods/User.cs index 99988f7e8..1de1086b9 100644 --- a/csharp/App/Backend/DataTypes/Methods/User.cs +++ b/csharp/App/Backend/DataTypes/Methods/User.cs @@ -1,8 +1,5 @@ -using System.Net.Http.Headers; using System.Net.Mail; using System.Security.Cryptography; -using System.Text.Json.Nodes; -using System.Text.RegularExpressions; using InnovEnergy.App.Backend.Database; using InnovEnergy.Lib.Utils; using Convert = System.Convert; @@ -48,7 +45,7 @@ public static class UserMethods public static IEnumerable DirectlyAccessibleInstallations(this User user) { return Db - .User2Installation + .InstallationAccess .Where(r => r.UserId == user.Id) .Select(r => r.InstallationId) .Select(Db.GetInstallationById) @@ -59,7 +56,7 @@ public static class UserMethods public static IEnumerable DirectlyAccessibleFolders(this User user) { return Db - .User2Folder + .FolderAccess .Where(r => r.UserId == user.Id) .Select(r => r.FolderId) .Select(Db.GetFolderById) @@ -88,7 +85,9 @@ public static class UserMethods private static IEnumerable Ancestors(this User user) { - return user.Unfold(Parent); + return user + .Unfold(Parent) + .Skip(1); // skip self } public static Boolean VerifyPassword(this User user, String password) @@ -126,7 +125,7 @@ public static class UserMethods public static Boolean HasDirectAccessTo(this User user, Folder folder) { return Db - .User2Folder + .FolderAccess .Any(r => r.FolderId == folder.Id && r.UserId == user.Id); } @@ -135,7 +134,8 @@ public static class UserMethods if (folder is null) return false; - return folder + return user.HasDirectAccessTo(folder) + || folder .Ancestors() .Any(user.HasDirectAccessTo); } @@ -143,8 +143,8 @@ public static class UserMethods public static Boolean HasDirectAccessTo(this User user, Installation installation) { return Db - .User2Installation - .Any(r => r.InstallationId == installation.Id && r.UserId == user.Id); + .InstallationAccess + .Any(r => r.UserId == user.Id && r.InstallationId == installation.Id); } public static Boolean HasAccessTo(this User user, Installation? installation) @@ -163,7 +163,6 @@ public static class UserMethods return other .Ancestors() - .Skip(1) // Important! skip self, user cannot delete or edit himself .Contains(user); } @@ -176,154 +175,7 @@ public static class UserMethods } - private static Byte[] HmacSha256Digest(String message, String secret) - { - // var encoding = new UTF8Encoding(); - // var keyBytes = encoding.GetBytes(secret); - // var messageBytes = encoding.GetBytes(message); - // var cryptographer = new HMACSHA256(keyBytes); - // return cryptographer.ComputeHash(messageBytes); - - var keyBytes = UTF8.GetBytes(secret); - var messageBytes = UTF8.GetBytes(message); - return HMACSHA256.HashData(keyBytes, messageBytes); - } - - private static String BuildSignature(String method, String path, String data, Int64 time, String secret) - { - var messageToSign = ""; - messageToSign += method + " /v2/" + path + "\n"; - messageToSign += data + "\n"; - - // query strings - messageToSign += "\n"; - // headers - messageToSign += "\n"; - - messageToSign += time; - - Console.WriteLine("Message to sign:\n" + messageToSign); - - var hmac = HmacSha256Digest(messageToSign, secret); - return Convert.ToBase64String(hmac); - } - - // public Object CreateAndSaveUserS3ApiKey(User user) - // { - // //EXOSCALE API URL - // const String url = "https://api-ch-dk-2.exoscale.com/v2/"; - // const String path = "access-key"; - // - // //TODO HIDE ME - // const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0"; - // const String apiKey = "EXOb98ec9008e3ec16e19d7b593"; - // - // var installationList = User2Installation - // .Where(i => i.UserId == user.Id) - // .SelectMany(i => Installations.Where(f => i.InstallationId == f.Id)) - // .ToList(); - // - // - // var instList = new JsonArray(); - // - // foreach (var installation in installationList) - // { - // instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"}); - // } - // - // var jsonPayload = new JsonObject { ["name"] = user.Email, ["operations"] = new JsonArray{ "list-sos-bucket", "get-sos-object" }, ["content"] = instList}; - // var stringPayload = jsonPayload.ToJsonString(); - // - // var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60; - // var signature = BuildSignature("POST", path, stringPayload, unixExpiration , secret); - // - // var authHeader = "credential="+apiKey+",expires="+unixExpiration+",signature="+signature; - // - // var client = new HttpClient(); - // client.DefaultRequestHeaders.Authorization = - // new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader); - // - // var content = new StringContent(stringPayload, Encoding.UTF8, "application/json"); - // - // - // var response = client.PostAsync(url+path, content).Result; - // - // if (response.StatusCode.ToString() != "OK") - // { - // return response; - // } - // - // var responseString = response.Content.ReadAsStringAsync().Result; - // return Enumerable.Last(Regex.Match(responseString, "key\\\":\\\"([A-Z])\\w+").ToString().Split('"')); - // // return SetUserS3ApiKey(user, newKey); - // - // } - - public static Object CreateAndSaveInstallationS3ApiKey(Installation installation) - { - //EXOSCALE API URL - const String url = "https://api-ch-dk-2.exoscale.com/v2/"; - const String path = "access-key"; - - //TODO HIDE ME - const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0"; - const String apiKey = "EXOb98ec9008e3ec16e19d7b593"; - - - var jsonPayload = new JsonObject - { - ["name"] = installation.Id, - ["operations"] = new JsonArray - { - "list-sos-bucket", - "get-sos-object" - }, - ["content"] = new JsonArray - { - new JsonObject - { - ["domain"] = "sos", - ["resource-name"] = installation.Name, - ["resource-type"] = "bucket" - } - } - }; - - var stringPayload = jsonPayload.ToJsonString(); - - var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60; - - var signature = BuildSignature("POST", path, stringPayload, unixExpiration, secret); - - var authHeader = "credential=" + apiKey + ",expires=" + unixExpiration + ",signature=" + signature; - - var client = new HttpClient(); - - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader); - - var content = new StringContent(stringPayload, UTF8, "application/json"); - - var response = client.PostAsync(url + path, content).Result; - - if (response.StatusCode.ToString() != "OK") - { - return response; - } - - var responseString = response.Content.ReadAsStringAsync().Result; - var newKey = Regex - .Match(responseString, "key\\\":\\\"([A-Z])\\w+") - .ToString() - .Split('"') - .Last(); - - installation.S3Key = newKey; - Db.Update(installation); - return newKey; - } - - // TODO private static Boolean IsValidEmail(String email) diff --git a/csharp/App/Backend/Database/Create.cs b/csharp/App/Backend/Database/Create.cs index 875b5ddef..537192476 100644 --- a/csharp/App/Backend/Database/Create.cs +++ b/csharp/App/Backend/Database/Create.cs @@ -29,10 +29,18 @@ public static partial class Db return Connection.Insert(user) > 0; } - public static Boolean Create(Session session) { return Connection.Insert(session) > 0; } + public static Boolean Create(InstallationAccess installationAccess) + { + return Connection.Insert(installationAccess) > 0; + } + + public static Boolean Create(FolderAccess folderAccess) + { + return Connection.Insert(folderAccess) > 0; + } } \ No newline at end of file diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 3e2fd815c..f7eaafd7f 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -20,8 +20,8 @@ public static partial class Db public static TableQuery Folders => Connection.Table(); public static TableQuery Installations => Connection.Table(); public static TableQuery Users => Connection.Table(); - public static TableQuery User2Folder => Connection.Table(); - public static TableQuery User2Installation => Connection.Table(); + public static TableQuery FolderAccess => Connection.Table(); + public static TableQuery InstallationAccess => Connection.Table(); static Db() @@ -33,19 +33,18 @@ public static partial class Db Connection.CreateTable(); Connection.CreateTable(); Connection.CreateTable(); - Connection.CreateTable(); - Connection.CreateTable(); + Connection.CreateTable(); + Connection.CreateTable(); Connection.CreateTable(); }); - var installation = Installations.First(); - UserMethods.CreateAndSaveInstallationS3ApiKey(installation); + - - Observable.Interval(TimeSpan.FromDays(1)) - .StartWith(0) // Do it right away (on startup) - .Subscribe(Cleanup); // and then daily + Observable.Interval(TimeSpan.FromDays(0.5)) + .StartWith(0) // Do it right away (on startup) + .SelectMany(Cleanup) + .Subscribe(); // and then daily } @@ -70,51 +69,12 @@ public static partial class Db return success; } - - public static Boolean AddToAccessibleInstallations(Int64 userId, Int64 updatedInstallationId) + private static async Task Cleanup(Int64 _) { - var con = new User2Installation - { - UserId = userId, - InstallationId = updatedInstallationId - }; - - try - { - Connection.Insert(con); - return true; - } - catch (Exception e) - { - return false; - } - } - - public static Boolean AddToAccessibleFolders(Int64 userId, Int64 updatedFolderId) - { - var con = new User2Folder - { - UserId = userId, - FolderId = updatedFolderId - }; - - try - { - Connection.Insert(con); - return true; - } - catch (Exception e) - { - return false; - } - } - - - private static void Cleanup(Int64 _) - { - DeleteS3Keys(); + await UpdateS3Urls(); DeleteStaleSessions(); + return true; } private static void DeleteStaleSessions() @@ -123,10 +83,9 @@ public static partial class Db Sessions.Delete(s => s.LastSeen < deadline); } - private static void DeleteS3Keys() + private static Task UpdateS3Urls() { - void DeleteKeys() => Installations.Do(i => i.S3Key = "").ForEach(Update); // TODO - - Connection.RunInTransaction(DeleteKeys); + var renewTasks = Installations.Select(i => i.RenewS3BucketUrl()).ToArray(); + return Task.WhenAll(renewTasks); } } \ No newline at end of file diff --git a/csharp/App/Backend/Database/Delete.cs b/csharp/App/Backend/Database/Delete.cs index 0e495afde..200b63855 100644 --- a/csharp/App/Backend/Database/Delete.cs +++ b/csharp/App/Backend/Database/Delete.cs @@ -21,7 +21,7 @@ public static partial class Db Boolean DeleteDescendantFolderAndItsDependencies(Folder f) { - User2Folder .Delete(r => r.FolderId == f.Id); + FolderAccess .Delete(r => r.FolderId == f.Id); Installations.Delete(r => r.ParentId == f.Id); return Folders.Delete(r => r.Id == f.Id) > 0; @@ -34,7 +34,7 @@ public static partial class Db Boolean DeleteInstallationAndItsDependencies() { - User2Installation.Delete(i => i.InstallationId == installation.Id); + InstallationAccess.Delete(i => i.InstallationId == installation.Id); return Installations.Delete(i => i.Id == installation.Id) > 0; } } @@ -45,8 +45,8 @@ public static partial class Db Boolean DeleteUserAndHisDependencies() { - User2Folder .Delete(u => u.UserId == user.Id); - User2Installation.Delete(u => u.UserId == user.Id); + FolderAccess .Delete(u => u.UserId == user.Id); + InstallationAccess.Delete(u => u.UserId == user.Id); return Users.Delete(u => u.Id == user.Id) > 0; } diff --git a/csharp/App/Backend/Database/Fake.cs b/csharp/App/Backend/Database/Fake.cs index ab144c11d..af7a4e7ee 100644 --- a/csharp/App/Backend/Database/Fake.cs +++ b/csharp/App/Backend/Database/Fake.cs @@ -61,7 +61,7 @@ public static partial class Db private static void GiveFakeUsersAccessToFolders() { - foreach (var uf in User2Folder) // remove existing relations + foreach (var uf in FolderAccess) // remove existing relations Connection.Delete(uf); var nFolders = Folders.Count(); @@ -70,7 +70,7 @@ public static partial class Db foreach (var user in Users) while (Random.Shared.Next((Int32)(nUsers - user.Id + 1)) != 0) { - var relation = new User2Folder + var relation = new FolderAccess { UserId = user.Id, FolderId = Random.Shared.Next(nFolders) + 1 @@ -81,7 +81,7 @@ public static partial class Db private static void GiveFakeUsersAccessToInstallations() { - foreach (var ui in User2Installation) // remove existing relations + foreach (var ui in InstallationAccess) // remove existing relations Connection.Delete(ui); var nbInstallations = Installations.Count(); @@ -89,7 +89,7 @@ public static partial class Db foreach (var user in Users) while (Random.Shared.Next(5) != 0) { - var relation = new User2Installation + var relation = new InstallationAccess { UserId = user.Id, InstallationId = Random.Shared.Next(nbInstallations) + 1 diff --git a/csharp/App/Backend/Relations/User2Folder.cs b/csharp/App/Backend/Relations/FolderAccess.cs similarity index 80% rename from csharp/App/Backend/Relations/User2Folder.cs rename to csharp/App/Backend/Relations/FolderAccess.cs index 0cd1fa191..545647a46 100644 --- a/csharp/App/Backend/Relations/User2Folder.cs +++ b/csharp/App/Backend/Relations/FolderAccess.cs @@ -2,7 +2,7 @@ using SQLite; namespace InnovEnergy.App.Backend.Relations; -public class User2Folder : Relation +public class FolderAccess : Relation { [Indexed] public Int64 UserId { get => Left ; init => Left = value;} [Indexed] public Int64 FolderId { get => Right; init => Right = value;} diff --git a/csharp/App/Backend/Relations/User2Installation.cs b/csharp/App/Backend/Relations/InstallationAccess.cs similarity index 79% rename from csharp/App/Backend/Relations/User2Installation.cs rename to csharp/App/Backend/Relations/InstallationAccess.cs index f898253db..c233ca953 100644 --- a/csharp/App/Backend/Relations/User2Installation.cs +++ b/csharp/App/Backend/Relations/InstallationAccess.cs @@ -2,7 +2,7 @@ using SQLite; namespace InnovEnergy.App.Backend.Relations; -public class User2Installation : Relation +public class InstallationAccess : Relation { [Indexed] public Int64 UserId { get => Left ; init => Left = value;} [Indexed] public Int64 InstallationId { get => Right; init => Right = value;} diff --git a/csharp/App/Backend/Relations/Session.cs b/csharp/App/Backend/Relations/Session.cs index 016ffa0ce..fa9a993d4 100644 --- a/csharp/App/Backend/Relations/Session.cs +++ b/csharp/App/Backend/Relations/Session.cs @@ -14,7 +14,7 @@ public class Session : Relation [Indexed] public DateTime LastSeen { get; set; } [Ignore] public Boolean Valid => DateTime.Now - LastSeen < MaxAge - && !User.IsNull(); + && !User.Email.IsNullOrEmpty(); [Ignore] public User User => _User ??= Db.GetUserById(UserId)!; @@ -40,5 +40,4 @@ public class Session : Relation return Convert.ToBase64String(token); } - } \ No newline at end of file diff --git a/csharp/App/Backend/db.sqlite b/csharp/App/Backend/db.sqlite index 2522ae1e7..f209c9536 100644 Binary files a/csharp/App/Backend/db.sqlite and b/csharp/App/Backend/db.sqlite differ