diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml new file mode 100644 index 0000000..aa9b7f0 --- /dev/null +++ b/.github/workflows/nuget-publish.yml @@ -0,0 +1,110 @@ +name: NuGet | Build and Publish + +on: + workflow_dispatch: + push: + branches: + - main + - dev + tags: [ "[0-9]+.[0-9]+.[0-9]+" ] + release: + types: + - published + + +env: + PROJECT_PATH: ./src/AuthentikNet.Api/AuthentikNet.Api.csproj + PACKAGE_OUTPUT_DIRECTORY: ${{ github.workspace }}/output + NUGET_SOURCE_URL: "https://api.nuget.org/v3/index.json" + +jobs: + build-nuget: + name: Build NuGet package + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore ${{ env.PROJECT_PATH }} + + - name: Extract version from project file + id: extract_version + shell: bash + run: | + VERSION=$(grep -oPm1 "(?<=)[^<]+" ${{ env.PROJECT_PATH }}) + PACKAGE_ID=$(grep -oPm1 "(?<=)[^<]+" ${{ env.PROJECT_PATH }} || basename ${{ env.PROJECT_PATH }} .csproj) + ALL_VERSIONS=$(curl -s "https://api.nuget.org/v3-flatcontainer/${PACKAGE_ID,,}/index.json" | jq -r '.versions[]' || true) + MAX_VERSION=$(echo "$ALL_VERSIONS" | sort -V | tail -n1) + if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then + FINAL_VERSION="$VERSION" + elif [[ "${GITHUB_REF}" == "refs/heads/dev" ]]; then + DEV_VERSIONS=$(echo "$ALL_VERSIONS" | grep "^${VERSION}-dev\." || true) + if [[ -z "$DEV_VERSIONS" ]]; then + N=0 + else + N=$(echo "$DEV_VERSIONS" | sed -E "s/^${VERSION}-dev\.([0-9]+)$/\1/" | sort -nr | head -n1) + N=$((N+1)) + fi + FINAL_VERSION="${VERSION}-dev.${N}" + else + SHORT_SHA=$(git rev-parse --short HEAD) + FINAL_VERSION="${VERSION}-${SHORT_SHA}" + fi + + if [[ -n "$MAX_VERSION" ]]; then + if [[ "$(printf '%s\n' "$MAX_VERSION" "$FINAL_VERSION" | sort -V | tail -n1)" != "$FINAL_VERSION" ]]; then + echo "Error: The version $FINAL_VERSION is not greater than the latest version $MAX_VERSION." + exit 1 + fi + fi + + echo "PROJECT_VERSION=$FINAL_VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $FINAL_VERSION" + + - name: Get short SHA + id: short_sha + run: echo "SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Build + run: dotnet build ${{ env.PROJECT_PATH }} --configuration Release --no-restore /p:Version="${{ steps.extract_version.outputs.PROJECT_VERSION }}" + + - name: Pack + run: dotnet pack ${{ env.PROJECT_PATH }} --configuration Release --no-build --output ${{ env.PACKAGE_OUTPUT_DIRECTORY }} /p:Version="${{ steps.extract_version.outputs.PROJECT_VERSION }}" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: nuget-package + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY }}/*.nupkg + + publish-nuget: + name: Publish NuGet package + needs: build-nuget + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' + runs-on: ubuntu-latest + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: nuget-package + path: ${{ env.PACKAGE_OUTPUT_DIRECTORY }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Publish to NuGet + run: | + for f in ${{ env.PACKAGE_OUTPUT_DIRECTORY }}/*.nupkg; do + echo "Publishing $f to NuGet..." + dotnet nuget push "$f" --source ${{ env.NUGET_SOURCE_URL }} --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate + done \ No newline at end of file diff --git a/AuthentikNet.sln b/AuthentikNet.sln index c509662..ed3fea4 100644 --- a/AuthentikNet.sln +++ b/AuthentikNet.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthentikNet.Api", "AuthentikNet.Api\AuthentikNet.Api.csproj", "{D0BDC4CF-C6DC-4DCC-A38A-589FA8FA9ACF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthentikNet.Api", "src\AuthentikNet.Api\AuthentikNet.Api.csproj", "{D0BDC4CF-C6DC-4DCC-A38A-589FA8FA9ACF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/AuthentikNet.Api/AuthentikNet.Api.csproj b/src/AuthentikNet.Api/AuthentikNet.Api.csproj similarity index 55% rename from AuthentikNet.Api/AuthentikNet.Api.csproj rename to src/AuthentikNet.Api/AuthentikNet.Api.csproj index 97f9498..3588225 100644 --- a/AuthentikNet.Api/AuthentikNet.Api.csproj +++ b/src/AuthentikNet.Api/AuthentikNet.Api.csproj @@ -4,20 +4,34 @@ net8.0 enable enable - 0.0.1-dev + + 2025.4.2 + Saph1s - Authentik api client - Copyright (c) Saph1s 2024 - https://github.com/Saph1s/AuthentikNet Saph1s + Authentik API client + Copyright 2024-2025 Anton "Saph1s" Babenko + + https://github.com/Saph1s/AuthentikNet + https://github.com/Saph1s/AuthentikNet + git + true true + + Apache-2.0 README.md - https://github.com/Saph1s/AuthentikNet + + + + authentik;sso;identity + Authentik compatible version - 2025.4.2 + + diff --git a/src/AuthentikNet.Api/Client/Admin/AdminApi.cs b/src/AuthentikNet.Api/Client/Admin/AdminApi.cs new file mode 100644 index 0000000..108d076 --- /dev/null +++ b/src/AuthentikNet.Api/Client/Admin/AdminApi.cs @@ -0,0 +1,173 @@ +using AuthentikNet.Api.Models; +using Version = AuthentikNet.Api.Models.Version; + +namespace AuthentikNet.Api.Client.Admin; + +public class AdminApi +{ + private readonly AuthentikClient _client; + + public AdminApi(AuthentikClient client) + { + _client = client; + } + + /// + /// Read-only view list all installed apps + /// + /// + /// + public async Task> AdminAppsList(CancellationToken cancellationToken = default) + { + return await _client.SendAsync>(HttpMethod.Get, "/admin/apps/", cancellationToken: cancellationToken); + } + + /// + /// Login Metrics per 1h + /// + /// + /// + public async Task AdminMetricsRetrieve(CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Get, "/admin/metrics/", + cancellationToken: cancellationToken); + } + + /// + /// Read-only view list all installed models + /// + /// + /// + public async Task> AdminModelsList(CancellationToken cancellationToken = default) + { + return await _client.SendAsync>(HttpMethod.Get, "/admin/models/", + cancellationToken: cancellationToken); + } + + /// + /// Get settings + /// + /// + /// + public async Task AdminSettingsRetrieve(CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Get, "/admin/settings/", + cancellationToken: cancellationToken); + } + + /// + /// Update settings + /// + /// Settings model + /// + /// + public async Task AdminSettingsUpdate(Settings data, CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Put, "/admin/settings/", data, cancellationToken); + } + + /// + /// Partial update settings + /// + /// PartialSettings moder + /// + /// + public async Task AdminSettingsPartialUpdate(PatchedSettingsRequest data, + CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Patch, "/admin/settings/", data, cancellationToken); + } + + /// + /// Get system information + /// + /// + /// + public async Task AdminSystemRetrieve(CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Get, "/admin/system/", + cancellationToken: cancellationToken); + } + + /// + /// Get system information + /// + /// + /// + public async Task AdminSystemCreate(CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Post, "/admin/system/", + cancellationToken: cancellationToken); + } + + /// + /// Get running and latest version + /// + /// + /// + public async Task AdminVersionRetrieve(CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Get, "/admin/version/", + cancellationToken: cancellationToken); + } + + /// + /// VersionHistory Viewset + /// + /// + /// Which field to use when ordering the results. + /// A search term. + /// + /// + /// + public async Task> AdminVersionHistoryList( + string? build = null, + string? ordering = null, + string? search = null, + string? version = null, + CancellationToken cancellationToken = default) + { + var url = "/admin/version/history/"; + var queryParameters = new Dictionary + { + { "build", build }, + { "ordering", ordering }, + { "search", search }, + { "version", version } + } + .Where(kv => kv.Value != null) + .ToDictionary(kv => kv.Key, kv => kv.Value!); + + if (queryParameters.Count > 0) + { + var query = string.Join("&", queryParameters + .Select(x => $"{x.Key}={Uri.EscapeDataString(x.Value)}")); + url += "?" + query; + } + + return await _client.SendAsync>(HttpMethod.Get, url, cancellationToken: cancellationToken); + } + + /// + /// VersionHistory Viewset + /// + /// A unique integer value identifying this Version history. + /// + /// + public async Task AdminVersionHistoryRetrieve(int id, CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Get, $"/admin/version/history/{id}/", + cancellationToken: cancellationToken); + } + + /// + /// Get currently connected worker count. + /// + /// + /// + public async Task AdminWorkersRetrieve(CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Get, "/admin/workers/", + cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Client/AuthentikClient.cs b/src/AuthentikNet.Api/Client/AuthentikClient.cs new file mode 100644 index 0000000..ca88f79 --- /dev/null +++ b/src/AuthentikNet.Api/Client/AuthentikClient.cs @@ -0,0 +1,108 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using AuthentikNet.Api.Client.Admin; +using AuthentikNet.Api.Client.Core; + +namespace AuthentikNet.Api.Client; + +public class AuthentikClient +{ + private readonly HttpClient _client; + private readonly AuthentikClientOptions _options; + + public AdminApi Admin { get; } + public CoreApi Core { get; } + + public AuthentikClient(HttpClient client, AuthentikClientOptions options) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + + _client.BaseAddress = new Uri(options.BaseUrl); + _client.Timeout = options.Timeout; + + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.Token); + _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("AuthentikNet.Api.Client", + $"{GetType().Assembly.GetName().Version}")); // TODO: Need to set correct version + + Admin = new AdminApi(this); + Core = new CoreApi(this); + } + + public AuthentikClient(AuthentikClientOptions options) : this(new HttpClient(), options) + { + } + + public async Task SendAsync(HttpMethod method, string path, object? data = null, + CancellationToken cancellationToken = default) + { + var request = new HttpRequestMessage(method, _options.BaseUrl + path); + try + { + if (data != null) + { + request.Content = new StringContent( + JsonSerializer.Serialize(data), + Encoding.UTF8, + "application/json" + ); + } + + var response = await _client.SendAsync(request, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + await HandleError(response); + } + + return await DeserializeResponseAsync(response); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private async Task DeserializeResponseAsync(HttpResponseMessage response) + { + var content = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(content) ?? + throw new AuthentikException("Failed to deserialize response", 500); + } + + private async Task HandleError(HttpResponseMessage response) + { + var content = await response.Content.ReadAsStringAsync(); + string errorMessage; + + if (response.Content.Headers.ContentType?.MediaType == "application/json") + { + try + { + var json = JsonDocument.Parse(content); + errorMessage = json.RootElement.ToString(); + } + catch (JsonException) + { + errorMessage = content; + } + } + else + { + errorMessage = content; + } + + throw response.StatusCode switch + { + HttpStatusCode.BadRequest => new AuthentikBadRequestException(errorMessage), + HttpStatusCode.Unauthorized => new AuthentikUnauthorizedException(errorMessage), + HttpStatusCode.Forbidden => new AuthentikForbiddenException(errorMessage), + HttpStatusCode.NotFound => new AuthentikNotFoundException(errorMessage), + _ => new AuthentikException(errorMessage, (int)response.StatusCode) + }; + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Client/AuthentikClientOptions.cs b/src/AuthentikNet.Api/Client/AuthentikClientOptions.cs new file mode 100644 index 0000000..a27f67d --- /dev/null +++ b/src/AuthentikNet.Api/Client/AuthentikClientOptions.cs @@ -0,0 +1,22 @@ +namespace AuthentikNet.Api.Client; + +/// +/// Authentik client options +/// +public class AuthentikClientOptions +{ + /// + /// Base URL for the Authentik API + /// + public string BaseUrl { get; set; } = string.Empty; + + /// + /// API token + /// + public string Token { get; set; } = string.Empty; + + /// + /// Timeout for the HTTP client + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Client/AuthentikException.cs b/src/AuthentikNet.Api/Client/AuthentikException.cs new file mode 100644 index 0000000..46ed12f --- /dev/null +++ b/src/AuthentikNet.Api/Client/AuthentikException.cs @@ -0,0 +1,54 @@ +namespace AuthentikNet.Api.Client; + +/// +/// Generic Authentik exception +/// +public class AuthentikException : Exception +{ + public int StatusCode { get; } + + public AuthentikException(string message, int statusCode) : base(message) + { + StatusCode = statusCode; + } +} + +/// +/// "Bad request" exception +/// +public class AuthentikBadRequestException : AuthentikException +{ + public AuthentikBadRequestException(string message) : base(message, 400) + { + } +} + +/// +/// "Unauthorized" exception +/// +public class AuthentikUnauthorizedException : AuthentikException +{ + public AuthentikUnauthorizedException(string message) : base(message, 401) + { + } +} + +/// +/// "Forbidden" exception +/// +public class AuthentikForbiddenException : AuthentikException +{ + public AuthentikForbiddenException(string message) : base(message, 403) + { + } +} + +/// +/// "Not found" exception +/// +public class AuthentikNotFoundException : AuthentikException +{ + public AuthentikNotFoundException(string message) : base(message, 404) + { + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Client/Core/CoreApi.cs b/src/AuthentikNet.Api/Client/Core/CoreApi.cs new file mode 100644 index 0000000..c7453dd --- /dev/null +++ b/src/AuthentikNet.Api/Client/Core/CoreApi.cs @@ -0,0 +1,189 @@ +using AuthentikNet.Api.Models; +using AuthentikNet.Api.Utils; + +namespace AuthentikNet.Api.Client.Core; + +public class CoreApi +{ + private readonly AuthentikClient _client; + + public CoreApi(AuthentikClient client) + { + _client = client; + } + + /// + /// Retrieve all users + /// + /// Attributes + /// + /// + /// + /// + /// + /// + /// Which field to use when ordering the results. + /// A page number within the paginated result set. + /// Number of results to return per page. + /// + /// + /// A search term. + /// + /// + /// + /// + /// + /// + public async Task CoreUsersList( + string? attributes = null, + string? email = null, + string[]? groupsByName = null, + string[]? groupsByPk = null, + bool? isActive = null, + bool? isSuperuser = null, + string? name = null, + string? ordering = null, + int? page = null, + int? pageSize = null, + string? path = null, + string? pathStartswith = null, + string? search = null, + string[]? type_ = null, + string? username = null, + Guid? uuid = null, + bool includeGroups = true, CancellationToken cancellationToken = default) + + { + var parameters = new Dictionary + { + { "attributes", attributes }, + { "email", email }, + { "groups_by_name", groupsByName }, + { "groups_by_pk", groupsByPk }, + { "is_active", isActive }, + { "is_superuser", isSuperuser }, + { "name", name }, + { "ordering", ordering }, + { "page", page }, + { "page_size", pageSize }, + { "path", path }, + { "path_startswith", pathStartswith }, + { "search", search }, + { "type", type_ }, + { "username", username }, + { "uuid", uuid }, + { "include_groups", includeGroups } + }.Where(kv => kv.Value != null) + .ToDictionary(kv => kv.Key, kv => kv.Value); + + var url = QueryStringBuilder.BuildQueryString("/core/users/", parameters); + + return await _client.SendAsync(HttpMethod.Get, url, + cancellationToken: cancellationToken); + } + + /// + /// Create user + /// + /// UserRequest + /// + /// + public async Task CoreUsersCreate(UserRequest data, CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Post, "/core/users/", data, cancellationToken); + } + + /// + /// Retrieve user + /// + /// A unique integer value identifying this User. + /// + /// + public async Task CoreUsersRetrieve(int id, CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Get, $"/core/users/{id}/", + cancellationToken: cancellationToken); + } + + /// + /// Partial update user + /// + /// A unique integer value identifying this User. + /// UserRequest + /// + /// + public async Task CoreUsersPartialUpdate(int id, PatchedUserRequest data, + CancellationToken cancellationToken = default) + { + return await _client.SendAsync(HttpMethod.Patch, $"/core/users/{id}/", data, cancellationToken); + } + + /// + /// Retrieve all groups + /// + /// + /// Attributes + /// + /// Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. + /// + /// Which field to use when ordering the results. + /// A page number within the paginated result set. + /// Number of results to return per page. + /// A search term. + /// + /// + /// + /// + public async Task CoreGroupsList( + int[]? membersByPk = null, + string? attributes = null, + bool? isSuperuser = null, + string[]? membersByName = null, + string? name = null, + string? ordering = null, + int? page = null, + int? pageSize = null, + string? search = null, + string[]? membersByUsername = null, + bool includeUsers = true, + CancellationToken cancellationToken = default) + { + // var url = "/core/groups/"; + var parameters = new Dictionary + { + { "attributes", attributes }, + { "is_superuser", isSuperuser }, + { "members_by_pk", membersByPk }, + { "members_by_name", membersByName }, + { "name", name }, + { "ordering", ordering }, + { "page", page }, + { "page_size", pageSize }, + { "search", search }, + { "members_by_username", membersByUsername }, + { "include_users", includeUsers } + }.Where(kv => kv.Value != null) + .ToDictionary(kv => kv.Key, kv => kv.Value); + + var url = QueryStringBuilder.BuildQueryString("/core/groups/", parameters); + + return await _client.SendAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); + } + + /// + /// Retrieve group + /// + /// A UUID string identifying this Group. + /// + /// + /// + public async Task CoreGroupsRetrieve( + Guid id, + bool includeUsers = true, + CancellationToken cancellationToken = default) + { + var url = $"/core/groups/{id}/?include_users={includeUsers}"; + + return await _client.SendAsync(HttpMethod.Get, url, cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Client/Ssf/SsfApi.cs b/src/AuthentikNet.Api/Client/Ssf/SsfApi.cs new file mode 100644 index 0000000..7b7e26b --- /dev/null +++ b/src/AuthentikNet.Api/Client/Ssf/SsfApi.cs @@ -0,0 +1,54 @@ +using AuthentikNet.Api.Models; + +namespace AuthentikNet.Api.Client.Ssf; + +public class SsfApi +{ + private readonly AuthentikClient _client; + + public SsfApi(AuthentikClient client) + { + _client = client; + } + + /// + /// SSFStream Viewset + /// + /// + /// + /// Which field to use when ordering the results + /// A page number within the paginated result set + /// Number of results to return per page + /// + /// A search term + /// + /// + public async Task SsfStreamsList(string? deliveryMethod, string? endpointUrl, + string? ordering, int? page, + int? pageSize, int? provider, string? search, CancellationToken cancellationToken = default) + { + var url = "/ssf/streams/"; + var queryParameters = new Dictionary + { + { "delivery_method", deliveryMethod }, + { "endpoint_url", endpointUrl }, + { "ordering", ordering }, + { "page", page }, + { "page_size", pageSize }, + { "provider", provider }, + { "search", search } + } + .Where(kv => kv.Value != null) + .ToDictionary(kv => kv.Key, kv => kv.Value!); + + if (queryParameters.Count > 0) + { + var query = string.Join("&", queryParameters + .Select(x => $"{x.Key}={Uri.EscapeDataString(x.Value.ToString()!)}")); + url += "?" + query; + } + + return await _client.SendAsync(HttpMethod.Get, url, + cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/LICENSE b/src/AuthentikNet.Api/LICENSE new file mode 100644 index 0000000..886bc77 --- /dev/null +++ b/src/AuthentikNet.Api/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024-2025 Anton "Saph1s" Babenko (https://github.com/saph1s) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/AccessDeniedChallenge.cs b/src/AuthentikNet.Api/Models/AccessDeniedChallenge.cs new file mode 100644 index 0000000..0e1f65d --- /dev/null +++ b/src/AuthentikNet.Api/Models/AccessDeniedChallenge.cs @@ -0,0 +1,6 @@ +namespace AuthentikNet.Api.Models; + +public class AccessDeniedChallenge +{ + +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/App.cs b/src/AuthentikNet.Api/Models/App.cs new file mode 100644 index 0000000..8db717e --- /dev/null +++ b/src/AuthentikNet.Api/Models/App.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class App +{ + [JsonPropertyName("name")] public required string Name { get; set; } + [JsonPropertyName("label")] public required string Label { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/Coordinate.cs b/src/AuthentikNet.Api/Models/Coordinate.cs new file mode 100644 index 0000000..0704dbd --- /dev/null +++ b/src/AuthentikNet.Api/Models/Coordinate.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class Coordinate +{ + [JsonPropertyName("x_cord")] public required int XCord { get; init; } + [JsonPropertyName("y_cord")] public required int YCord { get; init; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/DeliveryMethodEnum.cs b/src/AuthentikNet.Api/Models/DeliveryMethodEnum.cs new file mode 100644 index 0000000..92b4626 --- /dev/null +++ b/src/AuthentikNet.Api/Models/DeliveryMethodEnum.cs @@ -0,0 +1,21 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; + +namespace AuthentikNet.Api.Models; + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum DeliveryMethodEnum +{ + /// + /// PUSH + /// + [EnumMember(Value = "https://schemas.openid.net/secevent/risc/delivery-method/push")] + Push, + + /// + /// POLL + /// + [EnumMember(Value = "https://schemas.openid.net/secevent/risc/delivery-method/poll")] + Poll +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/EventsRequestedEnum.cs b/src/AuthentikNet.Api/Models/EventsRequestedEnum.cs new file mode 100644 index 0000000..35e78e5 --- /dev/null +++ b/src/AuthentikNet.Api/Models/EventsRequestedEnum.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; + +namespace AuthentikNet.Api.Models; + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum EventsRequestedEnum +{ + [EnumMember(Value = "https://schemas.openid.net/secevent/caep/event-type/session-revoked")] + CAEP_EVENT_TYPE_SESSION_REVOKED, + + [EnumMember(Value = "https://schemas.openid.net/secevent/caep/event-type/credential-change")] + CAEP_EVENT_TYPE_CREDENTIAL_CHANGE, + + [EnumMember(Value = "https://schemas.openid.net/secevent/ssf/event-type/verification")] + SSF_EVENT_TYPE_VERIFICATION +} \ No newline at end of file diff --git a/AuthentikNet.Api/Models/Group.cs b/src/AuthentikNet.Api/Models/Group.cs similarity index 82% rename from AuthentikNet.Api/Models/Group.cs rename to src/AuthentikNet.Api/Models/Group.cs index 2805bd8..8ff1e56 100644 --- a/AuthentikNet.Api/Models/Group.cs +++ b/src/AuthentikNet.Api/Models/Group.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; namespace AuthentikNet.Api.Models; @@ -12,7 +13,11 @@ public class Group [JsonPropertyName("parent_name")] public required string? ParentName { get; init; } [JsonPropertyName("users")] public List Users { get; set; } = []; [JsonPropertyName("users_obj")] public required List UsersObj { get; init; } - [JsonPropertyName("attributes")] public Dictionary Attributes { get; set; } = new(); + + [JsonPropertyName("attributes")] + [JsonConverter(typeof(DynamicAttributesJsonConverter))] + public object Attributes { get; set; } = new(); + [JsonPropertyName("roles")] public List Roles { get; set; } = []; [JsonPropertyName("roles_obj")] public required List RolesObj { get; init; } } \ No newline at end of file diff --git a/AuthentikNet.Api/Models/GroupMember.cs b/src/AuthentikNet.Api/Models/GroupMember.cs similarity index 79% rename from AuthentikNet.Api/Models/GroupMember.cs rename to src/AuthentikNet.Api/Models/GroupMember.cs index 75f76c5..a9e4220 100644 --- a/AuthentikNet.Api/Models/GroupMember.cs +++ b/src/AuthentikNet.Api/Models/GroupMember.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; namespace AuthentikNet.Api.Models; @@ -15,6 +16,10 @@ public class GroupMember [JsonPropertyName("is_active")] public bool IsActive { get; set; } [JsonPropertyName("last_login")] public DateTime? LastLogin { get; set; } [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; - [JsonPropertyName("attributes")] public Dictionary Attributes { get; set; } = new(); + + [JsonPropertyName("attributes")] + [JsonConverter(typeof(DynamicAttributesJsonConverter))] + public object Attributes { get; set; } = new(); + [JsonPropertyName("uid")] public required string Uid { get; init; } } \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/IntentEnum.cs b/src/AuthentikNet.Api/Models/IntentEnum.cs new file mode 100644 index 0000000..2deef8c --- /dev/null +++ b/src/AuthentikNet.Api/Models/IntentEnum.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; + +namespace AuthentikNet.Api.Models; + +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum IntentEnum +{ + [EnumMember(Value = "verification")] Verification, + [EnumMember(Value = "api")] Api, + [EnumMember(Value = "recovery")] Recovery, + [EnumMember(Value = "app_password")] AppPassword, +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/LoginMetrics.cs b/src/AuthentikNet.Api/Models/LoginMetrics.cs new file mode 100644 index 0000000..98d800c --- /dev/null +++ b/src/AuthentikNet.Api/Models/LoginMetrics.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class LoginMetrics +{ + [JsonPropertyName("logins")] public required List Logins { get; init; } + [JsonPropertyName("logins_failed")] public required List LoginsFailed { get; init; } + [JsonPropertyName("authorizations")] public required List Authorizations { get; init; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/PaginatedGroupList.cs b/src/AuthentikNet.Api/Models/PaginatedGroupList.cs new file mode 100644 index 0000000..856a7e0 --- /dev/null +++ b/src/AuthentikNet.Api/Models/PaginatedGroupList.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class PaginatedGroupList +{ + [JsonPropertyName("pagination")] public required Pagination Pagination { get; set; } + [JsonPropertyName("results")] public required List Results { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/PaginatedSSFStreamList.cs b/src/AuthentikNet.Api/Models/PaginatedSSFStreamList.cs new file mode 100644 index 0000000..4b8ba0f --- /dev/null +++ b/src/AuthentikNet.Api/Models/PaginatedSSFStreamList.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class PaginatedSSFStreamList +{ + [JsonPropertyName("pagination")] public required Pagination Pagination { get; set; } + [JsonPropertyName("results")] public required List Results { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/PaginatedUserList.cs b/src/AuthentikNet.Api/Models/PaginatedUserList.cs new file mode 100644 index 0000000..792241c --- /dev/null +++ b/src/AuthentikNet.Api/Models/PaginatedUserList.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class PaginatedUserList +{ + [JsonPropertyName("pagination")] public required Pagination Pagination { get; set; } + [JsonPropertyName("results")] public required List Results { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/Pagination.cs b/src/AuthentikNet.Api/Models/Pagination.cs new file mode 100644 index 0000000..f75a435 --- /dev/null +++ b/src/AuthentikNet.Api/Models/Pagination.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class Pagination +{ + [JsonPropertyName("next")] public required float Next { get; set; } + [JsonPropertyName("previous")] public required float Previous { get; set; } + [JsonPropertyName("count")] public required float Count { get; set; } + [JsonPropertyName("current")] public required float Current { get; set; } + [JsonPropertyName("total_pages")] public required float TotalPages { get; set; } + [JsonPropertyName("start_index")] public required float StartIndex { get; set; } + [JsonPropertyName("end_index")] public required float EndIndex { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/PatchedSettingsRequest.cs b/src/AuthentikNet.Api/Models/PatchedSettingsRequest.cs new file mode 100644 index 0000000..c84e336 --- /dev/null +++ b/src/AuthentikNet.Api/Models/PatchedSettingsRequest.cs @@ -0,0 +1,120 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +/// +/// Settings PATCH request +/// +public class PatchedSettingsRequest +{ + private int? _defaultTokenLength; + + /// + /// Configure how authentik should show avatars for users. + /// + [JsonPropertyName("avatars")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Avatars { get; set; } + + /// + /// Enable the ability for users to change their name. + /// + [JsonPropertyName("default_user_change_name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DefaultUserChangeName { get; set; } + + /// + /// Enable the ability for users to change their email address. + /// + [JsonPropertyName("default_user_change_email")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DefaultUserChangeEmail { get; set; } + + /// + /// Enable the ability for users to change their username. + /// + [JsonPropertyName("default_user_change_username")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DefaultUserChangeUsername { get; set; } + + /// + /// Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2). + /// + [JsonPropertyName("event_retention")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? EventRetention { get; set; } + + /// + /// Reputation cannot decrease lower than this value. Zero or negative. + /// + [JsonPropertyName("reputation_lower_limit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? ReputationLowerLimit { get; set; } + + /// + /// Reputation cannot increase higher than this value. Zero or positive. + /// + [JsonPropertyName("reputation_upper_limit")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? ReputationUpperLimit { get; set; } + + /// + /// The option configures the footer links on the flow executor pages. + /// + [JsonPropertyName("footer_links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List<(string href, string name)>? FooterLinks { get; set; } = []; + + /// + /// When enabled, all the events caused by a user will be deleted upon the user's deletion. + /// + [JsonPropertyName("gdpr_compliance")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? GdprCompliance { get; set; } + + /// + /// Globally enable/disable impersonation. + /// + [JsonPropertyName("impersonation")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Impersonation { get; set; } + + /// + /// Require administrators to provide a reason for impersonating a user. + /// + [JsonPropertyName("impersonation_require_reason")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? ImpersonationRequireReason { get; set; } + + /// + /// Default token duration + /// + [JsonPropertyName("default_token_duration")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? DefaultTokenDuration { get; set; } + + /// + /// Default token length + /// + [JsonPropertyName("default_token_length")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? DefaultTokenLength + { + get => _defaultTokenLength; + set + { + if (value < 1) + throw new ArgumentOutOfRangeException(nameof(DefaultTokenLength), + "Value must be greater or equal to 1 and less or equal to 2147483647"); + + _defaultTokenLength = value; + } + } + + /// + /// PatchedSettingsRequest constructor + /// + public PatchedSettingsRequest() + { + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/PatchedUserRequest.cs b/src/AuthentikNet.Api/Models/PatchedUserRequest.cs new file mode 100644 index 0000000..f300812 --- /dev/null +++ b/src/AuthentikNet.Api/Models/PatchedUserRequest.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; + +namespace AuthentikNet.Api.Models; + +public class PatchedUserRequest +{ + [JsonPropertyName("username")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Username { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Name { get; set; } + + [JsonPropertyName("is_active")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? IsActive { get; set; } + + [JsonPropertyName("last_login")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DateTime? LastLogin { get; set; } + + [JsonPropertyName("groups")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Groups { get; set; } + + [JsonPropertyName("email")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Email { get; set; } + + [JsonPropertyName("attributes")] + [JsonConverter(typeof(DynamicAttributesJsonConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public object? Attributes { get; set; } + + [JsonPropertyName("path")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Path { get; set; } + + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public UserTypeEnum? Type { get; set; } +} \ No newline at end of file diff --git a/AuthentikNet.Api/Models/Role.cs b/src/AuthentikNet.Api/Models/Role.cs similarity index 100% rename from AuthentikNet.Api/Models/Role.cs rename to src/AuthentikNet.Api/Models/Role.cs diff --git a/src/AuthentikNet.Api/Models/SSFProvider.cs b/src/AuthentikNet.Api/Models/SSFProvider.cs new file mode 100644 index 0000000..215e492 --- /dev/null +++ b/src/AuthentikNet.Api/Models/SSFProvider.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class SSFProvider +{ + [JsonPropertyName("pk")] public required int Pk { get; init; } + [JsonPropertyName("name")] public required string Name { get; set; } + + /// + /// Get object component so that we know how to edit the object + /// + [JsonPropertyName("component")] + public required string Component { get; init; } + + /// + /// Return object's verbose_name + /// + [JsonPropertyName("verbose_name")] + public required string VerboseName { get; init; } + + /// + /// Return object's plural verbose_name + /// + [JsonPropertyName("verbose_name_plural")] + public required string VerboseNamePlural { get; init; } + + /// + /// Return internal model name + /// + [JsonPropertyName("meta_model_name")] + public required string MetaModelName { get; init; } + + /// + /// Key used to sign the SSF Events. + /// + [JsonPropertyName("signing_key")] + public required Guid SigningKey { get; init; } + + [JsonPropertyName("token_obj")] public required Token TokenObj { get; init; } + + [JsonPropertyName("oidc_auth_providers")] + public List? OidcAuthProviders { get; set; } + + [JsonPropertyName("ssf_url")] public required string? SSFUrl { get; init; } + [JsonPropertyName("event_retention")] public string? EventRetention { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/SSFStream.cs b/src/AuthentikNet.Api/Models/SSFStream.cs new file mode 100644 index 0000000..2f05763 --- /dev/null +++ b/src/AuthentikNet.Api/Models/SSFStream.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class SSFStream +{ + [JsonPropertyName("pk")] public required Guid Pk { get; init; } + [JsonPropertyName("provider")] public required int Provider { get; set; } + [JsonPropertyName("provider_obj")] public required SSFProvider ProviderObj { get; init; } + [JsonPropertyName("delivery_method")] public required DeliveryMethodEnum DeliveryMethod { get; set; } + [JsonPropertyName("endpoint_url")] public string? EndpointUrl { get; set; } + [JsonPropertyName("events_requested")] public List? EventsRequested { get; set; } + [JsonPropertyName("format")] public required string Format { get; set; } + [JsonPropertyName("aud")] public List? Aud { get; init; } + [JsonPropertyName("iss")] public required string Iss { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/Settings.cs b/src/AuthentikNet.Api/Models/Settings.cs new file mode 100644 index 0000000..2b78be3 --- /dev/null +++ b/src/AuthentikNet.Api/Models/Settings.cs @@ -0,0 +1,100 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +/// +/// Settings +/// +public class Settings +{ + private int _defaultTokenLength; + + /// + /// Configure how authentik should show avatars for users. + /// + [JsonPropertyName("avatars")] + public required string Avatars { get; set; } + + /// + /// Enable the ability for users to change their name. + /// + [JsonPropertyName("default_user_change_name")] + public required bool DefaultUserChangeName { get; set; } + + /// + /// Enable the ability for users to change their email address. + /// + [JsonPropertyName("default_user_change_email")] + public required bool DefaultUserChangeEmail { get; set; } + + /// + /// Enable the ability for users to change their username. + /// + [JsonPropertyName("default_user_change_username")] + public required bool DefaultUserChangeUsername { get; set; } + + /// + /// Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2). + /// + [JsonPropertyName("event_retention")] + public required string EventRetention { get; set; } + + /// + /// Reputation cannot decrease lower than this value. Zero or negative. + /// + [JsonPropertyName("reputation_lower_limit")] + public required int ReputationLowerLimit { get; set; } + + /// + /// Reputation cannot increase higher than this value. Zero or positive. + /// + [JsonPropertyName("reputation_upper_limit")] + public required int ReputationUpperLimit { get; set; } + + /// + /// The option configures the footer links on the flow executor pages. + /// + [JsonPropertyName("footer_links")] + public required List<(string href, string name)> FooterLinks { get; set; } = []; + + /// + /// When enabled, all the events caused by a user will be deleted upon the user's deletion. + /// + [JsonPropertyName("gdpr_compliance")] + public required bool GdprCompliance { get; set; } + + /// + /// Globally enable/disable impersonation. + /// + [JsonPropertyName("impersonation")] + public required bool Impersonation { get; set; } + + /// + /// Require administrators to provide a reason for impersonating a user. + /// + [JsonPropertyName("impersonation_require_reason")] + public required bool ImpersonationRequireReason { get; set; } + + /// + /// Default token duration + /// + [JsonPropertyName("default_token_duration")] + public required string DefaultTokenDuration { get; set; } + + /// + /// Default token length + /// + [JsonPropertyName("default_token_length")] + public required int DefaultTokenLength + { + get => _defaultTokenLength; + set + { + if (value < 1) + throw new ArgumentOutOfRangeException(nameof(DefaultTokenLength), + "Value must be greater or equal to 1 and less or equal to 2147483647"); + + _defaultTokenLength = value; + } + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/SystemInfo.cs b/src/AuthentikNet.Api/Models/SystemInfo.cs new file mode 100644 index 0000000..7550896 --- /dev/null +++ b/src/AuthentikNet.Api/Models/SystemInfo.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class SystemInfo +{ + /// + /// Get HTTP Request headers + /// + [JsonPropertyName("http_headers")] + public required Dictionary HttpHeaders { get; init; } + + /// + /// Get HTTP host + /// + [JsonPropertyName("http_host")] + public required string HttpHost { get; init; } + + /// + /// Get HTTP Secure flag + /// + [JsonPropertyName("http_is_secure")] + public required bool HttpIsSecure { get; init; } + + /// + /// Get versions + /// + [JsonPropertyName("runtime")] + public required SystemInfoRuntime Runtime { get; init; } + + /// + /// Currently active brand + /// + [JsonPropertyName("brand")] + public required string Brand { get; init; } + + /// + /// Current server time + /// + [JsonPropertyName("server_time")] + public required DateTime ServerTime { get; init; } + + /// + /// Whether the embedded outpost is disabled + /// + [JsonPropertyName("embedded_outpost_disabled")] + public required bool EmbeddedOutpostDisabled { get; init; } + + /// + /// Get the FQDN configured on the embedded outpost + /// + [JsonPropertyName("embedded_outpost_host")] + public required string EmbeddedOutpostHost { get; init; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/SystemInfoRuntime.cs b/src/AuthentikNet.Api/Models/SystemInfoRuntime.cs new file mode 100644 index 0000000..1e99a7a --- /dev/null +++ b/src/AuthentikNet.Api/Models/SystemInfoRuntime.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class SystemInfoRuntime +{ + [JsonPropertyName("python_version")] public required string PythonVersion { get; set; } + [JsonPropertyName("environment")] public required string Environment { get; set; } + [JsonPropertyName("architecture")] public required string Architecture { get; set; } + [JsonPropertyName("platform")] public required string Platform { get; set; } + [JsonPropertyName("uname")] public required string Uname { get; set; } + [JsonPropertyName("openssl_version")] public required string OpensslVersion { get; set; } + + [JsonPropertyName("openssl_fips_enabled")] + public required bool? OpensslFipsEnabled { get; set; } + + [JsonPropertyName("authentik_version")] + public required string AuthentikVersion { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/Token.cs b/src/AuthentikNet.Api/Models/Token.cs new file mode 100644 index 0000000..19a8cc7 --- /dev/null +++ b/src/AuthentikNet.Api/Models/Token.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class Token +{ + [JsonPropertyName("pk")] public required Guid Pk { get; init; } + [JsonPropertyName("managed")] public string? Managed { get; set; } + [JsonPropertyName("identifier")] public required string Identifier { get; set; } + [JsonPropertyName("intent")] public IntentEnum? Intent { get; set; } + [JsonPropertyName("user")] public int? User { get; set; } + [JsonPropertyName("user_obj")] public required User UserObj { get; init; } + [JsonPropertyName("description")] public string? Description { get; set; } + [JsonPropertyName("expires")] public DateTime? Expires { get; set; } + [JsonPropertyName("expiring")] public bool? Expiring { get; set; } +} \ No newline at end of file diff --git a/AuthentikNet.Api/Models/User.cs b/src/AuthentikNet.Api/Models/User.cs similarity index 69% rename from AuthentikNet.Api/Models/User.cs rename to src/AuthentikNet.Api/Models/User.cs index 4c5668a..a66e1e8 100644 --- a/AuthentikNet.Api/Models/User.cs +++ b/src/AuthentikNet.Api/Models/User.cs @@ -1,5 +1,5 @@ -using System.Runtime.Serialization; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; namespace AuthentikNet.Api.Models; @@ -15,22 +15,15 @@ public class User [JsonPropertyName("groups_obj")] public required List GroupsObj { get; init; } [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; [JsonPropertyName("avatar")] public required string Avatar { get; init; } - [JsonPropertyName("attributes")] public Dictionary Attributes { get; set; } = new(); + + [JsonPropertyName("attributes")] + [JsonConverter(typeof(DynamicAttributesJsonConverter))] + public object Attributes { get; set; } = new(); + [JsonPropertyName("uid")] public required string Uid { get; init; } [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; - [JsonPropertyName("type")] public UserTypeEnum Type { get; set; } - [JsonPropertyName("uuid")] public required Guid Uuid { get; init; } -} -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum UserTypeEnum -{ - [EnumMember(Value = "internal")] Internal, - [EnumMember(Value = "external")] External, - - [EnumMember(Value = "service_account")] - ServiceAccount, + [JsonPropertyName("type")] public UserTypeEnum Type { get; set; } - [EnumMember(Value = "internal_service_account")] - InternalServiceAccount + [JsonPropertyName("uuid")] public required Guid Uuid { get; init; } } \ No newline at end of file diff --git a/AuthentikNet.Api/Models/UserGroup.cs b/src/AuthentikNet.Api/Models/UserGroup.cs similarity index 75% rename from AuthentikNet.Api/Models/UserGroup.cs rename to src/AuthentikNet.Api/Models/UserGroup.cs index ef00d6c..0e14175 100644 --- a/AuthentikNet.Api/Models/UserGroup.cs +++ b/src/AuthentikNet.Api/Models/UserGroup.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; namespace AuthentikNet.Api.Models; @@ -10,5 +11,8 @@ public class UserGroup [JsonPropertyName("is_superuser")] public bool IsSuperUser { get; set; } [JsonPropertyName("parent")] public Guid? Parent { get; init; } [JsonPropertyName("parent_name")] public required string? ParentName { get; init; } - [JsonPropertyName("attributes")] public Dictionary Attributes { get; set; } = new(); + + [JsonPropertyName("attributes")] + [JsonConverter(typeof(DynamicAttributesJsonConverter))] + public object Attributes { get; set; } = new(); } \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/UserRequest.cs b/src/AuthentikNet.Api/Models/UserRequest.cs new file mode 100644 index 0000000..ac0fd16 --- /dev/null +++ b/src/AuthentikNet.Api/Models/UserRequest.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; + +namespace AuthentikNet.Api.Models; + +public class UserRequest +{ + [JsonPropertyName("username")] public required string Username { get; set; } + [JsonPropertyName("name")] public required string Name { get; set; } + [JsonPropertyName("is_active")] public bool IsActive { get; set; } + [JsonPropertyName("last_login")] public DateTime? LastLogin { get; set; } + [JsonPropertyName("groups")] public List Groups { get; set; } = []; + [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; + + [JsonPropertyName("attributes")] + [JsonConverter(typeof(DynamicAttributesJsonConverter))] + public object Attributes { get; set; } = new(); + + [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; + [JsonPropertyName("type")] public UserTypeEnum Type { get; set; } + + /// + /// UserRequest constructor + /// + public UserRequest() + { + } + + /// + /// UserRequest constructor with username and name + /// + /// + /// + public UserRequest(string username, string name) + { + Username = username; + Name = name; + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/UserTypeEnum.cs b/src/AuthentikNet.Api/Models/UserTypeEnum.cs new file mode 100644 index 0000000..659ca03 --- /dev/null +++ b/src/AuthentikNet.Api/Models/UserTypeEnum.cs @@ -0,0 +1,34 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using AuthentikNet.Api.Utils; + +namespace AuthentikNet.Api.Models; + +/// +/// User account type +/// +[JsonConverter(typeof(JsonStringEnumMemberConverter))] +public enum UserTypeEnum +{ + /// + /// Internal + /// + [EnumMember(Value = "internal")] Internal, + + /// + /// External + /// + [EnumMember(Value = "external")] External, + + /// + /// Service account + /// + [EnumMember(Value = "service_account")] + ServiceAccount, + + /// + /// Internal service account + /// + [EnumMember(Value = "internal_service_account")] + InternalServiceAccount +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/Version.cs b/src/AuthentikNet.Api/Models/Version.cs new file mode 100644 index 0000000..012983d --- /dev/null +++ b/src/AuthentikNet.Api/Models/Version.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class Version +{ + /// + /// Get current version + /// + [JsonPropertyName("version_current")] + public required string VersionCurrent { get; init; } + + /// + /// Get latest version from cache + /// + [JsonPropertyName("version_latest")] + public required string VersionLatest { get; init; } + + /// + /// Check if latest version is valid + /// + [JsonPropertyName("version_latest_valid")] + public required bool VersionLatestValid { get; init; } + + /// + /// Get build hash, if version is not latest or released + /// + [JsonPropertyName("build_hash")] + public required string BuildHash { get; init; } + + /// + /// Check if we're running the latest version + /// + [JsonPropertyName("outdated")] + public required bool Outdated { get; init; } + + /// + /// Check if any outpost is outdated/has a version mismatch + /// + [JsonPropertyName("outpost_outdated")] + public required bool OutpostOutdated { get; init; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/VersionHistory.cs b/src/AuthentikNet.Api/Models/VersionHistory.cs new file mode 100644 index 0000000..d172d46 --- /dev/null +++ b/src/AuthentikNet.Api/Models/VersionHistory.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class VersionHistory +{ + [JsonPropertyName("id")] public required int Id { get; init; } + [JsonPropertyName("timestamp")] public required DateTime Timestamp { get; set; } + [JsonPropertyName("version")] public required string Version { get; set; } + [JsonPropertyName("build")] public required string Build { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Models/Workers.cs b/src/AuthentikNet.Api/Models/Workers.cs new file mode 100644 index 0000000..9fa4303 --- /dev/null +++ b/src/AuthentikNet.Api/Models/Workers.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Models; + +public class Workers +{ + [JsonPropertyName("worker_id")] public required int WorkerId { get; set; } + [JsonPropertyName("version")] public required string Version { get; set; } + [JsonPropertyName("version_matching")] public required bool VersionMatching { get; set; } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/NOTICE b/src/AuthentikNet.Api/NOTICE new file mode 100644 index 0000000..30bbde1 --- /dev/null +++ b/src/AuthentikNet.Api/NOTICE @@ -0,0 +1,6 @@ +Copyright 2024-2025 Anton "Saph1s" Babenko (https://github.com/saph1s) + +This product includes software developed by: +- Anton Babenko (https://github.com/saph1s) + +Licensed under the Apache License, Version 2.0 - see LICENSE file. \ No newline at end of file diff --git a/AuthentikNet.Api/README.md b/src/AuthentikNet.Api/README.md similarity index 100% rename from AuthentikNet.Api/README.md rename to src/AuthentikNet.Api/README.md diff --git a/src/AuthentikNet.Api/Utils/DynamicAttributesJsonConverter.cs b/src/AuthentikNet.Api/Utils/DynamicAttributesJsonConverter.cs new file mode 100644 index 0000000..ae576f7 --- /dev/null +++ b/src/AuthentikNet.Api/Utils/DynamicAttributesJsonConverter.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Utils; + +/// +/// Custom JSON converter for handling dynamic attributes. +/// +public class DynamicAttributesJsonConverter : JsonConverter +{ + /// + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return ParseValue(ref reader, options); + } + + private static object? ParseValue(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.StartObject => ParseObject(ref reader, options), + JsonTokenType.StartArray => ParseArray(ref reader, options), + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.TryGetInt64(out var l) ? l : reader.GetDouble(), + JsonTokenType.True => true, + JsonTokenType.False => false, + JsonTokenType.Null => null, + _ => throw new JsonException($"Unsupported JSON token: {reader.TokenType}") + }; + } + + private static Dictionary ParseObject(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var dictionary = new Dictionary(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + break; + + if (reader.TokenType != JsonTokenType.PropertyName) + throw new JsonException("Expected a property name."); + + var propertyName = reader.GetString(); + reader.Read(); + + var value = ParseValue(ref reader, options); + dictionary[propertyName!] = value; + } + + return dictionary; + } + + private static List ParseArray(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + var list = new List(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + break; + + var value = ParseValue(ref reader, options); + list.Add(value); + } + + return list; + } + + /// + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Utils/JsonStringEnumMemberConverter.cs b/src/AuthentikNet.Api/Utils/JsonStringEnumMemberConverter.cs new file mode 100644 index 0000000..1481c4e --- /dev/null +++ b/src/AuthentikNet.Api/Utils/JsonStringEnumMemberConverter.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AuthentikNet.Api.Utils; + +/// +/// Universal JSON converter for Enums with EnumMember attributes. +/// +public class JsonStringEnumMemberConverter : JsonConverter where T : struct, Enum +{ + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + foreach (var field in typeof(T).GetFields()) + { + var attr = field.GetCustomAttribute(); + if (attr != null && attr.Value == value) + return (T)field.GetValue(null)!; + } + + return Enum.Parse(value!); + } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var field = typeof(T).GetField(value.ToString()); + var attr = field?.GetCustomAttribute(); + writer.WriteStringValue(attr?.Value ?? value.ToString()); + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Utils/QueryStringBuilder.cs b/src/AuthentikNet.Api/Utils/QueryStringBuilder.cs new file mode 100644 index 0000000..8111b79 --- /dev/null +++ b/src/AuthentikNet.Api/Utils/QueryStringBuilder.cs @@ -0,0 +1,40 @@ +using System.Collections; + +namespace AuthentikNet.Api.Utils; + +public static class QueryStringBuilder +{ + public static string BuildQueryString(string baseUrl, Dictionary parameters) + { + var queryParams = new List(); + + foreach (var (key, value) in parameters) + { + switch (value) + { + case null: + continue; + case string str: + queryParams.Add($"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(str)}"); + break; + case IEnumerable enumerable: + { + foreach (var item in enumerable) + { + queryParams.Add($"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(item?.ToString() ?? "")}"); + } + + break; + } + default: + queryParams.Add($"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(value.ToString() ?? "")}"); + break; + } + } + + if (queryParams.Count == 0) + return baseUrl; + + return baseUrl + (baseUrl.Contains('?') ? "&" : "?") + string.Join("&", queryParams); + } +} \ No newline at end of file diff --git a/src/AuthentikNet.Api/Utils/UserTypeEnumConverter.cs b/src/AuthentikNet.Api/Utils/UserTypeEnumConverter.cs new file mode 100644 index 0000000..812b9ac --- /dev/null +++ b/src/AuthentikNet.Api/Utils/UserTypeEnumConverter.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using AuthentikNet.Api.Models; + +namespace AuthentikNet.Api.Utils; + +/// +/// Converter for . +/// +public class UserTypeEnumConverter : JsonConverter +{ + /// + public override UserTypeEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value switch + { + "internal" => UserTypeEnum.Internal, + "external" => UserTypeEnum.External, + "service_account" => UserTypeEnum.ServiceAccount, + "internal_service_account" => UserTypeEnum.InternalServiceAccount, + _ => throw new JsonException($"Unknown value: {value}") + }; + } + + /// + public override void Write(Utf8JsonWriter writer, UserTypeEnum value, JsonSerializerOptions options) + { + var stringValue = value switch + { + UserTypeEnum.Internal => "internal", + UserTypeEnum.External => "external", + UserTypeEnum.ServiceAccount => "service_account", + UserTypeEnum.InternalServiceAccount => "internal_service_account", + _ => throw new JsonException($"Unknown value: {value}") + }; + writer.WriteStringValue(stringValue); + } +} \ No newline at end of file