diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1a454..b021daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This is the changelog for [Authress SDK](readme.md). ## 1.4 ## * Support exponential back-off retries on unexpected failures. * Add optimized caching for authorization checks +* Add support for If-Unmodified-Since in group and access record apis. ## 1.3 ## * Upgrade to using dotnet 6.0 support. diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..fdede04 --- /dev/null +++ b/contributing.md @@ -0,0 +1,59 @@ + +## Using the OpenAPI Generator to generate new models + +#### Start container +```sh +podman pull docker://openapitools/openapi-generator-online +``` + +#### Start container at port 8888 and save the container id +```sh +CID=$(podman run -d -p 8888:8080 openapitools/openapi-generator-online) +sleep 10 + +# Execute an HTTP request to generate a Ruby client +curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{"openAPIUrl": "https://api.authress.io/", "options": { "useSingleRequestParameter": true, "packageName": "authress", "packageVersion": "99.99.99" } }' 'http://localhost:8888/api/gen/clients/csharp' + + +# RESPONSE: { "link":"http://localhost:8888/api/gen/download/c2d483.3.4672-40e9-91df-b9ffd18d22b8" } +``` + +### Download the generated zip file +```sh + +wget RESPONSE_LINK + +# Unzip the file +unzip SHA + +# Shutdown the openapi generator image +podman stop $CID && podman rm $CID +``` + +### Common review items +* [ ] Inputs to Constructor are (string: authress_api_url, string: service_client_access_key) +* [x] authress_api_url should sanitize https:// prefix and remove trailing `/`s +* [ ] Add authentication to the configuration class. +* [ ] Change configuration class name to be `AuthressSettings` + * Specify all the inputs to be consistent across languages +* [ ] constructors for classes should only have relevant input properties, for instances `links` are not required. +* [ ] Update documentation + * Make sure markdown is valid + * Remove unnecessary links + * Add first class examples to readme.md + api documentation + * Find failed UserId, RoleId, TenantId, GroupId, Action properties and convert them to strings +* [ ] Remove any unnecessary validations from object and parameter injection, often there are some even when properties are allowed to be null +* [ ] The service client code to generate a JWT from private key needs to be added +* [ ] Add UnauthorizedError type to the authorizeUser function +* [ ] GET query parameters should be an object +* [ ] Top level tags from the API should accessible from the base class: `authressClient.accessRecords.getRecords(...)` +* [ ] Automatic Retry + * [ ] Automatic fallback to cache +* [ ] In memory caching for authorization checks - memoize +* [ ] Unsigned int for all limits +* [ ] readonly properties are never specified as required for request bodies +* [ ] Update Documentation for the API +* [ ] Validate all enums are enums and can be null when they should be. +* [ ] Remove LocalHost from the docs +* [ ] Tests +* [x] If-unmodified-since should called `expectedLastModifiedTime`, accept string or dateTime and convert this to an ISO String diff --git a/src/Authress.SDK/Api/AccessRecordsApi.cs b/src/Authress.SDK/Api/AccessRecordsApi.cs index 9703e80..069d26e 100644 --- a/src/Authress.SDK/Api/AccessRecordsApi.cs +++ b/src/Authress.SDK/Api/AccessRecordsApi.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -119,38 +120,47 @@ public async Task GetRecord (string recordId) /// /// Update an access record. Updates an access record adding or removing user permissions to resources. /// - /// - /// The identifier of the access record. + /// /// The identifier of the access record. + /// + /// The expected last time the record was updated. /// AccessRecord - public async Task UpdateRecord (string recordId, AccessRecord body) + public async Task UpdateRecord (string recordId, AccessRecord accessRecord, DateTimeOffset? expectedLastModifiedTime = null) { - // verify the required parameter 'body' is set - if (body == null) throw new ArgumentNullException("Missing required parameter 'body'."); + // verify the required parameter 'record' is set + if (accessRecord == null) throw new ArgumentNullException("Missing required parameter 'accessRecord'."); // verify the required parameter 'recordId' is set if (recordId == null) throw new ArgumentNullException("Missing required parameter 'recordId'."); var path = $"/v1/records/{System.Web.HttpUtility.UrlEncode(recordId)}"; var client = await authressHttpClientProvider.GetHttpClientAsync(); - using (var response = await client.PostAsync(path, body.ToHttpContent())) + + using (var request = new HttpRequestMessage(HttpMethod.Post, path)) { - await response.ThrowIfNotSuccessStatusCode(); - return await response.Content.ReadAsAsync(); + request.Content = accessRecord.ToHttpContent(); + if (expectedLastModifiedTime.HasValue) { + request.Headers.Add("If-Unmodified-Since", expectedLastModifiedTime.Value.ToString("o", CultureInfo.InvariantCulture)); + } + using (var response = await client.PostAsync(path, accessRecord.ToHttpContent())) + { + await response.ThrowIfNotSuccessStatusCode(); + return await response.Content.ReadAsAsync(); + } } } /// /// Create access request Specify a request in the form of an access record that an admin can approve. /// - /// + /// /// AccessRequest - public async Task CreateRequest(AccessRequest body) + public async Task CreateRequest(AccessRequest accessRecord) { // verify the required parameter 'body' is set - if (body == null) throw new ArgumentNullException("Missing required parameter 'body'."); + if (accessRecord == null) throw new ArgumentNullException("Missing required parameter 'body'."); var path = "/v1/requests"; var client = await authressHttpClientProvider.GetHttpClientAsync(); - using (var response = await client.PostAsync(path, body.ToHttpContent())) + using (var response = await client.PostAsync(path, accessRecord.ToHttpContent())) { await response.ThrowIfNotSuccessStatusCode(); return await response.Content.ReadAsAsync(); diff --git a/src/Authress.SDK/Api/GroupsApi.cs b/src/Authress.SDK/Api/GroupsApi.cs index 500e015..6da00fb 100644 --- a/src/Authress.SDK/Api/GroupsApi.cs +++ b/src/Authress.SDK/Api/GroupsApi.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -99,19 +100,27 @@ async public Task GetGroups (int? limit = null, string filter = /// Update a group Updates a group adding or removing user. Change a group updates the permissions and roles the users have access to. (Groups have a maximum size of ~100KB) /// /// The identifier of the group. - /// + /// The Group data + /// The expected last time the group was updated /// Group - public async Task UpdateGroup (string groupId, Group body) + public async Task UpdateGroup (string groupId, Group group, DateTimeOffset? expectedLastModifiedTime = null) { - if (body == null) throw new ArgumentNullException("Missing required parameter 'body'."); + if (group == null) throw new ArgumentNullException("Missing required parameter 'group'."); if (groupId == null) throw new ArgumentNullException("Missing required parameter 'groupId'."); var path = $"/v1/groups/{System.Web.HttpUtility.UrlEncode(groupId)}"; var client = await authressHttpClientProvider.GetHttpClientAsync(); - using (var response = await client.PostAsync(path, body.ToHttpContent())) + using (var request = new HttpRequestMessage(HttpMethod.Post, path)) { - await response.ThrowIfNotSuccessStatusCode(); - return await response.Content.ReadAsAsync(); + request.Content = group.ToHttpContent(); + if (expectedLastModifiedTime.HasValue) { + request.Headers.Add("If-Unmodified-Since", expectedLastModifiedTime.Value.ToString("o", CultureInfo.InvariantCulture)); + } + using (var response = await client.SendAsync(request)) + { + await response.ThrowIfNotSuccessStatusCode(); + return await response.Content.ReadAsAsync(); + } } } diff --git a/src/Authress.SDK/Api/IAccessRecordsApi.cs b/src/Authress.SDK/Api/IAccessRecordsApi.cs index bc6ad84..34e7501 100644 --- a/src/Authress.SDK/Api/IAccessRecordsApi.cs +++ b/src/Authress.SDK/Api/IAccessRecordsApi.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Authress.SDK.DTO; @@ -11,9 +12,9 @@ public interface IAccessRecordsApi /// /// Claim a resource by an allowed user Claim a resource by allowing a user to pick an identifier and receive admin access to that resource if it hasn't already been claimed. This only works for resources specifically marked as "CLAIM". The result will be a new access record listing that user as the admin for this resource. /// - /// + /// /// ClaimResponse - Task CreateClaim (ClaimRequest body); + Task CreateClaim (ClaimRequest claimRequest); #region Access Requests /// @@ -27,9 +28,9 @@ public interface IAccessRecordsApi /// /// Create a new access record Specify user roles for specific resources. /// - /// + /// /// AccessRecord - Task CreateRecord (AccessRecord body); + Task CreateRecord (AccessRecord accessRecord); /// /// Deletes an access record. Remove an access record, removing associated permissions from all users in record. If a user has a permission from another record, that permission will not be removed. /// @@ -45,10 +46,11 @@ public interface IAccessRecordsApi /// /// Update an access record. Updates an access record adding or removing user permissions to resources. /// - /// /// The identifier of the access record. + /// /// + /// /// The expected last time the record was updated. /// AccessRecord - Task UpdateRecord (string recordId, AccessRecord body); + Task UpdateRecord (string recordId, AccessRecord accessRecord, DateTimeOffset? expectedLastModifiedTime = null); #endregion #region Access Requests @@ -58,9 +60,9 @@ public interface IAccessRecordsApi /// /// Specify a request in the form of an access record that an admin can approve. /// - /// + /// /// AccessRequest - Task CreateRequest (AccessRequest body); + Task CreateRequest (AccessRequest accessRecord); /// /// Deletes access request @@ -101,9 +103,9 @@ public interface IAccessRecordsApi /// Updates an access request, approving it or denying it. /// /// The identifier of the access request. - /// + /// /// AccessRequest - Task RespondToAccessRequest (string requestId, AccessRequestResponse body); + Task RespondToAccessRequest (string requestId, AccessRequestResponse accessRecord); #endregion #region Invites @@ -116,9 +118,9 @@ public interface IAccessRecordsApi /// 5. This accepts the invite. /// When the user accepts the invite they will automatically receive the permissions assigned in the Invite. Invites automatically expire after 7 days. /// - /// + /// /// Invite - Task CreateInvite (Invite body); + Task CreateInvite (Invite accessRecord); /// /// Accept invite diff --git a/src/Authress.SDK/Api/IGroupsApi.cs b/src/Authress.SDK/Api/IGroupsApi.cs index 87c662a..c1ebd09 100644 --- a/src/Authress.SDK/Api/IGroupsApi.cs +++ b/src/Authress.SDK/Api/IGroupsApi.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Authress.SDK.DTO; @@ -38,8 +39,9 @@ public interface IGroupsApi /// Update a group Updates a group adding or removing user. Change a group updates the permissions and roles the users have access to. (Groups have a maximum size of ~100KB) /// /// The identifier of the group. - /// + /// + /// The expected last time the group was updated /// Group - Task UpdateGroup (string groupId, Group body); + Task UpdateGroup (string groupId, Group group, DateTimeOffset? expectedLastModifiedTime = null); } } diff --git a/src/Authress.SDK/Client/HttpClientProvider.cs b/src/Authress.SDK/Client/HttpClientProvider.cs index 879c5ca..86cf026 100644 --- a/src/Authress.SDK/Client/HttpClientProvider.cs +++ b/src/Authress.SDK/Client/HttpClientProvider.cs @@ -176,9 +176,10 @@ public RewriteBaseUrlHandler(HttpMessageHandler innerHandler, string baseUrl) : protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + var mergedPath = request.RequestUri.AbsolutePath.StartsWith(baseUrl.AbsolutePath) ? request.RequestUri.AbsolutePath : baseUrl.AbsolutePath + request.RequestUri.AbsolutePath.Substring(1); var builder = new UriBuilder(baseUrl.Scheme, baseUrl.Host, baseUrl.Port) { - Path = MergePath(baseUrl.AbsolutePath, request.RequestUri.AbsolutePath), + Path = mergedPath, Query = request.RequestUri.Query, Fragment = request.RequestUri.Fragment }; @@ -186,9 +187,6 @@ protected override async Task SendAsync(HttpRequestMessage request.RequestUri = builder.Uri; return await base.SendAsync(request, cancellationToken); } - - private static string MergePath(string baseUrlPath, string requestPath) => - requestPath.StartsWith(baseUrlPath) ? requestPath : baseUrlPath + requestPath.Substring(1); } internal class RetryHandler : DelegatingHandler