Yrityskohtainen IP-rajoitus kirjautumiseen
Lisätty allowed_ips kenttä yrityksiin. Tyhjä = ei rajoitusta, muuten vain listatut IP:t/CIDR-alueet pääsevät kirjautumaan. Superadmin ohittaa aina IP-tarkistuksen (backdoor). Tarkistus tehdään login, check_auth ja company_switch -endpointeissa. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
88
api.php
88
api.php
@@ -130,6 +130,37 @@ function getClientIp(): string {
|
|||||||
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tarkista onko IP sallittujen listalla.
|
||||||
|
* Tyhjä lista = ei rajoitusta (kaikki sallittu).
|
||||||
|
* Tukee yksittäisiä IP-osoitteita ja CIDR-alueita (esim. 192.168.1.0/24).
|
||||||
|
*/
|
||||||
|
function isIpAllowed(string $ip, string $allowedIps): bool {
|
||||||
|
$allowedIps = trim($allowedIps);
|
||||||
|
if ($allowedIps === '') return true; // ei rajoitusta
|
||||||
|
$entries = preg_split('/[\s,]+/', $allowedIps, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
$ipLong = ip2long($ip);
|
||||||
|
if ($ipLong === false) return false;
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
$entry = trim($entry);
|
||||||
|
if ($entry === '') continue;
|
||||||
|
if (strpos($entry, '/') !== false) {
|
||||||
|
// CIDR-alue (esim. 192.168.1.0/24)
|
||||||
|
[$subnet, $bits] = explode('/', $entry, 2);
|
||||||
|
$bits = (int)$bits;
|
||||||
|
if ($bits < 0 || $bits > 32) continue;
|
||||||
|
$subnetLong = ip2long($subnet);
|
||||||
|
if ($subnetLong === false) continue;
|
||||||
|
$mask = $bits === 0 ? 0 : (~0 << (32 - $bits));
|
||||||
|
if (($ipLong & $mask) === ($subnetLong & $mask)) return true;
|
||||||
|
} else {
|
||||||
|
// Yksittäinen IP
|
||||||
|
if (ip2long($entry) === $ipLong) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAddress(string $addr): string {
|
function normalizeAddress(string $addr): string {
|
||||||
$addr = strtolower(trim($addr));
|
$addr = strtolower(trim($addr));
|
||||||
$addr = preg_replace('/\s+/', ' ', $addr);
|
$addr = preg_replace('/\s+/', ' ', $addr);
|
||||||
@@ -1115,6 +1146,28 @@ switch ($action) {
|
|||||||
echo json_encode(['error' => 'Sinulla ei ole oikeutta kirjautua tälle sivustolle.']);
|
echo json_encode(['error' => 'Sinulla ei ole oikeutta kirjautua tälle sivustolle.']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// IP-rajoitus: superadmin ohittaa aina
|
||||||
|
$allCompanies = dbLoadCompanies();
|
||||||
|
if ($u['role'] !== 'superadmin') {
|
||||||
|
$allowedCompanies = [];
|
||||||
|
foreach ($userCompanies as $ucId) {
|
||||||
|
foreach ($allCompanies as $comp) {
|
||||||
|
if ($comp['id'] === $ucId) {
|
||||||
|
if (isIpAllowed($ip, $comp['allowed_ips'] ?? '')) {
|
||||||
|
$allowedCompanies[] = $ucId;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($allowedCompanies)) {
|
||||||
|
dbRecordLoginAttempt($ip);
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'IP-osoitteesi ei ole sallittu.']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$userCompanies = $allowedCompanies;
|
||||||
|
}
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION['user_id'] = $u['id'];
|
$_SESSION['user_id'] = $u['id'];
|
||||||
$_SESSION['username'] = $u['username'];
|
$_SESSION['username'] = $u['username'];
|
||||||
@@ -1128,7 +1181,6 @@ switch ($action) {
|
|||||||
$_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
|
$_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : '';
|
||||||
}
|
}
|
||||||
// Hae yritysten nimet
|
// Hae yritysten nimet
|
||||||
$allCompanies = dbLoadCompanies();
|
|
||||||
$companyList = [];
|
$companyList = [];
|
||||||
foreach ($allCompanies as $comp) {
|
foreach ($allCompanies as $comp) {
|
||||||
if (in_array($comp['id'], $userCompanies)) {
|
if (in_array($comp['id'], $userCompanies)) {
|
||||||
@@ -1167,15 +1219,32 @@ switch ($action) {
|
|||||||
$_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : '';
|
$_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Hae yritysten nimet
|
// Hae yritysten nimet + IP-rajoitus
|
||||||
$userCompanyIds = $_SESSION['companies'] ?? [];
|
$userCompanyIds = $_SESSION['companies'] ?? [];
|
||||||
$allCompanies = dbLoadCompanies();
|
$allCompanies = dbLoadCompanies();
|
||||||
|
$ip = getClientIp();
|
||||||
$companyList = [];
|
$companyList = [];
|
||||||
foreach ($allCompanies as $comp) {
|
foreach ($allCompanies as $comp) {
|
||||||
if (in_array($comp['id'], $userCompanyIds)) {
|
if (in_array($comp['id'], $userCompanyIds)) {
|
||||||
|
// IP-rajoitus: superadmin ohittaa aina
|
||||||
|
if (($_SESSION['role'] ?? '') !== 'superadmin' && !isIpAllowed($ip, $comp['allowed_ips'] ?? '')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
|
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Jos IP-rajoitus poistaa kaikki yritykset (ei superadmin) → kirjaa ulos
|
||||||
|
if (empty($companyList) && ($_SESSION['role'] ?? '') !== 'superadmin') {
|
||||||
|
session_destroy();
|
||||||
|
echo json_encode(['authenticated' => false]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Päivitä session companies IP-suodatuksen mukaan
|
||||||
|
$allowedIds = array_column($companyList, 'id');
|
||||||
|
$_SESSION['companies'] = $allowedIds;
|
||||||
|
if (!in_array($_SESSION['company_id'] ?? '', $allowedIds) && !empty($allowedIds)) {
|
||||||
|
$_SESSION['company_id'] = $allowedIds[0];
|
||||||
|
}
|
||||||
// Hae allekirjoitukset (oletus generoituna jos omaa ei ole)
|
// Hae allekirjoitukset (oletus generoituna jos omaa ei ole)
|
||||||
$userSignatures = $u ? buildSignaturesWithDefaults($u, $u['companies'] ?? []) : [];
|
$userSignatures = $u ? buildSignaturesWithDefaults($u, $u['companies'] ?? []) : [];
|
||||||
// Brändäystiedot domain-pohjaisesti (sama kuin branding-endpoint)
|
// Brändäystiedot domain-pohjaisesti (sama kuin branding-endpoint)
|
||||||
@@ -2914,6 +2983,7 @@ switch ($action) {
|
|||||||
if (isset($input['enabled_modules']) && is_array($input['enabled_modules'])) {
|
if (isset($input['enabled_modules']) && is_array($input['enabled_modules'])) {
|
||||||
$c['enabled_modules'] = array_values($input['enabled_modules']);
|
$c['enabled_modules'] = array_values($input['enabled_modules']);
|
||||||
}
|
}
|
||||||
|
if (isset($input['allowed_ips'])) $c['allowed_ips'] = trim($input['allowed_ips']);
|
||||||
dbSaveCompany($c);
|
dbSaveCompany($c);
|
||||||
$found = true;
|
$found = true;
|
||||||
echo json_encode($c);
|
echo json_encode($c);
|
||||||
@@ -2957,6 +3027,20 @@ switch ($action) {
|
|||||||
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
|
echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// IP-rajoitus yritystä vaihdettaessa (superadmin ohittaa)
|
||||||
|
if (($_SESSION['role'] ?? '') !== 'superadmin') {
|
||||||
|
$companies = dbLoadCompanies();
|
||||||
|
foreach ($companies as $comp) {
|
||||||
|
if ($comp['id'] === $companyId) {
|
||||||
|
if (!isIpAllowed(getClientIp(), $comp['allowed_ips'] ?? '')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'IP-osoitteesi ei ole sallittu tälle yritykselle.']);
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$_SESSION['company_id'] = $companyId;
|
$_SESSION['company_id'] = $companyId;
|
||||||
echo json_encode(['success' => true, 'company_id' => $companyId]);
|
echo json_encode(['success' => true, 'company_id' => $companyId]);
|
||||||
break;
|
break;
|
||||||
|
|||||||
9
db.php
9
db.php
@@ -447,6 +447,7 @@ function initDatabase(): void {
|
|||||||
"ALTER TABLE tickets ADD COLUMN ticket_number INT DEFAULT NULL AFTER id",
|
"ALTER TABLE tickets ADD COLUMN ticket_number INT DEFAULT NULL AFTER id",
|
||||||
"ALTER TABLE mailboxes ADD COLUMN auto_reply_enabled BOOLEAN DEFAULT FALSE AFTER aktiivinen",
|
"ALTER TABLE mailboxes ADD COLUMN auto_reply_enabled BOOLEAN DEFAULT FALSE AFTER aktiivinen",
|
||||||
"ALTER TABLE mailboxes ADD COLUMN auto_reply_body TEXT AFTER auto_reply_enabled",
|
"ALTER TABLE mailboxes ADD COLUMN auto_reply_body TEXT AFTER auto_reply_enabled",
|
||||||
|
"ALTER TABLE companies ADD COLUMN allowed_ips TEXT DEFAULT '' AFTER enabled_modules",
|
||||||
];
|
];
|
||||||
foreach ($alters as $sql) {
|
foreach ($alters as $sql) {
|
||||||
try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ }
|
try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ }
|
||||||
@@ -473,6 +474,7 @@ function dbLoadCompanies(): array {
|
|||||||
// enabled_modules: JSON-array tai tyhjä (= kaikki päällä)
|
// enabled_modules: JSON-array tai tyhjä (= kaikki päällä)
|
||||||
$raw = $c['enabled_modules'] ?? '';
|
$raw = $c['enabled_modules'] ?? '';
|
||||||
$c['enabled_modules'] = $raw ? (json_decode($raw, true) ?: []) : [];
|
$c['enabled_modules'] = $raw ? (json_decode($raw, true) ?: []) : [];
|
||||||
|
$c['allowed_ips'] = $c['allowed_ips'] ?? '';
|
||||||
}
|
}
|
||||||
return $companies;
|
return $companies;
|
||||||
}
|
}
|
||||||
@@ -484,13 +486,13 @@ function dbSaveCompany(array $company): void {
|
|||||||
$enabledModules = $company['enabled_modules'] ?? [];
|
$enabledModules = $company['enabled_modules'] ?? [];
|
||||||
$enabledModulesJson = is_array($enabledModules) ? json_encode($enabledModules) : ($enabledModules ?: '');
|
$enabledModulesJson = is_array($enabledModules) ? json_encode($enabledModules) : ($enabledModules ?: '');
|
||||||
_dbExecute("
|
_dbExecute("
|
||||||
INSERT INTO companies (id, nimi, luotu, aktiivinen, primary_color, subtitle, logo_file, api_key, cors_origins, enabled_modules)
|
INSERT INTO companies (id, nimi, luotu, aktiivinen, primary_color, subtitle, logo_file, api_key, cors_origins, enabled_modules, allowed_ips)
|
||||||
VALUES (:id, :nimi, :luotu, :aktiivinen, :primary_color, :subtitle, :logo_file, :api_key, :cors_origins, :enabled_modules)
|
VALUES (:id, :nimi, :luotu, :aktiivinen, :primary_color, :subtitle, :logo_file, :api_key, :cors_origins, :enabled_modules, :allowed_ips)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
nimi = VALUES(nimi), aktiivinen = VALUES(aktiivinen),
|
nimi = VALUES(nimi), aktiivinen = VALUES(aktiivinen),
|
||||||
primary_color = VALUES(primary_color), subtitle = VALUES(subtitle),
|
primary_color = VALUES(primary_color), subtitle = VALUES(subtitle),
|
||||||
logo_file = VALUES(logo_file), api_key = VALUES(api_key), cors_origins = VALUES(cors_origins),
|
logo_file = VALUES(logo_file), api_key = VALUES(api_key), cors_origins = VALUES(cors_origins),
|
||||||
enabled_modules = VALUES(enabled_modules)
|
enabled_modules = VALUES(enabled_modules), allowed_ips = VALUES(allowed_ips)
|
||||||
", [
|
", [
|
||||||
'id' => $company['id'],
|
'id' => $company['id'],
|
||||||
'nimi' => $company['nimi'],
|
'nimi' => $company['nimi'],
|
||||||
@@ -502,6 +504,7 @@ function dbSaveCompany(array $company): void {
|
|||||||
'api_key' => $company['api_key'] ?? '',
|
'api_key' => $company['api_key'] ?? '',
|
||||||
'cors_origins' => $company['cors_origins'] ?? '',
|
'cors_origins' => $company['cors_origins'] ?? '',
|
||||||
'enabled_modules' => $enabledModulesJson,
|
'enabled_modules' => $enabledModulesJson,
|
||||||
|
'allowed_ips' => $company['allowed_ips'] ?? '',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Päivitä domainit
|
// Päivitä domainit
|
||||||
|
|||||||
@@ -760,6 +760,11 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="margin-top:1rem;">
|
||||||
|
<label style="font-weight:600;font-size:0.9rem;">Sallitut IP-osoitteet</label>
|
||||||
|
<textarea id="company-edit-allowed-ips" rows="3" style="font-family:monospace;font-size:0.85rem;" placeholder="192.168.1.100 10.0.0.0/8"></textarea>
|
||||||
|
<small style="color:#888;">Yksi IP tai CIDR per rivi. Tyhjä = ei rajoitusta. Superadmin ohittaa aina.</small>
|
||||||
|
</div>
|
||||||
<button class="btn-primary" id="btn-save-company-settings" style="font-size:0.85rem;">Tallenna asetukset</button>
|
<button class="btn-primary" id="btn-save-company-settings" style="font-size:0.85rem;">Tallenna asetukset</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Postilaatikot -->
|
<!-- Postilaatikot -->
|
||||||
|
|||||||
@@ -2302,6 +2302,9 @@ async function showCompanyDetail(id) {
|
|||||||
cb.checked = enabledMods.length === 0 ? DEFAULT_MODULES.includes(mod) : enabledMods.includes(mod);
|
cb.checked = enabledMods.length === 0 ? DEFAULT_MODULES.includes(mod) : enabledMods.includes(mod);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sallitut IP-osoitteet
|
||||||
|
document.getElementById('company-edit-allowed-ips').value = comp?.allowed_ips || '';
|
||||||
|
|
||||||
// Vaihda aktiivinen yritys jotta API-kutsut kohdistuvat oikein
|
// Vaihda aktiivinen yritys jotta API-kutsut kohdistuvat oikein
|
||||||
await apiCall('company_switch', 'POST', { company_id: id });
|
await apiCall('company_switch', 'POST', { company_id: id });
|
||||||
|
|
||||||
@@ -2434,12 +2437,13 @@ document.getElementById('btn-save-company-settings').addEventListener('click', a
|
|||||||
document.querySelectorAll('#modules-checkboxes input[data-module]:checked').forEach(cb => {
|
document.querySelectorAll('#modules-checkboxes input[data-module]:checked').forEach(cb => {
|
||||||
enabled_modules.push(cb.dataset.module);
|
enabled_modules.push(cb.dataset.module);
|
||||||
});
|
});
|
||||||
|
const allowed_ips = document.getElementById('company-edit-allowed-ips').value.trim();
|
||||||
try {
|
try {
|
||||||
await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi, subtitle, primary_color, domains, enabled_modules });
|
await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi, subtitle, primary_color, domains, enabled_modules, allowed_ips });
|
||||||
alert('Asetukset tallennettu!');
|
alert('Asetukset tallennettu!');
|
||||||
// Päivitä paikalliset tiedot
|
// Päivitä paikalliset tiedot
|
||||||
const comp = companiesTabData.find(c => c.id === currentCompanyDetail);
|
const comp = companiesTabData.find(c => c.id === currentCompanyDetail);
|
||||||
if (comp) { comp.nimi = nimi; comp.subtitle = subtitle; comp.primary_color = primary_color; comp.domains = domains; comp.enabled_modules = enabled_modules; }
|
if (comp) { comp.nimi = nimi; comp.subtitle = subtitle; comp.primary_color = primary_color; comp.domains = domains; comp.enabled_modules = enabled_modules; comp.allowed_ips = allowed_ips; }
|
||||||
const avail = availableCompanies.find(c => c.id === currentCompanyDetail);
|
const avail = availableCompanies.find(c => c.id === currentCompanyDetail);
|
||||||
if (avail) avail.nimi = nimi;
|
if (avail) avail.nimi = nimi;
|
||||||
populateCompanySelector();
|
populateCompanySelector();
|
||||||
|
|||||||
Reference in New Issue
Block a user