coolify/app/Http/Controllers/Api/TeamController.php
Andras Bacsai b3256d4df1 fix(security): harden model assignment and sensitive data handling
Restrict mass-assignable attributes across user/team/redis models and
switch privileged root/team creation paths to forceFill/forceCreate.

Encrypt legacy ClickHouse admin passwords via migration and cast the
correct ClickHouse password field as encrypted.

Tighten API and runtime exposure by removing sensitive team fields from
responses and sanitizing Git/compose error messages.

Expand security-focused feature coverage for command-injection and mass
assignment protections.
2026-03-29 20:56:04 +02:00

275 lines
8.1 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class TeamController extends Controller
{
private function removeSensitiveData($team)
{
$team->makeHidden([
'custom_server_limit',
'pivot',
]);
return serializeApiResponse($team);
}
#[OA\Get(
summary: 'List',
description: 'Get all teams.',
path: '/teams',
operationId: 'list-teams',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'List of teams.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Team')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function teams(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams->sortBy('id');
$teams = $teams->map(function ($team) {
return $this->removeSensitiveData($team);
});
return response()->json(
$teams,
);
}
#[OA\Get(
summary: 'Get',
description: 'Get team by TeamId.',
path: '/teams/{id}',
operationId: 'get-team-by-id',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'List of teams.',
content: new OA\JsonContent(ref: '#/components/schemas/Team')
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function team_by_id(Request $request)
{
$id = $request->id;
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$team = $this->removeSensitiveData($team);
return response()->json(
serializeApiResponse($team),
);
}
#[OA\Get(
summary: 'Members',
description: 'Get members by TeamId.',
path: '/teams/{id}/members',
operationId: 'get-members-by-team-id',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'List of members.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/User')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function members_by_id(Request $request)
{
$id = $request->id;
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$members = $team->members;
$members->makeHidden([
'pivot',
'email_change_code',
'email_change_code_expires_at',
]);
return response()->json(
serializeApiResponse($members),
);
}
#[OA\Get(
summary: 'Authenticated Team',
description: 'Get currently authenticated team.',
path: '/teams/current',
operationId: 'get-current-team',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'Current Team.',
content: new OA\JsonContent(ref: '#/components/schemas/Team')),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function current_team(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$team = auth()->user()->teams->where('id', $teamId)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
return response()->json(
$this->removeSensitiveData($team),
);
}
#[OA\Get(
summary: 'Authenticated Team Members',
description: 'Get currently authenticated team members.',
path: '/teams/current/members',
operationId: 'get-current-team-members',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'Currently authenticated team members.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/User')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function current_team_members(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$team = auth()->user()->teams->where('id', $teamId)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$team->members->makeHidden([
'pivot',
'email_change_code',
'email_change_code_expires_at',
]);
return response()->json(
serializeApiResponse($team->members),
);
}
}