Migrate the license server to .NET 10 - #7
Gaël Fraiteur (gfraiteur) wants to merge 42 commits into
Conversation
Introduce global.json, nuget.config, Directory.Build.props and Directory.Packages.props in preparation for the migration to .NET 10, and stop tracking IDE state (.idea, .vs, *.user) and the checked-in nuget.exe, which `dotnet restore` replaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move src/PostSharp.LicenseServer to src/PostSharp.LicenseServer.Web and test/PostSharp.LicenseServer.Test to tests/PostSharp.LicenseServer.Simulator, reflecting what each project is: a web front end and a load simulator. This commit contains no content changes, so that Git records the moves as renames and `git log --follow` keeps working across the migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add PostSharp.LicenseServer.Core, which holds the lease allocation rules that were previously spread across the LINQ to SQL data context and the WebForms handlers, and a test project that exercises them against an in-memory SQLite database. The ambient statics that made the engine untestable are now injected: the settings, the clock, the license parser, the email sender, the request lock and the audit signer all sit behind interfaces, with in-memory implementations for tests. Three defects surfaced while porting: - The audit signature was computed with the parameterless HMAC.Create(), which throws on .NET 5 and later and which, on .NET Framework, used a randomly generated key for every call. The chain was never verifiable. It is now HMAC-SHA256 under a persisted key. - Audit timestamps were serialized as UTC from values SQL Server returns as Unspecified, which shifted them by the server's UTC offset. The kind is now set when the value is read. - Foreign keys were populated by the LINQ to SQL navigation setters before a lease was signed. EF Core does not do this, so they are assigned explicitly; otherwise the license and overwritten-lease fields would silently be signed as empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add 130 tests over the lease allocation rules, the audit log and the configuration, running against an in-memory SQLite database so the suite needs no SQL Server. Two invariants that a reader of the diff could not see are pinned deliberately, and both were checked by reintroducing the defect and confirming that only the intended tests fail: - The foreign keys of a lease must be assigned before it is signed, because EF Core populates them later than LINQ to SQL did. - Close must sort before Open at the same instant, which is what the values of LeaseCountingPointKind encode, otherwise a machine handed from one lease to the next is briefly counted twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rewrite the seven .aspx pages and the master page as Razor Pages, and the three .ashx handlers as minimal API endpoints. The handler URLs are kept literally, because deployed PostSharp clients address them directly, as are the status codes and response bodies they check. The old page URLs now redirect permanently to the new ones. The usage graph no longer loads YUI 3.18.1 over plain HTTP from a content delivery network retired in 2014, which any HTTPS deployment blocked as mixed content. It uses Chart.js, served from the application itself so an isolated network needs no outbound access. Its data travels as JSON in its own element rather than being concatenated into a script body. Incidental fixes the rewrite made possible: - The graph rejects an unknown license and an out-of-range window instead of answering 500. - The demo data generator is confined to a development environment. It was previously reachable in production, on a deployment whose administrative pages are unrestricted by default. - Administrative pages can be restricted to Windows groups through LicenseServer:AdminRoles. The default stays open, as it shipped before, but now says so in the log at startup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The engine is chosen by LicenseServer:DatabaseProvider, which accepts SqlServer (the default, and the supported engine for a production installation) or Sqlite. SQLite lets the server be evaluated without SQL Server, and lets the test suite host the real application against a database held in memory. The integration tests therefore exercise the production pipeline -- routing, model binding, authorization, the endpoints and the pages -- rather than a parallel arrangement built for testing. Only the license parser, the email sender, the audit key and Windows authentication are substituted, because they need a signed license key, an SMTP server, a key file and a domain controller respectively. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the MSBuild Zip target, which depended on a Visual Studio web publishing extension and rewrote web.config at packaging time, with eng/Package.ps1. It runs the tests, publishes and produces the same PostSharp.LicenseServer.zip artifact. Publishing for the Windows runtime keeps the Linux and macOS native libraries out of the package, which takes it from 47 MB to 12 MB. Port the load simulator so that it builds against .NET 10, and leave it disabled: it needs a client that can download a lease, which is being written in SharpCrafters.Backstage. The reason is now recorded where the throw is. Rewrite the README, which still described Visual Studio 2015, and move the settings documentation out of the XML comment it lived in and into docs/configuration.md. Both state the four things that change for somebody upgrading: the hosting bundle is now required, settings moved to appsettings.json, connection strings may need Encrypt=False, and audit timestamps are no longer shifted on servers that do not run in UTC. The pages follow the reader's colour scheme, rather than rendering dark text on whatever background the browser chooses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There are no EF migrations, because CreateTables.sql is the source of truth and the server never creates or alters a SQL Server schema. These tests stand in for one: they generate the DDL from the model and check it against what CreateTables.sql produces -- the text and datetime column types, which identifier the application assigns and which the database generates, the constraint names, and the absence of cascading deletes. Without them a mapping could drift silently and be discovered by a customer, on their data. Verified by reintroducing the two mappings' absence and confirming that exactly the tests guarding them fail. No server is contacted, so the checks run anywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Take the design tokens, the logo and the page chrome from the PostSharp website (metalama-website) so that the license server looks like the rest of the product rather than like a 2011 administration page. - The tokens are copied from _sass/0-base/_tokens.scss as plain custom properties, so no build step is needed: the purple and cyan accents, the dark surfaces, the type scale and the radii. - The header reproduces the website's company bar: the deepest surface, 64px tall, with the PostSharp logo on the left. The footer carries the same legal line as the website. - Buttons follow the brand's signature: sharp, uppercase, two-pixel border. Headings are weight 400, because size does the work. - The usage graph moves onto the brand palette and takes its axis colours from the tokens, so it is part of the page rather than a white box dropped on it. The brand is dark-first and has no light palette, so neither has this any more. Monosten Pro, the website's display face, is deliberately NOT copied. It is a commercially licensed webfont marked "do not redistribute", licensed to the metalama.net owner, and this application is published under the MIT license and runs on customers' own servers. It is named first in the font stack and falls back to a monospace face, which keeps the character of the headings without redistributing anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The application already targeted .NET 10, but three things still assumed Windows. Each is now a choice rather than an assumption. - Authentication was IIS or Negotiate. It is now IISIntegrated, Negotiate or None, so a host with no domain to authenticate against can still serve leases; they are recorded without a user name, exactly as an anonymous request already was. Left unset, the scheme is detected from how the application is hosted, and which one was chosen is logged at startup, because guessing wrong is quiet rather than loud. - The release package was published for win-x64. It is now portable and runs wherever the .NET 10 runtime does. That takes it from 12 MB to 48 MB, which is the cost of carrying every platform's native libraries in one artifact. - The simulator imported Microsoft.Win32 for a type that comes from the PostSharp SDK. Add a container deployment: a Dockerfile, and a compose file that starts the server, a SQL Server database and a job that creates the schema from CreateTables.sql. It is a test deployment, and docs/docker.md says plainly which four things make it one. Add .gitattributes, so the working tree is the same on every platform. Verified by running the stack: the schema job creates the database from the unmodified CreateTables.sql, and the server answers on Linux against real SQL Server 2022. That also exercises the rewritten queries -- the anti-join, the seat-count grouping and the counting-point timeline -- on SQL Server for the first time, where they had previously only run on SQLite, and confirms that timestamps come back tagged as UTC from the engine that returns them unspecified. The audit-log timestamp test no longer asks for Windows time zone identifiers, and now proves what it was meant to: that a lease which has been through the database still serializes as UTC. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Audit integrity, build-server validation, export scalability, redirect compatibility, and grace-limit defects remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Migrates the license server from .NET Framework/WebForms/LINQ to SQL to .NET 10, Razor Pages, and EF Core while preserving its database and client protocols.
Changes:
- Splits licensing logic, web hosting, tests, and simulator into SDK-style projects.
- Adds selectable SQL Server/SQLite persistence, dependency injection, audit signing, authentication, and comprehensive tests.
- Replaces legacy WebForms UI, YUI graphing, packaging, and deployment infrastructure.
File summaries
| File | Description |
|---|---|
src/PostSharp.LicenseServer.Core/ |
New licensing, persistence, locking, email, security, options, and time abstractions. |
src/PostSharp.LicenseServer.Web/ |
New ASP.NET Core host, endpoints, Razor Pages, configuration, database script, and static assets. |
tests/PostSharp.LicenseServer.Tests/ |
Unit and integration coverage with SQLite and test doubles. |
tests/PostSharp.LicenseServer.Simulator/ |
Ported manual load simulator. |
src/PostSharp.LicenseServer/ |
Removed legacy WebForms and LINQ-to-SQL implementation. |
test/PostSharp.LicenseServer.Test/ |
Removed legacy .NET Framework simulator configuration. |
Directory.Build.props |
Defines common .NET project settings and metadata. |
Directory.Packages.props |
Centrally defines package versions. |
PostSharp.LicenseServer.slnx |
Defines the new solution structure. |
global.json |
Pins the .NET 10 SDK feature band. |
nuget.config |
Configures NuGet.org as the package source. |
Dockerfile |
Adds the Linux container build. |
docker-compose.yml |
Adds a test SQL Server deployment. |
eng/Package.ps1 |
Adds release publishing and ZIP packaging. |
README.md |
Documents installation and upgrade procedures. |
docs/configuration.md |
Documents runtime configuration. |
docs/docker.md |
Documents container deployment. |
.dockerignore |
Excludes unnecessary Docker build context. |
.gitattributes |
Standardizes text and binary handling. |
.gitignore |
Ignores new build, database, and IDE artifacts. |
.claude/launch.json |
Adds a local launch configuration. |
.idea/ |
Removes tracked Rider metadata. |
PostSharp.LicenseServer.sln.DotSettings.user |
Removes user-specific IDE settings. |
Review details
Files not reviewed (15)
- .idea/.idea.PostSharp.LicenseServer/.idea/.gitignore: Generated file
- .idea/.idea.PostSharp.LicenseServer/.idea/encodings.xml: Generated file
- .idea/.idea.PostSharp.LicenseServer/.idea/indexLayout.xml: Generated file
- .idea/.idea.PostSharp.LicenseServer/.idea/vcs.xml: Generated file
- .idea/config/applicationhost.config: Generated file
- src/PostSharp.LicenseServer/Admin/AddLicense.aspx.designer.cs: Generated file
- src/PostSharp.LicenseServer/Admin/Cancel.aspx.designer.cs: Generated file
- src/PostSharp.LicenseServer/Admin/Details.aspx.designer.cs: Generated file
- src/PostSharp.LicenseServer/Admin/Export.aspx.designer.cs: Generated file
- src/PostSharp.LicenseServer/Admin/GenerateDemoData.aspx.designer.cs: Generated file
- src/PostSharp.LicenseServer/DataClasses.designer.cs: Generated file
- src/PostSharp.LicenseServer/Default.aspx.designer.cs: Generated file
- src/PostSharp.LicenseServer/Graph.aspx.designer.cs: Generated file
- src/PostSharp.LicenseServer/Properties/Settings.Designer.cs: Generated file
- src/PostSharp.LicenseServer/Site.Master.designer.cs: Generated file
- Files reviewed: 158/191 changed files
- Comments generated: 9
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Nine defects were raised on the pull request. Each was real, and each had been inherited from the legacy implementation rather than introduced by the migration. The audit chain was the substantial one, and two findings shared a root cause: leases were signed before they were inserted. The signed payload therefore carried a lease identifier of zero while the exported line carried the one the database assigned, so a signature could not be recomputed from an export; and leases saved together all chained from the same predecessor, so removing one of them broke no later signature. Leases are now signed after the insert, in the order the database assigned, with the insert and the signature sharing a transaction so that a lease is never readable without its signature. A test recomputes a whole chain from the exported lines, which is what the chain is for. The rest: - A build agent was served any license whose key merely parsed, so an ineligible or expired one could be handed out, and the lease it was given was never clamped to the end of the license. Build agents now go through the same validation as anybody else, and keep only their exemption from consuming a seat. - A license with no seat limit that reached the grace period dereferenced a null maximum and answered 500 instead of denying the request. There is no capacity to exceed on such a license, so there is no grace period either. - Both redirects dropped the path base, so they left the application when it is installed below an IIS site root. - The audit export accepted a year outside the range of a date, passing validation and then failing while the date was constructed. - The audit export built the whole file in memory twice before answering. It is written to the response as the rows arrive, as the legacy handler did. - The usage graph floored the grace capacity where the allocator rounds it up, so a one-seat license with 20% grace was drawn as allowing one seat while the server granted two. - The user and machine names reaching the slow-request log are stripped of control characters, so they cannot forge a line in a plain-text log. The build-agent, unlimited-license, export-range and path-base fixes each have a test, checked by reintroducing the defect and confirming the intended test fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The audit log deliberately reduces user and machine names to hashes, so that an exported file can be shared without disclosing who works where. Writing those same names verbatim into the application log contradicted that, and let a caller forge a line in a plain-text log through the query string. The log now records the same hash the audit log uses, so an administrator can still correlate the two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This reverts commit 8ffb841.
The license server joins the Backstage 2027.0 family as the Backstage.LicenseServer product, whose repository and packages are named SharpCrafters.Backstage.LicenseServer. This commit only moves files, so that rename detection works and the next commit shows the real changes. The load simulator goes: it compiles only against the PostSharp SDK that the next commit removes, and SharpCrafters.Backstage already carries LicenseServerLoadSimulator, which drives this server through its own client. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The namespaces, assembly names and the solution follow the folders renamed in the previous commit. The published entry point becomes SharpCrafters.Backstage.LicenseServer.dll, which the Dockerfile, the packaging script and the documentation are updated for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The product is Backstage.LicenseServer of the Backstage 2027.0 family, defined in PostSharp.Engineering 2023.2.455. `./Build.ps1 build` and `./Build.ps1 test` replace the direct dotnet invocations, and the TeamCity configuration is generated from the product definition rather than written by hand. global.json and nuget.config are now generated by `./Build.ps1 prepare`, so they leave source control. eng/Package.ps1 goes with them: the release archive is built by the PackAndZip target of the web project and collected as the public artifact of the product, which is what the deployment uploads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PostSharp SDK is replaced by the licensing component of the product family this server now belongs to. The public API of SharpCrafters.Backstage covers everything the server reads from a license key, including the rules that derive a value from several fields, so the parser projects LicenseRegistrationProperties rather than reading the fields itself. The platform initialization the SDK required disappears with it. Two things the package does not provide are written here instead. The lease response is serialized by the server, because a client only ever parses one; the format is the one the SDK produced and is now pinned by a test. The hash that anonymises a name in the audit log is reproduced, because the method of Backstage is internal; the golden values in LeaseAuditLineTests prove it produces what PostSharp produced. The parser takes its licensing authority as an argument, so the tests sign real license keys with a key pair of their own and exercise the component end to end -- no license key signed by the production authority can live in a public repository. Two differences between the two libraries are handled explicitly: - The product names differ: the enumeration of PostSharp calls a product Ultimate where Backstage calls it PostSharpUltimate. An installation upgraded from a previous version holds the PostSharp spelling in ProductCode, while a client of the Backstage generation asks by the Backstage one, so a request naming a product would have found nothing. ProductCodes reconciles the two when matching, and new rows are written with the Backstage spelling. - A key that carries no grace period now gets thirty days rather than none, which is the default Backstage applies. LicenseKeyData.GraceDays is not nullable, so an absent field cannot be told from a field set to thirty and the previous value cannot be restored. Keys issued with an explicit grace period are unaffected. The grace percentage, which Backstage leaves null, keeps the thirty percent PostSharp applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The image used to build the sources itself, which no longer works: the licensing component comes from a private feed, and the build needs the global.json and nuget.config that `./Build.ps1 prepare` generates and that are not in source control. Neither belongs in a container a customer may build. The Dockerfile now has a single stage and copies artifacts/app, which is where the PackAndZip target leaves the contents of the release archive, so the container runs exactly what is released. The documentation follows, including the two differences an upgrade meets now that license keys are parsed by SharpCrafters.Backstage: the default grace period and the product name recorded in the ProductCode column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The archive carries the whole dependency tree of an ASP.NET Core application, and the sign service descends into a container and signs every executable it finds. Without a filter it would sign MailKit, the Azure libraries and the rest with our certificate, asserting authorship of code that is not ours. Also records that no default publisher matches the archive, so the upload to S3 still has to be added to the product definition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tests generated a key pair of their own, which verified the parser against a signature scheme rather than against the one the product uses. They now sign with the test licensing authority of SharpCrafters.Backstage, reached through TestLicenseKeyProvider in SharpCrafters.Backstage.Testing, and verify against the very authority object that signed. The ready-made keys that Backstage issues for its own tests parse here too, which is what shows the two agree on the whole of the format. That turned up a defect. An authority provider throws when it is asked for a key identifier it does not hold, so a license key whose signature named an authority nobody issued escaped TryParse as a KeyNotFoundException instead of being reported as invalid -- a 500 on the Add License page, for a value an administrator pastes into a web form. The parser now checks the identifier before verification. xunit moves to 2.9.3, which is the version SharpCrafters.Backstage.Testing requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
postsharp-ops/SharpCrafters.Backstage#2 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A load simulation needs a license key that the server serves, and every key the production authority signs is one that was sold. LicenseServer:TestLicensingAuthorities names the authorities a development server accepts besides that one, so a key pair generated for the occasion is enough to drive the server. The server refuses to start with the setting outside the Development environment rather than ignoring it: whoever holds the private half of such a pair can mint license keys that a server configured this way honours. It also refuses a key that takes one of the production identifiers, and builds each authority at startup so that a malformed key fails there rather than in the middle of a lease request. The key pair itself stays out of source control. App_Data is now ignored wherever it appears, not only at the repository root, so the generated audit signing key cannot be committed either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither showed up in the test suite, and both fail on a real server. The audit log export answered with a truncated response and HTTP 500 for any range that held a lease. Lease.Write writes the fields one by one, so the StreamWriter flushed synchronously once its buffer filled, and Kestrel refuses a synchronous write to a response body. The line is now built in memory and written with WriteLineAsync; it is the whole log, not one line of it, that must not be assembled in memory. The usage page answered with HTTP 500 when a user held two open leases on one machine. The timeline kept a list of machines per user, skipped an opening point for a machine already in it, and removed on every closing point, so the second close found nothing and threw. It now counts the open leases per machine, so the closes balance the opens. A lease that ends no later than it starts is dropped before the points are built, because its closing point sorts before its opening one. The tests that missed them are the more interesting part. - Every earlier export test ran against an empty database, where the endpoint returns an empty body without streaming anything. Adding leases is not enough either: three lines fit in the writer's buffer and never flush. The new test writes sixty, and runs against a response body that refuses a synchronous write, because TestServer accepts one whatever its AllowSynchronousIO property says. - LeaseCountingPointsTests recorded two open leases on one machine as data the timeline was entitled to refuse, on the assumption that the lease service never produces it. It does: a server whose clock moves backwards grants a second lease while the first is still open, and restarting a server with TimeAcceleration set is enough. That test is replaced by ones that count the seats. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The contract with the clients was spread between the endpoint that implements it, the client that speaks it and the tests that pin it. docs/protocol.md describes it in one place: the three endpoints, the arguments of a lease request and what each one decides, the format of a lease and the lenient parsing a client applies to it, every status code and denial message, the renewal policy that keeps the load proportional to developers rather than to builds, the accelerated clock, and the format and signature chain of the audit log. The behaviours a deployed client depends on are listed together at the end, because the reason not to change them is not visible from the code that implements them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chart was labelled "Concurrent users" and drew seats. The two differ for a user working on more machines than one seat covers: the allocator charges that person a second seat, and the chart counted two. It now counts the people holding a lease, which is what its axis, its legend and the two reference lines are expressed in -- the capacity of a license key is a number of concurrent users. Each point of the timeline now carries both numbers. LeaseCount keeps the seats, which is what the allocator compares to the capacity when it decides whether to grant a lease, and UserCount carries the people. Nothing but the chart reads the second one today. This makes the chart differ from the "In use" column of the license list, which counts seats. A license can therefore be at its capacity with the chart below the line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A seat is one user together with the machines that user works on, up to MachinesPerUser of them; a user working on more consumes one seat per that many machines, rounded up. The word was used throughout without ever being defined, which is what made it look like a concept the product does not have. The definition now sits on SeatCounter, on the setting, and in the configuration and protocol documents. The licence agreement states the same rule the other way round, as authorized users each entitled to a number of devices. GetActiveLeads becomes GetActiveSeats and LeaseCountingPoint.LeaseCount becomes SeatCount, so that the names say what they hold. GetActiveSeats counted the leases a user holds instead of the machines they work on: a user holding two leases on one machine was charged for two machines, which denies a colleague a lease the licence has the capacity for. The lease service avoids that by prolonging rather than granting a second lease, but a server whose clock has moved backwards grants one, and a load simulation produced it within minutes of a restart. The timeline behind the graph already counted machines. This also reverts the previous commit. The usage chart draws seats again, so the line and the two limits above it are in the same unit and the chart agrees with the "In use" column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page reports a number of seats, so it now says what one is. The rule is stated with the number of machines this server is configured with rather than with the default, so an installation that changed the setting is not told something untrue. ConcurrentUsers on the page model becomes Seats, which is what it has always held. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The legend drew a filled rectangle for each series, which matches neither the shape of what is on the chart nor the dash pattern that tells the capacity apart from the grace allowance. Each sample is now a line. The dash pattern and the width are carried over from the dataset explicitly, because the legend item Chart.js generates does not take them: without that the three samples are solid lines differing only in colour, while the lines they stand for are solid, dashed and dotted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sentence carried two ideas and a parenthesised plural: "one user together with the machines that user works on, up to 2 of them" and "one seat per 2 machine(s), rounded up". It now states one idea per sentence, in the plainest words the rule allows, and the noun agrees with the number so that a server configured with one machine per seat does not read "1 machines". The same wording is used wherever the rule is stated, so the product explains it one way: the details page, SeatCounter, the MachinesPerUser setting, and the configuration and protocol documents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel was a paragraph containing forms. A paragraph cannot contain a form, so the browser closed it at the first one: the panel rendered as an empty bordered rectangle and the buttons and the sentence below it landed outside, unstyled. It is a div now. The actions class it carries had no rule at all, so the two buttons of a disabled license stacked. It now lays them out in a row with the sentence that explains them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| published by IIS with Windows authentication answers 401 to an anonymous request. An installation | ||
| that enables Windows authentication therefore records who borrowed each lease. | ||
|
|
||
| Setting `LicenseServer:RequireAuthenticatedLeaseRequests` makes the server refuse an anonymous lease |
There was a problem hiding this comment.
Authentication is used to gate the admin interface only. Normal access is designed to be anonymous.
There was a problem hiding this comment.
Corrected in 6444349. The section now opens with "A lease request is anonymous by design" and says that authentication is there to gate the administrative interface; the note about credentials arriving is kept only to explain why a client sends them at all, which is that IIS with Windows authentication answers 401 to an anonymous request.
I had it backwards: the old text read as though authentication existed to attribute leases, with the admin pages as an afterthought.
|
|
||
| The suffix is load-bearing on the server side as well. Before the server compares a machine name to | ||
| its list of build servers it strips a trailing `-` followed by hexadecimal digits, so | ||
| `buildagent-1f2e` matches the configured name `buildagent`. A machine name that legitimately ends in |
There was a problem hiding this comment.
this should be discussed and fixed - probably on the client side
There was a problem hiding this comment.
Corrected in 6444349, and the sentence you flagged was wrong rather than merely worth discussing.
Exactly one trailing -<hex> group is stripped, and every client appends the hash, so a machine whose own name ends in hexadecimal keeps it: build-01 arrives as build-01-1f2e and is compared as build-01. BuildServerDetectionTests pins both directions. My claim that such a name is shortened would only hold for a client that sends no hash, and no shipped client does.
What is worth discussing is what is underneath it: the name and the hash travel in one argument, so the server has to find the hash by its shape. A separate argument would remove the guess. It can only be added beside machine rather than in place of it, since deployed clients send what they send. Say the word and I will file it against SharpCrafters.Backstage, where the client lives.
| **Both default to open**, which is how the license server has always shipped, so that an upgrade | ||
| cannot lock an administrator out of their own server. The administrative pages are the only way to | ||
| add or revoke a license, so setting `AdminRoles` is worth doing; until it is set, the server says so | ||
| in its log every time it starts. |
There was a problem hiding this comment.
Mention that the administrator is reponsible for securing the Admin folder; this is not taken in charge by the app. And say how, or let's disuss.
There was a problem hiding this comment.
Done in 6444349, as a new section "Securing the administrative pages", linked from the installation steps in the README.
It says the responsibility is the administrator's and that the server does not do it on its own, then gives two ways that can be combined: AdminRoles, which covers every page under /Admin and the export endpoint but needs a scheme that supplies Windows groups, and a URL authorization rule for the Admin path in the published web.config for anyone not authenticating against a domain. It notes that the rule needs the IIS URL Authorization role service, which is not installed by default, and that behind another web server or in a container the path has to be restricted by whatever sits in front.
One caveat: I have not run the IIS rule against a real IIS. It is the standard form, but it is untested in this work.
Authentication: a lease request is anonymous by design, and authentication is there to gate the administrative interface. The protocol document had it the other way round, as though authentication existed to attribute leases. Securing the administrative pages: the configuration document now says that it is the administrator's responsibility and how to do it, with the roles setting and with a URL authorization rule under IIS for whoever does not authenticate against a domain. The installation steps point at it. The machine argument: the claim that a machine name ending in hexadecimal is shortened was wrong. Exactly one group is stripped and every client appends the hash, so such a name survives. What is true is that the name and the hash travel in one argument and the server has to find the hash by its shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The compose file described itself as a test deployment and behaved like one: the database password sat in it in plain text and the audit signing key was a fixed value, so every deployment made from it shared one. It is now an ordinary deployment. The password comes from MSSQL_SA_PASSWORD in the environment and compose refuses to start without it, and the server generates its own audit signing key. docker-compose.test.yml is the opt-in: applied beside the main file, it puts the server in the Development environment, sets LicenseServer:SeedTestLicenses and accelerates the clock. SeedTestLicenses has the server issue itself the license keys it serves, which is what a trial and a load simulation need and what no production authority will ever sign for them. The server generates a licensing authority into its data directory, trusts it and adds one license key per product family. It refuses to start with the setting outside the Development environment, as it already did for TestLicensingAuthorities. The data directory is what has to outlive the container. It holds the audit signing key, whose loss restarts the signature chain, and now the test authority, whose loss stops the license keys already in the database from verifying. The image declares /app/App_Data as a volume so that a container started without a mount does not keep them in its own writable layer, and LicenseServer:DataDirectory moves the directory elsewhere. Verified against Docker under WSL: the default stack comes up with no license and a clock at 1, the test override seeds both licenses and serves a lease, and both keys survive the container being replaced with the volume kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The actions that change a license were three buttons in a panel at the bottom of the page. They are now one Manage button at the top, left of Back, which opens a menu, and each action states what it does in a dialog before it runs. They did not work. The page is reached as /Admin/Details?id=900001, and a form does not inherit the query string of the page that contains it, so every action posted to /Admin/Details?handler=Disable, matched no license and answered 404 as a blank page. The forms name the license now, and a test posts each of them the way a browser does, with the action and the antiforgery token the page itself supplies. The menu is a <details> element and the confirmation is added to forms that submit without it, so a browser that does not run admin-actions.js keeps every action and loses only the confirmation. The dialog clears its return value before it opens, because a dialog closed with the Escape key keeps the value of the previous close in some browsers, which would confirm an action nobody confirmed. A disabled license now says so in a callout. The state used to be legible only from which buttons the page was showing, and those are behind a closed menu now. Verified in the browser: the three actions reach the license, Keep it and the outside click leave it alone, the menu opens under its button at phone width instead of off the screen, and the delete button is dark on orange, which orange on purple was not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/health/live reports the state of the process and runs no check. /health adds the database and the licenses. /version reports the build that is deployed and the version of the licensing library it parses license keys with. All three are anonymous, like Lease.ashx and unlike the administrative pages: a load balancer holds no Windows credentials, and a probe answered with 401 reads as a server that is down. The database check queries the license table rather than opening a connection. The server never creates its own schema on SQL Server, so a database that accepts connections but was never given CreateTables.sql is the deployment mistake worth catching, and a connection test calls it healthy. The license check reports whether any license can serve a lease now, which is what distinguishes a server that is running from a server that is working: one whose licenses have all expired answers every request with 403 and looks perfectly well from the outside. It warns and never fails. Nothing that watches a probe can add a license, and the server goes on serving the leases it has already granted, so the endpoint stays at 200 and names the problem in its body and in a warning in the log. Only the process and the database fail the probe. Neither check returns the message of the exception that made it fail. A database exception carries the name of the server and sometimes the whole connection string, and these endpoints are anonymous, so the reason goes to the log and only a development server puts it in the response. LicenseAvailabilityService restates rules that LeaseService owns, so four cases assert that the two agree: free capacity, full with grace left, grace seats used up, and grace period over each answer the same as allocating a lease does. The grace limit is now computed in one place for both. The service reads and never writes, because the allocator starts the grace period of a license when it falls back on it and a probe that did the same would run that clock against an idle server. Verified against a running server: healthy with licenses, degraded with none, 503 with the schema dropped, and no password or host name in the body of a failure outside Development. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| SQL Server is the supported engine for a production installation. Create its schema by running | ||
| `Database\CreateTables.sql`; the server never creates or alters it, so an upgrade cannot surprise you. | ||
|
|
||
| SQLite is offered for evaluation, and is what the test suite uses. Its database is created on first |
There was a problem hiding this comment.
Say that SQLLite is supported for tests. Rewrite AI slop.
There was a problem hiding this comment.
Write how the DB engine is configured.
There was a problem hiding this comment.
Done in aeaf8b0. The Database section now says that SQLite is supported for tests and for evaluation, that the test suite of this repository and the development configuration of the web project both use it, and that it is not for a server that serves a team, with the reason: SQLite accepts one writer at a time, and every lease request writes.
The section is rewritten against the copywriting rules of the engineering skill, as is the rest of the document, the three other documents, and the comments and the XML documentation of the whole repository. What changed: the subject now comes first in each sentence, one sentence carries one idea, the text names the mechanism instead of describing it by analogy, and the abbreviations and the bold text are gone.
— Claude for Gaël Fraiteur (@gfraiteur)
There was a problem hiding this comment.
Done in aeaf8b0. The Database section now presents LicenseServer:DatabaseProvider and the connection string as one pair that must agree, because the engine named in the first setting interprets the second one. It gives a JSON block for each engine:
{
"LicenseServer": { "DatabaseProvider": "SqlServer" },
"ConnectionStrings": {
"SharpCrafters_LicenseServerConnectionString": "Server=db.example.com,1433;Database=PostSharpLicenseServer;Integrated Security=True;Encrypt=False"
}
}{
"LicenseServer": { "DatabaseProvider": "Sqlite" },
"ConnectionStrings": {
"SharpCrafters_LicenseServerConnectionString": "DataSource=licenseserver.db"
}
}— Claude for Gaël Fraiteur (@gfraiteur)
| | `LicenseServer:AdminRoles` | empty | The Windows groups allowed to reach the administrative pages, for example `["DOMAIN\\PostSharp Administrators"]`. | | ||
| | `LicenseServer:RequireAuthenticatedLeaseRequests` | `false` | Whether a lease request must be authenticated. | | ||
|
|
||
| The authentication scheme decides how the server learns who is borrowing a license, which is what it |
There was a problem hiding this comment.
Incorrect. The user info is given by the query string, the server trusts that. Authentication is used for AuthenticedUser otherwise only to secure access to the service in general and to the admin page specifically.
There was a problem hiding this comment.
Corrected in aeaf8b0. The text was wrong, and it was wrong in the protocol document as well, which said that authentication is how the server learns who borrows a license.
The Access section now opens with the source of the two values:
A lease request carries the user name and the machine name in its query string. The server trusts these two values and records them in the
UserNameandMachinecolumns, and it counts seats from them. Authentication does not provide them.
It then states the two purposes of authentication: it records the caller in the AuthenticatedUser column, next to the user name that the request declares, and it restricts access. RequireAuthenticatedLeaseRequests requires the caller of Lease.ashx to be authenticated, and AdminRoles restricts the administrative pages to the members of the groups it names.
docs/protocol.md now says the same thing, in its section on authentication.
— Claude for Gaël Fraiteur (@gfraiteur)
The documents and the comments were written in a register the copywriting rules of the engineering skill exclude: rhetorical openings, idiom, sentences that carry three ideas between dashes, bold for emphasis, and "i.e.". They now state the subject in the first clause, use one idea per sentence, name the mechanism, and assume a reader who does not speak English natively. The configuration document answers the review. It says that SQLite is supported for tests and for evaluation, and that it is not for a server that serves a team, because SQLite accepts one writer at a time. It presents DatabaseProvider and the connection string as one pair that must agree, with an example for each engine. It opens with a complete appsettings.json. The description of authentication was wrong, in the configuration document and in the protocol document. A lease request declares the user and the machine in its query string, the server trusts both values, and it counts seats from them. Authentication does not provide them. It records the caller in AuthenticatedUser, and it restricts access: RequireAuthenticatedLeaseRequests for Lease.ashx, AdminRoles for the administrative pages. Rewriting found two more inaccuracies. FixLease was documented as signing the lease, which it stopped doing when signing moved after the insert. The protocol document listed the order of the four parts of a lease as a compatibility constraint, and the client matches the parts by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server signed each lease of the audit log with a chained HMAC, and nothing verified it. The feature is removed: ILeaseSigner, HmacLeaseSigner, IAuditKeyProvider, FileAuditKeyProvider, the property Lease.HMAC and its mapping, the setting LicenseServer:AuditHmacKey and its validation. Saving a lease is one insert again. The signature was computed after the insert, because the audit line starts with the identifier that the database assigns, so a save ran two statements inside a transaction. The repository now calls SaveChanges once. An exported line has seven fields instead of eight. The eighth field carried the signature. It could not be verified even in principle: the legacy implementation called the parameterless HMAC.Create(), which generates a random key at every call. An existing database is unaffected. The column HMAC is nullable, so the server writes to such a database without a schema change, and the values already written stay as they are. CreateTables.sql keeps the column, with a comment, so that the script creates the table that deployed installations have. SchemaCompatibilityTests adds the column to the test database and inserts a lease through the repository. Verified against the database of the last end-to-end run, which has 268 leases with the column populated: every exported line has seven fields, and a lease request inserted lease 269. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Subheadings for the two mechanisms that restrict the administrative pages, instead of a paragraph that opens with "The first mechanism" and one that opens with "The second mechanism". The document no longer mentions a load balancer. The operating requirements of this server are low: a developer sends about one request per day, and a client that cannot reach the server keeps the lease it holds, so an interruption of a few hours affects nobody. The Monitoring section says this, and it says that the purpose of monitoring is to learn that the server needs attention, and not to fail over. The same text is removed from the health check, from the endpoints and from their tests. The Concurrency section still names the deployments that run several worker processes against one database, because they can grant more leases than the capacity of the license, but it now recommends one worker process first and names the deployments second. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| at `/Admin/Export.ashx` returns the whole audit log. There are two mechanisms, and you can combine | ||
| them. | ||
|
|
||
| The first mechanism is `LicenseServer:AdminRoles`. It covers every page under `/Admin` and the export |
There was a problem hiding this comment.
Use subtitles for mechanisms.
There was a problem hiding this comment.
Done in ab6f85d. The section "Restricting the administrative pages" now has a subheading per mechanism, AdminRoles and the rule of Internet Information Services, instead of two paragraphs that opened with "The first mechanism" and "The second mechanism".
— Claude for Gaël Fraiteur (@gfraiteur)
| | `/version` | Which build is deployed, and which version of the licensing library it uses to parse license keys. | | ||
|
|
||
| The server serves these three endpoints without authentication, as it serves `Lease.ashx`. A load | ||
| balancer and a monitoring agent have no Windows credentials, and a probe that receives 401 reports |
There was a problem hiding this comment.
Don't mention a load balancer. It's useless since this app typically receives max one request per day and per user. also an outage of a few hours should be with minimal consequences since the clients cache their lease for days. so operating requirements are minimal.
There was a problem hiding this comment.
Done in ab6f85d. No document mentions a load balancer any more. The Monitoring section states the operating requirements instead: one developer sends about one request per day, a client that cannot reach the server keeps the lease it holds, and monitoring exists to learn that the server needs attention rather than to fail over.
The Concurrency section was rewritten again in 97cc811, when the lock moved into the database. Several worker processes on one database are now serialized against each other, so the section no longer asks anybody to run one process.
— Claude for Gaël Fraiteur (@gfraiteur)
| The operating requirements of this server are low. One developer sends about one request per day, | ||
| because the client stores its lease and renews it after `NewLeaseDays` minus `MinLeaseDays` days. A | ||
| client that cannot reach the server keeps the lease it holds, so an interruption of a few hours | ||
| affects nobody. Monitor the server to learn that it needs attention, and not to fail over. |
There was a problem hiding this comment.
affects only new usersor devices who don't have a lease yet.
There was a problem hiding this comment.
Done in c1413f7. The sentence now says that an interruption affects only a user or a machine that holds no lease yet, which means a new user, a new machine, or one whose lease has expired.
— Claude for Gaël Fraiteur (@gfraiteur)
| | `LicenseServer:SeedTestLicenses` | `false` | Whether the server issues to itself the license keys it serves. | | ||
| | `LicenseServer:TestLicensingAuthorities` | empty | The licensing authorities whose license keys a development server accepts, in addition to the production authority. | | ||
|
|
||
| The server refuses to start when one of the last two settings is set outside the Development |
There was a problem hiding this comment.
what is a development environment? how is it detected?
There was a problem hiding this comment.
Done in c1413f7. The Testing section now says that the environment is the one of ASP.NET Core: it is read from ASPNETCORE_ENVIRONMENT, the server runs as Production when that variable is not set, and Development is set by Properties/launchSettings.json when the project is started from an editor or with dotnet run, and by nothing else. A published server therefore runs as Production unless an administrator sets the variable.
— Claude for Gaël Fraiteur (@gfraiteur)
|
|
||
| ## Testing | ||
|
|
||
| | Setting | Default | Meaning | |
There was a problem hiding this comment.
there should be an introduction saying that testing is for the developers of this product, not for customers. and explain what a development environment is.
There was a problem hiding this comment.
Done in c1413f7. The Testing section now opens by saying that its settings exist for the developers of the license server itself and for an evaluation of it, and that a customer who serves license keys needs none of them. The explanation of the Development environment follows the table.
— Claude for Gaël Fraiteur (@gfraiteur)
| /// <c>DOMAIN\PostSharp Administrators</c>. When the value is empty, the administrative pages are | ||
| /// not restricted, which is the behaviour of the legacy <c>Web.config</c>. | ||
| /// </summary> | ||
| public string[] AdminRoles { get; set; } = []; |
There was a problem hiding this comment.
It's actually never used.
There was a problem hiding this comment.
It was used once, by the startup warning, while the authorization policy read the configuration section a second time, key by key. That is what made the property look unused.
c1413f7 makes the property the single source: the section is bound once, before the policies are built, and both the administrative policy and the lease policy read that instance. RequireAuthenticatedLeaseRequests was read the same way and now comes from the same instance.
— Claude for Gaël Fraiteur (@gfraiteur)
| if ( parsedLicense.MinPostSharpVersion > this.serverVersion.LicensingLibraryVersion ) | ||
| { | ||
| errors[license.LicenseId] = string.Format( | ||
| "The license #{0} requires a higher version of the licensing library on the License Server. Please upgrade the License Server to >= {1}.{2}.{3}", |
There was a problem hiding this comment.
Rewrite to interpolated string everywhere.
There was a problem hiding this comment.
Done in c1413f7. The six calls of string.Format in LeaseService are interpolated strings. The text of every message is unchanged, including the d format of the dates, because the tests and the deployed clients read these messages.
— Claude for Gaël Fraiteur (@gfraiteur)
| <p> | ||
| The seat is released immediately. The lease itself is kept in the audit log. | ||
| </p> | ||
|
|
There was a problem hiding this comment.
Mention that this does not cancel the lease on the developer machine, which continues to hold it under expiration. The feature only allows to free the lease on the server.
There was a problem hiding this comment.
Done in c1413f7. The page now says that the seat is released on this server immediately, and that the machine of the user keeps the lease it already holds until that lease expires, because a client is never told that a lease was cancelled. Cancelling frees the seat for somebody else; it does not stop the product on that machine.
— Claude for Gaël Fraiteur (@gfraiteur)
| </div> | ||
|
|
||
| <p> | ||
| The export is a text file with one line per lease. User and machine names appear only as hashes, |
There was a problem hiding this comment.
no longer hashes
There was a problem hiding this comment.
Done in c1413f7. The page now says that the file has one line per lease, with the user name and the machine name as they were recorded, that it contains personal data, and that it is meant for the administrator of this server.
— Claude for Gaël Fraiteur (@gfraiteur)
| private readonly DateTimeOffset origin; | ||
| private readonly double acceleration; | ||
|
|
||
| public AcceleratedTimeProvider( TimeProvider inner, double acceleration ) |
There was a problem hiding this comment.
Use the abstraction and the implementation used by SharpCrafters.Backstage
There was a problem hiding this comment.
I looked at what Backstage offers, and I do not think this class can use it as it stands.
The abstraction is IDateTimeProvider, in SharpCrafters.Backstage.Infrastructure. Its implementation in the product, CurrentDateTimeProvider, reads the real clock. The accelerated implementation, AcceleratedDateTimeProvider, is in SharpCrafters.Backstage.Testing, and it is the client side of this protocol: it calls GET /GetTime.ashx on a license server and then follows the clock of that server. This server is the clock it follows, so it cannot follow itself.
The class here is the source of that reading. It is a TimeProvider of the base class library, which is what lets the tests use FakeTimeProvider, and what the repository, the services and the pages take.
There is also no inconsistency to fix today: the parser uses LicenseKeyData and its extensions, which read no clock, so no Backstage code validates a date behind the back of this provider.
What would you like here? Registering an IDateTimeProvider backed by this provider is possible, if you intend Backstage code that reads a clock to be called from the server later.
— Claude for Gaël Fraiteur (@gfraiteur)
The lock that serializes the lease requests is no longer held in the process. On SQL Server the request calls sp_getapplock with the owner Session and releases it at the end; on SQLite it opens its transaction with BEGIN IMMEDIATE, which takes the write lock of the file at once. The engine chooses the mechanism, so LeaseLockMode is gone, together with InProcessLeaseLock and NullLeaseLock, and a deployment of several worker processes on one database is serialized. MutexTimeout stays: a timeout is configuration. The test suite runs a second time against SQL Server when LICENSESERVER_TEST_SQLSERVER holds a connection string. No test is duplicated, because the engine is a property of the run. Each test receives a database of its own, created from CreateTables.sql, which also proves that the script and the model agree, and the databases are pooled because creating one costs about half a second. Two tests covering the lock hold a request at a named synchronization point, through ITestSynchronizationProvider, which the tests register and production does not have. The first test asserts that the second request waits and then sees the lease of the first; the second asserts the status 503 when the wait exceeds the timeout. The SQL Server run found two defects of its own. SeedTestLicensesTests sorted the column LicenseKey in the database, which Transact-SQL refuses for the type text, so the keys are sorted after they are read. The health check test that drops both tables returned its database to the pool, which left the next test without a schema, so a test that modifies the schema now calls DoNotReuse and the pool drops that database. The pool also reseeds the identity of the leases only when the table has received a row: on a table that has received none, DBCC CHECKIDENT makes the next row take the value itself, which is zero. The build now runs in a container, as the builds of the other products of the family do. That is what lets the build definition generate the second image, a Linux one carrying SQL Server 2022, which the configuration Tests on SQL Server builds and runs eng/TestSqlServer.ps1 in. The script also serves a developer machine: it starts the database service of docker-compose.yml when it finds no other server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PostgreSQL joins SQL Server as an engine supported in production, and SQLite stays the engine of the development loop. Set LicenseServer:DatabaseProvider to PostgreSql and run Database/CreateTables.PostgreSql.sql, which is the script an administrator runs, as CreateTables.sql is on SQL Server. The server still never creates the schema and never modifies it. The lock that serializes the lease requests is an advisory lock. The request calls pg_advisory_lock and releases it with pg_advisory_unlock, both of which belong to the session, which is the connection of the request. The wait is bounded by lock_timeout, taken from MutexTimeout. PostgreSQL then cancels the statement with the code 55P03, and the request is answered with the status 503, as it is on SQL Server. Two differences of the engine were found by running the suite rather than by reading. Npgsql refuses a DateTime whose kind is Unspecified, which is what a date built from a year and a month is, and the export builds one from its query string; the value converter of the context now sets the kind in both directions, which writes the same value SQL Server and SQLite have always stored. PostgreSQL folds an identifier that is not quoted to lower case, so the two tests that modify the schema quote the names of the tables. The comparison of a user name and of a machine name ignores the case, as it does on SQL Server, whose default collation ignores it. PostgreSQL offers no such collation, so the schema creates one from the International Components for Unicode, and the model names it. Without it, a user who signed in under two spellings would hold two sets of machines. The test suite runs against PostgreSQL when LICENSESERVER_TEST_POSTGRESQL holds a connection string, the way it already runs against SQL Server. Each test receives a database of its own, created from the script of the engine, and the databases are pooled. TRUNCATE takes the place of the DELETE and the reseed that SQL Server needs. eng/TestDatabase.ps1 replaces eng/TestSqlServer.ps1 and serves both engines. It starts the server the run needs: the one installed in the image, when the continuous integration build runs it, and a container of its own on a developer machine. The container of a test run carries no volume and publishes a port beside the default one, so a license server deployed on the same machine with docker-compose.yml is left alone. On a machine whose Docker engine runs inside the Windows Subsystem for Linux, the script reaches Docker through wsl. The build definition generates a second Linux image, carrying PostgreSQL 17 from the repository of the PostgreSQL project, and a second configuration, Tests on PostgreSQL, beside the one for SQL Server. Verified: 278 tests pass on each of the three engines, and the two images build and run their server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The audit log names the user and the machine. It used to write a hash of each name, which nothing could read, and the file is exported to be read: the administrator of the server uses it to learn which user holds a seat on which machine. The exported file therefore contains personal data, and the export page, the protocol document and the upgrade notes say so. StringHash had no other caller and is removed. A Metalama license names the lowest version of Metalama that can read it, in MinMetalamaVersion, which the signature algorithm of the key decides. That minimum is independent of the minimum PostSharp version, and a Metalama version number is lower than the PostSharp version number of the same year, so comparing a Metalama client against the PostSharp minimum refused leases it should grant. The minimum that applies is now the one of the family of the licensed product, the message names that family, and two tests cover the rule. ILeaseSerializer is removed. It had one implementation, no test replaced it, and the endpoint takes the class. ILicenseParser stays: CachingLicenseParser decorates it and every test that hosts the application replaces it with FakeLicenseParser. LicenseServerOptions is now the one place that reads the administrative roles. The policy read the configuration section a second time, key by key, so the property looked unused. The section is bound once, before the policies are built, and the two policies read that instance. The messages of LeaseService are interpolated strings. Their text is unchanged. The documents answer the rest of the review. The configuration document opens with the purpose of the server: compliance with the license agreement is the responsibility of the customer, the server measures usage inside the network of the customer, it is independent of the license audit, and a customer who uses it is eligible for a waiver of that audit. It also says that only the described deployments are supported, that an interruption affects a user who holds no lease yet, that the settings of the Testing section are for the developers of this product, and what the Development environment is and how it is detected. The container document says that its deployment is for development and testing, because the database it starts is Developer Edition. The protocol document says that clients of several versions use one server at the same time, with compatibility down to the versions released before PostSharp 5, and that the audit log has no auditor as its reader. The cancellation page says that the machine of the user keeps its lease until that lease expires. eng/.gitignore is removed. It held one entry, for a file the build system no longer writes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A class no longer uses a primary constructor. Thirty of them did, in the two projects of the product, in the tests and in the build definition. Each one now declares its fields, assigns them in a constructor, and reads them as this.field. A record keeps its primary constructor. eng/style carries the shared code style of PostSharp.Engineering: .editorconfig, CommonStyle.DotSettings for the JetBrains tools, and stylecop.json. The .editorconfig at the root of the repository is a symbolic link to eng/style/.editorconfig, which is what `Build.ps1 codestyle pull` creates, so a clone needs core.symlinks. README.md says this, and says which step of Rider no script performs. The style raised six warnings, and the build has none again. LeaseSerializer holds no state and becomes a static class, so the endpoint calls it directly and the container registers nothing; that is the end of the interface the review asked to remove. Two members of the tests become static, one call names its StringComparison, one names its IFormatProvider, and the two database pools carry a justification for CA1001: a pool lives in a static dictionary as long as the run, and its semaphores are released with the process. TestDatabase.ps1 chooses the Docker daemon by the operating system it reports, and no longer by whether a client answers. Docker Desktop in the Windows container mode answers and then refuses a Linux image, with "no matching manifest for windows", which is what happened on the machine this was written on. Verified: 280 tests pass on SQLite, on SQL Server and on PostgreSQL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The JetBrains tools reformatted every file against eng/style/CommonStyle.DotSettings, the profile named Custom. They wrote the copyright header of the company, replaced an explicit type with var, removed the newline at the end of a file, reflowed the Razor markup, and spaced the nested parentheses. Nothing else changed: the build has no warning, and 280 tests pass on SQLite, on SQL Server and on PostgreSQL. The header the tools write names LICENSE.md, and this repository had no license file, although the README states that the server is published under the MIT License. LICENSE.md now carries that license, in the form the other repositories of the company use, and the README links to it. SharpCrafters.Backstage.LicenseServer.slnx.DotSettings carries the team-shared settings layer of the solution, which names eng/style/CommonStyle.DotSettings. Rider reads it, so the manual step of "Manage Layers" is no longer needed. `Build.ps1 codestyle format` does not work here, and the README says so and gives the command that does. That command relies on the settings layer of the solution to find the cleanup profile, and the JetBrains tools do not read that layer for a solution in the slnx format: they answer "Unable to find the code cleanup profile with 'Custom' name", with either name for the settings file and with or without the absolute path in it. Passing --settings=eng/style/CommonStyle.DotSettings works, with the same layers disabled as the build system disables, so the result is the one the command would have produced. dotnet-tools.json is ignored. The format command writes it to pin the version of the JetBrains tools on the machine that runs them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
PostSharp.LicenseServer.Core(the rules and the data model),PostSharp.LicenseServer.Web(the pages, the endpoints and the composition root) and a test project. Folder renames were committed without content changes, sogit log --followstill works.http://from a CDN retired in 2014, which any HTTPS deployment blocked as mixed content. It now uses Chart.js, served from the application itself so an isolated network needs no outbound access, with its data passed as JSON rather than concatenated into a script body.LicenseServer:DatabaseProvider: SQL Server for a production installation, SQLite for evaluation and for the test suite. The integration tests host the real application against a database held in memory, so they exercise production routing, binding, authorization and endpoints rather than a parallel arrangement.Ziptarget, which needed a Visual Studio web publishing extension, witheng/Package.ps1. It produces the samePostSharp.LicenseServer.zip; publishing for the Windows runtime keeps the Linux and macOS native libraries out of it, taking it from 47 MB to 12 MB.Defects found while porting
GetSignaturecalled the parameterlessHMAC.Create(), which throws on .NET 5 and later and which, on .NET Framework, returned an HMAC-SHA1 under a randomly generated key for every call. The chain was therefore never verifiable by anyone. It is now HMAC-SHA256 under a key generated on first start and kept inApp_Data\audit-signing.key.Unspecifiedwere serialized as if they were UTC.Each of these is covered by a test that was checked by reintroducing the defect and confirming that only the intended tests fail. The same was done for the
Close-before-Openordering ofLeaseCountingPointKindand for the schema mappings.Compatibility
The database schema is unchanged, so an existing database is used as it is, with no migration step. There are deliberately no EF migrations:
CreateTables.sqlremains the source of truth, andSchemaCompatibilityTestsstands in for one by generating the DDL from the model and holding it to the column types, key generation and constraint names that script produces.Four things do change for somebody upgrading, all covered in the README:
Web.configtoappsettings.json, keeping their names. Seedocs/configuration.md.Encrypt=False, because the modern SQL client encrypts by default.Two behavioural notes:
LeaseLockMode.SqlApplicationLockis reserved for that case and currently fails with a clear message rather than misbehaving quietly.LicenseServer:AdminRoles, and the server warns at startup while they are not.The load simulator is ported so that it builds, but stays disabled: it needs a client that can download a lease, which is being written in SharpCrafters.Backstage.
🤖 Generated with Claude Code