diff --git a/cli/commands/generateOrgCaKeys.ts b/cli/commands/generateOrgCaKeys.ts
new file mode 100644
index 00000000..af822c81
--- /dev/null
+++ b/cli/commands/generateOrgCaKeys.ts
@@ -0,0 +1,121 @@
+import { CommandModule } from "yargs";
+import { db, orgs } from "@server/db";
+import { eq } from "drizzle-orm";
+import { encrypt } from "@server/lib/crypto";
+import { configFilePath1, configFilePath2 } from "@server/lib/consts";
+import { generateCA } from "@server/private/lib/sshCA";
+import fs from "fs";
+import yaml from "js-yaml";
+
+type GenerateOrgCaKeysArgs = {
+ orgId: string;
+ secret?: string;
+ force?: boolean;
+};
+
+export const generateOrgCaKeys: CommandModule<{}, GenerateOrgCaKeysArgs> = {
+ command: "generate-org-ca-keys",
+ describe:
+ "Generate SSH CA public/private key pair for an organization and store them in the database (private key encrypted with server secret)",
+ builder: (yargs) => {
+ return yargs
+ .option("orgId", {
+ type: "string",
+ demandOption: true,
+ describe: "The organization ID"
+ })
+ .option("secret", {
+ type: "string",
+ describe:
+ "Server secret used to encrypt the CA private key. If omitted, read from config file (config.yml or config.yaml)."
+ })
+ .option("force", {
+ type: "boolean",
+ default: false,
+ describe:
+ "Overwrite existing CA keys for the org if they already exist"
+ });
+ },
+ handler: async (argv: {
+ orgId: string;
+ secret?: string;
+ force?: boolean;
+ }) => {
+ try {
+ const { orgId, force } = argv;
+ let secret = argv.secret;
+
+ if (!secret) {
+ const configPath = fs.existsSync(configFilePath1)
+ ? configFilePath1
+ : fs.existsSync(configFilePath2)
+ ? configFilePath2
+ : null;
+
+ if (!configPath) {
+ console.error(
+ "Error: No server secret provided and config file not found. " +
+ "Expected config.yml or config.yaml in the config directory, or pass --secret."
+ );
+ process.exit(1);
+ }
+
+ const configContent = fs.readFileSync(configPath, "utf8");
+ const config = yaml.load(configContent) as {
+ server?: { secret?: string };
+ };
+
+ if (!config?.server?.secret) {
+ console.error(
+ "Error: No server.secret in config file. Pass --secret or set server.secret in config."
+ );
+ process.exit(1);
+ }
+ secret = config.server.secret;
+ }
+
+ const [org] = await db
+ .select({
+ orgId: orgs.orgId,
+ sshCaPrivateKey: orgs.sshCaPrivateKey,
+ sshCaPublicKey: orgs.sshCaPublicKey
+ })
+ .from(orgs)
+ .where(eq(orgs.orgId, orgId))
+ .limit(1);
+
+ if (!org) {
+ console.error(`Error: Organization with orgId "${orgId}" not found.`);
+ process.exit(1);
+ }
+
+ if (org.sshCaPrivateKey != null || org.sshCaPublicKey != null) {
+ if (!force) {
+ console.error(
+ "Error: This organization already has CA keys. Use --force to overwrite."
+ );
+ process.exit(1);
+ }
+ }
+
+ const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
+ const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
+
+ await db
+ .update(orgs)
+ .set({
+ sshCaPrivateKey: encryptedPrivateKey,
+ sshCaPublicKey: ca.publicKeyOpenSSH
+ })
+ .where(eq(orgs.orgId, orgId));
+
+ console.log("SSH CA keys generated and stored for org:", orgId);
+ console.log("\nPublic key (OpenSSH format):");
+ console.log(ca.publicKeyOpenSSH);
+ process.exit(0);
+ } catch (error) {
+ console.error("Error generating org CA keys:", error);
+ process.exit(1);
+ }
+ }
+};
diff --git a/cli/index.ts b/cli/index.ts
index d517064c..7605904e 100644
--- a/cli/index.ts
+++ b/cli/index.ts
@@ -8,6 +8,7 @@ import { clearExitNodes } from "./commands/clearExitNodes";
import { rotateServerSecret } from "./commands/rotateServerSecret";
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
import { deleteClient } from "./commands/deleteClient";
+import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
yargs(hideBin(process.argv))
.scriptName("pangctl")
@@ -17,5 +18,6 @@ yargs(hideBin(process.argv))
.command(rotateServerSecret)
.command(clearLicenseKeys)
.command(deleteClient)
+ .command(generateOrgCaKeys)
.demandCommand()
.help().argv;
diff --git a/messages/bg-BG.json b/messages/bg-BG.json
index b8cd3893..3fed7e09 100644
--- a/messages/bg-BG.json
+++ b/messages/bg-BG.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Ролята е премахната",
"accessRoleRemovedDescription": "Ролята беше успешно премахната.",
"accessRoleRequiredRemove": "Преди да изтриете тази роля, моля изберете нова роля, към която да прехвърлите настоящите членове.",
+ "network": "Мрежа",
"manage": "Управление",
"sitesNotFound": "Няма намерени сайтове.",
"pangolinServerAdmin": "Администратор на сървър - Панголин",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Частно",
"sidebarAccessControl": "Контрол на достъпа",
"sidebarLogsAndAnalytics": "Дневници и анализи",
+ "sidebarTeam": "Екип",
"sidebarUsers": "Потребители",
"sidebarAdmin": "Администратор",
"sidebarInvitations": "Покани",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Лог & Анализи",
"sidebarBluePrints": "Чертежи",
"sidebarOrganization": "Организация",
+ "sidebarManagement": "Управление",
"sidebarBillingAndLicenses": "Фактуриране & Лицензи",
"sidebarLogsAnalytics": "Анализи",
"blueprints": "Чертежи",
@@ -1289,7 +1292,6 @@
"parsedContents": "Парсирано съдържание (само за четене)",
"enableDockerSocket": "Активиране на Docker Чернова",
"enableDockerSocketDescription": "Активиране на Docker Socket маркировка за изтегляне на етикети на чернова. Пътят на гнездото трябва да бъде предоставен на Newt.",
- "enableDockerSocketLink": "Научете повече",
"viewDockerContainers": "Преглед на Docker контейнери",
"containersIn": "Контейнери в {siteName}",
"selectContainerDescription": "Изберете контейнер, който да ползвате като име на хост за целта. Натиснете порт, за да ползвате порт",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Времето е в секунди",
"requireDeviceApproval": "Изискват одобрение на устройства",
"requireDeviceApprovalDescription": "Потребители с тази роля трябва да имат нови устройства одобрени от администратор преди да могат да се свържат и да имат достъп до ресурси.",
+ "sshAccess": "SSH достъп",
+ "roleAllowSsh": "Разреши SSH",
+ "roleAllowSshAllow": "Разреши",
+ "roleAllowSshDisallow": "Забрани",
+ "roleAllowSshDescription": "Разреши на потребителите с тази роля да се свързват с ресурси чрез SSH. Когато е деактивирано, ролята не може да използва SSH достъп.",
+ "sshSudoMode": "Sudo достъп",
+ "sshSudoModeNone": "Няма",
+ "sshSudoModeNoneDescription": "Потребителят не може да изпълнява команди с sudo.",
+ "sshSudoModeFull": "Пълен Sudo",
+ "sshSudoModeFullDescription": "Потребителят може да изпълнява всяка команда с sudo.",
+ "sshSudoModeCommands": "Команди",
+ "sshSudoModeCommandsDescription": "Потребителят може да изпълнява само определени команди с sudo.",
+ "sshSudo": "Разреши sudo",
+ "sshSudoCommands": "Sudo команди",
+ "sshSudoCommandsDescription": "Списък с команди, които потребителят е разрешено да изпълнява с sudo.",
+ "sshCreateHomeDir": "Създай начална директория",
+ "sshUnixGroups": "Unix групи",
+ "sshUnixGroupsDescription": "Unix групи, в които да добавите потребителя на целевия хост.",
"retryAttempts": "Опити за повторно",
"expectedResponseCodes": "Очаквани кодове за отговор",
"expectedResponseCodesDescription": "HTTP статус код, указващ здравословно състояние. Ако бъде оставено празно, между 200-300 се счита за здравословно.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Контрол на достъпа.",
"editInternalResourceDialogAccessControlDescription": "Контролирайте кои роли, потребители и клиентски машини имат достъп до този ресурс, когато са свързани. Администраторите винаги имат достъп.",
"editInternalResourceDialogPortRangeValidationError": "Обхватът на портовете трябва да е \"*\" за всички портове или списък от разделени със запетая портове и диапазони (например: \"80,443,8000-9000\"). Портовете трябва да са между 1 и 65535.",
+ "internalResourceAuthDaemonStrategy": "Локация на SSH Auth Daemon",
+ "internalResourceAuthDaemonStrategyDescription": "Изберете къде ще работи демонът за SSH удостоверение: на сайта (Newt) или на отдалечен хост.",
+ "internalResourceAuthDaemonDescription": "Демонът за SSH удостоверение управлява подписването на SSH ключове и PAM удостоверение за този ресурс. Изберете дали да работи на сайта (Newt) или на отделен отдалечен хост. Вижте документацията за повече информация.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Изберете стратегия",
+ "internalResourceAuthDaemonStrategyLabel": "Местоположение",
+ "internalResourceAuthDaemonSite": "На сайта",
+ "internalResourceAuthDaemonSiteDescription": "Демонът за удостоверение работи на сайта (Newt).",
+ "internalResourceAuthDaemonRemote": "Отдалечен хост",
+ "internalResourceAuthDaemonRemoteDescription": "Демонът за удостоверение работи на хост, който не е сайтът.",
+ "internalResourceAuthDaemonPort": "Порт на демона (незадължителен)",
"orgAuthWhatsThis": "Къде мога да намеря идентификатора на организацията си?",
"learnMore": "Научете повече.",
"backToHome": "Връщане към началната страница.",
diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json
index e4ff7269..12c190a9 100644
--- a/messages/cs-CZ.json
+++ b/messages/cs-CZ.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Role odstraněna",
"accessRoleRemovedDescription": "Role byla úspěšně odstraněna.",
"accessRoleRequiredRemove": "Před odstraněním této role vyberte novou roli, do které chcete převést existující členy.",
+ "network": "Síť",
"manage": "Spravovat",
"sitesNotFound": "Nebyly nalezeny žádné stránky.",
"pangolinServerAdmin": "Správce serveru - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Soukromé",
"sidebarAccessControl": "Kontrola přístupu",
"sidebarLogsAndAnalytics": "Logy & Analytika",
+ "sidebarTeam": "Tým",
"sidebarUsers": "Uživatelé",
"sidebarAdmin": "Admin",
"sidebarInvitations": "Pozvánky",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Plány",
"sidebarOrganization": "Organizace",
+ "sidebarManagement": "Správa",
"sidebarBillingAndLicenses": "Fakturace a licence",
"sidebarLogsAnalytics": "Analytici",
"blueprints": "Plány",
@@ -1289,7 +1292,6 @@
"parsedContents": "Parsed content (Pouze pro čtení)",
"enableDockerSocket": "Povolit Docker plán",
"enableDockerSocketDescription": "Povolte seškrábání štítků na Docker Socket pro popisky plánů. Nová cesta musí být k dispozici.",
- "enableDockerSocketLink": "Zjistit více",
"viewDockerContainers": "Zobrazit kontejnery Dockeru",
"containersIn": "Kontejnery v {siteName}",
"selectContainerDescription": "Vyberte jakýkoli kontejner pro použití jako název hostitele pro tento cíl. Klikněte na port pro použití portu.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Čas je v sekundách",
"requireDeviceApproval": "Vyžadovat schválení zařízení",
"requireDeviceApprovalDescription": "Uživatelé s touto rolí potřebují nová zařízení schválená správcem, než se mohou připojit a přistupovat ke zdrojům.",
+ "sshAccess": "SSH přístup",
+ "roleAllowSsh": "Povolit SSH",
+ "roleAllowSshAllow": "Povolit",
+ "roleAllowSshDisallow": "Zakázat",
+ "roleAllowSshDescription": "Povolit uživatelům s touto rolí připojení k zdrojům přes SSH. Je-li zakázáno, role nemůže používat přístup SSH.",
+ "sshSudoMode": "Súdánský přístup",
+ "sshSudoModeNone": "Nic",
+ "sshSudoModeNoneDescription": "Uživatel nemůže spouštět příkazy se sudo.",
+ "sshSudoModeFull": "Úplný Súdán",
+ "sshSudoModeFullDescription": "Uživatel může spustit libovolný příkaz se sudo.",
+ "sshSudoModeCommands": "Příkazy",
+ "sshSudoModeCommandsDescription": "Uživatel může spustit pouze zadané příkazy s sudo.",
+ "sshSudo": "Povolit sudo",
+ "sshSudoCommands": "Sudo příkazy",
+ "sshSudoCommandsDescription": "Seznam příkazů, které může uživatel spouštět s sudo.",
+ "sshCreateHomeDir": "Vytvořit domovský adresář",
+ "sshUnixGroups": "Unixové skupiny",
+ "sshUnixGroupsDescription": "Unix skupiny přidají uživatele do cílového hostitele.",
"retryAttempts": "Opakovat pokusy",
"expectedResponseCodes": "Očekávané kódy odezvy",
"expectedResponseCodesDescription": "HTTP kód stavu, který označuje zdravý stav. Ponecháte-li prázdné, 200-300 je považováno za zdravé.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Řízení přístupu",
"editInternalResourceDialogAccessControlDescription": "Kontrolujte, které role, uživatelé a klienti mohou přistupovat k tomuto prostředku, když jsou připojeni. Admini mají vždy přístup.",
"editInternalResourceDialogPortRangeValidationError": "Rozsah portů musí být \"*\" pro všechny porty, nebo seznam portů a rozsahů oddělených čárkou (např. \"80,443,8000-9000\"). Porty musí být mezi 1 a 65535.",
+ "internalResourceAuthDaemonStrategy": "SSH Auth Démon umístění",
+ "internalResourceAuthDaemonStrategyDescription": "Zvolte, kde běží SSH autentizační démon: na stránce (Newt) nebo na vzdáleném serveru.",
+ "internalResourceAuthDaemonDescription": "SSH autentizační daemon zpracovává podpis SSH klíče a PAM autentizaci tohoto zdroje. Vyberte si, zda běží na webu (Newt) nebo na samostatném vzdáleném serveru. Více informací najdete v dokumentaci.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Vybrat strategii",
+ "internalResourceAuthDaemonStrategyLabel": "Poloha",
+ "internalResourceAuthDaemonSite": "Na stránce",
+ "internalResourceAuthDaemonSiteDescription": "Auth daemon běží na webu (Newt).",
+ "internalResourceAuthDaemonRemote": "Vzdálený server",
+ "internalResourceAuthDaemonRemoteDescription": "Auth daemon běží na hostitele, který není web.",
+ "internalResourceAuthDaemonPort": "Daemon port (volitelné)",
"orgAuthWhatsThis": "Kde najdu ID mé organizace?",
"learnMore": "Zjistit více",
"backToHome": "Zpět na domovskou stránku",
diff --git a/messages/de-DE.json b/messages/de-DE.json
index 8a5e9b68..39098d3b 100644
--- a/messages/de-DE.json
+++ b/messages/de-DE.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Rolle entfernt",
"accessRoleRemovedDescription": "Die Rolle wurde erfolgreich entfernt.",
"accessRoleRequiredRemove": "Bevor Sie diese Rolle löschen, wählen Sie bitte eine neue Rolle aus, zu der die bestehenden Mitglieder übertragen werden sollen.",
+ "network": "Netzwerk",
"manage": "Verwalten",
"sitesNotFound": "Keine Standorte gefunden.",
"pangolinServerAdmin": "Server-Admin - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Privat",
"sidebarAccessControl": "Zugriffskontrolle",
"sidebarLogsAndAnalytics": "Protokolle & Analysen",
+ "sidebarTeam": "Team",
"sidebarUsers": "Benutzer",
"sidebarAdmin": "Admin",
"sidebarInvitations": "Einladungen",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Log & Analytik",
"sidebarBluePrints": "Blaupausen",
"sidebarOrganization": "Organisation",
+ "sidebarManagement": "Management",
"sidebarBillingAndLicenses": "Abrechnung & Lizenzen",
"sidebarLogsAnalytics": "Analytik",
"blueprints": "Blaupausen",
@@ -1289,7 +1292,6 @@
"parsedContents": "Analysierte Inhalte (Nur lesen)",
"enableDockerSocket": "Docker Blueprint aktivieren",
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blueprintbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
- "enableDockerSocketLink": "Mehr erfahren",
"viewDockerContainers": "Docker Container anzeigen",
"containersIn": "Container in {siteName}",
"selectContainerDescription": "Wählen Sie einen Container, der als Hostname für dieses Ziel verwendet werden soll. Klicken Sie auf einen Port, um einen Port zu verwenden.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Zeit ist in Sekunden",
"requireDeviceApproval": "Gerätegenehmigungen erforderlich",
"requireDeviceApprovalDescription": "Benutzer mit dieser Rolle benötigen neue Geräte, die von einem Administrator genehmigt wurden, bevor sie sich verbinden und auf Ressourcen zugreifen können.",
+ "sshAccess": "SSH-Zugriff",
+ "roleAllowSsh": "SSH erlauben",
+ "roleAllowSshAllow": "Erlauben",
+ "roleAllowSshDisallow": "Nicht zulassen",
+ "roleAllowSshDescription": "Benutzern mit dieser Rolle erlauben, sich über SSH mit Ressourcen zu verbinden. Wenn deaktiviert, kann die Rolle keinen SSH-Zugriff verwenden.",
+ "sshSudoMode": "Sudo-Zugriff",
+ "sshSudoModeNone": "Keine",
+ "sshSudoModeNoneDescription": "Benutzer kann keine Befehle mit sudo ausführen.",
+ "sshSudoModeFull": "Volles Sudo",
+ "sshSudoModeFullDescription": "Benutzer kann jeden Befehl mit sudo ausführen.",
+ "sshSudoModeCommands": "Befehle",
+ "sshSudoModeCommandsDescription": "Benutzer kann nur die angegebenen Befehle mit sudo ausführen.",
+ "sshSudo": "sudo erlauben",
+ "sshSudoCommands": "Sudo-Befehle",
+ "sshSudoCommandsDescription": "Liste der Befehle, die der Benutzer mit sudo ausführen darf.",
+ "sshCreateHomeDir": "Home-Verzeichnis erstellen",
+ "sshUnixGroups": "Unix-Gruppen",
+ "sshUnixGroupsDescription": "Unix-Gruppen, zu denen der Benutzer auf dem Ziel-Host hinzugefügt wird.",
"retryAttempts": "Wiederholungsversuche",
"expectedResponseCodes": "Erwartete Antwortcodes",
"expectedResponseCodesDescription": "HTTP-Statuscode, der einen gesunden Zustand anzeigt. Wenn leer gelassen, wird 200-300 als gesund angesehen.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Zugriffskontrolle",
"editInternalResourceDialogAccessControlDescription": "Kontrollieren Sie, welche Rollen, Benutzer und Maschinen-Clients Zugriff auf diese Ressource haben, wenn sie verbunden sind. Admins haben immer Zugriff.",
"editInternalResourceDialogPortRangeValidationError": "Der Port-Bereich muss \"*\" für alle Ports sein, oder eine kommaseparierte Liste von Ports und Bereichen (z.B. \"80,443.8000-9000\"). Ports müssen zwischen 1 und 65535 liegen.",
+ "internalResourceAuthDaemonStrategy": "SSH Auth-Daemon Standort",
+ "internalResourceAuthDaemonStrategyDescription": "Wählen Sie aus, wo der SSH-Authentifizierungs-Daemon läuft: auf der Site (Newt) oder auf einem entfernten Host.",
+ "internalResourceAuthDaemonDescription": "Der SSH-Authentifizierungs-Daemon verarbeitet SSH-Schlüsselsignaturen und PAM-Authentifizierung für diese Ressource. Wählen Sie, ob sie auf der Website (Newt) oder auf einem separaten entfernten Host ausgeführt wird. Siehe die Dokumentation für mehr.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Strategie auswählen",
+ "internalResourceAuthDaemonStrategyLabel": "Standort",
+ "internalResourceAuthDaemonSite": "Vor Ort",
+ "internalResourceAuthDaemonSiteDescription": "Der Auth Daemon läuft auf der Seite (Newt).",
+ "internalResourceAuthDaemonRemote": "Entfernter Host",
+ "internalResourceAuthDaemonRemoteDescription": "Der Auth Daemon läuft auf einem Host, der nicht die Site ist.",
+ "internalResourceAuthDaemonPort": "Daemon-Port (optional)",
"orgAuthWhatsThis": "Wo finde ich meine Organisations-ID?",
"learnMore": "Mehr erfahren",
"backToHome": "Zurück zur Startseite",
diff --git a/messages/en-US.json b/messages/en-US.json
index 44d980c5..f12e2210 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -649,7 +649,7 @@
"resourcesUsersRolesAccess": "User and role-based access control",
"resourcesErrorUpdate": "Failed to toggle resource",
"resourcesErrorUpdateDescription": "An error occurred while updating the resource",
- "access": "Access",
+ "access": "Access Control",
"shareLink": "{resource} Share Link",
"resourceSelect": "Select resource",
"shareLinks": "Share Links",
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Role removed",
"accessRoleRemovedDescription": "The role has been successfully removed.",
"accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.",
+ "network": "Network",
"manage": "Manage",
"sitesNotFound": "No sites found.",
"pangolinServerAdmin": "Server Admin - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Private",
"sidebarAccessControl": "Access Control",
"sidebarLogsAndAnalytics": "Logs & Analytics",
+ "sidebarTeam": "Team",
"sidebarUsers": "Users",
"sidebarAdmin": "Admin",
"sidebarInvitations": "Invitations",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Blueprints",
"sidebarOrganization": "Organization",
+ "sidebarManagement": "Management",
"sidebarBillingAndLicenses": "Billing & Licenses",
"sidebarLogsAnalytics": "Analytics",
"blueprints": "Blueprints",
@@ -1288,8 +1291,7 @@
"contents": "Contents",
"parsedContents": "Parsed Contents (Read Only)",
"enableDockerSocket": "Enable Docker Blueprint",
- "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
- "enableDockerSocketLink": "Learn More",
+ "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in the documentation.",
"viewDockerContainers": "View Docker Containers",
"containersIn": "Containers in {siteName}",
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Time is in seconds",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
+ "sshAccess": "SSH Access",
+ "roleAllowSsh": "Allow SSH",
+ "roleAllowSshAllow": "Allow",
+ "roleAllowSshDisallow": "Disallow",
+ "roleAllowSshDescription": "Allow users with this role to connect to resources via SSH. When disabled, the role cannot use SSH access.",
+ "sshSudoMode": "Sudo Access",
+ "sshSudoModeNone": "None",
+ "sshSudoModeNoneDescription": "User cannot run commands with sudo.",
+ "sshSudoModeFull": "Full Sudo",
+ "sshSudoModeFullDescription": "User can run any command with sudo.",
+ "sshSudoModeCommands": "Commands",
+ "sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
+ "sshSudo": "Allow sudo",
+ "sshSudoCommands": "Sudo Commands",
+ "sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo.",
+ "sshCreateHomeDir": "Create Home Directory",
+ "sshUnixGroups": "Unix Groups",
+ "sshUnixGroupsDescription": "Unix groups to add the user to on the target host.",
"retryAttempts": "Retry Attempts",
"expectedResponseCodes": "Expected Response Codes",
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
@@ -1988,8 +2008,8 @@
"orgAuthNoAccount": "Don't have an account?",
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
"mustUpgradeToUse": "You must upgrade your subscription to use this feature.",
- "subscriptionRequiredTierToUse": "This feature requires {tier} or higher.",
- "upgradeToTierToUse": "Upgrade to {tier} or higher to use this feature.",
+ "subscriptionRequiredTierToUse": "This feature requires {tier}.",
+ "upgradeToTierToUse": "Upgrade to {tier} to use this feature.",
"subscriptionTierTier1": "Home",
"subscriptionTierTier2": "Team",
"subscriptionTierTier3": "Business",
@@ -2079,7 +2099,7 @@
"manageMachineClients": "Manage Machine Clients",
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
"machineClientsBannerTitle": "Servers & Automated Systems",
- "machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
+ "machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can be deployed as a CLI or a container.",
"machineClientsBannerPangolinCLI": "Pangolin CLI",
"machineClientsBannerOlmCLI": "Olm CLI",
"machineClientsBannerOlmContainer": "Container",
@@ -2305,7 +2325,7 @@
"logRetentionEndOfFollowingYear": "End of following year",
"actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization",
- "licenseRequiredToUse": "An Enterprise Edition license is required to use this feature. This feature is also available in Pangolin Cloud.",
+ "licenseRequiredToUse": "An Enterprise Edition license or Pangolin Cloud is required to use this feature.",
"ossEnterpriseEditionRequired": "The Enterprise Edition is required to use this feature. This feature is also available in Pangolin Cloud.",
"certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Access Control",
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.",
+ "internalResourceAuthDaemonStrategy": "SSH Auth Daemon Location",
+ "internalResourceAuthDaemonStrategyDescription": "Choose where the SSH authentication daemon runs: on the site (Newt) or on a remote host.",
+ "internalResourceAuthDaemonDescription": "The SSH authentication daemon handles SSH key signing and PAM authentication for this resource. Choose whether it runs on the site (Newt) or on a separate remote host. See the documentation for more.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Select Strategy",
+ "internalResourceAuthDaemonStrategyLabel": "Location",
+ "internalResourceAuthDaemonSite": "On Site",
+ "internalResourceAuthDaemonSiteDescription": "Auth daemon runs on the site (Newt).",
+ "internalResourceAuthDaemonRemote": "Remote Host",
+ "internalResourceAuthDaemonRemoteDescription": "Auth daemon runs on a host that is not the site.",
+ "internalResourceAuthDaemonPort": "Daemon Port (optional)",
"orgAuthWhatsThis": "Where can I find my organization ID?",
"learnMore": "Learn more",
"backToHome": "Go back to home",
diff --git a/messages/es-ES.json b/messages/es-ES.json
index d520aaf4..f3888e8b 100644
--- a/messages/es-ES.json
+++ b/messages/es-ES.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Rol eliminado",
"accessRoleRemovedDescription": "El rol se ha eliminado correctamente.",
"accessRoleRequiredRemove": "Antes de eliminar este rol, seleccione un nuevo rol al que transferir miembros existentes.",
+ "network": "Red",
"manage": "Gestionar",
"sitesNotFound": "Sitios no encontrados.",
"pangolinServerAdmin": "Admin Servidor - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Privado",
"sidebarAccessControl": "Control de acceso",
"sidebarLogsAndAnalytics": "Registros y análisis",
+ "sidebarTeam": "Equipo",
"sidebarUsers": "Usuarios",
"sidebarAdmin": "Admin",
"sidebarInvitations": "Invitaciones",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Registro y análisis",
"sidebarBluePrints": "Planos",
"sidebarOrganization": "Organización",
+ "sidebarManagement": "Gestión",
"sidebarBillingAndLicenses": "Facturación y licencias",
"sidebarLogsAnalytics": "Analíticas",
"blueprints": "Planos",
@@ -1289,7 +1292,6 @@
"parsedContents": "Contenido analizado (Sólo lectura)",
"enableDockerSocket": "Habilitar Plano Docker",
"enableDockerSocketDescription": "Activar el raspado de etiquetas de Socket Docker para etiquetas de planos. La ruta del Socket debe proporcionarse a Newt.",
- "enableDockerSocketLink": "Saber más",
"viewDockerContainers": "Ver contenedores Docker",
"containersIn": "Contenedores en {siteName}",
"selectContainerDescription": "Seleccione cualquier contenedor para usar como nombre de host para este objetivo. Haga clic en un puerto para usar un puerto.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "El tiempo está en segundos",
"requireDeviceApproval": "Requiere aprobaciones del dispositivo",
"requireDeviceApprovalDescription": "Los usuarios con este rol necesitan nuevos dispositivos aprobados por un administrador antes de poder conectarse y acceder a los recursos.",
+ "sshAccess": "Acceso a SSH",
+ "roleAllowSsh": "Permitir SSH",
+ "roleAllowSshAllow": "Permitir",
+ "roleAllowSshDisallow": "Rechazar",
+ "roleAllowSshDescription": "Permitir a los usuarios con este rol conectarse a recursos a través de SSH. Cuando está desactivado, el rol no puede usar acceso SSH.",
+ "sshSudoMode": "Acceso Sudo",
+ "sshSudoModeNone": "Ninguna",
+ "sshSudoModeNoneDescription": "El usuario no puede ejecutar comandos con sudo.",
+ "sshSudoModeFull": "Sudo completo",
+ "sshSudoModeFullDescription": "El usuario puede ejecutar cualquier comando con sudo.",
+ "sshSudoModeCommands": "Comandos",
+ "sshSudoModeCommandsDescription": "El usuario sólo puede ejecutar los comandos especificados con sudo.",
+ "sshSudo": "Permitir sudo",
+ "sshSudoCommands": "Comandos Sudo",
+ "sshSudoCommandsDescription": "Lista de comandos que el usuario puede ejecutar con sudo.",
+ "sshCreateHomeDir": "Crear directorio principal",
+ "sshUnixGroups": "Grupos Unix",
+ "sshUnixGroupsDescription": "Grupos Unix para agregar el usuario en el host de destino.",
"retryAttempts": "Intentos de Reintento",
"expectedResponseCodes": "Códigos de respuesta esperados",
"expectedResponseCodesDescription": "Código de estado HTTP que indica un estado saludable. Si se deja en blanco, se considera saludable de 200 a 300.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Control de acceso",
"editInternalResourceDialogAccessControlDescription": "Controla qué roles, usuarios y clientes de máquinas tienen acceso a este recurso cuando están conectados. Los administradores siempre tienen acceso.",
"editInternalResourceDialogPortRangeValidationError": "El rango de puertos debe ser \"*\" para todos los puertos, o una lista separada por comas de puertos y rangos (por ejemplo, \"80,443,8000-9000\"). Los puertos deben estar entre 1 y 65535.",
+ "internalResourceAuthDaemonStrategy": "Ubicación del demonio de autenticación SSSH",
+ "internalResourceAuthDaemonStrategyDescription": "Elija dónde se ejecuta el daemon de autenticación SSH: en el sitio (Newt) o en un host remoto.",
+ "internalResourceAuthDaemonDescription": "El daemon de autenticación SSSH maneja la firma de claves SSH y autenticación PAM para este recurso. Elija si se ejecuta en el sitio (Newt) o en un host remoto separado. Vea la documentación para más.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Seleccionar estrategia",
+ "internalResourceAuthDaemonStrategyLabel": "Ubicación",
+ "internalResourceAuthDaemonSite": "En el sitio",
+ "internalResourceAuthDaemonSiteDescription": "Auth daemon corre en el sitio (Newt).",
+ "internalResourceAuthDaemonRemote": "Host remoto",
+ "internalResourceAuthDaemonRemoteDescription": "El daemon Auth corre en un host que no es el sitio.",
+ "internalResourceAuthDaemonPort": "Puerto de demonio (opcional)",
"orgAuthWhatsThis": "¿Dónde puedo encontrar el ID de mi organización?",
"learnMore": "Más información",
"backToHome": "Volver a inicio",
diff --git a/messages/fr-FR.json b/messages/fr-FR.json
index 23d988b7..1ee3c645 100644
--- a/messages/fr-FR.json
+++ b/messages/fr-FR.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Rôle supprimé",
"accessRoleRemovedDescription": "Le rôle a été supprimé avec succès.",
"accessRoleRequiredRemove": "Avant de supprimer ce rôle, veuillez sélectionner un nouveau rôle pour transférer les membres existants.",
+ "network": "Réseau",
"manage": "Gérer",
"sitesNotFound": "Aucun site trouvé.",
"pangolinServerAdmin": "Admin Serveur - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Privé",
"sidebarAccessControl": "Contrôle d'accès",
"sidebarLogsAndAnalytics": "Journaux & Analytiques",
+ "sidebarTeam": "Equipe",
"sidebarUsers": "Utilisateurs",
"sidebarAdmin": "Administrateur",
"sidebarInvitations": "Invitations",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Journaux & Analytiques",
"sidebarBluePrints": "Configs",
"sidebarOrganization": "Organisation",
+ "sidebarManagement": "Gestion",
"sidebarBillingAndLicenses": "Facturation & Licences",
"sidebarLogsAnalytics": "Analyses",
"blueprints": "Configs",
@@ -1289,7 +1292,6 @@
"parsedContents": "Contenu analysé (lecture seule)",
"enableDockerSocket": "Activer la Config Docker",
"enableDockerSocketDescription": "Activer le ramassage d'étiquettes de socket Docker pour les étiquettes de plan. Le chemin de socket doit être fourni à Newt.",
- "enableDockerSocketLink": "En savoir plus",
"viewDockerContainers": "Voir les conteneurs Docker",
"containersIn": "Conteneurs en {siteName}",
"selectContainerDescription": "Sélectionnez n'importe quel conteneur à utiliser comme nom d'hôte pour cette cible. Cliquez sur un port pour utiliser un port.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Le temps est exprimé en secondes",
"requireDeviceApproval": "Exiger les autorisations de l'appareil",
"requireDeviceApprovalDescription": "Les utilisateurs ayant ce rôle ont besoin de nouveaux périphériques approuvés par un administrateur avant de pouvoir se connecter et accéder aux ressources.",
+ "sshAccess": "Accès SSH",
+ "roleAllowSsh": "Autoriser SSH",
+ "roleAllowSshAllow": "Autoriser",
+ "roleAllowSshDisallow": "Interdire",
+ "roleAllowSshDescription": "Autoriser les utilisateurs avec ce rôle à se connecter aux ressources via SSH. Lorsque désactivé, le rôle ne peut pas utiliser les accès SSH.",
+ "sshSudoMode": "Accès Sudo",
+ "sshSudoModeNone": "Aucun",
+ "sshSudoModeNoneDescription": "L'utilisateur ne peut pas exécuter de commandes avec sudo.",
+ "sshSudoModeFull": "Sudo complet",
+ "sshSudoModeFullDescription": "L'utilisateur peut exécuter n'importe quelle commande avec sudo.",
+ "sshSudoModeCommands": "Commandes",
+ "sshSudoModeCommandsDescription": "L'utilisateur ne peut exécuter que les commandes spécifiées avec sudo.",
+ "sshSudo": "Autoriser sudo",
+ "sshSudoCommands": "Commandes Sudo",
+ "sshSudoCommandsDescription": "Liste des commandes que l'utilisateur est autorisé à exécuter avec sudo.",
+ "sshCreateHomeDir": "Créer un répertoire personnel",
+ "sshUnixGroups": "Groupes Unix",
+ "sshUnixGroupsDescription": "Groupes Unix à ajouter à l'utilisateur sur l'hôte cible.",
"retryAttempts": "Tentatives de réessai",
"expectedResponseCodes": "Codes de réponse attendus",
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Contrôle d'accès",
"editInternalResourceDialogAccessControlDescription": "Contrôlez quels rôles, utilisateurs et clients de machine ont accès à cette ressource lorsqu'ils sont connectés. Les administrateurs ont toujours accès.",
"editInternalResourceDialogPortRangeValidationError": "La plage de ports doit être \"*\" pour tous les ports, ou une liste de ports et de plages séparés par des virgules (par exemple, \"80,443,8000-9000\"). Les ports doivent être compris entre 1 et 65535.",
+ "internalResourceAuthDaemonStrategy": "Emplacement du démon d'authentification SSH",
+ "internalResourceAuthDaemonStrategyDescription": "Choisissez où le démon d'authentification SSH s'exécute : sur le site (Newt) ou sur un hôte distant.",
+ "internalResourceAuthDaemonDescription": "Le démon d'authentification SSH gère la signature des clés SSH et l'authentification PAM pour cette ressource. Choisissez s'il fonctionne sur le site (Newt) ou sur un hôte distant séparé. Voir la documentation pour plus d'informations.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Choisir une stratégie",
+ "internalResourceAuthDaemonStrategyLabel": "Localisation",
+ "internalResourceAuthDaemonSite": "Sur le site",
+ "internalResourceAuthDaemonSiteDescription": "Le démon Auth fonctionne sur le site (Newt).",
+ "internalResourceAuthDaemonRemote": "Hôte distant",
+ "internalResourceAuthDaemonRemoteDescription": "Le démon Auth fonctionne sur un hôte qui n'est pas le site.",
+ "internalResourceAuthDaemonPort": "Port du démon (optionnel)",
"orgAuthWhatsThis": "Où puis-je trouver mon identifiant d'organisation ?",
"learnMore": "En savoir plus",
"backToHome": "Retour à l'accueil",
diff --git a/messages/it-IT.json b/messages/it-IT.json
index 77ff1c2b..c0786a0c 100644
--- a/messages/it-IT.json
+++ b/messages/it-IT.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Ruolo rimosso",
"accessRoleRemovedDescription": "Il ruolo è stato rimosso con successo.",
"accessRoleRequiredRemove": "Prima di eliminare questo ruolo, seleziona un nuovo ruolo a cui trasferire i membri esistenti.",
+ "network": "Rete",
"manage": "Gestisci",
"sitesNotFound": "Nessun sito trovato.",
"pangolinServerAdmin": "Server Admin - Pangolina",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Privato",
"sidebarAccessControl": "Controllo Accesso",
"sidebarLogsAndAnalytics": "Registri E Analisi",
+ "sidebarTeam": "Squadra",
"sidebarUsers": "Utenti",
"sidebarAdmin": "Amministratore",
"sidebarInvitations": "Inviti",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Progetti",
"sidebarOrganization": "Organizzazione",
+ "sidebarManagement": "Gestione",
"sidebarBillingAndLicenses": "Fatturazione E Licenze",
"sidebarLogsAnalytics": "Analisi",
"blueprints": "Progetti",
@@ -1289,7 +1292,6 @@
"parsedContents": "Sommario Analizzato (Solo Lettura)",
"enableDockerSocket": "Abilita Progetto Docker",
"enableDockerSocketDescription": "Abilita la raschiatura dell'etichetta Docker Socket per le etichette dei progetti. Il percorso del socket deve essere fornito a Newt.",
- "enableDockerSocketLink": "Scopri di più",
"viewDockerContainers": "Visualizza Contenitori Docker",
"containersIn": "Contenitori in {siteName}",
"selectContainerDescription": "Seleziona qualsiasi contenitore da usare come hostname per questo obiettivo. Fai clic su una porta per usare una porta.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Il tempo è in secondi",
"requireDeviceApproval": "Richiede Approvazioni Dispositivo",
"requireDeviceApprovalDescription": "Gli utenti con questo ruolo hanno bisogno di nuovi dispositivi approvati da un amministratore prima di poter connettersi e accedere alle risorse.",
+ "sshAccess": "Accesso SSH",
+ "roleAllowSsh": "Consenti SSH",
+ "roleAllowSshAllow": "Consenti",
+ "roleAllowSshDisallow": "Non Consentire",
+ "roleAllowSshDescription": "Consenti agli utenti con questo ruolo di connettersi alle risorse tramite SSH. Quando disabilitato, il ruolo non può utilizzare l'accesso SSH.",
+ "sshSudoMode": "Accesso Sudo",
+ "sshSudoModeNone": "Nessuno",
+ "sshSudoModeNoneDescription": "L'utente non può eseguire comandi con sudo.",
+ "sshSudoModeFull": "Sudo Completo",
+ "sshSudoModeFullDescription": "L'utente può eseguire qualsiasi comando con sudo.",
+ "sshSudoModeCommands": "Comandi",
+ "sshSudoModeCommandsDescription": "L'utente può eseguire solo i comandi specificati con sudo.",
+ "sshSudo": "Consenti sudo",
+ "sshSudoCommands": "Comandi Sudo",
+ "sshSudoCommandsDescription": "Elenco di comandi che l'utente può eseguire con sudo.",
+ "sshCreateHomeDir": "Crea Cartella Home",
+ "sshUnixGroups": "Gruppi Unix",
+ "sshUnixGroupsDescription": "Gruppi Unix su cui aggiungere l'utente sull'host di destinazione.",
"retryAttempts": "Tentativi di Riprova",
"expectedResponseCodes": "Codici di Risposta Attesi",
"expectedResponseCodesDescription": "Codice di stato HTTP che indica lo stato di salute. Se lasciato vuoto, considerato sano è compreso tra 200-300.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Controllo Accesso",
"editInternalResourceDialogAccessControlDescription": "Controlla quali ruoli, utenti e client macchina hanno accesso a questa risorsa quando connessi. Gli amministratori hanno sempre accesso.",
"editInternalResourceDialogPortRangeValidationError": "Il range delle porte deve essere \"*\" per tutte le porte, o un elenco di porte e intervalli separato da virgole (ad es. \"80,443,8000-9000\"). Le porte devono essere tra 1 e 65535.",
+ "internalResourceAuthDaemonStrategy": "Posizione Demone Autenticazione SSH",
+ "internalResourceAuthDaemonStrategyDescription": "Scegli dove funziona il demone di autenticazione SSH: sul sito (Newt) o su un host remoto.",
+ "internalResourceAuthDaemonDescription": "Il demone di autenticazione SSH gestisce la firma della chiave SSH e l'autenticazione PAM per questa risorsa. Scegli se viene eseguito sul sito (Newt) o su un host remoto separato. Vedi la documentazione per ulteriori informazioni.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Seleziona Strategia",
+ "internalResourceAuthDaemonStrategyLabel": "Posizione",
+ "internalResourceAuthDaemonSite": "Sul Sito",
+ "internalResourceAuthDaemonSiteDescription": "Il demone Auth viene eseguito sul sito (Nuovo).",
+ "internalResourceAuthDaemonRemote": "Host Remoto",
+ "internalResourceAuthDaemonRemoteDescription": "Il demone di autenticazione viene eseguito su un host che non è il sito.",
+ "internalResourceAuthDaemonPort": "Porta Demone (facoltativa)",
"orgAuthWhatsThis": "Dove posso trovare l'ID della mia organizzazione?",
"learnMore": "Scopri di più",
"backToHome": "Torna alla home",
diff --git a/messages/ko-KR.json b/messages/ko-KR.json
index 7d53b8b9..fddab00c 100644
--- a/messages/ko-KR.json
+++ b/messages/ko-KR.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "역할이 제거되었습니다",
"accessRoleRemovedDescription": "역할이 성공적으로 제거되었습니다.",
"accessRoleRequiredRemove": "이 역할을 삭제하기 전에 기존 구성원을 전송할 새 역할을 선택하세요.",
+ "network": "네트워크",
"manage": "관리",
"sitesNotFound": "사이트를 찾을 수 없습니다.",
"pangolinServerAdmin": "서버 관리자 - 판골린",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "비공개",
"sidebarAccessControl": "액세스 제어",
"sidebarLogsAndAnalytics": "로그 및 분석",
+ "sidebarTeam": "팀",
"sidebarUsers": "사용자",
"sidebarAdmin": "관리자",
"sidebarInvitations": "초대",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "로그 & 통계",
"sidebarBluePrints": "청사진",
"sidebarOrganization": "조직",
+ "sidebarManagement": "관리",
"sidebarBillingAndLicenses": "결제 및 라이선스",
"sidebarLogsAnalytics": "분석",
"blueprints": "청사진",
@@ -1289,7 +1292,6 @@
"parsedContents": "구문 분석된 콘텐츠 (읽기 전용)",
"enableDockerSocket": "Docker 청사진 활성화",
"enableDockerSocketDescription": "블루프린트 레이블을 위한 Docker 소켓 레이블 수집을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
- "enableDockerSocketLink": "자세히 알아보기",
"viewDockerContainers": "도커 컨테이너 보기",
"containersIn": "{siteName}의 컨테이너",
"selectContainerDescription": "이 대상을 위한 호스트 이름으로 사용할 컨테이너를 선택하세요. 포트를 사용하려면 포트를 클릭하세요.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "시간은 초 단위입니다",
"requireDeviceApproval": "장치 승인 요구",
"requireDeviceApprovalDescription": "이 역할을 가진 사용자는 장치가 연결되기 전에 관리자의 승인이 필요합니다.",
+ "sshAccess": "SSH 접속",
+ "roleAllowSsh": "SSH 허용",
+ "roleAllowSshAllow": "허용",
+ "roleAllowSshDisallow": "허용 안 함",
+ "roleAllowSshDescription": "이 역할을 가진 사용자가 SSH를 통해 리소스에 연결할 수 있도록 허용합니다. 비활성화되면 역할은 SSH 접속을 사용할 수 없습니다.",
+ "sshSudoMode": "Sudo 접속",
+ "sshSudoModeNone": "없음",
+ "sshSudoModeNoneDescription": "사용자는 sudo로 명령을 실행할 수 없습니다.",
+ "sshSudoModeFull": "전체 Sudo",
+ "sshSudoModeFullDescription": "사용자는 모든 명령을 sudo로 실행할 수 있습니다.",
+ "sshSudoModeCommands": "명령",
+ "sshSudoModeCommandsDescription": "사용자는 sudo로 지정된 명령만 실행할 수 있습니다.",
+ "sshSudo": "Sudo 허용",
+ "sshSudoCommands": "Sudo 명령",
+ "sshSudoCommandsDescription": "사용자가 sudo로 실행할 수 있도록 허용된 명령 목록입니다.",
+ "sshCreateHomeDir": "홈 디렉터리 생성",
+ "sshUnixGroups": "유닉스 그룹",
+ "sshUnixGroupsDescription": "대상 호스트에서 사용자를 추가할 유닉스 그룹입니다.",
"retryAttempts": "재시도 횟수",
"expectedResponseCodes": "예상 응답 코드",
"expectedResponseCodesDescription": "정상 상태를 나타내는 HTTP 상태 코드입니다. 비워 두면 200-300이 정상으로 간주됩니다.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "액세스 제어",
"editInternalResourceDialogAccessControlDescription": "연결 시 이 리소스에 대한 액세스 권한을 가지는 역할, 사용자, 그리고 머신 클라이언트를 제어합니다. 관리자는 항상 접근할 수 있습니다.",
"editInternalResourceDialogPortRangeValidationError": "모든 포트에 대해서는 \"*\"로, 아니면 쉼표로 구분된 포트 및 범위 목록(예: \"80,443,8000-9000\")을 설정해야 합니다. 포트는 1에서 65535 사이여야 합니다.",
+ "internalResourceAuthDaemonStrategy": "SSH 인증 데몬 위치",
+ "internalResourceAuthDaemonStrategyDescription": "SSH 인증 데몬이 작동하는 위치를 선택하세요: 사이트(Newt)에서 또는 원격 호스트에서.",
+ "internalResourceAuthDaemonDescription": "SSH 인증 데몬은 이 리소스를 위한 SSH 키 서명과 PAM 인증을 처리합니다. 사이트(Newt)에서 나 별도의 원격 호스트에서 실행할 것인지를 선택하세요. 자세한 내용은 문서를 참조하세요.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "전략 선택",
+ "internalResourceAuthDaemonStrategyLabel": "위치",
+ "internalResourceAuthDaemonSite": "사이트에서 인증 데몬이 실행됩니다(Newt).",
+ "internalResourceAuthDaemonSiteDescription": "인증 데몬이 사이트(Newt)에서 실행됩니다.",
+ "internalResourceAuthDaemonRemote": "원격 호스트",
+ "internalResourceAuthDaemonRemoteDescription": "인증 데몬이 사이트가 아닌 다른 호스트에서 실행됩니다.",
+ "internalResourceAuthDaemonPort": "데몬 포트 (선택 사항)",
"orgAuthWhatsThis": "조직 ID를 어디에서 찾을 수 있습니까?",
"learnMore": "자세히 알아보기",
"backToHome": "홈으로 돌아가기",
diff --git a/messages/nb-NO.json b/messages/nb-NO.json
index f86c556a..24e34e15 100644
--- a/messages/nb-NO.json
+++ b/messages/nb-NO.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Rolle fjernet",
"accessRoleRemovedDescription": "Rollen er vellykket fjernet.",
"accessRoleRequiredRemove": "Før du sletter denne rollen, vennligst velg en ny rolle å overføre eksisterende medlemmer til.",
+ "network": "Nettverk",
"manage": "Administrer",
"sitesNotFound": "Ingen områder funnet.",
"pangolinServerAdmin": "Server Admin - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Privat",
"sidebarAccessControl": "Tilgangskontroll",
"sidebarLogsAndAnalytics": "Logger og analyser",
+ "sidebarTeam": "Lag",
"sidebarUsers": "Brukere",
"sidebarAdmin": "Administrator",
"sidebarInvitations": "Invitasjoner",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Logg og analyser",
"sidebarBluePrints": "Tegninger",
"sidebarOrganization": "Organisasjon",
+ "sidebarManagement": "Administrasjon",
"sidebarBillingAndLicenses": "Fakturering & lisenser",
"sidebarLogsAnalytics": "Analyser",
"blueprints": "Tegninger",
@@ -1289,7 +1292,6 @@
"parsedContents": "Parastinnhold (kun lese)",
"enableDockerSocket": "Aktiver Docker blåkopi",
"enableDockerSocketDescription": "Aktiver skraping av Docker Socket for blueprint Etiketter. Socket bane må brukes for nye.",
- "enableDockerSocketLink": "Lær mer",
"viewDockerContainers": "Vis Docker-containere",
"containersIn": "Containere i {siteName}",
"selectContainerDescription": "Velg en hvilken som helst container for å bruke som vertsnavn for dette målet. Klikk på en port for å bruke en port.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Tid er i sekunder",
"requireDeviceApproval": "Krev enhetsgodkjenning",
"requireDeviceApprovalDescription": "Brukere med denne rollen trenger nye enheter godkjent av en admin før de kan koble seg og få tilgang til ressurser.",
+ "sshAccess": "SSH tilgang",
+ "roleAllowSsh": "Tillat SSH",
+ "roleAllowSshAllow": "Tillat",
+ "roleAllowSshDisallow": "Forby",
+ "roleAllowSshDescription": "Tillat brukere med denne rollen å koble til ressurser via SSH. Når deaktivert får rollen ikke tilgang til SSH.",
+ "sshSudoMode": "Sudo tilgang",
+ "sshSudoModeNone": "Ingen",
+ "sshSudoModeNoneDescription": "Brukeren kan ikke kjøre kommandoer med sudo.",
+ "sshSudoModeFull": "Full Sudo",
+ "sshSudoModeFullDescription": "Brukeren kan kjøre hvilken som helst kommando med sudo.",
+ "sshSudoModeCommands": "Kommandoer",
+ "sshSudoModeCommandsDescription": "Brukeren kan bare kjøre de angitte kommandoene med sudo.",
+ "sshSudo": "Tillat sudo",
+ "sshSudoCommands": "Sudo kommandoer",
+ "sshSudoCommandsDescription": "Liste av kommandoer brukeren har lov til å kjøre med sudo.",
+ "sshCreateHomeDir": "Opprett hjemmappe",
+ "sshUnixGroups": "Unix grupper",
+ "sshUnixGroupsDescription": "Unix grupper for å legge til brukeren til målverten.",
"retryAttempts": "Forsøk på nytt",
"expectedResponseCodes": "Forventede svarkoder",
"expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Tilgangskontroll",
"editInternalResourceDialogAccessControlDescription": "Kontroller hvilke roller, brukere og maskinklienter som har tilgang til denne ressursen når den er koblet til. Administratorer har alltid tilgang.",
"editInternalResourceDialogPortRangeValidationError": "Portsjiktet må være \"*\" for alle porter, eller en kommaseparert liste med porter og sjikt (f.eks. \"80,443,8000-9000\"). Porter må være mellom 1 og 65535.",
+ "internalResourceAuthDaemonStrategy": "SSH Auth Daemon Sted",
+ "internalResourceAuthDaemonStrategyDescription": "Velg hvor SSH-autentisering daemon kjører: på nettstedet (Newt) eller på en ekstern vert.",
+ "internalResourceAuthDaemonDescription": "SSH-godkjenning daemon håndterer SSH-nøkkel signering og PAM autentisering for denne ressursen. Velg om den kjører på nettstedet (Newt) eller på en separat ekstern vert. Se dokumentasjonen for mer.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Velg strategi",
+ "internalResourceAuthDaemonStrategyLabel": "Sted",
+ "internalResourceAuthDaemonSite": "På nettsted",
+ "internalResourceAuthDaemonSiteDescription": "Autentiser daemon kjører på nettstedet (Newt).",
+ "internalResourceAuthDaemonRemote": "Ekstern vert",
+ "internalResourceAuthDaemonRemoteDescription": "Autentiser daemon kjører på en vert som ikke er nettstedet.",
+ "internalResourceAuthDaemonPort": "Daemon Port (valgfritt)",
"orgAuthWhatsThis": "Hvor kan jeg finne min organisasjons-ID?",
"learnMore": "Lær mer",
"backToHome": "Gå tilbake til start",
diff --git a/messages/nl-NL.json b/messages/nl-NL.json
index 4c23dc85..51fac33d 100644
--- a/messages/nl-NL.json
+++ b/messages/nl-NL.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Rol verwijderd",
"accessRoleRemovedDescription": "De rol is succesvol verwijderd.",
"accessRoleRequiredRemove": "Voordat u deze rol verwijdert, selecteer een nieuwe rol om bestaande leden aan te dragen.",
+ "network": "Netwerk",
"manage": "Beheren",
"sitesNotFound": "Geen sites gevonden.",
"pangolinServerAdmin": "Serverbeheer - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Privé",
"sidebarAccessControl": "Toegangs controle",
"sidebarLogsAndAnalytics": "Logs & Analytics",
+ "sidebarTeam": "Team",
"sidebarUsers": "Gebruikers",
"sidebarAdmin": "Beheerder",
"sidebarInvitations": "Uitnodigingen",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Blauwdrukken",
"sidebarOrganization": "Organisatie",
+ "sidebarManagement": "Beheer",
"sidebarBillingAndLicenses": "Facturatie & Licenties",
"sidebarLogsAnalytics": "Analyses",
"blueprints": "Blauwdrukken",
@@ -1289,7 +1292,6 @@
"parsedContents": "Geparseerde inhoud (alleen lezen)",
"enableDockerSocket": "Schakel Docker Blauwdruk in",
"enableDockerSocketDescription": "Schakel Docker Socket label in voor blauwdruk labels. Pad naar Nieuw.",
- "enableDockerSocketLink": "Meer informatie",
"viewDockerContainers": "Bekijk Docker containers",
"containersIn": "Containers in {siteName}",
"selectContainerDescription": "Selecteer een container om als hostnaam voor dit doel te gebruiken. Klik op een poort om een poort te gebruiken.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Tijd is in seconden",
"requireDeviceApproval": "Vereist goedkeuring van apparaat",
"requireDeviceApprovalDescription": "Gebruikers met deze rol hebben nieuwe apparaten nodig die door een beheerder zijn goedgekeurd voordat ze verbinding kunnen maken met bronnen en deze kunnen gebruiken.",
+ "sshAccess": "SSH toegang",
+ "roleAllowSsh": "SSH toestaan",
+ "roleAllowSshAllow": "Toestaan",
+ "roleAllowSshDisallow": "Weigeren",
+ "roleAllowSshDescription": "Sta gebruikers met deze rol toe om verbinding te maken met bronnen via SSH. Indien uitgeschakeld kan de rol geen gebruik maken van SSH toegang.",
+ "sshSudoMode": "Sudo toegang",
+ "sshSudoModeNone": "geen",
+ "sshSudoModeNoneDescription": "Gebruiker kan geen commando's uitvoeren met sudo.",
+ "sshSudoModeFull": "Volledige Sudo",
+ "sshSudoModeFullDescription": "Gebruiker kan elk commando uitvoeren met een sudo.",
+ "sshSudoModeCommands": "Opdrachten",
+ "sshSudoModeCommandsDescription": "Gebruiker kan alleen de opgegeven commando's uitvoeren met de sudo.",
+ "sshSudo": "sudo toestaan",
+ "sshSudoCommands": "Sudo Commando's",
+ "sshSudoCommandsDescription": "Lijst van commando's die de gebruiker mag uitvoeren met een sudo.",
+ "sshCreateHomeDir": "Maak Home Directory",
+ "sshUnixGroups": "Unix groepen",
+ "sshUnixGroupsDescription": "Unix groepen om de gebruiker toe te voegen aan de doel host.",
"retryAttempts": "Herhaal Pogingen",
"expectedResponseCodes": "Verwachte Reactiecodes",
"expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Toegangs controle",
"editInternalResourceDialogAccessControlDescription": "Beheer welke rollen, gebruikers en machineclients toegang hebben tot deze bron wanneer ze zijn verbonden. Beheerders hebben altijd toegang.",
"editInternalResourceDialogPortRangeValidationError": "Poortbereik moet \"*\" zijn voor alle poorten, of een komma-gescheiden lijst van poorten en bereiken (bijv. \"80,443,8000-9000\"). Poorten moeten tussen 1 en 65535 zijn.",
+ "internalResourceAuthDaemonStrategy": "SSH Auth Daemon locatie",
+ "internalResourceAuthDaemonStrategyDescription": "Kies waar de SSH authenticatie daemon wordt uitgevoerd: op de website (Newt) of op een externe host.",
+ "internalResourceAuthDaemonDescription": "De SSH authenticatie daemon zorgt voor SSH sleutelondertekening en PAM authenticatie voor deze resource. Kies of het wordt uitgevoerd op de website (Nieuw) of op een afzonderlijke externe host. Zie de documentatie voor meer.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Selecteer strategie",
+ "internalResourceAuthDaemonStrategyLabel": "Locatie",
+ "internalResourceAuthDaemonSite": "In de site",
+ "internalResourceAuthDaemonSiteDescription": "Auth daemon draait op de site (Newt).",
+ "internalResourceAuthDaemonRemote": "Externe host",
+ "internalResourceAuthDaemonRemoteDescription": "Authenticatiedaemon draait op een host die niet de site is.",
+ "internalResourceAuthDaemonPort": "Daemon poort (optioneel)",
"orgAuthWhatsThis": "Waar kan ik mijn organisatie-ID vinden?",
"learnMore": "Meer informatie",
"backToHome": "Ga terug naar startpagina",
diff --git a/messages/pl-PL.json b/messages/pl-PL.json
index 84052ce9..e1f6256c 100644
--- a/messages/pl-PL.json
+++ b/messages/pl-PL.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Rola usunięta",
"accessRoleRemovedDescription": "Rola została pomyślnie usunięta.",
"accessRoleRequiredRemove": "Przed usunięciem tej roli, wybierz nową rolę do której zostaną przeniesieni obecni członkowie.",
+ "network": "Sieć",
"manage": "Zarządzaj",
"sitesNotFound": "Nie znaleziono witryn.",
"pangolinServerAdmin": "Administrator serwera - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Prywatny",
"sidebarAccessControl": "Kontrola dostępu",
"sidebarLogsAndAnalytics": "Logi i Analityki",
+ "sidebarTeam": "Drużyna",
"sidebarUsers": "Użytkownicy",
"sidebarAdmin": "Administrator",
"sidebarInvitations": "Zaproszenia",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Dziennik & Analityka",
"sidebarBluePrints": "Schematy",
"sidebarOrganization": "Organizacja",
+ "sidebarManagement": "Zarządzanie",
"sidebarBillingAndLicenses": "Płatność i licencje",
"sidebarLogsAnalytics": "Analityka",
"blueprints": "Schematy",
@@ -1289,7 +1292,6 @@
"parsedContents": "Przetworzona zawartość (tylko do odczytu)",
"enableDockerSocket": "Włącz schemat dokera",
"enableDockerSocketDescription": "Włącz etykietowanie kieszeni dokującej dla etykiet schematów. Ścieżka do gniazda musi być dostarczona do Newt.",
- "enableDockerSocketLink": "Dowiedz się więcej",
"viewDockerContainers": "Zobacz kontenery dokujące",
"containersIn": "Pojemniki w {siteName}",
"selectContainerDescription": "Wybierz dowolny kontener do użycia jako nazwa hosta dla tego celu. Kliknij port, aby użyć portu.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Czas w sekundach",
"requireDeviceApproval": "Wymagaj zatwierdzenia urządzenia",
"requireDeviceApprovalDescription": "Użytkownicy o tej roli potrzebują nowych urządzeń zatwierdzonych przez administratora, zanim będą mogli połączyć się i uzyskać dostęp do zasobów.",
+ "sshAccess": "Dostęp SSH",
+ "roleAllowSsh": "Zezwalaj na SSH",
+ "roleAllowSshAllow": "Zezwól",
+ "roleAllowSshDisallow": "Nie zezwalaj",
+ "roleAllowSshDescription": "Zezwalaj użytkownikom z tej roli na łączenie się z zasobami za pomocą SSH. Gdy wyłączone, rola nie może korzystać z dostępu SSH.",
+ "sshSudoMode": "Dostęp Sudo",
+ "sshSudoModeNone": "Brak",
+ "sshSudoModeNoneDescription": "Użytkownik nie może uruchamiać poleceń z sudo.",
+ "sshSudoModeFull": "Pełne Sudo",
+ "sshSudoModeFullDescription": "Użytkownik może uruchomić dowolne polecenie z sudo.",
+ "sshSudoModeCommands": "Polecenia",
+ "sshSudoModeCommandsDescription": "Użytkownik może uruchamiać tylko określone polecenia z sudo.",
+ "sshSudo": "Zezwól na sudo",
+ "sshSudoCommands": "Komendy Sudo",
+ "sshSudoCommandsDescription": "Lista poleceń, które użytkownik może uruchamiać z sudo.",
+ "sshCreateHomeDir": "Utwórz katalog domowy",
+ "sshUnixGroups": "Grupy Unix",
+ "sshUnixGroupsDescription": "Grupy Unix do dodania użytkownika do docelowego hosta.",
"retryAttempts": "Próby Ponowienia",
"expectedResponseCodes": "Oczekiwane Kody Odpowiedzi",
"expectedResponseCodesDescription": "Kod statusu HTTP, który wskazuje zdrowy status. Jeśli pozostanie pusty, uznaje się 200-300 za zdrowy.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Kontrola dostępu",
"editInternalResourceDialogAccessControlDescription": "Kontroluj, które role, użytkownicy i klienci maszyn mają dostęp do tego zasobu po połączeniu. Administratorzy zawsze mają dostęp.",
"editInternalResourceDialogPortRangeValidationError": "Zakres portów musi być \"*\" dla wszystkich portów lub listą portów i zakresów oddzielonych przecinkami (np. \"80,443,8000-9000\"). Porty muszą znajdować się w przedziale od 1 do 65535.",
+ "internalResourceAuthDaemonStrategy": "SSH Auth Daemon Lokalizacja",
+ "internalResourceAuthDaemonStrategyDescription": "Wybierz, gdzie działa demon uwierzytelniania SSH: na stronie (Newt) lub na zdalnym serwerze.",
+ "internalResourceAuthDaemonDescription": "Uwierzytelnianie SSH obsługuje podpisywanie klucza SSH i uwierzytelnianie PAM dla tego zasobu. Wybierz, czy działa na stronie (Newt), czy na oddzielnym serwerze zdalnym. Zobacz dokumentację dla więcej.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Wybierz strategię",
+ "internalResourceAuthDaemonStrategyLabel": "Lokalizacja",
+ "internalResourceAuthDaemonSite": "Na stronie",
+ "internalResourceAuthDaemonSiteDescription": "Demon Auth działa na stronie (nowy).",
+ "internalResourceAuthDaemonRemote": "Zdalny host",
+ "internalResourceAuthDaemonRemoteDescription": "Demon Auth działa na serwerze, który nie jest stroną.",
+ "internalResourceAuthDaemonPort": "Port Daemon (opcjonalnie)",
"orgAuthWhatsThis": "Gdzie mogę znaleźć swój identyfikator organizacji?",
"learnMore": "Dowiedz się więcej",
"backToHome": "Wróć do strony głównej",
diff --git a/messages/pt-PT.json b/messages/pt-PT.json
index 4cfac3c2..89f433cc 100644
--- a/messages/pt-PT.json
+++ b/messages/pt-PT.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Função removida",
"accessRoleRemovedDescription": "A função foi removida com sucesso.",
"accessRoleRequiredRemove": "Antes de apagar esta função, selecione uma nova função para transferir os membros existentes.",
+ "network": "Rede",
"manage": "Gerir",
"sitesNotFound": "Nenhum site encontrado.",
"pangolinServerAdmin": "Administrador do Servidor - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Privado",
"sidebarAccessControl": "Controle de Acesso",
"sidebarLogsAndAnalytics": "Registros e Análises",
+ "sidebarTeam": "Equipe",
"sidebarUsers": "Utilizadores",
"sidebarAdmin": "Administrador",
"sidebarInvitations": "Convites",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Registo & Análise",
"sidebarBluePrints": "Diagramas",
"sidebarOrganization": "Organização",
+ "sidebarManagement": "Gestão",
"sidebarBillingAndLicenses": "Faturamento e Licenças",
"sidebarLogsAnalytics": "Análises",
"blueprints": "Diagramas",
@@ -1289,7 +1292,6 @@
"parsedContents": "Conteúdo analisado (Somente Leitura)",
"enableDockerSocket": "Habilitar o Diagrama Docker",
"enableDockerSocketDescription": "Ativar a scraping de rótulo Docker para rótulos de diagramas. Caminho de Socket deve ser fornecido para Newt.",
- "enableDockerSocketLink": "Saiba mais",
"viewDockerContainers": "Ver contêineres Docker",
"containersIn": "Contêineres em {siteName}",
"selectContainerDescription": "Selecione qualquer contêiner para usar como hostname para este alvo. Clique em uma porta para usar uma porta.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "O tempo está em segundos",
"requireDeviceApproval": "Exigir aprovação do dispositivo",
"requireDeviceApprovalDescription": "Usuários com esta função precisam de novos dispositivos aprovados por um administrador antes que eles possam se conectar e acessar recursos.",
+ "sshAccess": "Acesso SSH",
+ "roleAllowSsh": "Permitir SSH",
+ "roleAllowSshAllow": "Autorizar",
+ "roleAllowSshDisallow": "Anular",
+ "roleAllowSshDescription": "Permitir que usuários com esta função se conectem a recursos via SSH. Quando desativado, a função não pode usar o acesso SSH.",
+ "sshSudoMode": "Acesso Sudo",
+ "sshSudoModeNone": "Nenhuma",
+ "sshSudoModeNoneDescription": "O usuário não pode executar comandos com o sudo.",
+ "sshSudoModeFull": "Sudo Completo",
+ "sshSudoModeFullDescription": "O usuário pode executar qualquer comando com sudo.",
+ "sshSudoModeCommands": "Comandos",
+ "sshSudoModeCommandsDescription": "Usuário só pode executar os comandos especificados com sudo.",
+ "sshSudo": "Permitir sudo",
+ "sshSudoCommands": "Comandos Sudo",
+ "sshSudoCommandsDescription": "Lista de comandos com permissão de executar com o sudo.",
+ "sshCreateHomeDir": "Criar Diretório Inicial",
+ "sshUnixGroups": "Grupos Unix",
+ "sshUnixGroupsDescription": "Grupos Unix para adicionar o usuário no host de destino.",
"retryAttempts": "Tentativas de Repetição",
"expectedResponseCodes": "Códigos de Resposta Esperados",
"expectedResponseCodesDescription": "Código de status HTTP que indica estado saudável. Se deixado em branco, 200-300 é considerado saudável.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Controle de Acesso",
"editInternalResourceDialogAccessControlDescription": "Controle quais funções, usuários e clientes de máquina podem acessar este recurso quando conectados. Os administradores sempre têm acesso.",
"editInternalResourceDialogPortRangeValidationError": "O intervalo de portas deve ser \"*\" para todas as portas, ou uma lista de portas e intervalos separados por vírgulas (por exemplo, \"80,443,8000-9000\"). As portas devem estar entre 1 e 65535.",
+ "internalResourceAuthDaemonStrategy": "Local do Daemon de autenticação SSH",
+ "internalResourceAuthDaemonStrategyDescription": "Escolha onde o daemon de autenticação SSH funciona: no site (Newt) ou em um host remoto.",
+ "internalResourceAuthDaemonDescription": "A autenticação SSH daemon lida com assinatura de chave SSH e autenticação PAM para este recurso. Escolha se ele é executado no site (Newt) ou em um host remoto separado. Veja a documentação para mais informações.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Selecione a estratégia",
+ "internalResourceAuthDaemonStrategyLabel": "Local:",
+ "internalResourceAuthDaemonSite": "No Site",
+ "internalResourceAuthDaemonSiteDescription": "O serviço de autenticação é executado no site (Newt).",
+ "internalResourceAuthDaemonRemote": "Host Remoto",
+ "internalResourceAuthDaemonRemoteDescription": "O serviço de autenticação é executado em um host que não é o site.",
+ "internalResourceAuthDaemonPort": "Porta do Daemon (opcional)",
"orgAuthWhatsThis": "Onde posso encontrar meu ID da organização?",
"learnMore": "Saiba mais",
"backToHome": "Voltar para a página inicial",
diff --git a/messages/ru-RU.json b/messages/ru-RU.json
index 1ecf87d8..7aa253db 100644
--- a/messages/ru-RU.json
+++ b/messages/ru-RU.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Роль удалена",
"accessRoleRemovedDescription": "Роль была успешно удалена.",
"accessRoleRequiredRemove": "Перед удалением этой роли выберите новую роль для переноса существующих участников.",
+ "network": "Сеть",
"manage": "Управление",
"sitesNotFound": "Сайты не найдены.",
"pangolinServerAdmin": "Администратор сервера - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Приватный",
"sidebarAccessControl": "Контроль доступа",
"sidebarLogsAndAnalytics": "Журналы и аналитика",
+ "sidebarTeam": "Команда",
"sidebarUsers": "Пользователи",
"sidebarAdmin": "Админ",
"sidebarInvitations": "Приглашения",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Журнал и аналитика",
"sidebarBluePrints": "Чертежи",
"sidebarOrganization": "Организация",
+ "sidebarManagement": "Управление",
"sidebarBillingAndLicenses": "Биллинг и лицензии",
"sidebarLogsAnalytics": "Статистика",
"blueprints": "Чертежи",
@@ -1289,7 +1292,6 @@
"parsedContents": "Переработанное содержимое (только для чтения)",
"enableDockerSocket": "Включить чертёж Docker",
"enableDockerSocketDescription": "Включить scraping ярлыка Docker Socket для ярлыков чертежей. Путь к сокету должен быть предоставлен в Newt.",
- "enableDockerSocketLink": "Узнать больше",
"viewDockerContainers": "Просмотр контейнеров Docker",
"containersIn": "Контейнеры в {siteName}",
"selectContainerDescription": "Выберите любой контейнер для использования в качестве имени хоста для этой цели. Нажмите на порт, чтобы использовать порт.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Время указано в секундах",
"requireDeviceApproval": "Требовать подтверждения устройства",
"requireDeviceApprovalDescription": "Пользователям с этой ролью нужны новые устройства, одобренные администратором, прежде чем они смогут подключаться и получать доступ к ресурсам.",
+ "sshAccess": "SSH доступ",
+ "roleAllowSsh": "Разрешить SSH",
+ "roleAllowSshAllow": "Разрешить",
+ "roleAllowSshDisallow": "Запретить",
+ "roleAllowSshDescription": "Разрешить пользователям с этой ролью подключаться к ресурсам через SSH. Если отключено, роль не может использовать доступ SSH.",
+ "sshSudoMode": "Sudo доступ",
+ "sshSudoModeNone": "Нет",
+ "sshSudoModeNoneDescription": "Пользователь не может запускать команды с sudo.",
+ "sshSudoModeFull": "Полная судо",
+ "sshSudoModeFullDescription": "Пользователь может запускать любую команду с помощью sudo.",
+ "sshSudoModeCommands": "Команды",
+ "sshSudoModeCommandsDescription": "Пользователь может запускать только указанные команды с помощью sudo.",
+ "sshSudo": "Разрешить sudo",
+ "sshSudoCommands": "Sudo Команды",
+ "sshSudoCommandsDescription": "Список команд, которые пользователю разрешено запускать с помощью sudo.",
+ "sshCreateHomeDir": "Создать домашний каталог",
+ "sshUnixGroups": "Unix группы",
+ "sshUnixGroupsDescription": "Unix группы для добавления пользователя на целевой хост.",
"retryAttempts": "Количество попыток повторного запроса",
"expectedResponseCodes": "Ожидаемые коды ответов",
"expectedResponseCodesDescription": "HTTP-код состояния, указывающий на здоровое состояние. Если оставить пустым, 200-300 считается здоровым.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Контроль доступа",
"editInternalResourceDialogAccessControlDescription": "Контролируйте, какие роли, пользователи и машинные клиенты имеют доступ к этому ресурсу при подключении. Администраторы всегда имеют доступ.",
"editInternalResourceDialogPortRangeValidationError": "Диапазон портов должен быть \"*\" для всех портов или списком портов и диапазонов через запятую (например, \"80,443,8000-9000\"). Порты должны находиться в диапазоне от 1 до 65535.",
+ "internalResourceAuthDaemonStrategy": "Местоположение демона по SSH",
+ "internalResourceAuthDaemonStrategyDescription": "Выберите, где работает демон аутентификации SSH: на сайте (Newt) или на удаленном узле.",
+ "internalResourceAuthDaemonDescription": "Демон аутентификации SSH обрабатывает подписание ключей SSH и аутентификацию PAM для этого ресурса. Выберите, запускать ли его на сайте (Newt) или на отдельном удаленном хосте. Подробности смотрите в документации.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Выберите стратегию",
+ "internalResourceAuthDaemonStrategyLabel": "Местоположение",
+ "internalResourceAuthDaemonSite": "На сайте",
+ "internalResourceAuthDaemonSiteDescription": "На сайте работает демон Auth (Newt).",
+ "internalResourceAuthDaemonRemote": "Удаленный хост",
+ "internalResourceAuthDaemonRemoteDescription": "Демон Auth запускается на хост, который не является сайтом.",
+ "internalResourceAuthDaemonPort": "Порт демона (опционально)",
"orgAuthWhatsThis": "Где я могу найти ID моей организации?",
"learnMore": "Узнать больше",
"backToHome": "Вернуться домой",
diff --git a/messages/tr-TR.json b/messages/tr-TR.json
index 7fb13369..4d3b7027 100644
--- a/messages/tr-TR.json
+++ b/messages/tr-TR.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "Rol kaldırıldı",
"accessRoleRemovedDescription": "Rol başarıyla kaldırıldı.",
"accessRoleRequiredRemove": "Bu rolü silmeden önce, mevcut üyeleri aktarmak için yeni bir rol seçin.",
+ "network": "Ağ",
"manage": "Yönet",
"sitesNotFound": "Site bulunamadı.",
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "Özel",
"sidebarAccessControl": "Erişim Kontrolü",
"sidebarLogsAndAnalytics": "Kayıtlar & Analitik",
+ "sidebarTeam": "Ekip",
"sidebarUsers": "Kullanıcılar",
"sidebarAdmin": "Yönetici",
"sidebarInvitations": "Davetiye",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "Kayıt & Analiz",
"sidebarBluePrints": "Planlar",
"sidebarOrganization": "Organizasyon",
+ "sidebarManagement": "Yönetim",
"sidebarBillingAndLicenses": "Faturalandırma & Lisanslar",
"sidebarLogsAnalytics": "Analitik",
"blueprints": "Planlar",
@@ -1289,7 +1292,6 @@
"parsedContents": "Verilerin Ayrıştırılmış İçeriği (Salt Okunur)",
"enableDockerSocket": "Docker Soketini Etkinleştir",
"enableDockerSocketDescription": "Plan etiketleri için Docker Socket etiket toplamasını etkinleştirin. Newt'e soket yolu sağlanmalıdır.",
- "enableDockerSocketLink": "Daha fazla bilgi",
"viewDockerContainers": "Docker Konteynerlerini Görüntüle",
"containersIn": "{siteName} içindeki konteynerler",
"selectContainerDescription": "Bu hedef için bir ana bilgisayar adı olarak kullanmak üzere herhangi bir konteyner seçin. Bir bağlantı noktası kullanmak için bir bağlantı noktasına tıklayın.",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "Zaman saniye cinsindendir",
"requireDeviceApproval": "Cihaz Onaylarını Gerektir",
"requireDeviceApprovalDescription": "Bu role sahip kullanıcıların yeni cihazlarının bağlanabilmesi ve kaynaklara erişebilmesi için bir yönetici tarafından onaylanması gerekiyor.",
+ "sshAccess": "SSH Erişimi",
+ "roleAllowSsh": "SSH'a İzin Ver",
+ "roleAllowSshAllow": "İzin Ver",
+ "roleAllowSshDisallow": "İzin Verme",
+ "roleAllowSshDescription": "Bu role sahip kullanıcıların SSH aracılığıyla kaynaklara bağlanmasına izin verin. Devre dışı bırakıldığında, rol SSH erişimini kullanamaz.",
+ "sshSudoMode": "Sudo Erişimi",
+ "sshSudoModeNone": "Hiçbiri",
+ "sshSudoModeNoneDescription": "Kullanıcı, sudo komutunu kullanarak komut çalıştıramaz.",
+ "sshSudoModeFull": "Tam Sudo",
+ "sshSudoModeFullDescription": "Kullanıcı, sudo komutuyla her türlü komutu çalıştırabilir.",
+ "sshSudoModeCommands": "Komutlar",
+ "sshSudoModeCommandsDescription": "Kullanıcı sadece belirtilen komutları sudo ile çalıştırabilir.",
+ "sshSudo": "Sudo'ya izin ver",
+ "sshSudoCommands": "Sudo Komutları",
+ "sshSudoCommandsDescription": "Kullanıcının sudo ile çalıştırmasına izin verilen komutların listesi.",
+ "sshCreateHomeDir": "Ev Dizini Oluştur",
+ "sshUnixGroups": "Unix Grupları",
+ "sshUnixGroupsDescription": "Hedef ana bilgisayarda kullanıcıya eklemek için Unix grupları.",
"retryAttempts": "Tekrar Deneme Girişimleri",
"expectedResponseCodes": "Beklenen Yanıt Kodları",
"expectedResponseCodesDescription": "Sağlıklı durumu gösteren HTTP durum kodu. Boş bırakılırsa, 200-300 arası sağlıklı kabul edilir.",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "Erişim Kontrolü",
"editInternalResourceDialogAccessControlDescription": "Bağlandığında bu kaynağa erişimi olan roller, kullanıcılar ve makine müşterilerini kontrol edin. Yöneticiler her zaman erişime sahiptir.",
"editInternalResourceDialogPortRangeValidationError": "Port aralığı, tüm portlar için \"*\" veya virgülle ayrılmış bir port ve aralık listesi olmalıdır (ör. \"80,443,8000-9000\"). Portlar 1 ile 65535 arasında olmalıdır.",
+ "internalResourceAuthDaemonStrategy": "SSH Kimlik Doğrulama Daemon Yeri",
+ "internalResourceAuthDaemonStrategyDescription": "SSH kimlik doğrulama sunucusunun nerede çalışacağını seçin: sitede (Newt) veya uzak bir ana bilgisayarda.",
+ "internalResourceAuthDaemonDescription": "SSH kimlik doğrulama sunucusu, bu kaynak için SSH anahtar imzalama ve PAM kimlik doğrulamasını yapar. Sitede (Newt) veya ayrı bir uzak ana bilgisayarda çalışıp çalışmayacağını seçin. Daha fazla bilgi için belgeleri görün.",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "Strateji Seçin",
+ "internalResourceAuthDaemonStrategyLabel": "Konum",
+ "internalResourceAuthDaemonSite": "Sitede",
+ "internalResourceAuthDaemonSiteDescription": "Kimlik doğrulama sunucusu sitede (Newt) çalışır.",
+ "internalResourceAuthDaemonRemote": "Uzak Ana Bilgisayar",
+ "internalResourceAuthDaemonRemoteDescription": "Kimlik doğrulama sunucusu, site olmayan bir ana bilgisayarda çalışır.",
+ "internalResourceAuthDaemonPort": "Daemon Portu (isteğe bağlı)",
"orgAuthWhatsThis": "Kuruluş kimliğimi nerede bulabilirim?",
"learnMore": "Daha fazla bilgi",
"backToHome": "Ana sayfaya geri dön",
diff --git a/messages/zh-CN.json b/messages/zh-CN.json
index 1542bfcd..6956af62 100644
--- a/messages/zh-CN.json
+++ b/messages/zh-CN.json
@@ -790,6 +790,7 @@
"accessRoleRemoved": "角色已删除",
"accessRoleRemovedDescription": "角色已成功删除。",
"accessRoleRequiredRemove": "删除此角色之前,请选择一个新角色来转移现有成员。",
+ "network": "网络",
"manage": "管理",
"sitesNotFound": "未找到站点。",
"pangolinServerAdmin": "服务器管理员 - Pangolin",
@@ -1249,6 +1250,7 @@
"sidebarClientResources": "非公开的",
"sidebarAccessControl": "访问控制",
"sidebarLogsAndAnalytics": "日志与分析",
+ "sidebarTeam": "团队",
"sidebarUsers": "用户",
"sidebarAdmin": "管理员",
"sidebarInvitations": "邀请",
@@ -1267,6 +1269,7 @@
"sidebarLogAndAnalytics": "日志与分析",
"sidebarBluePrints": "蓝图",
"sidebarOrganization": "组织",
+ "sidebarManagement": "管理",
"sidebarBillingAndLicenses": "帐单和许可证",
"sidebarLogsAnalytics": "分析",
"blueprints": "蓝图",
@@ -1289,7 +1292,6 @@
"parsedContents": "解析内容 (只读)",
"enableDockerSocket": "启用 Docker 蓝图",
"enableDockerSocketDescription": "启用 Docker Socket 标签擦除蓝图标签。套接字路径必须提供给新的。",
- "enableDockerSocketLink": "了解更多",
"viewDockerContainers": "查看停靠容器",
"containersIn": "{siteName} 中的容器",
"selectContainerDescription": "选择任何容器作为目标的主机名。点击端口使用端口。",
@@ -1643,6 +1645,24 @@
"timeIsInSeconds": "时间以秒为单位",
"requireDeviceApproval": "需要设备批准",
"requireDeviceApprovalDescription": "具有此角色的用户需要管理员批准的新设备才能连接和访问资源。",
+ "sshAccess": "SSH 访问",
+ "roleAllowSsh": "允许 SSH",
+ "roleAllowSshAllow": "允许",
+ "roleAllowSshDisallow": "不允许",
+ "roleAllowSshDescription": "允许具有此角色的用户通过 SSH 连接到资源。禁用时,角色不能使用 SSH 访问。",
+ "sshSudoMode": "Sudo 访问",
+ "sshSudoModeNone": "无",
+ "sshSudoModeNoneDescription": "用户不能用sudo运行命令。",
+ "sshSudoModeFull": "全苏多",
+ "sshSudoModeFullDescription": "用户可以用 sudo 运行任何命令。",
+ "sshSudoModeCommands": "命令",
+ "sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。",
+ "sshSudo": "允许Sudo",
+ "sshSudoCommands": "Sudo 命令",
+ "sshSudoCommandsDescription": "允许用户使用 sudo 运行的命令列表。",
+ "sshCreateHomeDir": "创建主目录",
+ "sshUnixGroups": "Unix 组",
+ "sshUnixGroupsDescription": "将用户添加到目标主机的Unix组。",
"retryAttempts": "重试次数",
"expectedResponseCodes": "期望响应代码",
"expectedResponseCodesDescription": "HTTP 状态码表示健康状态。如留空,200-300 被视为健康。",
@@ -2503,6 +2523,17 @@
"editInternalResourceDialogAccessControl": "访问控制",
"editInternalResourceDialogAccessControlDescription": "控制当连接到此资源时,哪些角色、用户和机器客户端可以访问。管理员始终具有访问权。",
"editInternalResourceDialogPortRangeValidationError": "端口范围必须为\"*\"表示所有端口,或一个用逗号分隔的端口和范围列表(例如:\"80,443,8000-9000\")。端口必须在1到65535之间。",
+ "internalResourceAuthDaemonStrategy": "SSH 认证守护进程位置",
+ "internalResourceAuthDaemonStrategyDescription": "选择 SSH 身份验证守护进程在哪里运行:站点(新建) 或远程主机。",
+ "internalResourceAuthDaemonDescription": "SSH 身份验证守护程序处理此资源的 SSH 密钥签名和PAM 身份验证。 选择它是在站点(新建)还是在单独的远程主机上运行。请参阅 文档。",
+ "internalResourceAuthDaemonDocsUrl": "https://docs.pangolin.net",
+ "internalResourceAuthDaemonStrategyPlaceholder": "选择策略",
+ "internalResourceAuthDaemonStrategyLabel": "地点",
+ "internalResourceAuthDaemonSite": "在站点",
+ "internalResourceAuthDaemonSiteDescription": "认证守护进程在站点上运行(新建)。",
+ "internalResourceAuthDaemonRemote": "远程主机",
+ "internalResourceAuthDaemonRemoteDescription": "认证守护进程运行在不是站点的主机上。",
+ "internalResourceAuthDaemonPort": "守护进程端口(可选)",
"orgAuthWhatsThis": "我的组织ID在哪里可以找到?",
"learnMore": "了解更多",
"backToHome": "返回首页",
diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts
index 7c252b8b..ae90020a 100644
--- a/server/db/pg/schema/schema.ts
+++ b/server/db/pg/schema/schema.ts
@@ -232,7 +232,11 @@ export const siteResources = pgTable("siteResources", {
aliasAddress: varchar("aliasAddress"),
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
- disableIcmp: boolean("disableIcmp").notNull().default(false)
+ disableIcmp: boolean("disableIcmp").notNull().default(false),
+ authDaemonPort: integer("authDaemonPort").default(22123),
+ authDaemonMode: varchar("authDaemonMode", { length: 32 })
+ .$type<"site" | "remote">()
+ .default("site")
});
export const clientSiteResources = pgTable("clientSiteResources", {
@@ -372,7 +376,11 @@ export const roles = pgTable("roles", {
isAdmin: boolean("isAdmin"),
name: varchar("name").notNull(),
description: varchar("description"),
- requireDeviceApproval: boolean("requireDeviceApproval").default(false)
+ requireDeviceApproval: boolean("requireDeviceApproval").default(false),
+ sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("none"), // "none" | "full" | "commands"
+ sshSudoCommands: text("sshSudoCommands").default("[]"),
+ sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
+ sshUnixGroups: text("sshUnixGroups").default("[]")
});
export const roleActions = pgTable("roleActions", {
@@ -1059,4 +1067,6 @@ export type SecurityKey = InferSelectModel;
export type WebauthnChallenge = InferSelectModel;
export type DeviceWebAuthCode = InferSelectModel;
export type RequestAuditLog = InferSelectModel;
-export type RoundTripMessageTracker = InferSelectModel;
+export type RoundTripMessageTracker = InferSelectModel<
+ typeof roundTripMessageTracker
+>;
diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts
index 04d4338a..64866e67 100644
--- a/server/db/sqlite/schema/schema.ts
+++ b/server/db/sqlite/schema/schema.ts
@@ -257,7 +257,11 @@ export const siteResources = sqliteTable("siteResources", {
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
disableIcmp: integer("disableIcmp", { mode: "boolean" })
.notNull()
- .default(false)
+ .default(false),
+ authDaemonPort: integer("authDaemonPort").default(22123),
+ authDaemonMode: text("authDaemonMode")
+ .$type<"site" | "remote">()
+ .default("site")
});
export const clientSiteResources = sqliteTable("clientSiteResources", {
@@ -679,7 +683,13 @@ export const roles = sqliteTable("roles", {
description: text("description"),
requireDeviceApproval: integer("requireDeviceApproval", {
mode: "boolean"
- }).default(false)
+ }).default(false),
+ sshSudoMode: text("sshSudoMode").default("none"), // "none" | "full" | "commands"
+ sshSudoCommands: text("sshSudoCommands").default("[]"),
+ sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
+ true
+ ),
+ sshUnixGroups: text("sshUnixGroups").default("[]")
});
export const roleActions = sqliteTable("roleActions", {
diff --git a/server/middlewares/integration/verifyApiKeyRoleAccess.ts b/server/middlewares/integration/verifyApiKeyRoleAccess.ts
index ffe223a6..62bfb946 100644
--- a/server/middlewares/integration/verifyApiKeyRoleAccess.ts
+++ b/server/middlewares/integration/verifyApiKeyRoleAccess.ts
@@ -23,9 +23,14 @@ export async function verifyApiKeyRoleAccess(
);
}
- const { roleIds } = req.body;
- const allRoleIds =
- roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
+ let allRoleIds: number[] = [];
+ if (!isNaN(singleRoleId)) {
+ // If roleId is provided in URL params, query params, or body (single), use it exclusively
+ allRoleIds = [singleRoleId];
+ } else if (req.body?.roleIds) {
+ // Only use body.roleIds if no single roleId was provided
+ allRoleIds = req.body.roleIds;
+ }
if (allRoleIds.length === 0) {
return next();
diff --git a/server/middlewares/verifyRoleAccess.ts b/server/middlewares/verifyRoleAccess.ts
index 91adf07c..8858ab53 100644
--- a/server/middlewares/verifyRoleAccess.ts
+++ b/server/middlewares/verifyRoleAccess.ts
@@ -23,8 +23,14 @@ export async function verifyRoleAccess(
);
}
- const roleIds = req.body?.roleIds;
- const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
+ let allRoleIds: number[] = [];
+ if (!isNaN(singleRoleId)) {
+ // If roleId is provided in URL params, query params, or body (single), use it exclusively
+ allRoleIds = [singleRoleId];
+ } else if (req.body?.roleIds) {
+ // Only use body.roleIds if no single roleId was provided
+ allRoleIds = req.body.roleIds;
+ }
if (allRoleIds.length === 0) {
return next();
diff --git a/server/private/lib/sshCA.ts b/server/private/lib/sshCA.ts
index 145dac61..6c9d1209 100644
--- a/server/private/lib/sshCA.ts
+++ b/server/private/lib/sshCA.ts
@@ -61,7 +61,10 @@ function encodeUInt64(value: bigint): Buffer {
* Decode a string from SSH wire format at the given offset
* Returns the string buffer and the new offset
*/
-function decodeString(data: Buffer, offset: number): { value: Buffer; newOffset: number } {
+function decodeString(
+ data: Buffer,
+ offset: number
+): { value: Buffer; newOffset: number } {
const len = data.readUInt32BE(offset);
const value = data.subarray(offset + 4, offset + 4 + len);
return { value, newOffset: offset + 4 + len };
@@ -91,7 +94,9 @@ function parseOpenSSHPublicKey(pubKeyLine: string): {
// Verify the key type in the blob matches
const { value: blobKeyType } = decodeString(keyData, 0);
if (blobKeyType.toString("utf8") !== keyType) {
- throw new Error(`Key type mismatch: ${blobKeyType.toString("utf8")} vs ${keyType}`);
+ throw new Error(
+ `Key type mismatch: ${blobKeyType.toString("utf8")} vs ${keyType}`
+ );
}
return { keyType, keyData, comment };
@@ -238,7 +243,7 @@ export interface SignedCertificate {
* @param comment - Optional comment for the CA public key
* @returns CA key pair and configuration info
*/
-export function generateCA(comment: string = "ssh-ca"): CAKeyPair {
+export function generateCA(comment: string = "pangolin-ssh-ca"): CAKeyPair {
// Generate Ed25519 key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519", {
publicKeyEncoding: { type: "spki", format: "pem" },
@@ -269,7 +274,7 @@ export function generateCA(comment: string = "ssh-ca"): CAKeyPair {
/**
* Get and decrypt the SSH CA keys for an organization.
- *
+ *
* @param orgId - Organization ID
* @param decryptionKey - Key to decrypt the CA private key (typically server.secret from config)
* @returns CA key pair or null if not found
@@ -307,7 +312,10 @@ export async function getOrgCAKeys(
key: privateKeyPem,
format: "pem"
});
- const publicKeyPem = pubKeyObj.export({ type: "spki", format: "pem" }) as string;
+ const publicKeyPem = pubKeyObj.export({
+ type: "spki",
+ format: "pem"
+ }) as string;
return {
privateKeyPem,
@@ -365,8 +373,8 @@ export function signPublicKey(
const serial = options.serial ?? BigInt(Date.now());
const certType = options.certType ?? 1; // 1 = user cert
const now = BigInt(Math.floor(Date.now() / 1000));
- const validAfter = options.validAfter ?? (now - 60n); // 1 minute ago
- const validBefore = options.validBefore ?? (now + 86400n * 365n); // 1 year from now
+ const validAfter = options.validAfter ?? now - 60n; // 1 minute ago
+ const validBefore = options.validBefore ?? now + 86400n * 365n; // 1 year from now
// Default extensions for user certificates
const defaultExtensions = [
@@ -422,10 +430,7 @@ export function signPublicKey(
]);
// Build complete certificate
- const certificate = Buffer.concat([
- certBody,
- encodeString(signatureBlob)
- ]);
+ const certificate = Buffer.concat([certBody, encodeString(signatureBlob)]);
// Format as OpenSSH certificate line
const certLine = `${certTypeString} ${certificate.toString("base64")} ${options.keyId}`;
diff --git a/server/private/routers/billing/featureLifecycle.ts b/server/private/routers/billing/featureLifecycle.ts
index 3e4b8a4a..9536a87f 100644
--- a/server/private/routers/billing/featureLifecycle.ts
+++ b/server/private/routers/billing/featureLifecycle.ts
@@ -25,7 +25,8 @@ import {
loginPageOrg,
orgs,
resources,
- roles
+ roles,
+ siteResources
} from "@server/db";
import { eq } from "drizzle-orm";
@@ -286,6 +287,10 @@ async function disableFeature(
await disableAutoProvisioning(orgId);
break;
+ case TierFeature.SshPam:
+ await disableSshPam(orgId);
+ break;
+
default:
logger.warn(
`Unknown feature ${feature} for org ${orgId}, skipping`
@@ -315,6 +320,12 @@ async function disableDeviceApprovals(orgId: string): Promise {
logger.info(`Disabled device approvals on all roles for org ${orgId}`);
}
+async function disableSshPam(orgId: string): Promise {
+ logger.info(
+ `Disabled SSH PAM options on all roles and site resources for org ${orgId}`
+ );
+}
+
async function disableLoginPageBranding(orgId: string): Promise {
const [existingBranding] = await db
.select()
diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts
index 17132c44..a1352342 100644
--- a/server/private/routers/external.ts
+++ b/server/private/routers/external.ts
@@ -514,7 +514,7 @@ authenticated.post(
verifyValidSubscription(tierMatrix.sshPam),
verifyOrgAccess,
verifyLimits,
- // verifyUserHasAction(ActionsEnum.signSshKey),
+ verifyUserHasAction(ActionsEnum.signSshKey),
logActionAudit(ActionsEnum.signSshKey),
ssh.signSshKey
);
diff --git a/server/private/routers/ssh/signSshKey.ts b/server/private/routers/ssh/signSshKey.ts
index 9ffce8c1..fbdee72d 100644
--- a/server/private/routers/ssh/signSshKey.ts
+++ b/server/private/routers/ssh/signSshKey.ts
@@ -13,7 +13,17 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
-import { db, newts, orgs, roundTripMessageTracker, siteResources, sites, userOrgs } from "@server/db";
+import {
+ db,
+ newts,
+ roles,
+ roundTripMessageTracker,
+ siteResources,
+ sites,
+ userOrgs
+} from "@server/db";
+import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -135,11 +145,26 @@ export async function signSshKey(
);
}
+ const isLicensed = await isLicensedOrSubscribed(
+ orgId,
+ tierMatrix.sshPam
+ );
+ if (!isLicensed) {
+ return next(
+ createHttpError(
+ HttpCode.FORBIDDEN,
+ "SSH key signing requires a paid plan"
+ )
+ );
+ }
+
let usernameToUse;
if (!userOrg.pamUsername) {
if (req.user?.email) {
// Extract username from email (first part before @)
- usernameToUse = req.user?.email.split("@")[0];
+ usernameToUse = req.user?.email
+ .split("@")[0]
+ .replace(/[^a-zA-Z0-9_-]/g, "");
if (!usernameToUse) {
return next(
createHttpError(
@@ -301,6 +326,29 @@ export async function signSshKey(
);
}
+ const [roleRow] = await db
+ .select()
+ .from(roles)
+ .where(eq(roles.roleId, roleId))
+ .limit(1);
+
+ let parsedSudoCommands: string[] = [];
+ let parsedGroups: string[] = [];
+ try {
+ parsedSudoCommands = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
+ if (!Array.isArray(parsedSudoCommands)) parsedSudoCommands = [];
+ } catch {
+ parsedSudoCommands = [];
+ }
+ try {
+ parsedGroups = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
+ if (!Array.isArray(parsedGroups)) parsedGroups = [];
+ } catch {
+ parsedGroups = [];
+ }
+ const homedir = roleRow?.sshCreateHomeDir ?? null;
+ const sudoMode = roleRow?.sshSudoMode ?? "none";
+
// get the site
const [newt] = await db
.select()
@@ -334,7 +382,7 @@ export async function signSshKey(
.values({
wsClientId: newt.newtId,
messageType: `newt/pam/connection`,
- sentAt: Math.floor(Date.now() / 1000),
+ sentAt: Math.floor(Date.now() / 1000)
})
.returning();
@@ -352,14 +400,17 @@ export async function signSshKey(
data: {
messageId: message.messageId,
orgId: orgId,
- agentPort: 22123,
+ agentPort: resource.authDaemonPort ?? 22123,
+ externalAuthDaemon: resource.authDaemonMode === "remote",
agentHost: resource.destination,
caCert: caKeys.publicKeyOpenSSH,
username: usernameToUse,
niceId: resource.niceId,
metadata: {
- sudo: true, // we are hardcoding these for now but should make configurable from the role or something
- homedir: true
+ sudoMode: sudoMode,
+ sudoCommands: parsedSudoCommands,
+ homedir: homedir,
+ groups: parsedGroups
}
}
});
diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts
index 12d0a199..8ef01a2f 100644
--- a/server/routers/client/updateClient.ts
+++ b/server/routers/client/updateClient.ts
@@ -6,7 +6,7 @@ import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
-import { eq, and } from "drizzle-orm";
+import { eq, and, ne } from "drizzle-orm";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
@@ -93,7 +93,8 @@ export async function updateClient(
.where(
and(
eq(clients.niceId, niceId),
- eq(clients.orgId, clients.orgId)
+ eq(clients.orgId, clients.orgId),
+ ne(clients.clientId, clientId)
)
)
.limit(1);
diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts
index 59aa86d2..1a5d8799 100644
--- a/server/routers/org/createOrg.ts
+++ b/server/routers/org/createOrg.ts
@@ -181,7 +181,10 @@ export async function createOrg(
}
if (build == "saas" && billingOrgIdForNewOrg) {
- const usage = await usageService.getUsage(billingOrgIdForNewOrg, FeatureId.ORGINIZATIONS);
+ const usage = await usageService.getUsage(
+ billingOrgIdForNewOrg,
+ FeatureId.ORGINIZATIONS
+ );
if (!usage) {
return next(
createHttpError(
@@ -218,11 +221,6 @@ export async function createOrg(
.from(domains)
.where(eq(domains.configManaged, true));
- // Generate SSH CA keys for the org
- // const ca = generateCA(`${orgId}-ca`);
- // const encryptionKey = config.getRawConfig().server.secret!;
- // const encryptedCaPrivateKey = encrypt(ca.privateKeyPem, encryptionKey);
-
const saasBillingFields =
build === "saas" && req.user && isFirstOrg !== null
? isFirstOrg
@@ -233,6 +231,19 @@ export async function createOrg(
}
: {};
+ const encryptionKey = config.getRawConfig().server.secret;
+ let sshCaFields: {
+ sshCaPrivateKey?: string;
+ sshCaPublicKey?: string;
+ } = {};
+ if (encryptionKey) {
+ const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
+ sshCaFields = {
+ sshCaPrivateKey: encrypt(ca.privateKeyPem, encryptionKey),
+ sshCaPublicKey: ca.publicKeyOpenSSH
+ };
+ }
+
const newOrg = await trx
.insert(orgs)
.values({
@@ -241,8 +252,7 @@ export async function createOrg(
subnet,
utilitySubnet,
createdAt: new Date().toISOString(),
- // sshCaPrivateKey: encryptedCaPrivateKey,
- // sshCaPublicKey: ca.publicKeyOpenSSH,
+ ...sshCaFields,
...saasBillingFields
})
.returning();
@@ -262,7 +272,8 @@ export async function createOrg(
orgId: newOrg[0].orgId,
isAdmin: true,
name: "Admin",
- description: "Admin role with the most permissions"
+ description: "Admin role with the most permissions",
+ sshSudoMode: "full"
})
.returning({ roleId: roles.roleId });
diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts
index 4f35739b..4a3e65fa 100644
--- a/server/routers/resource/updateResource.ts
+++ b/server/routers/resource/updateResource.ts
@@ -9,7 +9,7 @@ import {
Resource,
resources
} from "@server/db";
-import { eq, and } from "drizzle-orm";
+import { eq, and, ne } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -33,7 +33,15 @@ const updateResourceParamsSchema = z.strictObject({
const updateHttpResourceBodySchema = z
.strictObject({
name: z.string().min(1).max(255).optional(),
- niceId: z.string().min(1).max(255).regex(/^[a-zA-Z0-9-]+$/, "niceId can only contain letters, numbers, and dashes").optional(),
+ niceId: z
+ .string()
+ .min(1)
+ .max(255)
+ .regex(
+ /^[a-zA-Z0-9-]+$/,
+ "niceId can only contain letters, numbers, and dashes"
+ )
+ .optional(),
subdomain: subdomainSchema.nullable().optional(),
ssl: z.boolean().optional(),
sso: z.boolean().optional(),
@@ -248,14 +256,13 @@ async function updateHttpResource(
.where(
and(
eq(resources.niceId, updateData.niceId),
- eq(resources.orgId, resource.orgId)
+ eq(resources.orgId, resource.orgId),
+ ne(resources.resourceId, resource.resourceId) // exclude the current resource from the search
)
- );
+ )
+ .limit(1);
- if (
- existingResource &&
- existingResource.resourceId !== resource.resourceId
- ) {
+ if (existingResource) {
return next(
createHttpError(
HttpCode.CONFLICT,
@@ -343,7 +350,10 @@ async function updateHttpResource(
headers = null;
}
- const isLicensed = await isLicensedOrSubscribed(resource.orgId, tierMatrix.maintencePage);
+ const isLicensed = await isLicensedOrSubscribed(
+ resource.orgId,
+ tierMatrix.maintencePage
+ );
if (!isLicensed) {
updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined;
diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts
index edb8f1bd..e732b405 100644
--- a/server/routers/role/createRole.ts
+++ b/server/routers/role/createRole.ts
@@ -18,10 +18,17 @@ const createRoleParamsSchema = z.strictObject({
orgId: z.string()
});
+const sshSudoModeSchema = z.enum(["none", "full", "commands"]);
+
const createRoleSchema = z.strictObject({
name: z.string().min(1).max(255),
description: z.string().optional(),
- requireDeviceApproval: z.boolean().optional()
+ requireDeviceApproval: z.boolean().optional(),
+ allowSsh: z.boolean().optional(),
+ sshSudoMode: sshSudoModeSchema.optional(),
+ sshSudoCommands: z.array(z.string()).optional(),
+ sshCreateHomeDir: z.boolean().optional(),
+ sshUnixGroups: z.array(z.string()).optional()
});
export const defaultRoleAllowedActions: ActionsEnum[] = [
@@ -101,24 +108,40 @@ export async function createRole(
);
}
- const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
- if (!isLicensed) {
+ const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
+ if (!isLicensedDeviceApprovals) {
roleData.requireDeviceApproval = undefined;
}
+ const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam);
+ const roleInsertValues: Record = {
+ name: roleData.name,
+ orgId
+ };
+ if (roleData.description !== undefined) roleInsertValues.description = roleData.description;
+ if (roleData.requireDeviceApproval !== undefined) roleInsertValues.requireDeviceApproval = roleData.requireDeviceApproval;
+ if (isLicensedSshPam) {
+ if (roleData.sshSudoMode !== undefined) roleInsertValues.sshSudoMode = roleData.sshSudoMode;
+ if (roleData.sshSudoCommands !== undefined) roleInsertValues.sshSudoCommands = JSON.stringify(roleData.sshSudoCommands);
+ if (roleData.sshCreateHomeDir !== undefined) roleInsertValues.sshCreateHomeDir = roleData.sshCreateHomeDir;
+ if (roleData.sshUnixGroups !== undefined) roleInsertValues.sshUnixGroups = JSON.stringify(roleData.sshUnixGroups);
+ }
+
await db.transaction(async (trx) => {
const newRole = await trx
.insert(roles)
- .values({
- ...roleData,
- orgId
- })
+ .values(roleInsertValues as typeof roles.$inferInsert)
.returning();
+ const actionsToInsert = [...defaultRoleAllowedActions];
+ if (roleData.allowSsh) {
+ actionsToInsert.push(ActionsEnum.signSshKey);
+ }
+
await trx
.insert(roleActions)
.values(
- defaultRoleAllowedActions.map((action) => ({
+ actionsToInsert.map((action) => ({
roleId: newRole[0].roleId,
actionId: action,
orgId
diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts
index ec7f3b4b..d4cb580f 100644
--- a/server/routers/role/listRoles.ts
+++ b/server/routers/role/listRoles.ts
@@ -1,9 +1,10 @@
-import { db, orgs, roles } from "@server/db";
+import { db, orgs, roleActions, roles } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
-import { eq, sql } from "drizzle-orm";
+import { and, eq, inArray, sql } from "drizzle-orm";
+import { ActionsEnum } from "@server/auth/actions";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
@@ -37,7 +38,11 @@ async function queryRoles(orgId: string, limit: number, offset: number) {
name: roles.name,
description: roles.description,
orgName: orgs.name,
- requireDeviceApproval: roles.requireDeviceApproval
+ requireDeviceApproval: roles.requireDeviceApproval,
+ sshSudoMode: roles.sshSudoMode,
+ sshSudoCommands: roles.sshSudoCommands,
+ sshCreateHomeDir: roles.sshCreateHomeDir,
+ sshUnixGroups: roles.sshUnixGroups
})
.from(roles)
.leftJoin(orgs, eq(roles.orgId, orgs.orgId))
@@ -106,9 +111,28 @@ export async function listRoles(
const totalCountResult = await countQuery;
const totalCount = totalCountResult[0].count;
+ let rolesWithAllowSsh = rolesList;
+ if (rolesList.length > 0) {
+ const roleIds = rolesList.map((r) => r.roleId);
+ const signSshKeyRows = await db
+ .select({ roleId: roleActions.roleId })
+ .from(roleActions)
+ .where(
+ and(
+ inArray(roleActions.roleId, roleIds),
+ eq(roleActions.actionId, ActionsEnum.signSshKey)
+ )
+ );
+ const roleIdsWithSsh = new Set(signSshKeyRows.map((r) => r.roleId));
+ rolesWithAllowSsh = rolesList.map((r) => ({
+ ...r,
+ allowSsh: roleIdsWithSsh.has(r.roleId)
+ }));
+ }
+
return response(res, {
data: {
- roles: rolesList,
+ roles: rolesWithAllowSsh,
pagination: {
total: totalCount,
limit,
diff --git a/server/routers/role/updateRole.ts b/server/routers/role/updateRole.ts
index 51a33e32..7400e582 100644
--- a/server/routers/role/updateRole.ts
+++ b/server/routers/role/updateRole.ts
@@ -1,8 +1,9 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, type Role } from "@server/db";
-import { roles } from "@server/db";
-import { eq } from "drizzle-orm";
+import { roleActions, roles } from "@server/db";
+import { and, eq } from "drizzle-orm";
+import { ActionsEnum } from "@server/auth/actions";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -16,11 +17,18 @@ const updateRoleParamsSchema = z.strictObject({
roleId: z.string().transform(Number).pipe(z.int().positive())
});
+const sshSudoModeSchema = z.enum(["none", "full", "commands"]);
+
const updateRoleBodySchema = z
.strictObject({
name: z.string().min(1).max(255).optional(),
description: z.string().optional(),
- requireDeviceApproval: z.boolean().optional()
+ requireDeviceApproval: z.boolean().optional(),
+ allowSsh: z.boolean().optional(),
+ sshSudoMode: sshSudoModeSchema.optional(),
+ sshSudoCommands: z.array(z.string()).optional(),
+ sshCreateHomeDir: z.boolean().optional(),
+ sshUnixGroups: z.array(z.string()).optional()
})
.refine((data) => Object.keys(data).length > 0, {
error: "At least one field must be provided for update"
@@ -75,7 +83,9 @@ export async function updateRole(
}
const { roleId } = parsedParams.data;
- const updateData = parsedBody.data;
+ const body = parsedBody.data;
+ const { allowSsh, ...restBody } = body;
+ const updateData: Record = { ...restBody };
const role = await db
.select()
@@ -92,16 +102,14 @@ export async function updateRole(
);
}
- if (role[0].isAdmin) {
- return next(
- createHttpError(
- HttpCode.FORBIDDEN,
- `Cannot update a Admin role`
- )
- );
+ const orgId = role[0].orgId;
+ const isAdminRole = role[0].isAdmin;
+
+ if (isAdminRole) {
+ delete updateData.name;
+ delete updateData.description;
}
- const orgId = role[0].orgId;
if (!orgId) {
return next(
createHttpError(
@@ -111,18 +119,70 @@ export async function updateRole(
);
}
- const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
- if (!isLicensed) {
+ const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
+ if (!isLicensedDeviceApprovals) {
updateData.requireDeviceApproval = undefined;
}
- const updatedRole = await db
- .update(roles)
- .set(updateData)
- .where(eq(roles.roleId, roleId))
- .returning();
+ const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam);
+ if (!isLicensedSshPam) {
+ delete updateData.sshSudoMode;
+ delete updateData.sshSudoCommands;
+ delete updateData.sshCreateHomeDir;
+ delete updateData.sshUnixGroups;
+ } else {
+ if (Array.isArray(updateData.sshSudoCommands)) {
+ updateData.sshSudoCommands = JSON.stringify(updateData.sshSudoCommands);
+ }
+ if (Array.isArray(updateData.sshUnixGroups)) {
+ updateData.sshUnixGroups = JSON.stringify(updateData.sshUnixGroups);
+ }
+ }
- if (updatedRole.length === 0) {
+ const updatedRole = await db.transaction(async (trx) => {
+ const result = await trx
+ .update(roles)
+ .set(updateData as typeof roles.$inferInsert)
+ .where(eq(roles.roleId, roleId))
+ .returning();
+
+ if (result.length === 0) {
+ return null;
+ }
+
+ if (allowSsh === true) {
+ const existing = await trx
+ .select()
+ .from(roleActions)
+ .where(
+ and(
+ eq(roleActions.roleId, roleId),
+ eq(roleActions.actionId, ActionsEnum.signSshKey)
+ )
+ )
+ .limit(1);
+ if (existing.length === 0) {
+ await trx.insert(roleActions).values({
+ roleId,
+ actionId: ActionsEnum.signSshKey,
+ orgId: orgId!
+ });
+ }
+ } else if (allowSsh === false) {
+ await trx
+ .delete(roleActions)
+ .where(
+ and(
+ eq(roleActions.roleId, roleId),
+ eq(roleActions.actionId, ActionsEnum.signSshKey)
+ )
+ );
+ }
+
+ return result[0];
+ });
+
+ if (!updatedRole) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
@@ -132,7 +192,7 @@ export async function updateRole(
}
return response(res, {
- data: updatedRole[0],
+ data: updatedRole,
success: true,
error: false,
message: "Role updated successfully",
diff --git a/server/routers/site/updateSite.ts b/server/routers/site/updateSite.ts
index 44764362..ca0f7678 100644
--- a/server/routers/site/updateSite.ts
+++ b/server/routers/site/updateSite.ts
@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { sites } from "@server/db";
-import { eq, and } from "drizzle-orm";
+import { eq, and, ne } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -19,8 +19,8 @@ const updateSiteBodySchema = z
.strictObject({
name: z.string().min(1).max(255).optional(),
niceId: z.string().min(1).max(255).optional(),
- dockerSocketEnabled: z.boolean().optional(),
- remoteSubnets: z.string().optional()
+ dockerSocketEnabled: z.boolean().optional()
+ // remoteSubnets: z.string().optional()
// subdomain: z
// .string()
// .min(1)
@@ -86,18 +86,19 @@ export async function updateSite(
// if niceId is provided, check if it's already in use by another site
if (updateData.niceId) {
- const existingSite = await db
+ const [existingSite] = await db
.select()
.from(sites)
.where(
and(
eq(sites.niceId, updateData.niceId),
- eq(sites.orgId, sites.orgId)
+ eq(sites.orgId, sites.orgId),
+ ne(sites.siteId, siteId)
)
)
.limit(1);
- if (existingSite.length > 0 && existingSite[0].siteId !== siteId) {
+ if (existingSite) {
return next(
createHttpError(
HttpCode.CONFLICT,
@@ -107,22 +108,22 @@ export async function updateSite(
}
}
- // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
- if (updateData.remoteSubnets) {
- const subnets = updateData.remoteSubnets
- .split(",")
- .map((s) => s.trim());
- for (const subnet of subnets) {
- if (!isValidCIDR(subnet)) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- `Invalid CIDR format: ${subnet}`
- )
- );
- }
- }
- }
+ // // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
+ // if (updateData.remoteSubnets) {
+ // const subnets = updateData.remoteSubnets
+ // .split(",")
+ // .map((s) => s.trim());
+ // for (const subnet of subnets) {
+ // if (!isValidCIDR(subnet)) {
+ // return next(
+ // createHttpError(
+ // HttpCode.BAD_REQUEST,
+ // `Invalid CIDR format: ${subnet}`
+ // )
+ // );
+ // }
+ // }
+ // }
const updatedSite = await db
.update(sites)
diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts
index 48c298d3..bbdc3638 100644
--- a/server/routers/siteResource/createSiteResource.ts
+++ b/server/routers/siteResource/createSiteResource.ts
@@ -16,6 +16,8 @@ import {
isIpInCidr,
portRangeStringSchema
} from "@server/lib/ip";
+import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
import response from "@server/lib/response";
import logger from "@server/logger";
@@ -53,7 +55,9 @@ const createSiteResourceSchema = z
clientIds: z.array(z.int()),
tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema,
- disableIcmp: z.boolean().optional()
+ disableIcmp: z.boolean().optional(),
+ authDaemonPort: z.int().positive().optional(),
+ authDaemonMode: z.enum(["site", "remote"]).optional()
})
.strict()
.refine(
@@ -168,7 +172,9 @@ export async function createSiteResource(
clientIds,
tcpPortRangeString,
udpPortRangeString,
- disableIcmp
+ disableIcmp,
+ authDaemonPort,
+ authDaemonMode
} = parsedBody.data;
// Verify the site exists and belongs to the org
@@ -267,6 +273,11 @@ export async function createSiteResource(
}
}
+ const isLicensedSshPam = await isLicensedOrSubscribed(
+ orgId,
+ tierMatrix.sshPam
+ );
+
const niceId = await getUniqueSiteResourceName(orgId);
let aliasAddress: string | null = null;
if (mode == "host") {
@@ -277,25 +288,29 @@ export async function createSiteResource(
let newSiteResource: SiteResource | undefined;
await db.transaction(async (trx) => {
// Create the site resource
+ const insertValues: typeof siteResources.$inferInsert = {
+ siteId,
+ niceId,
+ orgId,
+ name,
+ mode: mode as "host" | "cidr",
+ destination,
+ enabled,
+ alias,
+ aliasAddress,
+ tcpPortRangeString,
+ udpPortRangeString,
+ disableIcmp
+ };
+ if (isLicensedSshPam) {
+ if (authDaemonPort !== undefined)
+ insertValues.authDaemonPort = authDaemonPort;
+ if (authDaemonMode !== undefined)
+ insertValues.authDaemonMode = authDaemonMode;
+ }
[newSiteResource] = await trx
.insert(siteResources)
- .values({
- siteId,
- niceId,
- orgId,
- name,
- mode: mode as "host" | "cidr",
- // protocol: mode === "port" ? protocol : null,
- // proxyPort: mode === "port" ? proxyPort : null,
- // destinationPort: mode === "port" ? destinationPort : null,
- destination,
- enabled,
- alias,
- aliasAddress,
- tcpPortRangeString,
- udpPortRangeString,
- disableIcmp
- })
+ .values(insertValues)
.returning();
const siteResourceId = newSiteResource.siteResourceId;
diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts
index ead1fc8a..5aec53c7 100644
--- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts
+++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts
@@ -78,6 +78,8 @@ function querySiteResourcesBase() {
tcpPortRangeString: siteResources.tcpPortRangeString,
udpPortRangeString: siteResources.udpPortRangeString,
disableIcmp: siteResources.disableIcmp,
+ authDaemonMode: siteResources.authDaemonMode,
+ authDaemonPort: siteResources.authDaemonPort,
siteName: sites.name,
siteNiceId: sites.niceId,
siteAddress: sites.address
diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts
index 4c19bea1..242b9226 100644
--- a/server/routers/siteResource/updateSiteResource.ts
+++ b/server/routers/siteResource/updateSiteResource.ts
@@ -32,6 +32,8 @@ import {
getClientSiteResourceAccess,
rebuildClientAssociationsFromSiteResource
} from "@server/lib/rebuildClientAssociations";
+import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
const updateSiteResourceParamsSchema = z.strictObject({
siteResourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -61,7 +63,9 @@ const updateSiteResourceSchema = z
clientIds: z.array(z.int()),
tcpPortRangeString: portRangeStringSchema,
udpPortRangeString: portRangeStringSchema,
- disableIcmp: z.boolean().optional()
+ disableIcmp: z.boolean().optional(),
+ authDaemonPort: z.int().positive().nullish(),
+ authDaemonMode: z.enum(["site", "remote"]).optional()
})
.strict()
.refine(
@@ -172,7 +176,9 @@ export async function updateSiteResource(
clientIds,
tcpPortRangeString,
udpPortRangeString,
- disableIcmp
+ disableIcmp,
+ authDaemonPort,
+ authDaemonMode
} = parsedBody.data;
const [site] = await db
@@ -198,6 +204,11 @@ export async function updateSiteResource(
);
}
+ const isLicensedSshPam = await isLicensedOrSubscribed(
+ existingSiteResource.orgId,
+ tierMatrix.sshPam
+ );
+
const [org] = await db
.select()
.from(orgs)
@@ -308,6 +319,18 @@ export async function updateSiteResource(
// wait some time to allow for messages to be handled
await new Promise((resolve) => setTimeout(resolve, 750));
+ const sshPamSet =
+ isLicensedSshPam &&
+ (authDaemonPort !== undefined || authDaemonMode !== undefined)
+ ? {
+ ...(authDaemonPort !== undefined && {
+ authDaemonPort
+ }),
+ ...(authDaemonMode !== undefined && {
+ authDaemonMode
+ })
+ }
+ : {};
[updatedSiteResource] = await trx
.update(siteResources)
.set({
@@ -319,7 +342,8 @@ export async function updateSiteResource(
alias: alias && alias.trim() ? alias : null,
tcpPortRangeString: tcpPortRangeString,
udpPortRangeString: udpPortRangeString,
- disableIcmp: disableIcmp
+ disableIcmp: disableIcmp,
+ ...sshPamSet
})
.where(
and(
@@ -397,6 +421,18 @@ export async function updateSiteResource(
);
} else {
// Update the site resource
+ const sshPamSet =
+ isLicensedSshPam &&
+ (authDaemonPort !== undefined || authDaemonMode !== undefined)
+ ? {
+ ...(authDaemonPort !== undefined && {
+ authDaemonPort
+ }),
+ ...(authDaemonMode !== undefined && {
+ authDaemonMode
+ })
+ }
+ : {};
[updatedSiteResource] = await trx
.update(siteResources)
.set({
@@ -408,7 +444,8 @@ export async function updateSiteResource(
alias: alias && alias.trim() ? alias : null,
tcpPortRangeString: tcpPortRangeString,
udpPortRangeString: udpPortRangeString,
- disableIcmp: disableIcmp
+ disableIcmp: disableIcmp,
+ ...sshPamSet
})
.where(
and(eq(siteResources.siteResourceId, siteResourceId))
diff --git a/server/setup/scriptsSqlite/1.16.0.ts b/server/setup/scriptsSqlite/1.16.0.ts
new file mode 100644
index 00000000..1e8ca4fd
--- /dev/null
+++ b/server/setup/scriptsSqlite/1.16.0.ts
@@ -0,0 +1,29 @@
+import { __DIRNAME, APP_PATH } from "@server/lib/consts";
+import Database from "better-sqlite3";
+import path from "path";
+
+const version = "1.16.0";
+
+export default async function migration() {
+ console.log(`Running setup script ${version}...`);
+
+ const location = path.join(APP_PATH, "db", "db.sqlite");
+ const db = new Database(location);
+
+ // set all admin role sudo to "full"; all other roles to "none"
+ // all roles set hoemdir to true
+
+ // generate ca certs for all orgs?
+ // set authDaemonMode to "site" for all orgs
+
+ try {
+ db.transaction(() => {})();
+
+ console.log(`Migrated database`);
+ } catch (e) {
+ console.log("Failed to migrate db:", e);
+ throw e;
+ }
+
+ console.log(`${version} migration complete`);
+}
diff --git a/src/app/[orgId]/settings/clients/user/[niceId]/layout.tsx b/src/app/[orgId]/settings/clients/user/[niceId]/layout.tsx
index 7d8059aa..2d9934cb 100644
--- a/src/app/[orgId]/settings/clients/user/[niceId]/layout.tsx
+++ b/src/app/[orgId]/settings/clients/user/[niceId]/layout.tsx
@@ -47,7 +47,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
/>
-