feat: token permissions

feat: handle sensitive data
feat: handle read-only data
This commit is contained in:
Andras Bacsai 2024-07-02 12:15:58 +02:00
parent 1249b1ece9
commit c39d6dd407
28 changed files with 328 additions and 201 deletions

View File

@ -21,6 +21,27 @@
class ApplicationsController extends Controller class ApplicationsController extends Controller
{ {
private function removeSensitiveData($application)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($application);
}
$application->makeHidden([
'custom_labels',
'dockerfile',
'docker_compose',
'docker_compose_raw',
'manual_webhook_secret_bitbucket',
'manual_webhook_secret_gitea',
'manual_webhook_secret_github',
'manual_webhook_secret_gitlab',
'private_key_id',
]);
return serializeApiResponse($application);
}
public function applications(Request $request) public function applications(Request $request)
{ {
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
@ -32,7 +53,7 @@ public function applications(Request $request)
$applications->push($projects->pluck('applications')->flatten()); $applications->push($projects->pluck('applications')->flatten());
$applications = $applications->flatten(); $applications = $applications->flatten();
$applications = $applications->map(function ($application) { $applications = $applications->map(function ($application) {
return serializeApiResponse($application); return $this->removeSensitiveData($application);
}); });
return response()->json([ return response()->json([
@ -484,10 +505,6 @@ public function application_by_uuid(Request $request)
if (! $uuid) { if (! $uuid) {
return response()->json(['success' => false, 'message' => 'UUID is required.'], 400); return response()->json(['success' => false, 'message' => 'UUID is required.'], 400);
} }
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) { if (! $application) {
return response()->json(['success' => false, 'message' => 'Application not found.'], 404); return response()->json(['success' => false, 'message' => 'Application not found.'], 404);
@ -495,7 +512,7 @@ public function application_by_uuid(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => serializeApiResponse($application), 'data' => $this->removeSensitiveData($application),
]); ]);
} }
@ -625,7 +642,7 @@ public function update_by_uuid(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => serializeApiResponse($application), 'data' => $this->removeSensitiveData($application),
]); ]);
} }
@ -635,10 +652,6 @@ public function envs_by_uuid(Request $request)
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) { if (! $application) {

View File

@ -20,6 +20,27 @@
class DatabasesController extends Controller class DatabasesController extends Controller
{ {
private function removeSensitiveData($database)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($database);
}
$database->makeHidden([
'internal_db_url',
'external_db_url',
'postgres_password',
'dragonfly_password',
'redis_password',
'mongo_initdb_root_password',
'keydb_password',
'clickhouse_admin_password',
]);
return serializeApiResponse($database);
}
public function databases(Request $request) public function databases(Request $request)
{ {
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
@ -32,7 +53,7 @@ public function databases(Request $request)
$databases = $databases->merge($project->databases()); $databases = $databases->merge($project->databases());
} }
$databases = $databases->map(function ($database) { $databases = $databases->map(function ($database) {
return serializeApiResponse($database); return $this->removeSensitiveData($database);
}); });
return response()->json([ return response()->json([
@ -57,7 +78,7 @@ public function database_by_uuid(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => serializeApiResponse($database), 'data' => $this->removeSensitiveData($database),
]); ]);
} }

View File

@ -20,6 +20,20 @@
class DeployController extends Controller class DeployController extends Controller
{ {
private function removeSensitiveData($deployment)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($deployment);
}
$deployment->makeHidden([
'logs',
]);
return serializeApiResponse($deployment);
}
public function deployments(Request $request) public function deployments(Request $request)
{ {
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
@ -61,7 +75,7 @@ public function deployment_by_uuid(Request $request)
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => serializeApiResponse($deployment->makeHidden('logs')), 'data' => $this->removeSensitiveData($deployment),
]); ]);
} }

View File

