Merge remote-tracking branch 'origin/next' into automation/sync-main-to-next

This commit is contained in:
Andras Bacsai
2026-10-26 12:40:59 +01:00
134 changed files with 4031 additions and 1325 deletions
+2 -1
View File
@@ -23,6 +23,7 @@ use App\Traits\HasNoindexDomains;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ReleasesManagedDnsRecords;
use App\Traits\ScopedByAccessGrants;
use Database\Factories\ApplicationFactory;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -139,7 +140,7 @@ use Symfony\Component\Yaml\Yaml;
class Application extends BaseModel
{
/** @use HasFactory<ApplicationFactory> */
use Auditable, ClearsGlobalSearchCache, HasComposeVolumeWarnings, HasConfiguration, HasFactory, HasMaintenancePage, HasMetrics, HasNoindexDomains, HasSafeStringAttribute, HasSecretManager, ReleasesManagedDnsRecords, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasComposeVolumeWarnings, HasConfiguration, HasFactory, HasMaintenancePage, HasMetrics, HasNoindexDomains, HasSafeStringAttribute, HasSecretManager, ReleasesManagedDnsRecords, ScopedByAccessGrants, SoftDeletes;
public const MAX_DOCKER_COMPOSE_SIZE_BYTES = 5 * 1024 * 1024;
+15
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Casts\EncryptedArrayCast;
use App\Enums\ApplicationDeploymentStatus;
use App\Services\Access\AccessResolver;
use App\Services\Security\SensitiveDataRedactor;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -49,6 +50,20 @@ use OpenApi\Attributes as OA;
)]
class ApplicationDeploymentQueue extends Model
{
/**
* Only deployments of applications the authenticated member or API token can see.
*/
public function scopeVisibleToCurrentActor(Builder $query): Builder
{
foreach (app(AccessResolver::class)->currentLayers() as $layer) {
if (! $layer->coversWholeTeam()) {
return $query->whereIn('application_id', Application::query()->pluck('id')->map(fn ($id) => (string) $id));
}
}
return $query;
}
/**
* Log redaction variants of remote secrets. Kept in memory only, never saved.
*
+4 -2
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use App\Services\Access\AccessResolver;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
@@ -150,10 +151,11 @@ class ApplicationSetting extends Model
*/
public static function isContainerNamePrefixInUse(string $prefix, Server $server, ?int $ignoreApplicationId = null): bool
{
return $server->applications()->contains(function (Application $application) use ($prefix, $ignoreApplicationId) {
// Hidden applications of a scoped member still count, or two names on one server could collide.
return app(AccessResolver::class)->withoutVisibilityScope(fn () => $server->applications()->contains(function (Application $application) use ($prefix, $ignoreApplicationId) {
return $application->id !== $ignoreApplicationId
&& in_array($prefix, [$application->uuid, $application->settings->custom_container_name_prefix, $application->settings->custom_internal_name], true);
});
}));
}
public function stopGracePeriodSeconds(): int
+2 -1
View File
@@ -5,6 +5,7 @@ namespace App\Models;
use App\Traits\Auditable;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use OpenApi\Attributes as OA;
@@ -24,7 +25,7 @@ use OpenApi\Attributes as OA;
class Environment extends BaseModel
{
use Auditable, HasFactory;
use ClearsGlobalSearchCache;
use ClearsGlobalSearchCache, ScopedByAccessGrants;
use HasSafeStringAttribute;
protected $fillable = [
+2
View File
@@ -13,12 +13,14 @@ class PersonalAccessToken extends SanctumPersonalAccessToken
'expires_at',
'api_token_expiration_warning_sent_at',
'team_id',
'scopes',
];
protected function casts(): array
{
return [
'api_token_expiration_warning_sent_at' => 'datetime',
'scopes' => 'array',
];
}
}
+2 -1
View File
@@ -5,6 +5,7 @@ namespace App\Models;
use App\Traits\Auditable;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Collection;
use OpenApi\Attributes as OA;
@@ -22,7 +23,7 @@ use OpenApi\Attributes as OA;
class Project extends BaseModel
{
use Auditable, HasFactory;
use ClearsGlobalSearchCache;
use ClearsGlobalSearchCache, ScopedByAccessGrants;
use HasSafeStringAttribute;
protected $fillable = [
@@ -0,0 +1,43 @@
<?php
namespace App\Models\Scopes;
use App\Models\Environment;
use App\Models\Project;
use App\Services\Access\AccessResolver;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
/**
* Hides projects, environments, and resources that the authenticated member or API token has no grant for.
*
* It filters only when the actor has no team-wide grant, so owners, admins, team-wide members and tokens,
* queue workers, and console commands see the same rows as before.
*/
class VisibleThroughAccessGrants implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$resolver = app(AccessResolver::class);
if ($resolver->isResolving()) {
return;
}
foreach ($resolver->currentLayers() as $layer) {
if ($layer->coversWholeTeam()) {
continue;
}
if ($model instanceof Project) {
$builder->whereIn($model->qualifyColumn('id'), $layer->visibleProjectIds());
} elseif ($model instanceof Environment) {
$builder->whereIn($model->qualifyColumn('id'), $layer->visibleEnvironmentIds());
} else {
$builder->where(fn (Builder $query) => $query
->whereIn($model->qualifyColumn('environment_id'), $layer->fullEnvironmentIds())
->orWhereIn($model->qualifyColumn('uuid'), $layer->resourceUuids()));
}
}
}
}
+2 -1
View File
@@ -12,6 +12,7 @@ use App\Traits\HasComposeVolumeWarnings;
use App\Traits\HasMaintenancePage;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -46,7 +47,7 @@ use OpenApi\Attributes as OA;
)]
class Service extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasComposeVolumeWarnings, HasFactory, HasMaintenancePage, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasComposeVolumeWarnings, HasFactory, HasMaintenancePage, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
private static $parserVersion = '5';
+2 -1
View File
@@ -9,6 +9,7 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -16,7 +17,7 @@ use Throwable;
class StandaloneClickhouse extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
protected array $auditExclude = ['last_online_at'];
+2 -1
View File
@@ -9,13 +9,14 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneDragonfly extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
protected $fillable = [
'uuid',
+2 -1
View File
@@ -9,13 +9,14 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneKeydb extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
protected $fillable = [
'uuid',
+2 -1
View File
@@ -9,6 +9,7 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphTo;
@@ -16,7 +17,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
protected $fillable = [
'uuid',
+2 -1
View File
@@ -9,13 +9,14 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMongodb extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
protected $fillable = [
'uuid',
+2 -1
View File
@@ -9,13 +9,14 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMysql extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
protected $fillable = [
'uuid',
+2 -1
View File
@@ -9,13 +9,14 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandalonePostgresql extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
protected $fillable = [
'uuid',
+2 -1
View File
@@ -9,13 +9,14 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneRedis extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
protected array $auditExclude = ['last_online_at'];
+2 -1
View File
@@ -9,6 +9,7 @@ use App\Traits\HasLogRedactionSetting;
use App\Traits\HasMetrics;
use App\Traits\HasSafeStringAttribute;
use App\Traits\HasSecretManager;
use App\Traits\ScopedByAccessGrants;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -17,7 +18,7 @@ use InvalidArgumentException;
class StandaloneSqlite extends BaseModel
{
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasLogRedactionSetting, HasMetrics, HasSafeStringAttribute, HasSecretManager, ScopedByAccessGrants, SoftDeletes;
public const DATA_DIRECTORY = '/var/lib/sqlite';
+1 -1
View File
@@ -337,7 +337,7 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
public function members()
{
return $this->belongsToMany(User::class, 'team_user', 'team_id', 'user_id')->withPivot('role');
return $this->belongsToMany(User::class, 'team_user', 'team_id', 'user_id')->withPivot('role', 'access');
}
public function subscription()
+12 -1
View File
@@ -3,14 +3,17 @@
namespace App\Models;
use App\Actions\User\RevokeUserTeamTokens;
use App\Enums\AccessLevel;
use App\Jobs\UpdateStripeCustomerEmailJob;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\EmailChangeVerification;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use App\Services\Access\AccessResolver;
use App\Services\ChangelogService;
use App\Traits\DeletesUserSessions;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Messages\MailMessage;
@@ -285,7 +288,7 @@ class User extends Authenticatable implements PasskeyUser, SendsEmail
public function teams()
{
return $this->belongsToMany(Team::class)->withPivot('role');
return $this->belongsToMany(Team::class)->withPivot('role', 'access');
}
public function changelogReads()
@@ -484,6 +487,14 @@ class User extends Authenticatable implements PasskeyUser, SendsEmail
return data_get($team, 'pivot.role');
}
/**
* Check the access grants of the user (and the API token of the request) on a project, environment, or resource.
*/
public function canAccess(AccessLevel $level, Model $target): bool
{
return app(AccessResolver::class)->allows($this, $level, $target);
}
/**
* Check if the user is an admin or owner of a specific team
*/