aboutsummaryrefslogtreecommitdiff
path: root/Plugins/OAuth2ClientApplications/src
diff options
context:
space:
mode:
Diffstat (limited to 'Plugins/OAuth2ClientApplications/src')
-rw-r--r--Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs380
-rw-r--r--Plugins/OAuth2ClientApplications/src/Endpoints/UserAppValidator.cs67
-rw-r--r--Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj58
-rw-r--r--Plugins/OAuth2ClientApplications/src/OAuth2ClientAppsEntryPoint.cs65
4 files changed, 570 insertions, 0 deletions
diff --git a/Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs b/Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs
new file mode 100644
index 0000000..1eb2371
--- /dev/null
+++ b/Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs
@@ -0,0 +1,380 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: OAuth2ClientApplications
+* File: ApplicationEndpoint.cs
+*
+* ApplicationEndpoint.cs is part of OAuth2ClientApplications which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* OAuth2ClientApplications is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* OAuth2ClientApplications is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with OAuth2ClientApplications. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+using VNLib.Utils.Memory;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Accounts;
+using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.Oauth.Applications;
+using VNLib.Plugins.Extensions.Validation;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Sql;
+using static VNLib.Plugins.Essentials.Statics;
+
+
+namespace OAuth2ClientApplications.Endpoints
+{
+ [ConfigurationName("applications")]
+ internal sealed class ApplicationEndpoint : ProtectedWebEndpoint
+ {
+ private sealed class ApplicationMessage
+ {
+ [JsonPropertyName("name")]
+ public string? ApplicationName { get; set; }
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+ [JsonPropertyName("client_id")]
+ public string? ClientID { get; set; }
+ [JsonPropertyName("raw_secret")]
+ public string? RawSecret { get; set; }
+ [JsonPropertyName("Id")]
+ public string? ApplicationID { get; set; }
+ [JsonPropertyName("permissions")]
+ public string? Permissions { get; set; }
+ }
+
+ private readonly ApplicationStore Applications;
+ private readonly int MaxAppsPerUser;
+ private readonly string MaxAppOverloadMessage;
+
+ private static readonly UserAppValidator Validator = new();
+
+ public ApplicationEndpoint(PluginBase plugin, IReadOnlyDictionary<string, JsonElement> config)
+ {
+ //Get configuration variables from plugin
+ string? path = config["path"].GetString();
+ MaxAppsPerUser = config["max_apps_per_user"].GetInt32();
+
+ InitPathAndLog(path, plugin.Log);
+ //Load apps
+ Applications = new(plugin.GetContextOptions(), plugin.GetPasswords());
+
+ //Complie overload message
+ MaxAppOverloadMessage = $"You have reached the limit of {MaxAppsPerUser} applications, this application cannot be created";
+ }
+
+ protected override async ValueTask<VfReturnType> GetAsync(HttpEntity ev)
+ {
+ //Try to get a single application from the database
+
+ //Get a single specific application from an appid
+ if (ev.QueryArgs.TryGetNonEmptyValue("Id", out string? appid))
+ {
+ appid = ValidatorExtensions.OnlyAlphaRegx.Replace(appid, "");
+ //Execute get single app
+ UserApplication? singeApp = await Applications.GetSingleAsync(appid, ev.Session.UserID);
+ if (singeApp == null)
+ {
+ ev.CloseResponse(HttpStatusCode.NotFound);
+ return VfReturnType.VirtualSkip;
+ }
+ ev.CloseResponseJson(HttpStatusCode.OK, singeApp);
+ return VfReturnType.VirtualSkip;
+ }
+ //Process a "get all"
+ else
+ {
+ //Create list to store all applications
+ List<UserApplication> applications = Applications.ListRental.Rent();
+ try
+ {
+ //Get all applications to fill the list
+ _ = await Applications.GetCollectionAsync(applications, ev.Session.UserID, MaxAppsPerUser);
+ //Write response (will convert json as needed before releasing the list)
+ ev.CloseResponseJson(HttpStatusCode.OK, applications);
+ return VfReturnType.VirtualSkip;
+ }
+ finally
+ {
+ //Return the list
+ Applications.ListRental.Return(applications);
+ }
+ }
+ }
+ protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
+ {
+ //Default response
+ WebMessage webm = new();
+ //Oauth is only available for local accounts
+ if (!entity.Session.HasLocalAccount())
+ {
+ webm.Result = "OAuth is only available for internal user accounts";
+ entity.CloseResponseJson(HttpStatusCode.Forbidden, webm);
+ return VfReturnType.VirtualSkip;
+ }
+ if (entity.QueryArgs.IsArgumentSet("action", "create"))
+ {
+ return await CreateAppAsync(entity);
+ }
+
+ //Update the application secret
+ else if (entity.QueryArgs.IsArgumentSet("action", "secret"))
+ {
+ using JsonDocument? update = await entity.GetJsonFromFileAsync();
+
+ if(webm.Assert(update != null, "Invalid request"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Update message will include a challenge and an app id
+ string? appId = update.RootElement.GetPropString("Id");
+ string? challenge = update.RootElement.GetPropString("challenge");
+
+ if (string.IsNullOrWhiteSpace(appId))
+ {
+ return VfReturnType.NotFound;
+ }
+
+ /*
+ * A secret update requires a client challenge because
+ * it can log-out active sessions and break access to
+ * other applications
+ */
+
+ if (string.IsNullOrWhiteSpace(challenge) || !entity.Session.VerifyChallenge(challenge))
+ {
+ //return unauthorized
+ webm.Result = "Please check your password";
+ entity.CloseResponseJson(HttpStatusCode.Unauthorized, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Update the app's secret
+ using PrivateString? secret = await Applications.UpdateSecretAsync(entity.Session.UserID, appId);
+
+ if (webm.Assert(secret != null, "Failed to update the application secret"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.InternalServerError, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ /*
+ * We must return the secret to the user.
+ *
+ * The PrivateString must be casted and serialized
+ * while the using statment is in scope
+ */
+ ApplicationMessage result = new()
+ {
+ ApplicationID = appId,
+ //Send raw secret
+ RawSecret = (string)secret
+ };
+ //Must write response while password is in scope
+ entity.CloseResponseJson(HttpStatusCode.OK, result);
+ return VfReturnType.VirtualSkip;
+ }
+ else if (entity.QueryArgs.IsArgumentSet("action", "delete"))
+ {
+ using JsonDocument? update = await entity.GetJsonFromFileAsync();
+
+ if(webm.Assert(update != null, "Invalid request"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Update message will include a challenge and an app id
+ string? appId = update.RootElement.GetPropString("Id");
+ string? challenge = update.RootElement.GetPropString("challenge");
+
+ if (string.IsNullOrWhiteSpace(appId))
+ {
+ return VfReturnType.NotFound;
+ }
+
+ /*
+ * A secret update requires a client challenge because
+ * it can log-out active sessions and break access to
+ * other applications
+ */
+ if (string.IsNullOrWhiteSpace(challenge) || !entity.Session.VerifyChallenge(challenge))
+ {
+ webm.Result = "Please check your password";
+ //return unauthorized
+ entity.CloseResponseJson(HttpStatusCode.Unauthorized, webm);
+ return VfReturnType.VirtualSkip;
+ }
+ //Try to delete the app
+ if (await Applications.DeleteAsync(appId, entity.Session.UserID))
+ {
+ entity.CloseResponse(HttpStatusCode.NoContent);
+ return VfReturnType.VirtualSkip;
+ }
+ }
+ else
+ {
+ webm.Result = "The update type specified is not defined";
+ entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
+ return VfReturnType.VirtualSkip;
+ }
+ return VfReturnType.BadRequest;
+ }
+ protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity)
+ {
+ ValErrWebMessage webm = new();
+
+ //Oauth is only available for local accounts
+ if (!entity.Session.HasLocalAccount())
+ {
+ webm.Result = "OAuth is only available for internal user accounts";
+ entity.CloseResponseJson(HttpStatusCode.Forbidden, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Get the application from client
+ UserApplication? app = await entity.GetJsonFromFileAsync<UserApplication>(SR_OPTIONS);
+
+ if (webm.Assert(app != null, "Application is empty"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, app);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //set user-id
+ app.UserId = entity.Session.UserID;
+ //remove permissions
+ app.Permissions = null;
+
+ //perform validation on the application update (should remove unused fields)
+ if (!Validator.Validate(app, webm))
+ {
+ entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Update the app's meta
+ if (await Applications.UpdateAsync(app))
+ {
+ //Send the app to the client
+ entity.CloseResponse(HttpStatusCode.NoContent);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //The app was not found and could not be updated
+ return VfReturnType.NotFound;
+ }
+
+ private async ValueTask<VfReturnType> CreateAppAsync(HttpEntity entity)
+ {
+ ValErrWebMessage webm = new();
+
+ //Get the application from client
+ UserApplication? newApp = await entity.GetJsonFromFileAsync<UserApplication>(SR_OPTIONS);
+
+ if (webm.Assert(newApp != null, "Application is empty"))
+ {
+ entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Validate the new application
+ if (!Validator.Validate(newApp, webm))
+ {
+ entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //If no premissions are specified, set to "none"
+ if (string.IsNullOrWhiteSpace(newApp.Permissions))
+ {
+ newApp.Permissions = "none";
+ }
+
+ //See if the user has enough room for more apps
+ long appCount = await Applications.GetCountAsync(entity.Session.UserID);
+
+ if (appCount == -1)
+ {
+ webm.Result = $"There was a server error during creation of your application";
+ Log.Error("There was an error retreiving the number of applications for user {id}", entity.Session.UserID);
+ entity.CloseResponseJson(HttpStatusCode.InternalServerError, webm);
+ return VfReturnType.VirtualSkip;
+ }
+ if (webm.Assert(appCount < MaxAppsPerUser, MaxAppOverloadMessage))
+ {
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Parse permission string an re-build it to clean it up
+ newApp.Permissions = ParsePermissions(newApp.Permissions);
+ //Set user-id
+ newApp.UserId = entity.Session.UserID;
+ //Create the new application
+ if (!await Applications.CreateAsync(newApp))
+ {
+ webm.Result = "The was an issue creating your application";
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Make sure to dispose the secret
+ using PrivateString secret = newApp.RawSecret!;
+
+ //Success, now respond to the client with the new app information
+ ApplicationMessage mess = new()
+ {
+ ApplicationID = newApp.Id,
+ ApplicationName = newApp.AppName,
+ RawSecret = (string)secret,
+ ClientID = newApp.ClientId,
+ Description = newApp.AppDescription
+ };
+
+ //Must write response while the secret is in scope
+ entity.CloseResponseJson(HttpStatusCode.Created, mess);
+ return VfReturnType.VirtualSkip;
+ }
+
+ private static string ParsePermissions(string permissions)
+ {
+ StringBuilder builder = new();
+ //Local function for splitting permissions
+ static void SplitCb(ReadOnlySpan<char> permission, StringBuilder builder)
+ {
+ builder.Append(permission);
+ builder.Append(',');
+ }
+ //Split permissions at comma and clean up the entires
+ permissions.AsSpan().Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries, SplitCb, builder);
+ //return the string
+ return builder.ToString();
+ }
+ }
+} \ No newline at end of file
diff --git a/Plugins/OAuth2ClientApplications/src/Endpoints/UserAppValidator.cs b/Plugins/OAuth2ClientApplications/src/Endpoints/UserAppValidator.cs
new file mode 100644
index 0000000..46051e9
--- /dev/null
+++ b/Plugins/OAuth2ClientApplications/src/Endpoints/UserAppValidator.cs
@@ -0,0 +1,67 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: OAuth2ClientApplications
+* File: UserAppValidator.cs
+*
+* UserAppValidator.cs is part of OAuth2ClientApplications which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* OAuth2ClientApplications is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* OAuth2ClientApplications is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with OAuth2ClientApplications. If not, see http://www.gnu.org/licenses/.
+*/
+
+using FluentValidation;
+using FluentValidation.Results;
+
+using VNLib.Plugins.Extensions.Validation;
+using VNLib.Plugins.Essentials.Oauth.Applications;
+
+namespace OAuth2ClientApplications.Endpoints
+{
+ internal class UserAppValidator : AbstractValidator<UserApplication>
+ {
+ public UserAppValidator()
+ {
+ //Name rules
+ RuleFor(p => p.AppName)
+ .Length(1, 50)
+ .WithName("App name")
+ .SpecialCharacters()
+ .WithName("App name");
+ //Description rules
+ RuleFor(app => app.AppDescription)
+ .SpecialCharacters()
+ .WithName("Description")
+ .MaximumLength(100)
+ .WithName("Description");
+ RuleFor(app => app.Permissions)
+ .MaximumLength(100)
+ .SpecialCharacters()
+ .WithMessage("Invalid permissions");
+ }
+
+ public override ValidationResult Validate(ValidationContext<UserApplication> context)
+ {
+ //Get a ref to the app
+ UserApplication app = context.InstanceToValidate;
+ //remove unused fields
+ app.ClientId = null;
+ app.SecretHash = null;
+ //validate the rest of the app
+ return base.Validate(context);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj b/Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj
new file mode 100644
index 0000000..6275a42
--- /dev/null
+++ b/Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj
@@ -0,0 +1,58 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <Company>Vaughn Nugent</Company>
+ <Authors>Vaughn Nugent</Authors>
+ <AssemblyVersion>1.0.2.1</AssemblyVersion>
+ <FileVersion>1.0.2.1</FileVersion>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <Version>1.0.2.1</Version>
+
+ </PropertyGroup>
+
+ <!-- Resolve nuget dll files and store them in the output dir -->
+ <PropertyGroup>
+ <!--Enable dynamic loading-->
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ <Private>false</Private>
+ <GenerateDocumentationFile>False</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="FluentValidation" Version="11.4.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Validation\src\VNLib.Plugins.Extensions.Validation.csproj" />
+ <ProjectReference Include="..\..\..\Libs\VNLib.Plugins.Essentials.Oauth\src\VNLib.Plugins.Essentials.Oauth.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Update="OAuth2ClientApplications.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;F:\Programming\vnlib\devplugins\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>
diff --git a/Plugins/OAuth2ClientApplications/src/OAuth2ClientAppsEntryPoint.cs b/Plugins/OAuth2ClientApplications/src/OAuth2ClientAppsEntryPoint.cs
new file mode 100644
index 0000000..5d257a1
--- /dev/null
+++ b/Plugins/OAuth2ClientApplications/src/OAuth2ClientAppsEntryPoint.cs
@@ -0,0 +1,65 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: OAuth2ClientApplications
+* File: OAuth2ClientAppsEntryPoint.cs
+*
+* OAuth2ClientAppsEntryPoint.cs is part of OAuth2ClientApplications which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* OAuth2ClientApplications is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* OAuth2ClientApplications is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with OAuth2ClientApplications. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Collections.Generic;
+
+using OAuth2ClientApplications.Endpoints;
+
+using VNLib.Utils.Logging;
+using VNLib.Plugins;
+using VNLib.Plugins.Extensions.Loading.Routing;
+
+namespace OAuth2ClientApplications
+{
+ public sealed class OAuth2ClientAppsEntryPoint : PluginBase
+ {
+ public override string PluginName => "OAuth2.ClientApps";
+
+ protected override void OnLoad()
+ {
+ try
+ {
+ //Route the applications endpoint
+ this.Route<ApplicationEndpoint>();
+
+ Log.Information("Plugin Loaded");
+ }
+ catch (KeyNotFoundException kne)
+ {
+ Log.Error("Missing required configuration variables, {reason}", kne.Message);
+ }
+ }
+
+ protected override void OnUnLoad()
+ {
+ Log.Information("Plugin unloaded");
+ }
+
+ protected override void ProcessHostCommand(string cmd)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}