@ -7,17 +7,36 @@
class TeamController extends Controller class TeamController extends Controller
{ {
private function removeSensitiveData($team)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($team);
}
$team->makeHidden([
'smtp_username',
'smtp_password',
'resend_api_key',
'telegram_token',
]);
return serializeApiResponse($team);
}
public function teams(Request $request) public function teams(Request $request)
{ {
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$teams = auth()->user()->teams; $teams = auth()->user()->teams->sortBy('id');
$teams = $teams->map(function ($team) {
return $this->removeSensitiveData($team);
});
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => serializeApiResponse($teams), 'data' => $teams,
]); ]);
} }
@ -33,6 +52,7 @@ public function team_by_id(Request $request)
if (is_null($team)) { if (is_null($team)) {
return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404);
} }
$team = $this->removeSensitiveData($team);
return response()->json([ return response()->json([
'success' => true, 'success' => true,
@ -52,10 +72,11 @@ public function members_by_id(Request $request)
if (is_null($team)) { if (is_null($team)) {
return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); return response()->json(['success' => false, 'message' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404);
} }
$members = $team->members;
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'data' => serializeApiResponse($team->members), 'data' => serializeApiResponse($members),
]); ]);
} }

View File

@ -67,5 +67,7 @@ class Kernel extends HttpKernel
'signed' => \App\Http\Middleware\ValidateSignature::class, 'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
]; ];
} }

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class OnlyRootApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
return response()->json(['success' => false, 'message' => 'You are not allowed to perform this action.'], 403);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ReadOnlyApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
if ($token->can('read-only')) {
return response()->json(['success' => false, 'message' => 'You are not allowed to perform this action.'], 403);
}
return $next($request);
}
}

View File

@ -332,8 +332,7 @@ public function handle(): void
private function backup_standalone_mongodb(string $databaseWithCollections): void private function backup_standalone_mongodb(string $databaseWithCollections): void
{ {
try { try {
ray($this->database->toArray()); $url = $this->database->internal_db_url;
$url = $this->database->get_db_url(useInternal: true);
if ($databaseWithCollections === 'all') { if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
if (str($this->database->image)->startsWith('mongo:4.0')) { if (str($this->database->image)->startsWith('mongo:4.0')) {

View File

@ -46,10 +46,8 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->get_db_url(true); $this->db_url = $this->database->internal_db_url;
if ($this->database->is_public) { $this->db_url_public = $this->database->external_db_url;
$this->db_url_public = $this->database->get_db_url();
}
$this->server = data_get($this->database, 'destination.server'); $this->server = data_get($this->database, 'destination.server');
} }
@ -87,13 +85,12 @@ public function instantSave()
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.'); $this->dispatch('success', 'Database is no longer publicly accessible.');
} }
$this->db_url_public = $this->database->external_db_url;
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public; $this->database->is_public = ! $this->database->is_public;

View File

@ -44,10 +44,8 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->get_db_url(true); $this->db_url = $this->database->internal_db_url;
if ($this->database->is_public) { $this->db_url_public = $this->database->external_db_url;
$this->db_url_public = $this->database->get_db_url();
}
$this->server = data_get($this->database, 'destination.server'); $this->server = data_get($this->database, 'destination.server');
} }
@ -102,13 +100,12 @@ public function instantSave()
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.'); $this->dispatch('success', 'Database is no longer publicly accessible.');
} }
$this->db_url_public = $this->database->external_db_url;
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public; $this->database->is_public = ! $this->database->is_public;

View File

@ -46,10 +46,8 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->get_db_url(true); $this->db_url = $this->database->internal_db_url;
if ($this->database->is_public) { $this->db_url_public = $this->database->external_db_url;
$this->db_url_public = $this->database->get_db_url();
}
$this->server = data_get($this->database, 'destination.server'); $this->server = data_get($this->database, 'destination.server');
} }
@ -108,13 +106,12 @@ public function instantSave()
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.'); $this->dispatch('success', 'Database is no longer publicly accessible.');
} }
$this->db_url_public = $this->database->external_db_url;
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public; $this->database->is_public = ! $this->database->is_public;

View File

@ -52,10 +52,8 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->get_db_url(true); $this->db_url = $this->database->internal_db_url;
if ($this->database->is_public) { $this->db_url_public = $this->database->external_db_url;
$this->db_url_public = $this->database->get_db_url();
}
$this->server = data_get($this->database, 'destination.server'); $this->server = data_get($this->database, 'destination.server');
} }
@ -114,13 +112,12 @@ public function instantSave()
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.'); $this->dispatch('success', 'Database is no longer publicly accessible.');
} }
$this->db_url_public = $this->database->external_db_url;
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public; $this->database->is_public = ! $this->database->is_public;

