Create authorization policies for OrganizationAdmin and ServerAdmin. Use these within the routing system via Authorize attribute instead of directly checking on each page.

This commit is contained in:
Jared Goodwin 2023-06-14 15:06:13 -07:00
parent 5383343e0a
commit bc690e8beb
18 changed files with 612 additions and 511 deletions

View File

@ -1,7 +1,13 @@
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<div class="jumbotron">
<h3>You are not authorized to access this resource.</h3>
</div>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization;
namespace Remotely.Server.Auth;
public class OrganizationAdminRequirement : IAuthorizationRequirement
{
}

View File

@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Remotely.Shared.Models;
using System.Threading.Tasks;
namespace Remotely.Server.Auth;
public class OrganizationAdminRequirementHandler : AuthorizationHandler<OrganizationAdminRequirement>
{
private readonly UserManager<RemotelyUser> _userManager;
public OrganizationAdminRequirementHandler(UserManager<RemotelyUser> userManager)
{
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OrganizationAdminRequirement requirement)
{
if (context.User.Identity?.IsAuthenticated != true)
{
context.Fail();
return;
}
var user = await _userManager.GetUserAsync(context.User);
if (user?.IsAdministrator != true)
{
context.Fail();
return;
}
context.Succeed(requirement);
}
}

View File

@ -0,0 +1,8 @@
namespace Remotely.Server.Auth;
public static class PolicyNames
{
public const string TwoFactorRequired = nameof(TwoFactorRequired);
public const string OrganizationAdminRequired = nameof(OrganizationAdminRequired);
public const string ServerAdminRequired = nameof(ServerAdminRequired);
}

View File

@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Authorization;
namespace Remotely.Server.Auth;
public class ServerAdminRequirement : IAuthorizationRequirement
{
}

View File

@ -0,0 +1,36 @@
#nullable enable
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Remotely.Shared.Models;
using System.Threading.Tasks;
namespace Remotely.Server.Auth;
public class ServerAdminRequirementHandler : AuthorizationHandler<ServerAdminRequirement>
{
private readonly UserManager<RemotelyUser> _userManager;
public ServerAdminRequirementHandler(UserManager<RemotelyUser> userManager)
{
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ServerAdminRequirement requirement)
{
if (context.User.Identity?.IsAuthenticated != true)
{
context.Fail();
return;
}
var user = await _userManager.GetUserAsync(context.User);
if (user?.IsServerAdmin != true)
{
context.Fail();
return;
}
context.Succeed(requirement);
}
}

View File

@ -8,6 +8,6 @@ namespace Remotely.Server.Auth
{
public class TwoFactorRequiredRequirement : IAuthorizationRequirement
{
public const string PolicyName = "TwoFactorRequired";
}
}

View File

@ -11,13 +11,10 @@
private bool _loaderShown;
private string _statusMessage = string.Empty;
protected override Task OnAfterRenderAsync(bool firstRender)
protected override Task OnInitializedAsync()
{
if (firstRender)
{
Messenger.Register<ShowLoaderMessage>(this, HandleShowLoaderMessage);
}
return base.OnAfterRenderAsync(firstRender);
Messenger.Register<ShowLoaderMessage>(this, HandleShowLoaderMessage);
return base.OnInitializedAsync();
}
private async Task HandleShowLoaderMessage(ShowLoaderMessage message)

View File

@ -1,5 +1,5 @@
@page "/api-keys"
@attribute [Authorize]
@attribute [Authorize(Policy = PolicyNames.OrganizationAdminRequired)]
@inherits AuthComponentBase
@inject IDataService DataService

View File

