1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-06-21 11:44:02 +00:00
Commit graph

1489 commits

Author SHA1 Message Date
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
lukinovec
bdbfbd4561 Remove extra variable 2026-05-04 12:32:25 +02:00
lukinovec
de913486e0 Specify exception message in assertions 2026-05-04 12:27:46 +02:00
lukinovec
6ed9975e85 Catch broader range of exceptions (harden() in DB bootstrapper) 2026-05-04 12:14:39 +02:00
lukinovec
98a808bb98 Quote schema names in GRANT statements
PermissionControlledPostgreSQLDatabaseManager now uses the same quoting in GRANT statements as its schema counterpart.
2026-05-04 11:59:11 +02:00
github-actions[bot]
fec170ada9 Fix code style (php-cs-fixer) 2026-05-04 09:55:13 +00:00
lukinovec
338526d9fb Query for MySQL defaults instead of assuming them in charset test 2026-05-04 11:54:45 +02:00
lukinovec
2b3466f951 Check the current DB name instead of configured one in harden() 2026-05-04 11:48:55 +02:00
lukinovec
405aaafb4e Handle MySQL charset and collation
Make createDatabase execute CREATE DATABASE without passing charset and collation so that if these parameters are null, the MySQL server's defaults will be used. Only add charset and collation to the statement if they're not null.
2026-05-04 11:15:51 +02:00
Thomas
23b18c93a0
Skip DB deletion when create_database=false, add ignoreFailures (#1394)
Database deletion is now skipped by default if the tenant has the
`create_database` internal attribute set to false, meaning it was likely
created without a database. This skip can be opted out of by changing a
static property.

It also adds an opt-in static property for ignoring any other failures
during database deletion, to allow continuing execution of the delete
pipeline.

---------

Co-authored-by: Samuel Štancl <samuel@archte.ch>
2026-05-01 21:57:19 +02:00
41701aff5f
phpstan fix: Model covariants in Scope generics
Builds on changes in recent commit:
Commit ID: c32f52ce7c
Change ID: qsnosyvyulxzrnzorpxqwqqztmqorsmk
2026-05-01 16:09:52 +02:00
53f44762ca
docker: change mssql env yaml syntax 2026-05-01 15:55:06 +02:00
lukinovec
ea20eb13b6 Validate in-memory DBs outside of isInMemory
isInMemory should check if the name looks ilke an in-memory database name and return bool (it shouldn't throw validation errors).

Also, make the validation methods non-static.
2026-05-01 15:22:40 +02:00
lukinovec
429e0985fd Improve code quality and comments 2026-05-01 15:17:38 +02:00
lukinovec
26c161a940 Add regression test for makeConnectionConfig not working correctly with custom $path
In makeConnectionConfig, changing the $this->getPath($databaseName) line back to `$baseConfig['database'] = database_path($databaseName);` will make the added test fail.
2026-05-01 15:16:54 +02:00
lukinovec
7660ddd3ab Improve readability of harden() call 2026-05-01 14:43:27 +02:00
lukinovec
7f93f4460a Test that the SQLite DB manager recognizes in-memory DBs 2026-05-01 14:35:18 +02:00
lukinovec
9a9adc0d99 Use getPath() in makeConnectionConfig()
makeConnectionConfig() would use database_path() to generate the DB path, which is correct only when the $path static property is null.
2026-05-01 14:27:56 +02:00
lukinovec
e48d822772 Validate SQLite DB name unconditionally in getPath() 2026-05-01 14:15:47 +02:00
github-actions[bot]
7683befa54 Fix code style (php-cs-fixer) 2026-05-01 12:10:13 +00:00
lukinovec
48b4837905 Validate in-memory db names, move SQLite-specific methods to the SQLiteManager 2026-05-01 14:09:56 +02:00
lukinovec
7363318f6e Make in-memory DB detection more strict
In-memory DBs have to start with "file:_tenancy_inmemory_". This prevents path traversal.
2026-05-01 13:09:37 +02:00
lukinovec
b1f0d0a43c
Get central DB from config in harden test
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-05-01 12:34:28 +02:00
lukinovec
2ae1f79d50 Cover empty string parameters 2026-05-01 12:32:03 +02:00
lukinovec
0ce3d863ce DATABASE_URL test: set config for both datasets 2026-05-01 12:11:00 +02:00
lukinovec
52f6857302 If harden throws an exception, revert connection back to central 2026-05-01 12:08:02 +02:00
lukinovec
f5f5f1d4aa Fix DB bootstrapper test
"database tenancy bootstrapper throws an exception if DATABASE_URL is set" was failing with the null $databaseUrl because the tenant DB was never created. This test was ignored during test runs because the test file lacked the 'Test' suffix.
2026-05-01 11:53:27 +02:00
github-actions[bot]
fc6a931a32 Fix code style (php-cs-fixer) 2026-05-01 09:50:30 +00:00
lukinovec
fbd1e02564 Correct DatabaseTenancyBootstrapper test filename
DatabaseTenancyBootstrapper is ignored by ./t, it should be suffixed with 'Test'.
2026-05-01 11:50:01 +02:00