View File

@ -50,10 +50,8 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->get_db_url(true); $this->db_url = $this->database->internal_db_url;
if ($this->database->is_public) { $this->db_url_public = $this->database->external_db_url;
$this->db_url_public = $this->database->get_db_url();
}
$this->server = data_get($this->database, 'destination.server'); $this->server = data_get($this->database, 'destination.server');
} }
@ -115,13 +113,12 @@ public function instantSave()
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.'); $this->dispatch('success', 'Database is no longer publicly accessible.');
} }
$this->db_url_public = $this->database->external_db_url;
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public; $this->database->is_public = ! $this->database->is_public;

View File

@ -52,10 +52,8 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->get_db_url(true); $this->db_url = $this->database->internal_db_url;
if ($this->database->is_public) { $this->db_url_public = $this->database->external_db_url;
$this->db_url_public = $this->database->get_db_url();
}
$this->server = data_get($this->database, 'destination.server'); $this->server = data_get($this->database, 'destination.server');
} }
@ -113,13 +111,12 @@ public function instantSave()
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.'); $this->dispatch('success', 'Database is no longer publicly accessible.');
} }
$this->db_url_public = $this->database->external_db_url;
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public; $this->database->is_public = ! $this->database->is_public;

View File

@ -72,10 +72,8 @@ public function getListeners()
public function mount() public function mount()
{ {
$this->db_url = $this->database->get_db_url(true); $this->db_url = $this->database->internal_db_url;
if ($this->database->is_public) { $this->db_url_public = $this->database->external_db_url;
$this->db_url_public = $this->database->get_db_url();
}
$this->server = data_get($this->database, 'destination.server'); $this->server = data_get($this->database, 'destination.server');
} }
@ -118,13 +116,12 @@ public function instantSave()
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.'); $this->dispatch('success', 'Database is no longer publicly accessible.');
} }
$this->db_url_public = $this->database->external_db_url;
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public; $this->database->is_public = ! $this->database->is_public;

View File

@ -46,10 +46,8 @@ class General extends Component
public function mount() public function mount()
{ {
$this->db_url = $this->database->get_db_url(true); $this->db_url = $this->database->internal_db_url;
if ($this->database->is_public) { $this->db_url_public = $this->database->external_db_url;
$this->db_url_public = $this->database->get_db_url();
}
$this->server = data_get($this->database, 'destination.server'); $this->server = data_get($this->database, 'destination.server');
} }
@ -102,13 +100,12 @@ public function instantSave()
return; return;
} }
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.'); $this->dispatch('success', 'Database is now publicly accessible.');
} else { } else {
StopDatabaseProxy::run($this->database); StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.'); $this->dispatch('success', 'Database is no longer publicly accessible.');
} }
$this->db_url_public = $this->database->external_db_url;
$this->database->save(); $this->database->save();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public; $this->database->is_public = ! $this->database->is_public;

View File

