Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@ This file is a running track of new features and fixes to each version of the pa

This project follows [Semantic Versioning](http://semver.org) guidelines.

## v4.3.1

### Changes

- Tokens will be revoked when an administrator privileges are removed #132

#### From v4.3.0-rc.1

- Added guest agent support for changing Windows user passwords #120
- This feature is still experimental. Please provide feedback on
our [Discord community](https://discord.convoypanel.com/) and report bugs on
our [GitHub
repository](https://github.com/ConvoyPanel/panel/issues).
- Servers will now automatically start after unsuspension #119
- Fixed parsing of user realm types #126
- Fixed broken redirect when unauthenticated while accessing certain admin routes #123
- Fixed fetching of nameservers when there are none present #125

## v4.3.0-rc.1

> [!IMPORTANT]
> The source between v4 and v10 will begin to diverge starting here. Once v10 is complete, v4's commit history will be
> abandoned. We will not be introducing any changes to the database structure in v4 to prevent any conflicts with v10's
> database structure.

### Changes

- Added guest agent support for changing Windows user passwords #120
- Servers will now automatically start after unsuspension #119
- Fixed parsing of user realm types #126
- Fixed broken redirect when unauthenticated while accessing certain admin routes #123
- Fixed fetching of nameservers when there are none present #125

## v4.2.4

### Changes
Expand Down
3 changes: 3 additions & 0 deletions app/Enums/Node/Access/RealmType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ enum RealmType: string
{
case PAM = 'pam';
case PVE = 'pve';
case LDAP = 'ldap';
case AD = 'ad';
case OPEN_ID = 'openid';
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function index(Request $request, AddressPool $addressPool)
AllowedFilter::exact('server_id')->nullable(),
],
)
->paginate(min($request->query('per_page', 50), 100))->appends(
->paginate(min($request->query('per_page', 50), 999999))->appends(
$request->query(),
);

Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/Admin/Nodes/AddressController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function index(Request $request, Node $node)
new FiltersAddressWildcard(),
), AllowedFilter::exact('server_id')->nullable()],
)
->paginate(min($request->query('per_page', 50), 100))->appends(
->paginate(min($request->query('per_page', 50), 999999))->appends(
$request->query(),
);

Expand Down
43 changes: 27 additions & 16 deletions app/Http/Controllers/Admin/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,37 @@
use Convoy\Http\Requests\Admin\Users\StoreUserRequest;
use Convoy\Http\Requests\Admin\Users\UpdateUserRequest;
use Convoy\Models\Filters\FiltersUserWildcard;
use Convoy\Models\SSOToken;
use Convoy\Models\User;
use Convoy\Services\Api\JWTService;
use Convoy\Transformers\Admin\UserTransformer;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

use function is_null;

class UserController extends ApiController
{
public function __construct(private JWTService $JWTService)
{
public function __construct(
private JWTService $JWTService,
private ConnectionInterface $connection,
) {
}

public function index(Request $request)
{
$users = QueryBuilder::for(User::query())
->withCount(['servers'])
->allowedFilters(
[AllowedFilter::exact('id'), 'name', AllowedFilter::exact(
'email',
), AllowedFilter::custom('*', new FiltersUserWildcard())],
)
->paginate(min($request->query('per_page', 50), 100))->appends(
->withCount(['servers'])
->allowedFilters(
[AllowedFilter::exact('id'), 'name', AllowedFilter::exact(
'email',
), AllowedFilter::custom('*', new FiltersUserWildcard())],
)
->paginate(min($request->query('per_page', 50), 100))->appends(
$request->query(),
);

Expand Down Expand Up @@ -61,12 +65,19 @@ public function store(StoreUserRequest $request)

public function update(UpdateUserRequest $request, User $user)
{
$user->update([
'name' => $request->name,
'email' => $request->email,
'root_admin' => $request->root_admin,
...(is_null($request->password) ? [] : ['password' => Hash::make($request->password)]),
]);
$this->connection->transaction(function () use ($request, $user) {
$requestRootAdmin = $request->boolean('root_admin');
if ($user->root_admin !== $requestRootAdmin && ! $requestRootAdmin) {
$user->tokens()->delete();
}

$user->update([
'name' => $request->name,
'email' => $request->email,
'root_admin' => $request->root_admin,
...(is_null($request->password) ? [] : ['password' => Hash::make($request->password)]),
]);
});

$user->loadCount(['servers']);

Expand Down
17 changes: 11 additions & 6 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
Expand All @@ -21,7 +22,7 @@
*/
class User extends Model implements AuthenticatableContract, AuthorizableContract
{
use HasApiTokens, HasFactory, Notifiable, Authenticatable, Authorizable;
use Authenticatable, Authorizable, HasApiTokens, HasFactory, Notifiable;

/**
* The attributes that are mass assignable.
Expand Down Expand Up @@ -75,19 +76,23 @@ public function toReactObject(): array
}

public function createToken(
string $name,
string $name,
ApiKeyType $type,
array $abilities = ['*'],
): NewAccessToken
{
array $abilities = ['*'],
): NewAccessToken {
$token = $this->tokens()->create([
'type' => $type,
'name' => $name,
'token' => hash('sha256', $plainTextToken = Str::random(40)),
'abilities' => $abilities,
]);

return new NewAccessToken($token, $token->getKey() . '|' . $plainTextToken);
return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
}

public function tokens(): MorphMany
{
return $this->morphMany(PersonalAccessToken::class, 'tokenable');
}

public function servers(): HasMany
Expand Down
36 changes: 18 additions & 18 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,40 @@ public function boot(): void
{
Route::bind('server', function ($value) {
return Server::query()->where(strlen($value) === 8 ? 'uuid_short' : 'uuid', $value)
->firstOrFail();
->firstOrFail();
});

$this->routes(function () {
Route::middleware('web')->group(function () {
Route::middleware('guest')->group(base_path('routes/auth.php'));

Route::middleware(['auth.session'])
->group(base_path('routes/base.php'));
->group(base_path('routes/base.php'));

Route::middleware(['auth'])->prefix('/api/client')
->as('client.')
->scopeBindings()
->group(base_path('routes/api-client.php'));
->as('client.')
->scopeBindings()
->group(base_path('routes/api-client.php'));

Route::middleware(['auth', AdminAuthenticate::class])
->prefix('/api/admin')
->as('admin.')
->scopeBindings()
->group(base_path('routes/api-admin.php'));
->prefix('/api/admin')
->as('admin.')
->scopeBindings()
->group(base_path('routes/api-admin.php'));
});

Route::middleware(['api'])->group(function () {
Route::middleware(['auth:sanctum'])
->prefix('/api/application')
->as('application.')
->scopeBindings()
->group(base_path('routes/api-application.php'));
Route::middleware(['auth:sanctum', AdminAuthenticate::class])
->prefix('/api/application')
->as('application.')
->scopeBindings()
->group(base_path('routes/api-application.php'));

Route::middleware([CotermAuthenticate::class])
->prefix('/api/coterm')
->as('coterm.')
->scopeBindings()
->group(base_path('routes/api-coterm.php'));
->prefix('/api/coterm')
->as('coterm.')
->scopeBindings()
->group(base_path('routes/api-coterm.php'));
});
});
}
Expand Down
61 changes: 61 additions & 0 deletions app/Repositories/Proxmox/Server/ProxmoxGuestAgentRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Convoy\Repositories\Proxmox\Server;

use Convoy\Models\Server;
use Webmozart\Assert\Assert;
use Convoy\Repositories\Proxmox\ProxmoxRepository;
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;

class ProxmoxGuestAgentRepository extends ProxmoxRepository
{
/**
* Get Guest Agent status.
*
* @return mixed
*
* @throws ProxmoxConnectionException
*/
public function guestAgentOs()
{
Assert::isInstanceOf($this->server, Server::class);

$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
'server' => $this->server->vmid,
])
->get('/api2/json/nodes/{node}/qemu/{server}/agent/get-osinfo')
->json();

return $this->getData($response);
}

/**
* Update Guest Agent password for Administrator user.
*
* @param string $password
* @return mixed
*
* @throws ProxmoxConnectionException
*/
public function updateGuestAgentPassword(string $username, string $password)
{
Assert::isInstanceOf($this->server, Server::class);

$params = [
'username' => $username,
'password' => $password,
];

$response = $this->getHttpClient()
->withUrlParameters([
'node' => $this->node->cluster,
'server' => $this->server->vmid,
])
->post('/api2/json/nodes/{node}/qemu/{server}/agent/set-user-password', $params)
->json();

return $this->getData($response);
}
}
12 changes: 6 additions & 6 deletions app/Services/Servers/CloudinitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace Convoy\Services\Servers;

use Convoy\Models\Server;
use Illuminate\Support\Arr;
use Convoy\Data\Server\Proxmox\Config\AddressConfigData;
use Convoy\Data\Server\Deployments\CloudinitAddressConfigData;
use Convoy\Repositories\Proxmox\Server\ProxmoxConfigRepository;
use Convoy\Data\Server\Proxmox\Config\AddressConfigData;
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;
use Convoy\Models\Server;
use Convoy\Repositories\Proxmox\Server\ProxmoxConfigRepository;
use Illuminate\Support\Arr;

/**
* Class SnapshotService
Expand Down Expand Up @@ -46,9 +46,9 @@ public function updateHostname(Server $server, string $hostname)

public function getNameservers(Server $server)
{
$nameservers = collect($this->configRepository->setServer($server)->getConfig())->where('key', '=', 'nameserver')->firstOrFail()['value'];
$nameservers = collect($this->configRepository->setServer($server)->getConfig())->where('key', '=', 'nameserver')->first();

return $nameservers ? explode(' ', $nameservers) : [];
return $nameservers ? explode(' ', $nameservers['value']) : [];
}

public function updateNameservers(Server $server, array $nameservers)
Expand Down
31 changes: 23 additions & 8 deletions app/Services/Servers/ServerAuthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,45 @@

use Convoy\Models\Server;
use Convoy\Repositories\Proxmox\Server\ProxmoxConfigRepository;
use Convoy\Repositories\Proxmox\Server\ProxmoxGuestAgentRepository;
use Illuminate\Support\Str;

class ServerAuthService
{
public function __construct(private ProxmoxConfigRepository $configRepository)
public function __construct(private ProxmoxConfigRepository $configRepository, private ProxmoxGuestAgentRepository $guestAgentRepository)
{
}

public function updatePassword(Server $server, string $password)
public function updatePassword(Server $server, string $password): void
{
// if (!empty($password)) {
// Always store CIPassword first
$this->configRepository->setServer($server)->update(['cipassword' => $password]);
// } else {
// $this->configRepository->setServer($server)->update(['delete' => 'cipassword']);
// }

try {
$osInfo = $this->guestAgentRepository->setServer($server)->guestAgentOs();

// If we have valid OS info, decide which username to use
if (is_array($osInfo) && isset($osInfo['result']['name'])) {
$osName = $osInfo['result']['name'];
$username = Str::contains(Str::lower($osName), 'windows') ? 'Administrator' : 'root';

$this->guestAgentRepository
->setServer($server)
->updateGuestAgentPassword($username, $password);
}
} catch (\Exception $e) {
// Optionally log or handle exceptions
}
}

public function getSSHKeys(Server $server)
public function getSSHKeys(Server $server): string
{
$raw = collect($this->configRepository->setServer($server)->getConfig())->where('key', '=', 'sshkeys')->first()['value'] ?? '';

return rawurldecode($raw);
}

public function updateSSHKeys(Server $server, ?string $keys)
public function updateSSHKeys(Server $server, ?string $keys): void
{
if (! empty($keys)) {
$this->configRepository->setServer($server)->update(['sshkeys' => rawurlencode($keys)]);
Expand Down
Loading