migrado de rlm_rest a rlm_sql_postgress y rlm_sql
This commit is contained in:
@@ -12,11 +12,14 @@ services:
|
|||||||
- RADIUS_HOST=freeradius
|
- RADIUS_HOST=freeradius
|
||||||
- RADIUS_AUTH_PORT=1812
|
- RADIUS_AUTH_PORT=1812
|
||||||
- RADIUS_SECRET=${RADIUS_SHARED_SECRET:-testing123}
|
- RADIUS_SECRET=${RADIUS_SHARED_SECRET:-testing123}
|
||||||
|
- PGHOST=postgres
|
||||||
|
- PGPORT=5432
|
||||||
|
- PGDATABASE=radius
|
||||||
|
- PGUSER=radius
|
||||||
|
- PGPASSWORD=radius
|
||||||
volumes:
|
volumes:
|
||||||
- ./node-api/index.js:/app/index.js:ro
|
- ./node-api/index.js:/app/index.js:ro
|
||||||
- ./node-api/public:/app/public:ro
|
- ./node-api/public:/app/public:ro
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./freeradius/mods-config/files/authorize:/shared/authorize
|
|
||||||
networks:
|
networks:
|
||||||
- radius_net
|
- radius_net
|
||||||
|
|
||||||
@@ -24,27 +27,43 @@ services:
|
|||||||
image: freeradius/freeradius-server:3.2.2
|
image: freeradius/freeradius-server:3.2.2
|
||||||
depends_on:
|
depends_on:
|
||||||
- node
|
- node
|
||||||
|
- postgres
|
||||||
ports:
|
ports:
|
||||||
- "1812:1812/udp"
|
- "1812:1812/udp"
|
||||||
- "1813:1813/udp"
|
- "1813:1813/udp"
|
||||||
environment:
|
environment:
|
||||||
- REST_ENDPOINT=http://node:3000
|
|
||||||
- RADIUS_CLIENTS_CIDR=${RADIUS_CLIENTS_CIDR:-0.0.0.0/0}
|
- RADIUS_CLIENTS_CIDR=${RADIUS_CLIENTS_CIDR:-0.0.0.0/0}
|
||||||
- RADIUS_SHARED_SECRET=${RADIUS_SHARED_SECRET:-testing123}
|
- RADIUS_SHARED_SECRET=${RADIUS_SHARED_SECRET:-testing123}
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./freeradius/mods-available/sql:/etc/freeradius/mods-available/sql:ro
|
||||||
|
- ./freeradius/mods-available/sql:/etc/freeradius/mods-enabled/sql:ro
|
||||||
- ./freeradius/mods-available/rest:/etc/freeradius/mods-available/rest:ro
|
- ./freeradius/mods-available/rest:/etc/freeradius/mods-available/rest:ro
|
||||||
- ./freeradius/mods-available/rest:/etc/freeradius/mods-enabled/rest:ro
|
- ./freeradius/mods-available/rest:/etc/freeradius/mods-enabled/rest:ro
|
||||||
- ./freeradius/mods-available/rest_inner:/etc/freeradius/mods-available/rest_inner:ro
|
|
||||||
- ./freeradius/mods-available/rest_inner:/etc/freeradius/mods-enabled/rest_inner:ro
|
|
||||||
- ./freeradius/mods-enabled/eap:/etc/freeradius/mods-enabled/eap:ro
|
- ./freeradius/mods-enabled/eap:/etc/freeradius/mods-enabled/eap:ro
|
||||||
- ./freeradius/sites-enabled/default:/etc/freeradius/sites-enabled/default:ro
|
- ./freeradius/sites-enabled/default:/etc/freeradius/sites-enabled/default:ro
|
||||||
- ./freeradius/sites-enabled/inner-tunnel:/etc/freeradius/sites-enabled/inner-tunnel:ro
|
- ./freeradius/sites-enabled/inner-tunnel:/etc/freeradius/sites-enabled/inner-tunnel:ro
|
||||||
- ./freeradius/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize:ro
|
|
||||||
- ./freeradius/clients.conf:/etc/freeradius/clients.conf:ro
|
- ./freeradius/clients.conf:/etc/freeradius/clients.conf:ro
|
||||||
command: ["-X"]
|
command: ["-X"]
|
||||||
networks:
|
networks:
|
||||||
- radius_net
|
- radius_net
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=radius
|
||||||
|
- POSTGRES_USER=radius
|
||||||
|
- POSTGRES_PASSWORD=radius
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./postgres/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- radius_net
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
radius_net:
|
radius_net:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|||||||
9
freeradius/Dockerfile
Normal file
9
freeradius/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM freeradius/freeradius-server:3.2.2
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends freeradius-postgresql \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Default command preserved by base image
|
||||||
|
|
||||||
28
freeradius/mods-available/sql
Normal file
28
freeradius/mods-available/sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
sql {
|
||||||
|
driver = "rlm_sql_postgresql"
|
||||||
|
dialect = "postgresql"
|
||||||
|
|
||||||
|
server = "postgres"
|
||||||
|
port = 5432
|
||||||
|
login = "radius"
|
||||||
|
password = "radius"
|
||||||
|
radius_db = "radius"
|
||||||
|
|
||||||
|
pool {
|
||||||
|
start = 2
|
||||||
|
min = 1
|
||||||
|
max = 5
|
||||||
|
spare = 1
|
||||||
|
uses = 0
|
||||||
|
lifetime = 0
|
||||||
|
cleanup_interval = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave default queries location
|
||||||
|
# queries = ${modconfdir}/sql/main/${dialect}/queries.conf
|
||||||
|
|
||||||
|
read_clients = no
|
||||||
|
|
||||||
|
# We only use per-user tables; disable group processing to avoid extra schema
|
||||||
|
read_groups = no
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Managed by Node dashboard; do not edit manually
|
# Managed by Node dashboard; do not edit manually
|
||||||
user1 Cleartext-Password := "contra1"
|
# user1 Cleartext-Password := "contra1"
|
||||||
Tunnel-Type = VLAN,
|
# Tunnel-Type = VLAN,
|
||||||
Tunnel-Medium-Type = IEEE-802,
|
# Tunnel-Medium-Type = IEEE-802,
|
||||||
Tunnel-Private-Group-Id = "2"
|
# Tunnel-Private-Group-Id = "2"
|
||||||
|
|
||||||
user2 Cleartext-Password := "contra2"
|
user2 Cleartext-Password := "contra2"
|
||||||
Tunnel-Type = VLAN,
|
Tunnel-Type = VLAN,
|
||||||
@@ -14,10 +14,10 @@ prueba2 Cleartext-Password := "contra2"
|
|||||||
Tunnel-Medium-Type = IEEE-802,
|
Tunnel-Medium-Type = IEEE-802,
|
||||||
Tunnel-Private-Group-Id = "2"
|
Tunnel-Private-Group-Id = "2"
|
||||||
|
|
||||||
dario Cleartext-Password := "contra1"
|
# dario Cleartext-Password := "contra1"
|
||||||
Tunnel-Type = VLAN,
|
# Tunnel-Type = VLAN,
|
||||||
Tunnel-Medium-Type = IEEE-802,
|
# Tunnel-Medium-Type = IEEE-802,
|
||||||
Tunnel-Private-Group-Id = "2"
|
# Tunnel-Private-Group-Id = "2"
|
||||||
|
|
||||||
margie Cleartext-Password := "bonita"
|
margie Cleartext-Password := "bonita"
|
||||||
Tunnel-Type = VLAN,
|
Tunnel-Type = VLAN,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ server default {
|
|||||||
eap
|
eap
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
# MAC-Auth / Portal: Llama a la API REST para decidir y añadir atributos
|
# Cargar atributos desde SQL (VLAN/bw, etc.)
|
||||||
rest
|
sql
|
||||||
# Laboratorio: aceptar todo en flujos no EAP
|
# Laboratorio: aceptar todo en flujos no EAP
|
||||||
update control {
|
update control {
|
||||||
Auth-Type := Accept
|
Auth-Type := Accept
|
||||||
@@ -35,12 +35,14 @@ server default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
accounting {
|
accounting {
|
||||||
|
# Enviar eventos de accounting al dashboard (solo logging)
|
||||||
rest
|
rest
|
||||||
ok
|
ok
|
||||||
}
|
}
|
||||||
|
|
||||||
post-auth {
|
post-auth {
|
||||||
# Obtener atributos de VLAN/otros desde el API
|
# Log de eventos post-auth al dashboard (no modifica la respuesta)
|
||||||
rest.post-auth
|
rest.post-auth
|
||||||
|
# Para EAP, los atributos del túnel interno se copian (use_tunneled_reply = yes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,21 @@ server inner-tunnel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authorize {
|
authorize {
|
||||||
# Obtener credenciales del usuario desde el API (debe devolver Cleartext-Password)
|
# Cargar credenciales/atributos del usuario desde SQL
|
||||||
rest_inner
|
sql
|
||||||
# Fallback/local: también consultar backend 'files' (user1/user2)
|
# En caso de que el módulo SQL no haya poblado Cleartext-Password, obténlo vía xlat
|
||||||
|
update control {
|
||||||
|
Cleartext-Password := "%{sql:SELECT value FROM radcheck WHERE username='%{User-Name}' AND attribute='Cleartext-Password' ORDER BY id DESC LIMIT 1}"
|
||||||
|
}
|
||||||
|
# Cargar atributos de respuesta desde SQL (VLAN y ancho de banda) para PEAP (se copian al outer)
|
||||||
|
update reply {
|
||||||
|
Tunnel-Type := "%{sql:SELECT value FROM radreply WHERE username='%{User-Name}' AND attribute='Tunnel-Type' ORDER BY id DESC LIMIT 1}"
|
||||||
|
Tunnel-Medium-Type := "%{sql:SELECT value FROM radreply WHERE username='%{User-Name}' AND attribute='Tunnel-Medium-Type' ORDER BY id DESC LIMIT 1}"
|
||||||
|
Tunnel-Private-Group-Id := "%{sql:SELECT value FROM radreply WHERE username='%{User-Name}' AND attribute='Tunnel-Private-Group-Id' ORDER BY id DESC LIMIT 1}"
|
||||||
|
WISPr-Bandwidth-Max-Down := "%{sql:SELECT value FROM radreply WHERE username='%{User-Name}' AND attribute='WISPr-Bandwidth-Max-Down' ORDER BY id DESC LIMIT 1}"
|
||||||
|
WISPr-Bandwidth-Max-Up := "%{sql:SELECT value FROM radreply WHERE username='%{User-Name}' AND attribute='WISPr-Bandwidth-Max-Up' ORDER BY id DESC LIMIT 1}"
|
||||||
|
}
|
||||||
|
# Fallback/local: también consultar backend 'files'
|
||||||
files
|
files
|
||||||
# Procesar EAP (PEAP) y MS-CHAPv2
|
# Procesar EAP (PEAP) y MS-CHAPv2
|
||||||
eap
|
eap
|
||||||
@@ -23,6 +35,6 @@ server inner-tunnel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post-auth {
|
post-auth {
|
||||||
# Nada aquí; el outer post-auth añadirá VLAN
|
# Nada: los atributos se copian fuera si use_tunneled_reply = yes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import morgan from 'morgan';
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import http from 'http';
|
|
||||||
import dgram from 'dgram';
|
import dgram from 'dgram';
|
||||||
import radius from 'radius';
|
import radius from 'radius';
|
||||||
|
import pkgPg from 'pg';
|
||||||
|
const { Pool } = pkgPg;
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -23,8 +24,6 @@ const MAX_REQUESTS = parseInt(process.env.MAX_REQUESTS || '200', 10);
|
|||||||
const RADIUS_HOST = process.env.RADIUS_HOST || 'freeradius';
|
const RADIUS_HOST = process.env.RADIUS_HOST || 'freeradius';
|
||||||
const RADIUS_AUTH_PORT = parseInt(process.env.RADIUS_AUTH_PORT || '1812', 10);
|
const RADIUS_AUTH_PORT = parseInt(process.env.RADIUS_AUTH_PORT || '1812', 10);
|
||||||
const RADIUS_SECRET = process.env.RADIUS_SECRET || process.env.RADIUS_SHARED_SECRET || 'tamosbien';
|
const RADIUS_SECRET = process.env.RADIUS_SECRET || process.env.RADIUS_SHARED_SECRET || 'tamosbien';
|
||||||
const DOCKER_SOCK = process.env.DOCKER_SOCK || '/var/run/docker.sock';
|
|
||||||
const FREERADIUS_CONTAINER = process.env.FREERADIUS_CONTAINER || 'radiusnucleo-freeradius-1';
|
|
||||||
|
|
||||||
// Requests store + SSE clients
|
// Requests store + SSE clients
|
||||||
const requests = [];
|
const requests = [];
|
||||||
@@ -47,98 +46,98 @@ function broadcastStatus(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTH_FILE = process.env.AUTH_FILE || '/shared/authorize';
|
// Postgres connection for user management (rlm_sql)
|
||||||
|
const PGHOST = process.env.PGHOST || 'postgres';
|
||||||
|
const PGPORT = parseInt(process.env.PGPORT || '5432', 10);
|
||||||
|
const PGDATABASE = process.env.PGDATABASE || 'radius';
|
||||||
|
const PGUSER = process.env.PGUSER || 'radius';
|
||||||
|
const PGPASSWORD = process.env.PGPASSWORD || 'radius';
|
||||||
|
const pool = new Pool({ host: PGHOST, port: PGPORT, database: PGDATABASE, user: PGUSER, password: PGPASSWORD });
|
||||||
|
|
||||||
function parseUsersFromText(text) {
|
// SQL helpers: users in radcheck/radreply
|
||||||
const users = [];
|
async function readUsersFromDb() {
|
||||||
const lines = text.split(/\r?\n/);
|
const client = await pool.connect();
|
||||||
let i = 0;
|
|
||||||
while (i < lines.length) {
|
|
||||||
const line = lines[i];
|
|
||||||
const m = line.match(/^\s*(#?)\s*([^\s#]+)\s+Cleartext-Password\s*:=\s*"([^"]+)"/);
|
|
||||||
if (m) {
|
|
||||||
const disabled = m[1] === '#';
|
|
||||||
const username = m[2];
|
|
||||||
const password = m[3];
|
|
||||||
let vlan = undefined;
|
|
||||||
i++;
|
|
||||||
while (i < lines.length && lines[i].trim() !== '') {
|
|
||||||
const l = lines[i].replace(/^\s*#\s*/, '');
|
|
||||||
const mv = l.match(/Tunnel-Private-Group-Id\s*=\s*"?(\d+)"?/i);
|
|
||||||
if (mv) vlan = mv[1];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
users.push({ username, password, vlan: vlan || VLAN_ID, disabled });
|
|
||||||
while (i < lines.length && lines[i].trim() === '') i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return users;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readUsersFromFile() {
|
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(AUTH_FILE, 'utf8').catch(() => '');
|
const q = `
|
||||||
return parseUsersFromText(data);
|
SELECT rc.username,
|
||||||
} catch {
|
rc.value AS password,
|
||||||
return [];
|
EXISTS (
|
||||||
}
|
SELECT 1 FROM radcheck r2
|
||||||
}
|
WHERE r2.username = rc.username AND r2.attribute = 'Auth-Type' AND r2.value = 'Reject'
|
||||||
|
) AS disabled,
|
||||||
async function writeUsersToFile(users) {
|
COALESCE((
|
||||||
const header = '# Managed by Node dashboard; do not edit manually\n';
|
SELECT rr.value FROM radreply rr
|
||||||
const chunks = [];
|
WHERE rr.username = rc.username AND rr.attribute = 'Tunnel-Private-Group-Id'
|
||||||
for (const u of users) {
|
ORDER BY rr.id DESC LIMIT 1
|
||||||
const lines = [
|
), $1) AS vlan
|
||||||
`${u.username} Cleartext-Password := "${u.password}"`,
|
FROM radcheck rc
|
||||||
` Tunnel-Type = VLAN,`,
|
WHERE rc.attribute = 'Cleartext-Password'
|
||||||
` Tunnel-Medium-Type = IEEE-802,`,
|
ORDER BY rc.username ASC`;
|
||||||
` Tunnel-Private-Group-Id = "${String(u.vlan || VLAN_ID)}"`,
|
const { rows } = await client.query(q, [String(VLAN_ID)]);
|
||||||
''
|
return rows.map(r => ({ username: r.username, password: r.password, vlan: String(r.vlan), disabled: !!r.disabled }));
|
||||||
];
|
|
||||||
if (u.disabled) {
|
|
||||||
chunks.push(lines.map(l => l ? `# ${l}` : '').join('\n'));
|
|
||||||
} else {
|
|
||||||
chunks.push(lines.join('\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await fs.writeFile(AUTH_FILE, header + chunks.join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function persistUsersToFreeradius(users) {
|
|
||||||
try {
|
|
||||||
await writeUsersToFile(users);
|
|
||||||
triggerRadiusReload().catch(() => {});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to persist users to FreeRADIUS files:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function triggerRadiusReload() {
|
|
||||||
try {
|
|
||||||
radiusReloading = true;
|
|
||||||
broadcastStatus({ radius_reloading: true });
|
|
||||||
// Call Docker Engine API over unix socket: POST /containers/{id}/kill?signal=HUP
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const req = http.request({
|
|
||||||
method: 'POST',
|
|
||||||
socketPath: DOCKER_SOCK,
|
|
||||||
path: `/v1.41/containers/${encodeURIComponent(FREERADIUS_CONTAINER)}/kill?signal=HUP`,
|
|
||||||
}, (res) => {
|
|
||||||
res.resume();
|
|
||||||
res.on('end', resolve);
|
|
||||||
});
|
|
||||||
req.on('error', reject);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to HUP FreeRADIUS:', e?.message || e);
|
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
client.release();
|
||||||
radiusReloading = false;
|
}
|
||||||
broadcastStatus({ radius_reloading: false });
|
}
|
||||||
}, 1500);
|
|
||||||
|
async function upsertUserToDb(user) {
|
||||||
|
const { username, password, vlan, disabled } = user;
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
// Password
|
||||||
|
await client.query("DELETE FROM radcheck WHERE username = $1 AND attribute = 'Cleartext-Password'", [username]);
|
||||||
|
await client.query(
|
||||||
|
"INSERT INTO radcheck (username, attribute, op, value) VALUES ($1,'Cleartext-Password',':=',$2)",
|
||||||
|
[username, password]
|
||||||
|
);
|
||||||
|
// Disabled flag
|
||||||
|
await client.query("DELETE FROM radcheck WHERE username = $1 AND attribute = 'Auth-Type'", [username]);
|
||||||
|
if (disabled) {
|
||||||
|
await client.query(
|
||||||
|
"INSERT INTO radcheck (username, attribute, op, value) VALUES ($1,'Auth-Type',':=','Reject')",
|
||||||
|
[username]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Reply attributes (VLAN and bandwidth)
|
||||||
|
const attrs = [
|
||||||
|
['Tunnel-Type', 'VLAN'],
|
||||||
|
['Tunnel-Medium-Type', 'IEEE-802'],
|
||||||
|
['Tunnel-Private-Group-Id', String(vlan || VLAN_ID)],
|
||||||
|
['WISPr-Bandwidth-Max-Down', String(MAX_DOWN)],
|
||||||
|
['WISPr-Bandwidth-Max-Up', String(MAX_UP)],
|
||||||
|
];
|
||||||
|
await client.query(
|
||||||
|
"DELETE FROM radreply WHERE username = $1 AND attribute IN ('Tunnel-Type','Tunnel-Medium-Type','Tunnel-Private-Group-Id','WISPr-Bandwidth-Max-Down','WISPr-Bandwidth-Max-Up')",
|
||||||
|
[username]
|
||||||
|
);
|
||||||
|
for (const [attr, val] of attrs) {
|
||||||
|
await client.query(
|
||||||
|
"INSERT INTO radreply (username, attribute, op, value) VALUES ($1,$2,':=',$3)",
|
||||||
|
[username, attr, String(val)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await client.query('COMMIT');
|
||||||
|
} catch (e) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUserFromDb(username) {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
await client.query('DELETE FROM radcheck WHERE username = $1', [username]);
|
||||||
|
await client.query('DELETE FROM radreply WHERE username = $1', [username]);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
} catch (e) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,97 +205,57 @@ app.post('/accounting', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Authorize inner-tunnel (EAP): devolver Cleartext-Password para el usuario
|
// Authorize inner-tunnel (EAP): devolver Cleartext-Password para el usuario
|
||||||
app.post('/authorize-inner', (req, res) => {
|
app.post('/authorize-inner', async (req, res) => res.status(410).json({}));
|
||||||
console.log('--- RADIUS Authorize (inner) ---');
|
|
||||||
console.log(JSON.stringify(req.body, null, 2));
|
|
||||||
|
|
||||||
const attrs = normalizeAttributes(req.body);
|
// Post-auth: return reply attributes like VLAN based on user mapping
|
||||||
const username = (attrs['User-Name'] || '').toString();
|
// Post-auth events: log only (rlm_rest calls here)
|
||||||
const list = await readUsersFromFile();
|
app.post('/post-auth', async (req, res) => {
|
||||||
const entry = list.find(u => u.username === username && !u.disabled);
|
try {
|
||||||
const password = entry?.password;
|
const attrs = normalizeAttributes(req.body);
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
pushRequest({
|
pushRequest({
|
||||||
id: Date.now() + ':' + Math.random().toString(16).slice(2),
|
id: Date.now() + ':' + Math.random().toString(16).slice(2),
|
||||||
ts: new Date().toISOString(),
|
ts: new Date().toISOString(),
|
||||||
type: 'authorize-inner',
|
type: 'post-auth',
|
||||||
attrs,
|
attrs,
|
||||||
decision: 'notfound',
|
|
||||||
});
|
});
|
||||||
// No devolvemos nada -> FreeRADIUS seguirá su flujo y probablemente rechace
|
} catch (e) {
|
||||||
return res.status(200).json({});
|
console.error('post-auth log error:', e);
|
||||||
}
|
}
|
||||||
|
return res.status(200).json({});
|
||||||
pushRequest({
|
|
||||||
id: Date.now() + ':' + Math.random().toString(16).slice(2),
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
type: 'authorize-inner',
|
|
||||||
attrs,
|
|
||||||
decision: 'provide-password',
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
control: [
|
|
||||||
{ name: 'Cleartext-Password', value: String(password) }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Post-auth: return reply attributes like VLAN based on user mapping
|
|
||||||
app.post('/post-auth', (req, res) => {
|
|
||||||
const attrs = normalizeAttributes(req.body);
|
|
||||||
const username = (attrs['User-Name'] || '').toString();
|
|
||||||
const list = await readUsersFromFile();
|
|
||||||
const entry = list.find(u => u.username === username && !u.disabled);
|
|
||||||
const vlan = entry?.vlan || VLAN_ID;
|
|
||||||
return res.status(200).json({
|
|
||||||
reply: [
|
|
||||||
{ name: 'Tunnel-Type', value: 'VLAN' },
|
|
||||||
{ name: 'Tunnel-Medium-Type', value: 'IEEE-802' },
|
|
||||||
{ name: 'Tunnel-Private-Group-Id', value: String(vlan) },
|
|
||||||
{ name: 'WISPr-Bandwidth-Max-Down', value: String(MAX_DOWN) },
|
|
||||||
{ name: 'WISPr-Bandwidth-Max-Up', value: String(MAX_UP) }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Users API
|
// Users API
|
||||||
app.get('/api/users', async (req, res) => {
|
app.get('/api/users', async (req, res) => {
|
||||||
const items = await readUsersFromFile();
|
const items = await readUsersFromDb();
|
||||||
res.json({ items });
|
res.json({ items });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/users', async (req, res) => {
|
app.post('/api/users', async (req, res) => {
|
||||||
const { username, password, vlan, disabled } = req.body || {};
|
const { username, password, vlan, disabled } = req.body || {};
|
||||||
if (!username || !password) return res.status(400).json({ ok: false, error: 'username and password required' });
|
if (!username || !password) return res.status(400).json({ ok: false, error: 'username and password required' });
|
||||||
const items = await readUsersFromFile();
|
|
||||||
const idx = items.findIndex(u => u.username === String(username));
|
|
||||||
const user = { username: String(username), password: String(password), vlan: String(vlan || VLAN_ID), disabled: !!disabled };
|
const user = { username: String(username), password: String(password), vlan: String(vlan || VLAN_ID), disabled: !!disabled };
|
||||||
if (idx >= 0) items[idx] = user; else items.push(user);
|
await upsertUserToDb(user);
|
||||||
await persistUsersToFreeradius(items);
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.patch('/api/users/:username', async (req, res) => {
|
app.patch('/api/users/:username', async (req, res) => {
|
||||||
const uname = req.params.username;
|
const uname = String(req.params.username);
|
||||||
const { password, vlan, disabled } = req.body || {};
|
const { password, vlan, disabled } = req.body || {};
|
||||||
const items = await readUsersFromFile();
|
const current = (await readUsersFromDb()).find(u => u.username === uname);
|
||||||
const idx = items.findIndex(u => u.username === uname);
|
if (!current) return res.status(404).json({ ok: false, error: 'not_found' });
|
||||||
if (idx < 0) return res.status(404).json({ ok: false, error: 'not_found' });
|
const next = {
|
||||||
if (password !== undefined) items[idx].password = String(password);
|
username: uname,
|
||||||
if (vlan !== undefined) items[idx].vlan = String(vlan);
|
password: password !== undefined ? String(password) : current.password,
|
||||||
if (disabled !== undefined) items[idx].disabled = !!disabled;
|
vlan: vlan !== undefined ? String(vlan) : current.vlan,
|
||||||
await persistUsersToFreeradius(items);
|
disabled: disabled !== undefined ? !!disabled : current.disabled,
|
||||||
|
};
|
||||||
|
await upsertUserToDb(next);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/users/:username', async (req, res) => {
|
app.delete('/api/users/:username', async (req, res) => {
|
||||||
const uname = req.params.username;
|
const uname = String(req.params.username);
|
||||||
const items = await readUsersFromFile();
|
await deleteUserFromDb(uname);
|
||||||
const next = items.filter(u => u.username !== uname);
|
|
||||||
if (next.length === items.length) return res.status(404).json({ ok: false, error: 'not_found' });
|
|
||||||
await persistUsersToFreeradius(next);
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"radius": "^1.1.0"
|
"radius": "^1.1.0",
|
||||||
|
"pg": "^8.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
postgres/init/01-schema.sql
Normal file
21
postgres/init/01-schema.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
-- Minimal FreeRADIUS schema for users via rlm_sql (PostgreSQL)
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS radcheck (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
attribute VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
op CHAR(2) NOT NULL DEFAULT ':=',
|
||||||
|
value VARCHAR(253) NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS radreply (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
attribute VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
op CHAR(2) NOT NULL DEFAULT ':=',
|
||||||
|
value VARCHAR(253) NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS radcheck_user ON radcheck (username);
|
||||||
|
CREATE INDEX IF NOT EXISTS radreply_user ON radreply (username);
|
||||||
|
|
||||||
Reference in New Issue
Block a user