@ -10,6 +10,12 @@ class ApiTokens extends Component
public $tokens = []; public $tokens = [];
public bool $viewSensitiveData = false;
public bool $readOnly = true;
public array $permissions = ['read-only'];
public function render() public function render()
{ {
return view('livewire.security.api-tokens'); return view('livewire.security.api-tokens');
@ -17,7 +23,33 @@ public function render()
public function mount() public function mount()
{ {
$this->tokens = auth()->user()->tokens; $this->tokens = auth()->user()->tokens->sortByDesc('created_at');
}
public function updatedViewSensitiveData()
{
if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']);
} else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']);
}
if (count($this->permissions) == 0) {
$this->permissions = ['*'];
}
}
public function updatedReadOnly()
{
if ($this->readOnly) {
$this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']);
} else {
$this->permissions = array_diff($this->permissions, ['read-only']);
}
if (count($this->permissions) == 0) {
$this->permissions = ['*'];
}
} }
public function addNewToken() public function addNewToken()
@ -26,7 +58,13 @@ public function addNewToken()
$this->validate([ $this->validate([
'description' => 'required|min:3|max:255', 'description' => 'required|min:3|max:255',
]); ]);
$token = auth()->user()->createToken($this->description); // if ($this->viewSensitiveData) {
// $this->permissions[] = 'view:sensitive';
// }
// if ($this->readOnly) {
// $this->permissions[] = 'read-only';
// }
$token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens; $this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken); session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -195,7 +195,7 @@ public function type(): string
protected function internalDbUrl(): Attribute protected function internalDbUrl(): Attribute
{ {
return new Attribute( return new Attribute(
get: fn () => "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}", get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}",
); );
} }
@ -204,7 +204,7 @@ protected function externalDbUrl(): Attribute
return new Attribute( return new Attribute(
get: function () { get: function () {
if ($this->is_public && $this->public_port) { if ($this->is_public && $this->public_port) {
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
} }
return null; return null;
@ -212,15 +212,6 @@ protected function externalDbUrl(): Attribute
); );
} }
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && ! $useInternal) {
return $this->externalDbUrl;
} else {
return $this->internalDbUrl;
}
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);

View File

@ -212,15 +212,6 @@ protected function externalDbUrl(): Attribute
); );
} }
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && ! $useInternal) {
return $this->externalDbUrl;
} else {
return $this->internalDbUrl;
}
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);

View File

@ -212,15 +212,6 @@ protected function externalDbUrl(): Attribute
); );
} }
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && ! $useInternal) {
return $this->externalDbUrl;
} else {
return $this->internalDbUrl;
}
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);

View File

@ -212,15 +212,6 @@ protected function externalDbUrl(): Attribute
); );
} }
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && ! $useInternal) {
return $this->externalDbUrl;
} else {
return $this->internalDbUrl;
}
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);

View File

@ -232,15 +232,6 @@ protected function externalDbUrl(): Attribute
); );
} }
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && ! $useInternal) {
return $this->externalDbUrl;
} else {
return $this->internalDbUrl;
}
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);

View File

@ -213,15 +213,6 @@ protected function externalDbUrl(): Attribute
); );
} }
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && ! $useInternal) {
return $this->externalDbUrl;
} else {
return $this->internalDbUrl;
}
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);

View File

@ -213,15 +213,6 @@ protected function externalDbUrl(): Attribute
); );
} }
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && ! $useInternal) {
return $this->externalDbUrl;
} else {
return $this->internalDbUrl;
}
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);

View File

@ -208,15 +208,6 @@ protected function externalDbUrl(): Attribute
); );
} }
public function get_db_url(bool $useInternal = false)
{
if ($this->is_public && ! $useInternal) {
return $this->externalDbUrl;
} else {
return $this->internalDbUrl;
}
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);

View File

@ -19,37 +19,67 @@ function invalidTokenResponse()
function serializeApiResponse($data) function serializeApiResponse($data)
{ {
if (! $data instanceof Collection) { if ($data instanceof Collection) {
$data = collect($data); $data = $data->map(function ($d) {
} $d = collect($d)->sortKeys();
$data = $data->sortKeys(); $created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
$created_at = data_get($data, 'created_at'); }
$updated_at = data_get($data, 'updated_at'); if ($updated_at) {
if ($created_at) { unset($d['updated_at']);
unset($data['created_at']); $d['updated_at'] = $updated_at;
$data['created_at'] = $created_at; }
if (data_get($d, 'name')) {
$d = $d->prepend($d['name'], 'name');
}
if (data_get($d, 'description')) {
$d = $d->prepend($d['description'], 'description');
}
if (data_get($d, 'uuid')) {
$d = $d->prepend($d['uuid'], 'uuid');
}
} if (! is_null(data_get($d, 'id'))) {
if ($updated_at) { $d = $d->prepend($d['id'], 'id');
unset($data['updated_at']); }
$data['updated_at'] = $updated_at;
}
if (data_get($data, 'name')) {
$data = $data->prepend($data['name'], 'name');
}
if (data_get($data, 'description')) {
$data = $data->prepend($data['description'], 'description');
}
if (data_get($data, 'uuid')) {
$data = $data->prepend($data['uuid'], 'uuid');
}
if (data_get($data, 'id')) { return $d;
$data = $data->prepend($data['id'], 'id'); });
}
return $data; return $data;
} else {
$d = collect($data)->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
}
if ($updated_at) {
unset($d['updated_at']);
$d['updated_at'] = $updated_at;
}
if (data_get($d, 'name')) {
$d = $d->prepend($d['name'], 'name');
}
if (data_get($d, 'description')) {
$d = $d->prepend($d['description'], 'description');
}
if (data_get($d, 'uuid')) {
$d = $d->prepend($d['uuid'], 'uuid');
}
if (! is_null(data_get($d, 'id'))) {
$d = $d->prepend($d['id'], 'id');
}
return $d;
}
} }
function sharedDataApplications() function sharedDataApplications()

