diff --git a/DysonNetwork.Pass/.gitignore b/DysonNetwork.Pass/.gitignore index d97800d..00ddf85 100644 --- a/DysonNetwork.Pass/.gitignore +++ b/DysonNetwork.Pass/.gitignore @@ -1 +1,2 @@ -/wwwroot/dist \ No newline at end of file +/wwwroot/dist +/Keys \ No newline at end of file diff --git a/DysonNetwork.Pass/Account/Account.cs b/DysonNetwork.Pass/Account/Account.cs index 7015c9a..6370e3c 100644 --- a/DysonNetwork.Pass/Account/Account.cs +++ b/DysonNetwork.Pass/Account/Account.cs @@ -12,7 +12,7 @@ namespace DysonNetwork.Pass.Account; [Index(nameof(Name), IsUnique = true)] public class Account : ModelBase { - public Guid Id { get; set; } + public Guid Id { get; set; } = Guid.NewGuid(); [MaxLength(256)] public string Name { get; set; } = string.Empty; [MaxLength(256)] public string Nick { get; set; } = string.Empty; [MaxLength(32)] public string Language { get; set; } = string.Empty; diff --git a/DysonNetwork.Pass/Account/AccountService.cs b/DysonNetwork.Pass/Account/AccountService.cs index d8ff6b6..57c0c44 100644 --- a/DysonNetwork.Pass/Account/AccountService.cs +++ b/DysonNetwork.Pass/Account/AccountService.cs @@ -84,79 +84,69 @@ public class AccountService( bool isActivated = false ) { - await using var transaction = await db.Database.BeginTransactionAsync(); - try + var dupeNameCount = await db.Accounts.Where(a => a.Name == name).CountAsync(); + if (dupeNameCount > 0) + throw new InvalidOperationException("Account name has already been taken."); + + var account = new Account { - var dupeNameCount = await db.Accounts.Where(a => a.Name == name).CountAsync(); - if (dupeNameCount > 0) - throw new InvalidOperationException("Account name has already been taken."); - - var account = new Account + Name = name, + Nick = nick, + Language = language, + Contacts = new List { - Name = name, - Nick = nick, - Language = language, - Contacts = new List + new() { - new() - { - Type = AccountContactType.Email, - Content = email, - VerifiedAt = isEmailVerified ? SystemClock.Instance.GetCurrentInstant() : null, - IsPrimary = true - } - }, - AuthFactors = password is not null - ? new List - { - new AccountAuthFactor - { - Type = AccountAuthFactorType.Password, - Secret = password, - EnabledAt = SystemClock.Instance.GetCurrentInstant() - }.HashSecret() - } - : [], - Profile = new AccountProfile() - }; - - if (isActivated) - { - account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); - var defaultGroup = await db.PermissionGroups.FirstOrDefaultAsync(g => g.Key == "default"); - if (defaultGroup is not null) - { - db.PermissionGroupMembers.Add(new PermissionGroupMember - { - Actor = $"user:{account.Id}", - Group = defaultGroup - }); + Type = AccountContactType.Email, + Content = email, + VerifiedAt = isEmailVerified ? SystemClock.Instance.GetCurrentInstant() : null, + IsPrimary = true } - } - else - { - var spell = await spells.CreateMagicSpell( - account, - MagicSpellType.AccountActivation, - new Dictionary + }, + AuthFactors = password is not null + ? new List + { + new AccountAuthFactor { - { "contact_method", account.Contacts.First().Content } - } - ); - await spells.NotifyMagicSpell(spell, true); - } + Type = AccountAuthFactorType.Password, + Secret = password, + EnabledAt = SystemClock.Instance.GetCurrentInstant() + }.HashSecret() + } + : [], + Profile = new AccountProfile() + }; - db.Accounts.Add(account); - await db.SaveChangesAsync(); - - await transaction.CommitAsync(); - return account; - } - catch + if (isActivated) { - await transaction.RollbackAsync(); - throw; + account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); + var defaultGroup = await db.PermissionGroups.FirstOrDefaultAsync(g => g.Key == "default"); + if (defaultGroup is not null) + { + db.PermissionGroupMembers.Add(new PermissionGroupMember + { + Actor = $"user:{account.Id}", + Group = defaultGroup + }); + } } + + db.Accounts.Add(account); + await db.SaveChangesAsync(); + + if (isActivated) return account; + + var spell = await spells.CreateMagicSpell( + account, + MagicSpellType.AccountActivation, + new Dictionary + { + { "contact_method", account.Contacts.First().Content } + } + ); + await spells.NotifyMagicSpell(spell, true); + + return account; } public async Task CreateAccount(OidcUserInfo userInfo) diff --git a/DysonNetwork.Pass/Auth/Session.cs b/DysonNetwork.Pass/Auth/Session.cs index 149fb65..94d3bd8 100644 --- a/DysonNetwork.Pass/Auth/Session.cs +++ b/DysonNetwork.Pass/Auth/Session.cs @@ -31,6 +31,7 @@ public class AuthSession : ModelBase LastGrantedAt = LastGrantedAt?.ToTimestamp(), ExpiredAt = ExpiredAt?.ToTimestamp(), AccountId = AccountId.ToString(), + Account = Account.ToProtoValue(), ChallengeId = ChallengeId.ToString(), Challenge = Challenge.ToProtoValue(), AppId = AppId?.ToString() diff --git a/DysonNetwork.Pass/Program.cs b/DysonNetwork.Pass/Program.cs index 2b32e21..6a18a1b 100644 --- a/DysonNetwork.Pass/Program.cs +++ b/DysonNetwork.Pass/Program.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using DysonNetwork.Pass; using DysonNetwork.Pass.Pages.Data; using DysonNetwork.Pass.Startup; @@ -6,7 +5,6 @@ using DysonNetwork.Shared.Http; using DysonNetwork.Shared.PageData; using DysonNetwork.Shared.Registry; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.FileProviders; var builder = WebApplication.CreateBuilder(args); @@ -18,11 +16,12 @@ builder.Services.AddAppMetrics(); // Add application services builder.Services.AddRegistryService(builder.Configuration); -builder.Services.AddPusherService(); builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppRateLimiting(); builder.Services.AddAppAuthentication(); builder.Services.AddAppSwagger(); +builder.Services.AddPusherService(); +builder.Services.AddDriveService(); // Add flush handlers and websocket handlers builder.Services.AddAppFlushHandlers(); diff --git a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs index d9fa82e..baa05df 100644 --- a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs @@ -75,6 +75,7 @@ public static class ApplicationConfiguration app.MapGrpcService(); app.MapGrpcService(); app.MapGrpcService(); + app.MapGrpcService(); return app; } diff --git a/DysonNetwork.Pusher/Program.cs b/DysonNetwork.Pusher/Program.cs index 150912c..1694200 100644 --- a/DysonNetwork.Pusher/Program.cs +++ b/DysonNetwork.Pusher/Program.cs @@ -16,6 +16,7 @@ builder.Services.AddAppRateLimiting(); builder.Services.AddAppAuthentication(); builder.Services.AddAppSwagger(); builder.Services.AddDysonAuth(); +builder.Services.AddAccountService(); // Add flush handlers and websocket handlers builder.Services.AddAppFlushHandlers(); diff --git a/DysonNetwork.Shared/Auth/AuthScheme.cs b/DysonNetwork.Shared/Auth/AuthScheme.cs index 7ddf4b0..5b35cb7 100644 --- a/DysonNetwork.Shared/Auth/AuthScheme.cs +++ b/DysonNetwork.Shared/Auth/AuthScheme.cs @@ -29,8 +29,6 @@ public class DysonTokenAuthHandler( try { - var now = SystemClock.Instance.GetCurrentInstant(); - // Validate token and extract session ID AuthSession session; try @@ -59,8 +57,20 @@ public class DysonTokenAuthHandler( new("token_type", tokenInfo.Type.ToString()) }; - // return AuthenticateResult.Success(ticket); - return AuthenticateResult.NoResult(); + // Add scopes as claims + session.Challenge.Scopes.ToList().ForEach(scope => claims.Add(new Claim("scope", scope))); + + // Add superuser claim if applicable + if (session.Account.IsSuperuser) + claims.Add(new Claim("is_superuser", "1")); + + // Create the identity and principal + var identity = new ClaimsIdentity(claims, AuthConstants.SchemeName); + var principal = new ClaimsPrincipal(identity); + + var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName); + + return AuthenticateResult.Success(ticket); } catch (Exception ex) { diff --git a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs index 172fe05..e62970d 100644 --- a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs +++ b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs @@ -49,6 +49,18 @@ public static class GrpcClientHelper return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, clientCertPassword)); } + + public static async Task CreateActionLogServiceClient( + IEtcdClient etcdClient, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass"); + return new ActionLogService.ActionLogServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, + clientCertPassword)); + } public static async Task CreateAuthServiceClient( IEtcdClient etcdClient, diff --git a/DysonNetwork.Shared/Registry/ServiceHelper.cs b/DysonNetwork.Shared/Registry/ServiceHelper.cs index cf67ccb..d30576f 100644 --- a/DysonNetwork.Shared/Registry/ServiceHelper.cs +++ b/DysonNetwork.Shared/Registry/ServiceHelper.cs @@ -40,7 +40,21 @@ public static class ServiceHelper .CreateAccountServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword) .GetAwaiter() .GetResult(); - }); + }); + + services.AddSingleton(sp => + { + var etcdClient = sp.GetRequiredService(); + var config = sp.GetRequiredService(); + var clientCertPath = config["Service:ClientCert"]!; + var clientKeyPath = config["Service:ClientKey"]!; + var clientCertPassword = config["Service:CertPassword"]; + + return GrpcClientHelper + .CreateActionLogServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword) + .GetAwaiter() + .GetResult(); + }); return services; } diff --git a/DysonNetwork.Shared/Registry/ServiceRegistry.cs b/DysonNetwork.Shared/Registry/ServiceRegistry.cs index 535326a..4562a89 100644 --- a/DysonNetwork.Shared/Registry/ServiceRegistry.cs +++ b/DysonNetwork.Shared/Registry/ServiceRegistry.cs @@ -8,8 +8,12 @@ namespace DysonNetwork.Shared.Registry; public class ServiceRegistry(IEtcdClient etcd, ILogger logger) { - public async Task RegisterService(string serviceName, string serviceUrl, long leaseTtlSeconds = 60, - CancellationToken cancellationToken = default) + public async Task RegisterService( + string serviceName, + string serviceUrl, + long leaseTtlSeconds = 60, + CancellationToken cancellationToken = default + ) { var key = $"/services/{serviceName}"; var leaseResponse = await etcd.LeaseGrantAsync( diff --git a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj index 4e3511a..ccf0734 100644 --- a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj +++ b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj @@ -177,6 +177,7 @@ <_ContentIncludedByDefault Remove="Pages\Auth\VerifyFactor.cshtml" /> <_ContentIncludedByDefault Remove="Pages\Checkpoint\CheckpointPage.cshtml" /> <_ContentIncludedByDefault Remove="Pages\Spell\MagicSpellPage.cshtml" /> + <_ContentIncludedByDefault Remove="Keys\Solian.json" />