Adds a --skip-tenants option to all tenant artisan commands
(`tenants:run`, `tenants:migrate`, `tenants:rollback`, `tenants:seed`,
`tenants:up`, `tenants:down`).
The option is the complement of the existing `--tenants` option instead
of specifying which tenants to include, you specify which to exclude.
---------
Co-authored-by: Jimish Gamit <unique.jimish@gmail.com>
Co-authored-by: Samuel Stancl <samuel@archte.ch>
Co-authored-by: lukinovec <lukinovec@gmail.com>
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>
> 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>
> 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>
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>
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>
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>
The `CreateTenantStorage` and `DeleteTenantStorage` listeners were used
alongside JobPipelines. When the `TenantCreated` JobPipeline had
`shouldBeQueued(true)` and the `Listeners\CreateTenantStorage` was
uncommented, the listener would throw an exception
(`Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException
Database tenantX.sqlite does not exist.`) because at the time of
executing the listener, the tenant DB wasn't created yet.
The same issue could likely also occur in the `DeleteTenantStorage`
listener as it uses `tenancy()->run()` to resolve the tenant's storage
path which wouldn't work if the tenant's database (or other resources)
was already deleted, making initialization impossible.
This PR changes `DeleteTenantStorage` into a job and puts it (commented)
into the job pipeline, so that it can be queued with the rest of the
jobs. It also removes `CreateTenantStorage` because it should be
redundant with the FilesystemTenancyBootstrapper creating the same paths
automatically when storage path is suffixed.
The old classes are kept but deprecated for backwards compatibility.
We've also added some edge case hardening to `DeleteTenantStorage` to
make sure it never deletes the central storage path directory, which
previously could in theory occur due to a misconfiguration if a user
enabled this job/listener but disabled storage path suffixing.
Co-authored-by: Samuel Štancl <samuel@archte.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
At the moment, `where()` cannot be used correctly while using
`withoutPending()`. For example, if we have a single non-pending tenant
in our DB (with ID 'foo'), queries like
`Tenant::withoutPending()->where('id', 'nonexistent')->first()`will
incorrectly return the non-pending tenant ('foo').
This is because `withoutPending()` does
`$builder->whereNull('data->pending_since')->orWhereNull('data')`. These
two aren't grouped, so `withoutPending()->where('id', 'nonexistent')`
basically translates to "WHERE data->pending_since IS NULL **OR (data IS
NULL AND id = 'nonexistent')**". So the query will include all tenants
whose `pending_since` is null (= all non-pending tenants).
Grouping `->whereNull('data->pending_since')->orWhereNull('data')` in a
closure passed to a separate `where()` fixes this issue.
phpstan started failing with '... implements generic interface
Illuminate\Database\Eloquent\Scope but does not specify its types:
TModel'. We solve this by adding an implements docblock to the scopes
implementing that interface. They're fairly generic - we just use the
Model type itself in the code - so we use Model for the type parameter.
This prevents race conditions that may occur if there are two concurrent
processes trying to create the storage path for the tenant. The
storagePath() method runs during bootstrap() which can easily happen
in two places at once. The race condition specifically occurs in between
the is_dir() check and the mkdir() call, the latter producing an
exception if the dir already exist. We simply ignore any error coming
out of mkdir() and then check for success separately.
We could omit that success check since failure is unlikely and would
only occur due to a server misconfiguration that would manifest itself
in other ways as well, but this way the simple TOC/TOU race condition
is prevented while other errors are still reported.
We apply the same change to the mkdir() in scopeSessions() as the logic
is similar.
Resolves#1452
Instead of handling just the default channel, make LogTenancyBootstrapper handle all the channels that should be affected (= $storagePathChannels, channels from $channelOverrides and the default channel in case 'stack' is the default)
Some bootstrappers read attributes of the tenant during bootstrap() but
don't respond to changes made to the tenant afterwards.
Therefore, when making changes to the tenant that'd affect the behavior
of a bootstrapper, it's necessary to reinitialize tenancy (if it matters
that changes are reflected immediately). This adds a convenience helper
for that purpose.
- Update ci.yml and composer.json
- Wrap single database tenancy trait scopes in whenBooted()
- Update SessionSeparationTest to use laravel-cache- prefix in L13
and laravel_cache_ in <=L12. Our own prefix remains tenant_%tenant%_
(as configured in tenancy.cache.prefix). We could update this to be
tenant-%tenant%- from now on for consistency with Laravel's prefixes
(changed in https://github.com/laravel/framework/pull/56172) but I'm
not sure yet. _ seems to read a bit better but perhaps consistency
is more important. We may change this later and it can be adjusted
in userland easily (since it's just a config option).
Using `URL::temporarySignedRoute()` in tenant context with
`UrlGeneratorBootstrapper` enabled doesn't work the same as `route()`.
The bypass parameter doesn't actually bypass the route name prefixing.
`route()` is called in the `parent::temporarySignedRoute()` call, and
because the bypass parameter is removed before calling
`parent::temporarySignedRoute()`, the underlying `route()` call doesn't
get the bypass parameter and it ends up attempting to generate URL for a
route with the name prefixed with 'tenant.'.
This PR adds the bypass parameter back after `prepareRouteInputs()`, so
that `parent::temporarySignedRoute()` receives it, and the underlying
`route()` call respects it. Also added basic tests for the
`URL::temporarySignedRoute()` behavior (the new bypass parameter test
works as a regression test).
This PR fixes the URL override example in TenancyServiceProvider stub
(the commented `overrideUrlInTenantContext()` segment). If the tenant
doesn't have any domain, set the root URL back to the original one.
This pull request adds improved PHPDoc type annotations to several
Eloquent relationship methods, enhancing static analysis and developer
experience. These changes clarify the expected return types for
relationships, making the codebase easier to understand and work with.
Relationship method type annotations:
* Added a detailed return type annotation to the `tenant` method in the
`BelongsToTenant` trait, specifying the related model and the current
class.
* Added a detailed return type annotation to the `domains` method in the
`HasDomains` trait, specifying the related model and the current class.
* Added a detailed return type annotation to the `tenants` method in the
`ResourceSyncing` class, specifying the related model and the current
class.