View File

@ -3,29 +3,59 @@
API Tokens | Coolify API Tokens | Coolify
</x-slot> </x-slot>
<x-security.navbar /> <x-security.navbar />
<div class="flex gap-2"> <div class="pb-4 ">
<h2 class="pb-4">API Tokens</h2> <h2>API Tokens</h2>
<x-helper <div>Tokens are created with the current team as scope. You will only have access to this team's resources.
helper="Tokens are created with the current team as scope. You will only have access to this team's resources." /> </div>
</div> </div>
<h4>Create New Token</h4> <h3>New Token</h3>
<form class="flex items-end gap-2 pt-4" wire:submit='addNewToken'> <form class="flex flex-col gap-2 pt-4" wire:submit='addNewToken'>
<x-forms.input required id="description" label="Description" /> <div class="flex items-end gap-2">
<x-forms.button type="submit">Create New Token</x-forms.button> <x-forms.input required id="description" label="Description" />
<x-forms.button type="submit">Create New Token</x-forms.button>
</div>
<div class="flex">
Permissions <x-helper class="px-1" helper="These permissions will be granted to the token." /><span
class="pr-1">:</span>
<div class="flex gap-1 font-bold dark:text-white">
@if ($permissions)
@foreach ($permissions as $permission)
@if ($permission === '*')
<div>All (root/admin access), be careful!</div>
@else
<div>{{ $permission }}</div>
@endif
@endforeach
@endif
</div>
</div>
<h4>Token Permissions</h4>
<div class="w-64">
<x-forms.checkbox label="Read-only" wire:model.live="readOnly"></x-forms.checkbox>
<x-forms.checkbox label="View Sensitive Data" wire:model.live="viewSensitiveData"></x-forms.checkbox>
</div>
</form> </form>
@if (session()->has('token')) @if (session()->has('token'))
<div class="py-4 font-bold dark:text-warning">Please copy this token now. For your security, it won't be shown again. <div class="py-4 font-bold dark:text-warning">Please copy this token now. For your security, it won't be shown
again.
</div> </div>
<div class="pb-4 font-bold dark:text-white"> {{ session('token') }}</div> <div class="pb-4 font-bold dark:text-white"> {{ session('token') }}</div>
@endif @endif
<h4 class="py-4">Issued Tokens</h4> <h3 class="py-4">Issued Tokens</h3>
<div class="grid gap-2 lg:grid-cols-1"> <div class="grid gap-2 lg:grid-cols-1">
@forelse ($tokens as $token) @forelse ($tokens as $token)
<div class="flex items-center gap-2"> <div class="flex flex-col gap-1 p-2 border dark:border-coolgray-200 hover:no-underline">
<div <div>Description: {{ $token->name }}</div>
class="flex items-center gap-2 group-hover:dark:text-white p-2 border border-coolgray-200 hover:dark:text-white hover:no-underline min-w-[24rem] cursor-default"> <div>Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}</div>
<div>{{ $token->name }}</div> <div class="flex gap-1">
@if ($token->abilities)
Abilities:
@foreach ($token->abilities as $ability)
<div class="font-bold dark:text-white">{{ $ability }}</div>
@endforeach
@endif
</div> </div>
<x-modal-confirmation isErrorButton action="revoke({{ data_get($token, 'id') }})"> <x-modal-confirmation isErrorButton action="revoke({{ data_get($token, 'id') }})">
<x-slot:button-title> <x-slot:button-title>
Revoke token Revoke token

