1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-06-20 22:04:02 +00:00
Commit graph

1517 commits

Author SHA1 Message Date
lukinovec
0d177c7e9b Use simple tenants table check during hardening
For a niche opt-in feature, checking if the tenants table is in the current schema is good enough for determining if the current DB is central -- the solution where we added getCurrentDatabaseName to each manager was overly complicated for this (though a bit more correct, not worth the added complexity).
2026-06-12 15:34:39 +02:00
github-actions[bot]
ac54c4f65b Fix code style (php-cs-fixer) 2026-06-10 11:24:24 +00:00
lukinovec
c0713d6e66 Add newline after var annotation 2026-06-10 13:23:59 +02:00
github-actions[bot]
397e9ecd93 Fix code style (php-cs-fixer) 2026-06-10 11:22:13 +00:00
lukinovec
34d19e94e2 Make hardening work with all db/schema managers
Previously, hardening only worked with databases, not with schemas. Also test that hardening works with all relevant db managers.
2026-06-10 13:21:41 +02:00
github-actions[bot]
b743720c7c Fix code style (php-cs-fixer) 2026-06-10 05:57:06 +00:00
lukinovec
028b985e54 Improve annotations
Add newline after @var tenant annotation, make usage of`DB::getDatabaseName clearer.
2026-06-10 07:56:30 +02:00
c9fa41111d
update docblocks (methods were changed to static props), validate empty
strings
2026-06-09 19:05:53 -07:00
github-actions[bot]
4760ed9375 Fix code style (php-cs-fixer) 2026-06-10 01:56:43 +00:00
a7aa03158d
refactor: only accept single values in validateParameter()
this is to make handling null easier (previous Arr::wrap() approach
turned null into an empty array rather than [null] requiring two
separate null checks) and testing easier (we use empty arrays as
examples of invalid values in some tests which would previously be
accepted when passed individually as validateParameter([]) rather than
being part of a wider [something, [], ...] array)

also restrict passing null to validatePassword()

also minor grammar fix in the validateParameter() docblock
2026-06-09 18:54:11 -07:00
0fdc59dc5d
comment grammar 2026-06-09 16:11:16 -07:00
lukinovec
1ae7d58fab Convert allowlist methods into static properties 2026-06-09 10:03:44 +02:00
lukinovec
93f77a5881 Fix PHPStan error 2026-06-09 10:00:28 +02:00
lukinovec
540e3635e2 Improve hardening
Make hardening work correctly even for named SQLite DBs, also make the related test test named SQLite DBs instead of just MySQL (the SQLite dataset fails when the DatabaseTenancyBootstrapper changes get reverted).
2026-06-09 09:56:02 +02:00
github-actions[bot]
7972da5475 Fix code style (php-cs-fixer) 2026-06-09 07:03:00 +00:00
lukinovec
565bc41bf3 Use a more specific central db check in the hardening feature
Instead of just checking the presence of the tenants table on the current connection to determine if the table is/isn't tenant, check the current database's name, and if it's the central DB name, throw the runtime exception.
2026-06-09 09:02:31 +02:00
lukinovec
48b8aac42d Consider null parameters invalid
Parameters passed to validateParameter should always be non-null, and if they're null, an exception is thrown.
2026-06-09 08:15:07 +02:00
fbffeb84b3
improve docblocks for allowlists 2026-06-08 16:21:03 -07:00
13e32dd6ab
update docblock on $harden 2026-06-08 15:49:39 -07:00
b3d11587ae
cast numeric params to string params 2026-06-08 15:32:45 -07:00
88156d175d
improve test name 2026-06-08 15:09:30 -07:00
3fb0176878
improve mysql charset/collation validation test 2026-06-08 14:59:40 -07:00
lukinovec
36782ebee7 Move mysql charset/collation validation assertions to a dedicated test 2026-06-08 11:51:35 +02:00
lukinovec
d28abdec89 Delete redundant db username config 2026-06-08 11:44:57 +02:00
lukinovec
cf7e086bf7 Clean up SQLiteDatabaseManager::$path in afterEach 2026-06-08 11:42:03 +02:00
lukinovec
49356a5513 Use more specific exception assertions 2026-06-08 11:37:06 +02:00
lukinovec
407197b190 Use datasets in hardening tests 2026-06-08 11:26:07 +02:00
lukinovec
b3111f1dde Name second param passed to validateDatabaseName for clarity 2026-06-08 11:17:59 +02:00
lukinovec
f9636b15cf Use Arr::wrap instead of (array) 2026-06-08 11:08:34 +02:00
lukinovec
9ea085ef05
Improve wording
Co-authored-by: Samuel Stancl <samuel@archte.ch>
2026-06-08 11:02:34 +02:00
lukinovec
4386a3b1a3 Improve annotations in ValidatesDatabaseParameters 2026-06-08 10:38:23 +02:00
lukinovec
b7045c52d8 Rename harden() to verifyTenantCanUseDatabase() 2026-06-08 10:18:51 +02:00
lukinovec
42a2c8efd9 Improve $harden annotation 2026-06-08 10:03:07 +02:00
lukinovec
b4244be658 Determine data column and internal prefix dynamically instead of hardcoding in harden() 2026-06-08 10:02:49 +02:00
lukinovec
6e82a9ee55 Change @mixin annotations to @see 2026-06-08 09:06:10 +02:00
9055b61a04
Merge branch 'master' into validate-sql-parameters 2026-06-07 14:17:28 -07:00
lukinovec
dfb0e1ad66
TenancyUrlGenerator: override toRoute(), refactor (#1439)
This PR adds the `toRoute()` method override to `TenancyUrlGenerator`.
`toRoute()` now attempts to find a tenant equivalent of the passed route
(= a route with the same name as the passed one, but with the tenant
prefix) and generates URL for the tenant route. This behavior can be
bypassed using the bypass parameter, like with the `route()` method
override `TenancyUrlGenerator` had until now.

The primary reason for adding this is that Livewire v4 no longer uses
the `route()` helper (which automatically prefixes the passed route name
because of the override in `TenancyUrlGenerator`) in
`Livewire::getUpdateUri()`. Now, it uses `toRoute()`
(544aa3dfb8 (diff-e7609f8b0a60bde5a85067803d4e2f08f235c7cee9225a51ea67a85ff9a1d694R52)),
which didn't automatically swap the route for its 'tenant.'-prefixed
equivalent in tenant context (until now). So for the Livewire
integration to work with path identification, we need to override
`toRoute()` as described.

The `temporarySignedRoute()` override got removed because
`temporarySignedRoute()` calls `route()` under the hood, there's no need
to specifically override `temporarySignedRoute()`.

> Note: Browsing old convos, it seems like the `temporarySignedRoute()`
override was needed to make Livewire file uploads work with path
identification, but it's not needed anymore. TenancyUrlGenerator had
some changes since then, and now, I can't see the _exact_ reason why we
needed the override (`temporarySignedRoute()` uses `route()` under the
hood, so the only thing that should really matter is overriding
`route()`/`toRoute()`). It was likely a leftover from some older
implementation.

The `route()` override got simplified. Since `route()` uses `toRoute()`
under the hood, the `route()` override only has to have the prefixing
logic. The rest is delegated to `toRoute()`.

> Note: Even though we override `toRoute()` now which `route()` uses for
generating the URLs, we still need to override `route()` for its
`$this->routes->getByName($name)` call to receive the prefixed name. For
example, if `route()` wasn't overridden, and we only had one route:
`tenant.foo` (no central `foo` route), and we'd call `route('foo')`,
we'd get an exception saying that route "foo" wasn't found, even if
automatic route name prefixing was enabled and `toRoute()` was
overridden. With the `route()` override, `route('foo')` acts as if we
passed 'tenant.foo' instead of 'foo'.

Comments in TenancyUrlGenerator and UrlGeneratorBootstrapper got updated
to be more accurate. All _intentionally_ affected methods are listed in
TenancyUrlGenerator's docblock.

---------

Co-authored-by: Samuel Stancl <samuel@archte.ch>
2026-06-06 14:52:37 -07:00
lukinovec
ad4c924d5c
[MINOR BC] Create pending tenants with pending_since, improve --with-pending (#1458)
> Minor breaking change: Pending tenants would previously go through the
creation pipeline as *not* pending and would only be marked as pending
after full creation. Now, pending tenants go through the creation
process with pending_since set from the start.

Pending tenants aren't getting their `pending_since` set until they're
created completely (e.g. their DB was created, migrated and seeded --
first, the tenant is created fully, and only after that, the tenant is
updated to have `pending_since`). This is a problem if someone wants to
e.g. add a job to the `DatabaseCreated` job pipeline that would check
`$this->tenant->pending()`. Since at the point of `DatabaseCreated`, the
tenant's `pending_since` isn't set yet, `$this->tenant->pending()`
returns `false`, even for tenants created using `createPending()`. So
instead of letting the pending tenant get fully created, and only after
that, setting its `pending_since` (using `update()`), we now set
`pending_since` in `create()`. `CreatingPendingTenant` is now dispatched
from the `static::creating` hook, and `PendingTenantCreated` is
dispatched from `static::created` for consistency.

Setting `pending_since` right in `create()` made the `MigrateDatabase`
and `SeedDatabase` jobs exclude the pending tenants during their
creation if the `tenancy.pending.include_in_queries` config was set to
`false` -- in that case, these jobs would never migrate or seed the
databases of pending tenants. So these jobs now pass `--with-pending` to
their underlying commands, with the value set in their `$includePending`
static property (`true` by default). This overrides the
`tenancy.pending.include_in_queries` config -- unless the
`$includePending` properties are set to `false`, these jobs will always
include pending tenants.

The `--with-pending` tenant command option originally worked just to
opt-in for including pending tenants in the command. Now,
`--with-pending` can accept values (`true`/`1` or `false`/`0`), so e.g.
- `tenants:run foo` with
`--with-pending`/`--with-pending=true`/`--with-pending=1` includes
pending tenants
- `tenants:run foo` with `--with-pending=false`/`--with-pending=0`
**excludes** pending tenants (also `--with-pending=foobar` -- invalid
input, considered `false`)

Passing `--with-pending` makes the command bypass the
`tenancy.pending.include_in_queries` config (so e.g. if
`tenancy.pending.include_in_queries` is set to `true`, and
`--with-pending=false` is passed to a command, the command will exclude
pending tenants). When `--with-pending` is not passed, the command will
include or exclude pending tenants based on the
`tenancy.pending.include_in_queries` config.

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Samuel Štancl <samuel@archte.ch>
2026-06-05 15:36:57 -07:00
lukinovec
c0fbf6dcbd
[MINOR BC] UserImpersonation: store auth guard in session, add $logout param to stopImpersonating() (#1437)
> Minor breaking change: `session('tenancy_impersonating')` doesn't work
anymore. Use `session('tenancy_impersonation_guard')` instead.

The 'tenancy_impersonating' session variable got replaced by
'tenancy_impersonation_guard'. `UserImpersonation::stopImpersonating()`
now calls `logout()` on the guard retrieved by
`session()->get('tenancy_impersonation_guard')` instead of calling
`logout()` on the _current_ auth guard. Now. if you create the
impersonation token with guard 'web', and call
`UserImpersonation::stopImpersonating()`, for example in a route that
has the `auth:sanctum` middleware (= the current guard in that route
would be `RequestGuard` which doesn't even have the `logout()` method --
not the guard for which the impersonation token was created), the method
will correctly log the user out of the 'web' guard using which he was
actually authenticated instead of the current guard of the visited route
(which doesn't have to be the same guard for which impersonation
started).

`UserImpersonation::stopImpersonating()` now also accepts the `$logout`
parameter, which is `true` by default. If `false` is passed, the method
just forgets `tenancy_impersonation_guard` from session without logging
out.

`UserImpersonation::stopImpersonating()` now throws an exception if
impersonation wasn't active at the point of calling the method.

---------

Co-authored-by: Samuel Stancl <samuel@archte.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-05 14:15:19 -07:00
lukinovec
da7eb94c07
Remove redundant universal route check from PreventAccess MW (#1427)
The PreventAcessFromUnwantedDomains MW had the
`tenancy()->routeIsUniversal($route)` check either for returning early,
or it was a leftover from some older implementation, so I removed it.

The middleware aborts if the
`$this->accessingTenantRouteFromCentralDomain($request, $route) ||
$this->accessingCentralRouteFromTenantDomain($request, $route)` check
passes. Meaning, **for the middleware to abort, the route has to be
either in central or tenant mode**. When the route is in universal mode,
the middleware will never reach `return $abortRequest()`. `return
$next($request)` will always get reached, even when the `||
tenancy()->routeIsUniversal($route)` check is deleted from the previous
condition, so that check was basically useless.

Since the docblock for the class does mention the behavior for universal
routes explicitly, we've instead added a comment documenting that things
work this way. That's probably the most reasonable way to have this
explicit behavior for universal routes easily understandable in this
fairly complex logic without redundant code.

Resolves #1418

---------

Co-authored-by: Samuel Štancl <samuel@archte.ch>
2026-05-12 23:59:21 +02:00
lukinovec
ec06dcc52e
Correct DomainTenantResolver::isSubdomain() check (#1425)
Added a failing test for determining if a host is a subdomain, then
fixed `DomainTenantResolver::isSubdomain()` (similar fix as in #1423)
and a related assertion.

Previously, while having `tenancy.identification.central_domains` set to
e.g. `['site.com']`, the `isSubdomain()` check consider `tenantsite.com`
a subdomain because it ends with `site.com`. Now, instead of the
`endsWith()` check, the method checks if the passed domain is in the
configured central domains. If it is, it returns `false`. Otherwise,
loop through all the central domains and check if the passed domain
matches any of the central domains prefixed with a dot (e.g.
`tenant.site.com` would be considered a subdomain, `tenant.site.com`
wouldn't).

Because in InitializeTenancyByDomainOrSubdomain, if tenancy fails to
initialize using a subdomain (before this PR's changes, e.g.
`tenantsite.com` would be considered a subdomain, and `tenantsite` would
be used for initializing tenancy), it'll catch the exception and use the
whole domain for identification instead, this error will likely never be
noticed in real-world usage. So this PR corrects the subdomain detection
logic, but the real-world impact of that is negligible.

> Note: The subdomain error catching logic in domainOrSubdomain ID MW
was added in v4. If we applied this change in v3, it'd fix a real issue
where domainOrSubdomain ID MW would just fail at the subdomain
initialization, without attempting domain initialization after the
failure.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Samuel Štancl <samuel@archte.ch>
2026-05-11 14:26:06 +02:00
lukinovec
d9ae27425d Delete redundant cleanup 2026-05-05 10:06:41 +02:00
lukinovec
519c819e28 Delete user created in validation test 2026-05-05 08:30:18 +02:00
lukinovec
649c8027f4 Use unique DB names and passwords in test 2026-05-04 17:14:16 +02:00
lukinovec
099a666dbc Add valid password assertion 2026-05-04 15:38:42 +02:00
lukinovec
587f347b64 Restore default charset after assertion 2026-05-04 15:19:12 +02:00
lukinovec
bbd8f6fd98 Add parentheses to instanceof check 2026-05-04 14:37:01 +02:00
lukinovec
03318752b6 Specify charset and collation config in test 2026-05-04 13:40:42 +02:00
lukinovec
66ae88a325 Fix non-string parameter validation assertion 2026-05-04 13:26:01 +02:00
lukinovec
e59195eefe Improve coverage
Cover non-string parameter validation and in-memory DB name validation
2026-05-04 13:04:57 +02:00