@ -1,5 +1,6 @@
@page "/branding"
@inherits AuthComponentBase
@attribute [Authorize(Policy = PolicyNames.OrganizationAdminRequired)]
@inject IDataService DataService
@inject IToastService ToastService
@inject IJsInterop JsInterop
@ -8,70 +9,61 @@
<h3 class="mb-3">Branding</h3>
<AlertBanner Message="@_alertMessage" />
@if (User?.IsAdministrator != true)
{
<h5 class="text-muted">Only organization administrators can view this page.</h5>
}
else
{
<AlertBanner Message="@_alertMessage" />
<div class="row">
<div class="col-sm-8 col-lg-6 col-xl-5">
<div class="form-group">
<label class="mb-1">Branding Areas</label>
<br />
<img class="img-fluid" src="/images/Remotely_Desktop.png" />
</div>
</div>
</div>
<div class="row">
<div class="col-sm-8 col-lg-6 col-xl-5">
<div class="form-group">
<label class="mb-1">Branding Areas</label>
<br />
<img class="img-fluid" src="/images/Remotely_Desktop.png" />
<div class="row">
<div class="col-sm-8 col-lg-6 col-xl-5">
<EditForm OnValidSubmit="HandleValidSubmit" Model="_inputModel">
<DataAnnotationsValidator />
<div class="form-group mb-5">
<label class="text-info font-weight-bold">Product Name</label>
<InputText class="form-control" @bind-Value="_inputModel.ProductName" />
<ValidationMessage For="() => _inputModel.ProductName" />
</div>
</div>
@if (!string.IsNullOrWhiteSpace(_base64Icon))
{
<div class="form-group mb-4">
<label class="text-info font-weight-bold">Current Icon</label>
<br />
<img class="img-fluid" src="data:image/png;base64,@_base64Icon" />
</div>
}
<div class="form-group mb-5">
<label class="text-info font-weight-bold">New Icon</label>
<InputFile type="file" accept="image/png" class="form-control" OnChange="IconUploadInputChanged" />
</div>
<div class="form-group mb-5">
<label class="text-info font-weight-bold">Title Foreground</label>
<ColorPicker @bind-Color="_inputModel.TitleForegroundColor" />
</div>
<div class="form-group mb-5">
<label class="text-info font-weight-bold">Title Background</label>
<ColorPicker @bind-Color="_inputModel.TitleBackgroundColor" />
</div>
<div class="form-group mb-5">
<label class="text-info font-weight-bold">Button Color</label>
<ColorPicker @bind-Color="_inputModel.TitleButtonColor" />
</div>
<div class="form-group text-right">
<button class="btn btn-secondary mr-3" type="button" @onclick="ResetBranding">Reset</button>
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</EditForm>
</div>
<div class="row">
<div class="col-sm-8 col-lg-6 col-xl-5">
<EditForm OnValidSubmit="HandleValidSubmit" Model="_inputModel">
<DataAnnotationsValidator />
<div class="form-group mb-5">
<label class="text-info font-weight-bold">Product Name</label>
<InputText class="form-control" @bind-Value="_inputModel.ProductName" />
<ValidationMessage For="() => _inputModel.ProductName" />
</div>
@if (!string.IsNullOrWhiteSpace(_base64Icon))
{
<div class="form-group mb-4">
<label class="text-info font-weight-bold">Current Icon</label>
<br />
<img class="img-fluid" src="data:image/png;base64,@_base64Icon" />
</div>
}
<div class="form-group mb-5">
<label class="text-info font-weight-bold">New Icon</label>
<InputFile type="file" accept="image/png" class="form-control" OnChange="IconUploadInputChanged" />
</div>
<div class="form-group mb-5">
<label class="text-info font-weight-bold">Title Foreground</label>
<ColorPicker @bind-Color="_inputModel.TitleForegroundColor" />
</div>
<div class="form-group mb-5">
<label class="text-info font-weight-bold">Title Background</label>
<ColorPicker @bind-Color="_inputModel.TitleBackgroundColor" />
</div>
<div class="form-group mb-5">
<label class="text-info font-weight-bold">Button Color</label>
<ColorPicker @bind-Color="_inputModel.TitleButtonColor" />
</div>
<div class="form-group text-right">
<button class="btn btn-secondary mr-3" type="button" @onclick="ResetBranding">Reset</button>
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</EditForm>
</div>
</div>
}
</div>
@code {
private string _alertMessage;
private string _base64Icon;

View File

@ -1,5 +1,5 @@
@page "/manage-organization"
@attribute [Authorize]
@attribute [Authorize(Policy = PolicyNames.OrganizationAdminRequired)]
@inherits AuthComponentBase
<h3 class="mb-3">Manage Organization</h3>

View File

@ -1,4 +1,5 @@
@page "/scripts/{activeTab?}"
@attribute [Authorize]
@inherits AuthComponentBase
@using System.Collections
@inject IDataService DataService

View File

@ -1,395 +1,387 @@
@page "/server-config"
@attribute [Authorize]
@attribute [Authorize(Policy = PolicyNames.ServerAdminRequired)]
@inherits AuthComponentBase
@if (User?.IsServerAdmin == true)
{
<AlertBanner Message="@_alertMessage" />
<AlertBanner Message="@_alertMessage" />
<h3 class="mt-3">Server Info</h3>
<h3 class="mt-3">Server Info</h3>
<div class="row">
<div class="col-md-8">
<div class="mb-4" style="display:flex; justify-content: space-evenly">
<div class="d-inline-block text-center mr-2">
<label>Devices Online</label>
<br />
<span class="badge badge-primary p-2">@ServiceSessionCache.GetAllDevices().Count</span>
</div>
<div class="d-inline-block text-center mr-2">
<label>Devices Outdated</label>
<br />
<div class="row">
<div class="col-md-8">
<div class="mb-4" style="display:flex; justify-content: space-evenly">
<div class="d-inline-block text-center mr-2">
<label>Devices Online</label>
<br />
<span class="badge badge-primary p-2">@ServiceSessionCache.GetAllDevices().Count</span>
</div>
<button class="btn btn-sm btn-secondary" @onclick="ShowOutdatedDevices">
<i class="oi oi-question-mark"></i>
</button>
<span class="badge badge-primary p-2 mx-2">@(OutdatedDevices?.Count() ?? 0)</span>
<button class="btn btn-secondary btn-sm" type="button" title="Update All" @onclick="UpdateAllDevices">
<span class="oi oi-reload"></span>
</button>
</div>
<div class="d-inline-block text-center mr-2">
<label>Devices Outdated</label>
<br />
<div class="d-inline-block text-center mr-2">
<label>Devices Total</label>
<br />
<span class="badge badge-primary p-2">@TotalDevices</span>
</div>
<div class="d-inline-block text-center mr-2">
<label>Active Users</label>
<br />
<span class="badge badge-primary p-2">@CircuitManager.Connections.Count</span>
</div>
<button class="btn btn-sm btn-secondary" @onclick="ShowOutdatedDevices">
<i class="oi oi-question-mark"></i>
</button>
<span class="badge badge-primary p-2 mx-2">@(OutdatedDevices?.Count() ?? 0)</span>
<button class="btn btn-secondary btn-sm" type="button" title="Update All" @onclick="UpdateAllDevices">
<span class="oi oi-reload"></span>
</button>
</div>
<div class="d-inline-block text-center mr-2">
<label>Devices Total</label>
<br />
<span class="badge badge-primary p-2">@TotalDevices</span>
</div>
<div class="d-inline-block text-center mr-2">
<label>Active Users</label>
<br />
<span class="badge badge-primary p-2">@CircuitManager.Connections.Count</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<h3 class="mt3">Server Admins</h3>
<div class="form-group">
<label class="control-label">Server Admins</label>
<br />
<div class="small">
<input class="align-middle mr-1" type="checkbox" @bind="_showMyOrgAdminsOnly" />
<span class="align-middle">Show my organization only</span>
</div>
<div class="small">
<input class="align-middle mr-1" type="checkbox" @bind="_showAdminsOnly" />
<span class="align-middle">Show current admins only</span>
</div>
<br />
<div class="list-box">
@foreach (var user in UserList)
{
<div @key="user.Id">
<input type="checkbox" disabled="@(user.Id == User.Id)" checked="@user.IsServerAdmin" @onchange="ev => SetIsServerAdmin(ev, user)" />
<span class="ml-2 align-top" style="line-height:1.3em">@user.UserName</span>
</div>
}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<EditForm Model="Input" OnValidSubmit="HandleValidSubmit" autocomplete="off">
<DataAnnotationsValidator />
<h3 class="mt-3">Application Settings</h3>
<ValidationSummary Model="Input" />
<div class="row">
<div class="col-md-8">
<h3 class="mt3">Server Admins</h3>
<div class="form-group">
<label class="control-label">Server Admins</label>
<label>Allow API Login</label>
<br />
<div class="small">
<input class="align-middle mr-1" type="checkbox" @bind="_showMyOrgAdminsOnly" />
<span class="align-middle">Show my organization only</span>
</div>
<div class="small">
<input class="align-middle mr-1" type="checkbox" @bind="_showAdminsOnly" />
<span class="align-middle">Show current admins only</span>
</div>
<InputCheckbox @bind-Value="Input.AllowApiLogin" autocomplete="off" />
<br />
<div class="list-box">
@foreach (var user in UserList)
{
<div @key="user.Id">
<input type="checkbox" disabled="@(user.Id == User.Id)" checked="@user.IsServerAdmin" @onchange="ev => SetIsServerAdmin(ev, user)" />
<span class="ml-2 align-top" style="line-height:1.3em">@user.UserName</span>
</div>
}
</div>
<ValidationMessage For="() => Input.AllowApiLogin" />
</div>
</div>
<div class="form-group">
<label class="control-label">Banned Devices</label>
<br />
<select size="4" class="form-control" @bind="_bannedDeviceSelected">
@foreach (var bannedDevice in Input.BannedDevices)
{
<option @key="bannedDevice" value="@bannedDevice">@bannedDevice</option>
}
</select>
<div class="text-right mb-2 mt-1">
<button type="button" class="btn btn-secondary" @onclick="RemoveBannedDevice">Remove</button>
</div>
<div class="input-group">
<input placeholder="Add banned device ID, name, or IP"
class="form-control" autocomplete="off"
@bind="_bannedDeviceToAdd"
@bind:event="oninput"
@onkeydown="HandleBannedDeviceKeyDown" />
<button type="button" class="btn btn-secondary" @onclick="AddBannedDevice">Add</button>
</div>
<ValidationMessage For="() => Input.BannedDevices" />
</div>
<div class="form-group">
<label class="control-label">Data Retention in Days</label>
<br />
<InputNumber @bind-Value="Input.DataRetentionInDays" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.DataRetentionInDays" />
</div>
<div class="form-group">
<label class="control-label">Database Provider</label>
<br />
<select class="form-control" @bind="Input.DBProvider">
@foreach (var provider in Enum.GetValues<DbProvider>())
{
<option @key="provider" value="@provider">@provider</option>
}
</select>
<br />
<ValidationMessage For="() => Input.DBProvider" />
</div>
<div class="form-group">
<label>Enable Windows Event Log</label>
<br />
<InputCheckbox @bind-Value="Input.EnableWindowsEventLog" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.EnableWindowsEventLog" />
</div>
<div class="form-group">
<label>Enforce Attended Access</label>
<br />
<InputCheckbox @bind-Value="Input.EnforceAttendedAccess" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.EnforceAttendedAccess" />
</div>
<div class="form-group">
<label>Force Client HTTPS</label>
<br />
<InputCheckbox @bind-Value="Input.ForceClientHttps" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.ForceClientHttps" />
</div>
<div class="form-group">
<label class="control-label">Known Proxies</label>
<br />
<select class="form-control" @bind="_knownProxySelected" size="4">
@foreach (var proxy in Input.KnownProxies)
{
<option @key="proxy" value="@proxy">@proxy</option>
}
</select>
<div class="text-right mb-2 mt-1">
<button type="button" class="btn btn-secondary" @onclick="RemoveKnownProxy">Remove</button>
</div>
<div class="input-group">
<input @bind="_knownProxyToAdd"
@bind:event="oninput"
placeholder="Add a known proxy"
class="form-control"
autocomplete="off"
@onkeydown="HandleKnownProxyKeyDown" />
<button type="button" class="btn btn-secondary" @onclick="AddKnownProxy">Add</button>
</div>
<ValidationMessage For="() => Input.KnownProxies" />
</div>
<div class="form-group">
<label class="control-label">Max Organization Count</label>
<br />
<InputNumber @bind-Value="Input.MaxOrganizationCount" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.MaxOrganizationCount" />
</div>
<div class="form-group">
<label class="control-label">Max Concurrent Updates</label>
<br />
<InputNumber @bind-Value="Input.MaxConcurrentUpdates" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.MaxConcurrentUpdates" />
</div>
<div class="form-group">
<label class="control-label">Message of the Day</label>
<br />
<InputText @bind-Value="Input.MessageOfTheDay" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.MessageOfTheDay" />
</div>
<div class="form-group">
<label>Redirect to HTTPS</label>
<br />
<InputCheckbox @bind-Value="Input.RedirectToHttps" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RedirectToHttps" />
</div>
<div class="form-group">
<label>Remote Control Notify User</label>
<br />
<InputCheckbox @bind-Value="Input.RemoteControlNotifyUser" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RemoteControlNotifyUser" />
</div>
<div class="form-group">
<label>Require Authentication on Remote Control Page</label>
<br />
<InputCheckbox @bind-Value="Input.RemoteControlRequiresAuthentication" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RemoteControlRequiresAuthentication" />
</div>
<div class="form-group">
<label class="control-label">Remote Control Session Limit</label>
<br />
<InputNumber @bind-Value="Input.RemoteControlSessionLimit" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RemoteControlSessionLimit" />
</div>
<div class="form-group">
<label>Require 2FA</label>
<br />
<InputCheckbox @bind-Value="Input.Require2FA" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.Require2FA" />
</div>
<div class="form-group">
<label class="control-label">SMTP Display Name</label>
<br />
<InputText @bind-Value="Input.SmtpDisplayName" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpDisplayName" />
</div>
<div class="form-group">
<label class="control-label">SMTP Email</label>
<br />
<InputText type="email" @bind-Value="Input.SmtpEmail" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpEmail" />
</div>
<div class="form-group">
<label class="control-label">SMTP Host</label>
<br />
<InputText @bind-Value="Input.SmtpHost" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpHost" />
</div>
<div class="form-group">
<label class="control-label">SMTP Port</label>
<br />
<InputNumber @bind-Value="Input.SmtpPort" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpPort" />
</div>
<div class="form-group">
<label class="control-label">SMTP Check Certificate Revocation</label>
<div class="small text-muted">
This sometimes needs to be disabled for Let's Encrypt certificates.
</div>
<br />
<InputCheckbox @bind-Value="Input.SmtpCheckCertificateRevocation" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpCheckCertificateRevocation" />
</div>
<div class="form-group">
<label class="control-label">SMTP Local Domain</label>
<br />
<InputText @bind-Value="Input.SmtpLocalDomain" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpLocalDomain" />
</div>
<div class="form-group">
<label class="control-label">SMTP Username</label>
<br />
<InputText @bind-Value="Input.SmtpUserName" class="form-control" autocomplete="new-password" />
<br />
<ValidationMessage For="() => Input.SmtpUserName" />
</div>
<div class="form-group">
<label class="control-label">SMTP Password</label>
<br />
<InputText type="password" @bind-Value="Input.SmtpPassword" class="form-control" autocomplete="new-password" />
<br />
<ValidationMessage For="() => Input.SmtpPassword" />
</div>
<div class="form-group text-right">
<button id="sendTestEmailButton" type="button" class="btn btn-secondary" @onclick="SaveAndTestSmtpSettings">Test</button>
</div>
<div class="form-group">
<label class="control-label">Theme</label>
<br />
<select class="form-control" @bind="Input.Theme">
@foreach (var theme in Enum.GetValues<Theme>())
{
<option @key="theme" value="@theme">@theme</option>
}
</select>
<br />
<ValidationMessage For="() => Input.Theme" />
</div>
<div class="form-group">
<label class="control-label">Trusted CORS Origins</label>
<br />
<select class="form-control" @bind="_trustedCorsOriginSelected" size="4">
@foreach (var origin in Input.TrustedCorsOrigins)
{
<option @key="origin" value="@origin">@origin</option>
}
</select>
<div class="text-right mb-2 mt-1">
<button type="button" class="btn btn-secondary" @onclick="RemoveTrustedCorsOrigin">Remove</button>
</div>
<div class="input-group">
<input placeholder="Add trusted URL"
class="form-control"
autocomplete="off"
@bind="_trustedCorsOriginToAdd"
@bind:event="oninput"
@onkeydown="HandleTrustedCorsKeyDown" />
<button id="trustedCorsAddButton" type="button" class="btn btn-secondary" @onclick="AddTrustedCorsOrigin">Add</button>
</div>
<ValidationMessage For="() => Input.TrustedCorsOrigins" />
</div>
<div class="form-group">
<label>Use HSTS</label>
<br />
<InputCheckbox @bind-Value="Input.UseHsts" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.UseHsts" />
</div>
<div class="form-group">
<label>Use HTTP Logging</label>
<br />
<InputCheckbox @bind-Value="Input.UseHttpLogging" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.UseHttpLogging" />
</div>
<h4>Connection Strings</h4>
<div class="form-group">
<label class="control-label">PostgreSQL</label>
<br />
<InputText @bind-Value="ConnectionStrings.PostgreSQL" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.PostgreSQL" />
</div>
<div class="form-group">
<label class="control-label">SQLite</label>
<br />
<InputText @bind-Value="ConnectionStrings.SQLite" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.SQLite" />
</div>
<div class="form-group">
<label class="control-label">SQL Server</label>
<br />
<InputText @bind-Value="ConnectionStrings.SQLServer" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.SQLServer" />
</div>
<div class="form-group mt-3">
<button type="button" class="btn btn-primary" @onclick="Save">Save</button>
</div>
</EditForm>
</div>
<div class="row">
<div class="col-md-8">
<EditForm Model="Input" OnValidSubmit="HandleValidSubmit" autocomplete="off">
<DataAnnotationsValidator />
<h3 class="mt-3">Application Settings</h3>
<ValidationSummary Model="Input" />
<div class="form-group">
<label>Allow API Login</label>
<br />
<InputCheckbox @bind-Value="Input.AllowApiLogin" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.AllowApiLogin" />
</div>
<div class="form-group">
<label class="control-label">Banned Devices</label>
<br />
<select size="4" class="form-control" @bind="_bannedDeviceSelected">
@foreach (var bannedDevice in Input.BannedDevices)
{
<option @key="bannedDevice" value="@bannedDevice">@bannedDevice</option>
}
</select>
<div class="text-right mb-2 mt-1">
<button type="button" class="btn btn-secondary" @onclick="RemoveBannedDevice">Remove</button>
</div>
<div class="input-group">
<input placeholder="Add banned device ID, name, or IP"
class="form-control" autocomplete="off"
@bind="_bannedDeviceToAdd"
@bind:event="oninput"
@onkeydown="HandleBannedDeviceKeyDown" />
<button type="button" class="btn btn-secondary" @onclick="AddBannedDevice">Add</button>
</div>
<ValidationMessage For="() => Input.BannedDevices" />
</div>
<div class="form-group">
<label class="control-label">Data Retention in Days</label>
<br />
<InputNumber @bind-Value="Input.DataRetentionInDays" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.DataRetentionInDays" />
</div>
<div class="form-group">
<label class="control-label">Database Provider</label>
<br />
<select class="form-control" @bind="Input.DBProvider">
@foreach (var provider in Enum.GetValues<DbProvider>())
{
<option @key="provider" value="@provider">@provider</option>
}
</select>
<br />
<ValidationMessage For="() => Input.DBProvider" />
</div>
<div class="form-group">
<label>Enable Windows Event Log</label>
<br />
<InputCheckbox @bind-Value="Input.EnableWindowsEventLog" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.EnableWindowsEventLog" />
</div>
<div class="form-group">
<label>Enforce Attended Access</label>
<br />
<InputCheckbox @bind-Value="Input.EnforceAttendedAccess" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.EnforceAttendedAccess" />
</div>
<div class="form-group">
<label>Force Client HTTPS</label>
<br />
<InputCheckbox @bind-Value="Input.ForceClientHttps" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.ForceClientHttps" />
</div>
<div class="form-group">
<label class="control-label">Known Proxies</label>
<br />
<select class="form-control" @bind="_knownProxySelected" size="4">
@foreach (var proxy in Input.KnownProxies)
{
<option @key="proxy" value="@proxy">@proxy</option>
}
</select>
<div class="text-right mb-2 mt-1">
<button type="button" class="btn btn-secondary" @onclick="RemoveKnownProxy">Remove</button>
</div>
<div class="input-group">
<input @bind="_knownProxyToAdd"
@bind:event="oninput"
placeholder="Add a known proxy"
class="form-control"
autocomplete="off"
@onkeydown="HandleKnownProxyKeyDown" />
<button type="button" class="btn btn-secondary" @onclick="AddKnownProxy">Add</button>
</div>
<ValidationMessage For="() => Input.KnownProxies" />
</div>
<div class="form-group">
<label class="control-label">Max Organization Count</label>
<br />
<InputNumber @bind-Value="Input.MaxOrganizationCount" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.MaxOrganizationCount" />
</div>
<div class="form-group">
<label class="control-label">Max Concurrent Updates</label>
<br />
<InputNumber @bind-Value="Input.MaxConcurrentUpdates" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.MaxConcurrentUpdates" />
</div>
<div class="form-group">
<label class="control-label">Message of the Day</label>
<br />
<InputText @bind-Value="Input.MessageOfTheDay" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.MessageOfTheDay" />
</div>
<div class="form-group">
<label>Redirect to HTTPS</label>
<br />
<InputCheckbox @bind-Value="Input.RedirectToHttps" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RedirectToHttps" />
</div>
<div class="form-group">
<label>Remote Control Notify User</label>
<br />
<InputCheckbox @bind-Value="Input.RemoteControlNotifyUser" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RemoteControlNotifyUser" />
</div>
<div class="form-group">
<label>Require Authentication on Remote Control Page</label>
<br />
<InputCheckbox @bind-Value="Input.RemoteControlRequiresAuthentication" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RemoteControlRequiresAuthentication" />
</div>
<div class="form-group">
<label class="control-label">Remote Control Session Limit</label>
<br />
<InputNumber @bind-Value="Input.RemoteControlSessionLimit" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.RemoteControlSessionLimit" />
</div>
<div class="form-group">
<label>Require 2FA</label>
<br />
<InputCheckbox @bind-Value="Input.Require2FA" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.Require2FA" />
</div>
<div class="form-group">
<label class="control-label">SMTP Display Name</label>
<br />
<InputText @bind-Value="Input.SmtpDisplayName" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpDisplayName" />
</div>
<div class="form-group">
<label class="control-label">SMTP Email</label>
<br />
<InputText type="email" @bind-Value="Input.SmtpEmail" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpEmail" />
</div>
<div class="form-group">
<label class="control-label">SMTP Host</label>
<br />
<InputText @bind-Value="Input.SmtpHost" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpHost" />
</div>
<div class="form-group">
<label class="control-label">SMTP Port</label>
<br />
<InputNumber @bind-Value="Input.SmtpPort" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpPort" />
</div>
<div class="form-group">
<label class="control-label">SMTP Check Certificate Revocation</label>
<div class="small text-muted">
This sometimes needs to be disabled for Let's Encrypt certificates.
</div>
<br />
<InputCheckbox @bind-Value="Input.SmtpCheckCertificateRevocation" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpCheckCertificateRevocation" />
</div>
<div class="form-group">
<label class="control-label">SMTP Local Domain</label>
<br />
<InputText @bind-Value="Input.SmtpLocalDomain" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.SmtpLocalDomain" />
</div>
<div class="form-group">
<label class="control-label">SMTP Username</label>
<br />
<InputText @bind-Value="Input.SmtpUserName" class="form-control" autocomplete="new-password" />
<br />
<ValidationMessage For="() => Input.SmtpUserName" />
</div>
<div class="form-group">
<label class="control-label">SMTP Password</label>
<br />
<InputText type="password" @bind-Value="Input.SmtpPassword" class="form-control" autocomplete="new-password" />
<br />
<ValidationMessage For="() => Input.SmtpPassword" />
</div>
<div class="form-group text-right">
<button id="sendTestEmailButton" type="button" class="btn btn-secondary" @onclick="SaveAndTestSmtpSettings">Test</button>
</div>
<div class="form-group">
<label class="control-label">Theme</label>
<br />
<select class="form-control" @bind="Input.Theme">
@foreach (var theme in Enum.GetValues<Theme>())
{
<option @key="theme" value="@theme">@theme</option>
}
</select>
<br />
<ValidationMessage For="() => Input.Theme" />
</div>
<div class="form-group">
<label class="control-label">Trusted CORS Origins</label>
<br />
<select class="form-control" @bind="_trustedCorsOriginSelected" size="4">
@foreach (var origin in Input.TrustedCorsOrigins)
{
<option @key="origin" value="@origin">@origin</option>
}
</select>
<div class="text-right mb-2 mt-1">
<button type="button" class="btn btn-secondary" @onclick="RemoveTrustedCorsOrigin">Remove</button>
</div>
<div class="input-group">
<input placeholder="Add trusted URL"
class="form-control"
autocomplete="off"
@bind="_trustedCorsOriginToAdd"
@bind:event="oninput"
@onkeydown="HandleTrustedCorsKeyDown" />
<button id="trustedCorsAddButton" type="button" class="btn btn-secondary" @onclick="AddTrustedCorsOrigin">Add</button>
</div>
<ValidationMessage For="() => Input.TrustedCorsOrigins" />
</div>
<div class="form-group">
<label>Use HSTS</label>
<br />
<InputCheckbox @bind-Value="Input.UseHsts" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.UseHsts" />
</div>
<div class="form-group">
<label>Use HTTP Logging</label>
<br />
<InputCheckbox @bind-Value="Input.UseHttpLogging" autocomplete="off" />
<br />
<ValidationMessage For="() => Input.UseHttpLogging" />
</div>
<h4>Connection Strings</h4>
<div class="form-group">
<label class="control-label">PostgreSQL</label>
<br />
<InputText @bind-Value="ConnectionStrings.PostgreSQL" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.PostgreSQL" />
</div>
<div class="form-group">
<label class="control-label">SQLite</label>
<br />
<InputText @bind-Value="ConnectionStrings.SQLite" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.SQLite" />
</div>
<div class="form-group">
<label class="control-label">SQL Server</label>
<br />
<InputText @bind-Value="ConnectionStrings.SQLServer" class="form-control" autocomplete="off" />
<br />
<ValidationMessage For="() => ConnectionStrings.SQLServer" />
</div>
<div class="form-group mt-3">
<button type="button" class="btn btn-primary" @onclick="Save">Save</button>
</div>
</EditForm>
</div>
</div>
}
else
{
<h5 class="text-muted">Only organization administrators can view this page.</h5>
}
</div>

View File

@ -1,6 +1,6 @@
@page "/server-logs"
@using System.Collections.ObjectModel;
@attribute [Authorize]
@attribute [Authorize(Policy = PolicyNames.ServerAdminRequired)]
@inherits AuthComponentBase
@inject IDataService DataService
@ -11,65 +11,57 @@
<h3 class="mb-3">Server Logs</h3>
@if (User?.IsAdministrator == true)
{
<div class="buttons-row mb-3">
<div>
<button class="btn btn-danger" type="button" @onclick="ClearAllLogs">
Delete Logs
</button>
</div>
<div>
<button class="btn btn-primary"
onclick="window.open('/API/ServerLogs/Download', '_blank')">
Download Logs
</button>
</div>
<div>
<button class="btn btn-primary"
onclick="window.open('/API/ScriptResults', '_blank')">
Download Script History
</button>
</div>
<div class="buttons-row mb-3">
<div>
<button class="btn btn-danger" type="button" @onclick="ClearAllLogs">
Delete Logs
</button>
</div>
<div class="filters-row mb-3">
<div style="display:inline-block">
<strong>Type:</strong>
<br />
<select class="form-control-sm" @bind="LogLevel">
<option value="">All</option>
@foreach (var eventType in Enum.GetValues(typeof(LogLevel)))
{
<option @key="eventType" value="@eventType">@eventType</option>
}
</select>
</div>
<div style="display:inline-block">
<strong>Filter:</strong>
<br />
<input type="text" @bind="MessageFilter" @bind:event="oninput" />
</div>
<div style="display:inline-block">
<strong>From:</strong>
<br />
<input type="date" @bind="FromDate" />
</div>
<div style="display:inline-block">
<strong>To:</strong>
<br />
<input type="date" @bind="ToDate" />
</div>
<div>
<button class="btn btn-primary"
onclick="window.open('/API/ServerLogs/Download', '_blank')">
Download Logs
</button>
</div>
<div>
<button class="btn btn-primary"
onclick="window.open('/API/ScriptResults', '_blank')">
Download Script History
</button>
</div>
</div>
<div class="filters-row mb-3">
<div style="display:inline-block">
<strong>Type:</strong>
<br />
<select class="form-control-sm" @bind="LogLevel">
<option value="">All</option>
@foreach (var eventType in Enum.GetValues(typeof(LogLevel)))
{
<option @key="eventType" value="@eventType">@eventType</option>
}
</select>
</div>
<div style="display:inline-block">
<strong>Filter:</strong>
<br />
<input type="text" @bind="MessageFilter" @bind:event="oninput" />
</div>
<div style="display:inline-block">
<strong>From:</strong>
<br />
<input type="date" @bind="FromDate" />
</div>
<div style="display:inline-block">
<strong>To:</strong>
<br />
<input type="date" @bind="ToDate" />
</div>
</div>
<textarea readonly class="logs-content bg-dark text-white">
@(string.Join(Environment.NewLine, _filteredLogs))
</textarea>
}
else
{
<h5 class="text-muted">Only organization administrators can view this page.</h5>
}
<textarea readonly class="logs-content bg-dark text-white">
@(string.Join(Environment.NewLine, _filteredLogs))
</textarea>
@code {
private readonly string _messageDebounceKey = Guid.NewGuid().ToString();
@ -98,14 +90,14 @@ else
private LogLevel? LogLevel
{
get => _logLevel;
set
set
{
_logLevel = value;
_ = RefreshLogs();
}
}
private string MessageFilter
private string MessageFilter
{
get => _messageFilter;
set
@ -113,7 +105,7 @@ else
_messageFilter = value;
Debouncer.Debounce(
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1),
() => _ = RefreshLogs(),
_messageDebounceKey);
}
@ -136,10 +128,20 @@ else
protected override async Task OnInitializedAsync()
{
await RefreshLogs();
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await RefreshLogs();
}
}
private async Task ClearAllLogs()
{
var result = await JsInterop.Confirm("Are you sure you want to delete all previous logs? Today's logs are retained.");

View File

@ -100,12 +100,25 @@ services.AddIdentity<RemotelyUser, IdentityRole>(options =>
services.AddScoped<IAuthorizationHandler, TwoFactorRequiredHandler>();
services.AddScoped<IAuthorizationHandler, OrganizationAdminRequirementHandler>();
services.AddScoped<IAuthorizationHandler, ServerAdminRequirementHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy(TwoFactorRequiredRequirement.PolicyName, builder =>
options.AddPolicy(PolicyNames.TwoFactorRequired, builder =>
{
builder.Requirements.Add(new TwoFactorRequiredRequirement());
});
options.AddPolicy(PolicyNames.OrganizationAdminRequired, builder =>
{
builder.Requirements.Add(new OrganizationAdminRequirement());
});
options.AddPolicy(PolicyNames.ServerAdminRequired, builder =>
{
builder.Requirements.Add(new ServerAdminRequirement());
});
});
services.AddRazorPages();
@ -337,13 +350,16 @@ void ConfigureSerilog(WebApplicationBuilder webAppBuilder)
{
loggerConfiguration
.Enrich.FromLogContext()
.WriteTo.Console()
.Enrich.WithThreadId()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}")
.WriteTo.File($"{logPath}/Remotely_Server.log",
rollingInterval: RollingInterval.Day,
retainedFileTimeLimit: TimeSpan.FromDays(dataRetentionDays),
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}",
shared: true);
}
// https://github.com/serilog/serilog-aspnetcore#two-stage-initialization
var loggerConfig = new LoggerConfiguration();
ApplySharedLoggerConfig(loggerConfig);
Log.Logger = loggerConfig.CreateBootstrapLogger();

View File

@ -34,6 +34,7 @@
<PackageReference Include="Nihs.SimpleMessenger" Version="1.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

View File

@ -2,7 +2,7 @@
@using Remotely.Server.Auth
@inherits LayoutComponentBase
<AuthorizeView Policy="@TwoFactorRequiredRequirement.PolicyName">
<AuthorizeView Policy="@PolicyNames.TwoFactorRequired">
<Authorized>
<div class="page">
<div class="sidebar">

View File

@ -22,4 +22,5 @@
@using Remotely.Server.Components.TabControl
@using System.Collections.Concurrent
@using Remotely.Server.Components.Scripts
@using Remotely.Server.Components.TreeView
@using Remotely.Server.Components.TreeView
@using Remotely.Server.Auth;