View File

@ -10,6 +10,8 @@
use App\Http\Controllers\Api\ServersController; use App\Http\Controllers\Api\ServersController;
use App\Http\Controllers\Api\TeamController; use App\Http\Controllers\Api\TeamController;
use App\Http\Middleware\ApiAllowed; use App\Http\Middleware\ApiAllowed;
use App\Http\Middleware\OnlyRootApiToken;
use App\Http\Middleware\ReadOnlyApiToken;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -31,7 +33,7 @@
}); });
Route::group([ Route::group([
'middleware' => ['auth:sanctum'], 'middleware' => ['auth:sanctum', OnlyRootApiToken::class],
'prefix' => 'v1', 'prefix' => 'v1',
], function () { ], function () {
Route::get('/enable', function () { Route::get('/enable', function () {
@ -81,13 +83,13 @@
Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details']); Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details']);
Route::get('/security/keys', [SecurityController::class, 'keys']); Route::get('/security/keys', [SecurityController::class, 'keys']);
Route::post('/security/keys', [SecurityController::class, 'create_key']); Route::post('/security/keys', [SecurityController::class, 'create_key'])->middleware([ReadOnlyApiToken::class]);
Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid']); Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid']);
Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key']); Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key'])->middleware([ReadOnlyApiToken::class]);
Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key']); Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key'])->middleware([ReadOnlyApiToken::class]);
Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy']); Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy'])->middleware([ReadOnlyApiToken::class]);
Route::get('/deployments', [DeployController::class, 'deployments']); Route::get('/deployments', [DeployController::class, 'deployments']);
Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid']); Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid']);
@ -99,29 +101,29 @@
Route::get('/resources', [ResourcesController::class, 'resources']); Route::get('/resources', [ResourcesController::class, 'resources']);
Route::get('/applications', [ApplicationsController::class, 'applications']); Route::get('/applications', [ApplicationsController::class, 'applications']);
Route::post('/applications', [ApplicationsController::class, 'create_application']); Route::post('/applications', [ApplicationsController::class, 'create_application'])->middleware([ReadOnlyApiToken::class]);
Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid']); Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid']);
Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid']); Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid'])->middleware([ReadOnlyApiToken::class]);
Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid']); Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid'])->middleware([ReadOnlyApiToken::class]);
Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs_by_uuid']); Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs_by_uuid']);
Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env']); Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env'])->middleware([ReadOnlyApiToken::class]);
Route::post('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs']); Route::post('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs'])->middleware([ReadOnlyApiToken::class]);
Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid']); Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid']);
Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid']); Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware([ReadOnlyApiToken::class]);
Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [ApplicationsController::class, 'action_deploy']); Route::match(['get', 'post'], '/applications/{uuid}/action/deploy', [ApplicationsController::class, 'action_deploy'])->middleware([ReadOnlyApiToken::class]);
Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [ApplicationsController::class, 'action_restart']); Route::match(['get', 'post'], '/applications/{uuid}/action/restart', [ApplicationsController::class, 'action_restart'])->middleware([ReadOnlyApiToken::class]);
Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [ApplicationsController::class, 'action_stop']); Route::match(['get', 'post'], '/applications/{uuid}/action/stop', [ApplicationsController::class, 'action_stop'])->middleware([ReadOnlyApiToken::class]);
Route::get('/databases', [DatabasesController::class, 'databases']); Route::get('/databases', [DatabasesController::class, 'databases']);
Route::post('/databases', [DatabasesController::class, 'create_database']); Route::post('/databases', [DatabasesController::class, 'create_database'])->middleware([ReadOnlyApiToken::class]);
Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid']); Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid']);
// Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid']); // Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid']);
Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware([ReadOnlyApiToken::class]);
Route::delete('/envs/{env_uuid}', [EnvironmentVariablesController::class, 'delete_env_by_uuid']); Route::delete('/envs/{env_uuid}', [EnvironmentVariablesController::class, 'delete_env_by_uuid'])->middleware([ReadOnlyApiToken::class]);
}); });