@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Devices;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\TenantDeviceSetting;
|
||||
use App\Network\Clients\RouterOs\RouterOsApiClient;
|
||||
use App\Network\Drivers\Mikrotik\MikrotikDriver;
|
||||
use RuntimeException;
|
||||
|
||||
class MikrotikActivationService
|
||||
{
|
||||
public function activate(Device $device): array
|
||||
{
|
||||
abort_unless($device->vendor?->slug === 'mikrotik' && $device->hasConnection('routeros_api'), 422, 'Aktivasi otomatis saat ini hanya mendukung MikroTik RouterOS API.');
|
||||
$setting = TenantDeviceSetting::where('tenant_id', $device->tenant_id)->first();
|
||||
if (! $setting) {
|
||||
throw new RuntimeException('BASE_SETTING_REQUIRED: atur Base User dan Password terlebih dahulu.');
|
||||
}
|
||||
$credential = $device->credentials()->where('is_active', true)->orderByDesc('is_master')->first();
|
||||
if (! $credential) {
|
||||
throw new RuntimeException('CREDENTIAL_REQUIRED: tambahkan credential login perangkat terlebih dahulu.');
|
||||
}
|
||||
|
||||
$client = $this->client($device, $credential->username, $credential->password, $setting);
|
||||
$driver = new MikrotikDriver($client);
|
||||
$deviceInfo = $driver->getDeviceInfo();
|
||||
$provisioned = $driver->provisionBaseAccess($setting->base_username, $setting->base_password);
|
||||
|
||||
// Never clean existing accounts until the new RADIQ account is proven usable.
|
||||
$managementDriver = new MikrotikDriver($this->client($device, $setting->base_username, $setting->base_password, $setting));
|
||||
$managementDriver->testConnection();
|
||||
$cleanup = $device->remove_legacy_users_on_activation
|
||||
? $managementDriver->cleanupLegacyUsers($setting->base_username)
|
||||
: ['deleted' => [], 'preserved' => []];
|
||||
|
||||
$device->credentials()->update(['is_master' => false]);
|
||||
$device->credentials()->updateOrCreate(
|
||||
['name' => 'Base User RADIQ'],
|
||||
[
|
||||
'tenant_id' => $device->tenant_id,
|
||||
'username' => $setting->base_username,
|
||||
'password' => $setting->base_password,
|
||||
'connection_type' => 'routeros_api',
|
||||
'privilege_type' => 'master',
|
||||
'is_master' => true,
|
||||
'is_active' => true,
|
||||
'last_verified_at' => now(),
|
||||
],
|
||||
);
|
||||
|
||||
return $provisioned + ['device_info' => $deviceInfo, 'cleanup' => $cleanup];
|
||||
}
|
||||
|
||||
private function client(Device $device, string $username, string $password, TenantDeviceSetting $setting): RouterOsApiClient
|
||||
{
|
||||
return new RouterOsApiClient($device->management_address, $device->portFor('routeros_api'), $username, $password, $setting->connection_timeout, $setting->use_tls, $setting->verify_tls);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Encryption;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantEncryptionKey;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
class TenantEnvelopeEncryption
|
||||
{
|
||||
private const PREFIX = 'radiq:v1:';
|
||||
|
||||
public function ensureKey(int $tenantId, ?int $actorId = null): TenantEncryptionKey
|
||||
{
|
||||
return TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $tenantId)->where('is_active', true)->first()
|
||||
?? $this->createKey($tenantId, null, $actorId);
|
||||
}
|
||||
|
||||
public function encrypt(int $tenantId, string $plaintext, ?TenantEncryptionKey $key = null): string
|
||||
{
|
||||
$key ??= $this->ensureKey($tenantId);
|
||||
$dek = Crypt::decryptString($key->wrapped_key);
|
||||
$nonce = random_bytes(12);
|
||||
$tag = '';
|
||||
$ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $dek, OPENSSL_RAW_DATA, $nonce, $tag, (string) $tenantId);
|
||||
if ($ciphertext === false) {
|
||||
throw new RuntimeException('ENCRYPTION_FAILED');
|
||||
}
|
||||
|
||||
return self::PREFIX.$key->id.':'.base64_encode($nonce).':'.base64_encode($tag).':'.base64_encode($ciphertext);
|
||||
}
|
||||
|
||||
public function decrypt(int $tenantId, string $payload): string
|
||||
{
|
||||
if (! str_starts_with($payload, self::PREFIX)) {
|
||||
return Crypt::decryptString($payload);
|
||||
}
|
||||
$parts = explode(':', $payload, 6);
|
||||
if (count($parts) !== 6) {
|
||||
throw new RuntimeException('INVALID_ENCRYPTED_PAYLOAD');
|
||||
}
|
||||
$key = TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $tenantId)->findOrFail((int) $parts[2]);
|
||||
$dek = Crypt::decryptString($key->wrapped_key);
|
||||
$plaintext = openssl_decrypt(base64_decode($parts[5], true), 'aes-256-gcm', $dek, OPENSSL_RAW_DATA, base64_decode($parts[3], true), base64_decode($parts[4], true), (string) $tenantId);
|
||||
if ($plaintext === false) {
|
||||
throw new RuntimeException('DECRYPTION_FAILED');
|
||||
}
|
||||
|
||||
return $plaintext;
|
||||
}
|
||||
|
||||
public function rotate(int $tenantId, ?string $manualKey, int $actorId): TenantEncryptionKey
|
||||
{
|
||||
return DB::transaction(function () use ($tenantId, $manualKey, $actorId): TenantEncryptionKey {
|
||||
Tenant::whereKey($tenantId)->lockForUpdate()->firstOrFail();
|
||||
$oldKeys = TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $tenantId)->lockForUpdate()->get();
|
||||
$newKey = $this->createKey($tenantId, $manualKey, $actorId, ($oldKeys->max('version') ?? 0) + 1);
|
||||
foreach (DB::table('device_credentials')->where('tenant_id', $tenantId)->lockForUpdate()->get(['id', 'password', 'enable_password']) as $credential) {
|
||||
DB::table('device_credentials')->where('id', $credential->id)->update([
|
||||
'password' => $this->encrypt($tenantId, $this->decrypt($tenantId, $credential->password), $newKey),
|
||||
'enable_password' => $credential->enable_password === null ? null : $this->encrypt($tenantId, $this->decrypt($tenantId, $credential->enable_password), $newKey),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
foreach (DB::table('tenant_device_settings')->where('tenant_id', $tenantId)->lockForUpdate()->get(['id', 'base_password']) as $setting) {
|
||||
DB::table('tenant_device_settings')->where('id', $setting->id)->update(['base_password' => $this->encrypt($tenantId, $this->decrypt($tenantId, $setting->base_password), $newKey), 'updated_at' => now()]);
|
||||
}
|
||||
foreach (DB::table('device_access_users')->where('tenant_id', $tenantId)->lockForUpdate()->get(['id', 'password']) as $accessUser) {
|
||||
DB::table('device_access_users')->where('id', $accessUser->id)->update(['password' => $this->encrypt($tenantId, $this->decrypt($tenantId, $accessUser->password), $newKey), 'updated_at' => now()]);
|
||||
}
|
||||
TenantEncryptionKey::withoutGlobalScope('tenant')->where('tenant_id', $tenantId)->where('id', '!=', $newKey->id)->where('is_active', true)->update(['is_active' => false, 'retired_at' => now()]);
|
||||
|
||||
return $newKey;
|
||||
});
|
||||
}
|
||||
|
||||
private function createKey(int $tenantId, ?string $manualKey, ?int $actorId, int $version = 1): TenantEncryptionKey
|
||||
{
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
$dek = $manualKey === null ? random_bytes(32) : hash_hkdf('sha256', $manualKey, 32, 'RADIQ-NDM:'.$tenant->uuid);
|
||||
|
||||
return TenantEncryptionKey::withoutGlobalScope('tenant')->create([
|
||||
'tenant_id' => $tenantId, 'version' => $version, 'wrapped_key' => Crypt::encryptString($dek),
|
||||
'is_active' => true, 'source' => $manualKey === null ? 'generated' : 'manual', 'created_by' => $actorId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Models\DeploymentInstallation;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantDevicePolicy;
|
||||
use App\Models\User;
|
||||
use App\Services\Encryption\TenantEnvelopeEncryption;
|
||||
use App\Support\SystemRolePermissions;
|
||||
use App\Support\TenantContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class TenantProvisioningService
|
||||
{
|
||||
public function __construct(private readonly TenantContext $context) {}
|
||||
|
||||
/** @param array{name:string,slug:string,is_active?:bool,owner_name:string,owner_email:string,owner_password:string} $data */
|
||||
public function createWithOwner(array $data): Tenant
|
||||
{
|
||||
return DB::transaction(function () use ($data): Tenant {
|
||||
$tenant = Tenant::create([
|
||||
'name' => $data['name'],
|
||||
'slug' => $data['slug'],
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
]);
|
||||
TenantDevicePolicy::create(['tenant_id' => $tenant->id]);
|
||||
app(TenantEnvelopeEncryption::class)->ensureKey($tenant->id);
|
||||
$deploymentType = $data['deployment_type'] ?? 'managed_cloud';
|
||||
DeploymentInstallation::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'name' => $deploymentType === 'self_hosted' ? 'Server Tenant' : 'RADIQ Managed Cloud',
|
||||
'deployment_type' => $deploymentType,
|
||||
'domain' => $data['deployment_domain'] ?? null,
|
||||
'instance_key_hash' => hash('sha256', random_bytes(32)),
|
||||
'status' => $deploymentType === 'managed_cloud' ? 'active' : 'pending',
|
||||
'activated_at' => $deploymentType === 'managed_cloud' ? now() : null,
|
||||
]);
|
||||
|
||||
$this->context->set($tenant->id);
|
||||
setPermissionsTeamId($tenant->id);
|
||||
|
||||
try {
|
||||
$roles = $this->createDefaultRoles($tenant);
|
||||
|
||||
$owner = User::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'name' => $data['owner_name'],
|
||||
'email' => $data['owner_email'],
|
||||
'password' => $data['owner_password'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
$owner->forceFill(['email_verified_at' => now()])->save();
|
||||
$owner->assignRole($roles[SystemRole::TenantAdmin->value]);
|
||||
} finally {
|
||||
$this->context->clear();
|
||||
setPermissionsTeamId(null);
|
||||
}
|
||||
|
||||
return $tenant;
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array<string, Role> */
|
||||
private function createDefaultRoles(Tenant $tenant): array
|
||||
{
|
||||
$definitions = SystemRolePermissions::tenantRoles();
|
||||
|
||||
$roles = [];
|
||||
|
||||
foreach ($definitions as $name => $permissions) {
|
||||
$role = Role::create(['tenant_id' => $tenant->id, 'name' => $name, 'guard_name' => 'web']);
|
||||
$role->syncPermissions($permissions);
|
||||
$roles[$name] = $role;
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user