mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-18 06:09:51 +00:00
Compare commits
180 Commits
1.18.3
...
resource-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9df995e76 | ||
|
|
efb1d69ac9 | ||
|
|
107986d848 | ||
|
|
b6c8fbe43b | ||
|
|
4208a9f372 | ||
|
|
3c82a228fb | ||
|
|
a4aa29e48a | ||
|
|
0f82ba6627 | ||
|
|
1df5d9fac8 | ||
|
|
5189583d73 | ||
|
|
b794d2aa40 | ||
|
|
c69059b227 | ||
|
|
b27b62d4c8 | ||
|
|
ee8290d68c | ||
|
|
82e8e79b16 | ||
|
|
2d428d2fa0 | ||
|
|
0005c11a0a | ||
|
|
f91d914ec6 | ||
|
|
b6caeda0a5 | ||
|
|
77d17af15b | ||
|
|
264c6bf4e8 | ||
|
|
4aa72eb1a3 | ||
|
|
a066a68e1a | ||
|
|
9fb677e952 | ||
|
|
88d8414eb8 | ||
|
|
5f3fafb1b0 | ||
|
|
de1338a8cd | ||
|
|
0800aa2a61 | ||
|
|
4959d66ac1 | ||
|
|
9320df8be6 | ||
|
|
13ec6b6620 | ||
|
|
2ca3ef019c | ||
|
|
724e41a54f | ||
|
|
ce5e62d216 | ||
|
|
874dc2b33e | ||
|
|
3b2622d590 | ||
|
|
c81d855741 | ||
|
|
3bce8d3596 | ||
|
|
ee2a1e2bc3 | ||
|
|
a0f3ee74f9 | ||
|
|
82a36fd632 | ||
|
|
c5084137ab | ||
|
|
65ec8da100 | ||
|
|
e76e7581a5 | ||
|
|
a97a4b6ec1 | ||
|
|
e38bbde348 | ||
|
|
026260ddfb | ||
|
|
97be5eb7d5 | ||
|
|
d7b96ba3f5 | ||
|
|
b42672530f | ||
|
|
b6b2dbd8ab | ||
|
|
975f3a01f5 | ||
|
|
4de2dfff85 | ||
|
|
27d230647f | ||
|
|
114486608e | ||
|
|
10fa9274d0 | ||
|
|
cbdc74768f | ||
|
|
10f95896aa | ||
|
|
5b8994d143 | ||
|
|
c46ef2fe9c | ||
|
|
4cd025dd91 | ||
|
|
ce04ea9720 | ||
|
|
a3ce382725 | ||
|
|
4eb49e3e60 | ||
|
|
2a9481023a | ||
|
|
8ed01372b8 | ||
|
|
451f3d24a8 | ||
|
|
c4b3656fad | ||
|
|
54c1dd3bae | ||
|
|
a8f4d2b7d1 | ||
|
|
51f1693dbd | ||
|
|
b33a6e6fac | ||
|
|
fc2c13a686 | ||
|
|
f4602a120e | ||
|
|
7ccceeea0d | ||
|
|
f81f78f294 | ||
|
|
6cab223f12 | ||
|
|
7b05c02508 | ||
|
|
5922bfb1a0 | ||
|
|
43f2e32231 | ||
|
|
20ebdc6289 | ||
|
|
a80ae49a33 | ||
|
|
660197eef1 | ||
|
|
f3eb823bc3 | ||
|
|
61c13db090 | ||
|
|
ccbd793f52 | ||
|
|
d13e6896a8 | ||
|
|
83a36ead10 | ||
|
|
b61b74b0b5 | ||
|
|
01b068c50f | ||
|
|
fee44ce960 | ||
|
|
1906504a86 | ||
|
|
36bcba332c | ||
|
|
304ab1964c | ||
|
|
b286096c7b | ||
|
|
a22a4b6e74 | ||
|
|
9a680d2374 | ||
|
|
f80e212b07 | ||
|
|
8a39b3fd45 | ||
|
|
61ec938b00 | ||
|
|
6686de6788 | ||
|
|
79636cbb30 | ||
|
|
2fa1bc6cdc | ||
|
|
c5f6d822ca | ||
|
|
4de4bf9625 | ||
|
|
5d956080f2 | ||
|
|
f8e18de2fc | ||
|
|
884482ec35 | ||
|
|
9b43948fa4 | ||
|
|
bcd6cd99cc | ||
|
|
37ceba6b81 | ||
|
|
dfe42e9016 | ||
|
|
38aa2dace8 | ||
|
|
136c3eff0c | ||
|
|
642999c8b1 | ||
|
|
c5fc49b4fa | ||
|
|
cd5a38b1eb | ||
|
|
595842c2c9 | ||
|
|
82d5276ade | ||
|
|
51eb782831 | ||
|
|
de2980e1bc | ||
|
|
8a3c0d9a08 | ||
|
|
1a5e9f1005 | ||
|
|
f42c013f33 | ||
|
|
42c9bda939 | ||
|
|
cbce9fae3a | ||
|
|
e44b15ecd5 | ||
|
|
7f6ca31757 | ||
|
|
a1eb248474 | ||
|
|
be2b1fd1ce | ||
|
|
20b65f549e | ||
|
|
1dc8be373c | ||
|
|
22b2e6b3d4 | ||
|
|
89e7107a47 | ||
|
|
0a69131c38 | ||
|
|
590f2c29b3 | ||
|
|
0ddcce6fe1 | ||
|
|
8a54fb7f23 | ||
|
|
5c280b024e | ||
|
|
033cc62ce7 | ||
|
|
4c69b7a64e | ||
|
|
e7ab9b3f37 | ||
|
|
3143662f82 | ||
|
|
18964ba2a3 | ||
|
|
f862404c5c | ||
|
|
c292578f80 | ||
|
|
7b02d4104d | ||
|
|
2ef5d90e13 | ||
|
|
d6a8021613 | ||
|
|
c5231d37f6 | ||
|
|
4d803a40c9 | ||
|
|
1d709b551a | ||
|
|
335411de4c | ||
|
|
0e4abdf4b6 | ||
|
|
267b40b73c | ||
|
|
ba9a0c5e3c | ||
|
|
9e0b7ff0d7 | ||
|
|
003bf7fdf3 | ||
|
|
c3fdda026b | ||
|
|
a53363d064 | ||
|
|
ee21e1faa7 | ||
|
|
e409a34a09 | ||
|
|
7177ab7f77 | ||
|
|
801f6fb661 | ||
|
|
805d82b8d9 | ||
|
|
bd6d790495 | ||
|
|
2305163474 | ||
|
|
dda53dcb16 | ||
|
|
2c3e768867 | ||
|
|
8d682ed9ad | ||
|
|
47fe497ca1 | ||
|
|
4d5f364663 | ||
|
|
c3db8b972f | ||
|
|
cfced63ba1 | ||
|
|
51aa55f963 | ||
|
|
e7df24841e | ||
|
|
e6fd4c32c4 | ||
|
|
f6590aedbd | ||
|
|
3cb9e02533 | ||
|
|
4d792350ef |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -17,9 +17,9 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite*
|
||||||
!Dockerfile.sqlite
|
!Dockerfile.sqlite
|
||||||
*.sqlite3
|
*.sqlite3*
|
||||||
*.log
|
*.log
|
||||||
.machinelogs*.json
|
.machinelogs*.json
|
||||||
*-audit.json
|
*-audit.json
|
||||||
@@ -54,3 +54,4 @@ hydrateSaas.ts
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
drizzle.config.ts
|
drizzle.config.ts
|
||||||
server/setup/migrations.ts
|
server/setup/migrations.ts
|
||||||
|
solo.yml
|
||||||
60
cli/commands/disableUser2fa.ts
Normal file
60
cli/commands/disableUser2fa.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, users } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA for a user by email address.
|
||||||
|
*/
|
||||||
|
type DisableUser2faArgs = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const disableUser2fa: CommandModule<{}, DisableUser2faArgs> = {
|
||||||
|
command: "disable-user-2fa",
|
||||||
|
describe: "Disable 2FA for a user (sets twoFactorEnabled=false, clears secret)",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs.option("email", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "User email address"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: { email: string }) => {
|
||||||
|
try {
|
||||||
|
const { email } = argv;
|
||||||
|
console.log(`Looking for user with email: ${email}`);
|
||||||
|
|
||||||
|
// Find the user by email
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, email))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error(`User with email '${email}' not found`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.twoFactorEnabled) {
|
||||||
|
console.log(`2FA is already disabled for user '${email}'.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user: disable 2FA and clear secret
|
||||||
|
await db.update(users)
|
||||||
|
.set({
|
||||||
|
twoFactorEnabled: false,
|
||||||
|
twoFactorSecret: null,
|
||||||
|
twoFactorSetupRequested: false
|
||||||
|
})
|
||||||
|
.where(eq(users.userId, user.userId));
|
||||||
|
|
||||||
|
console.log(`2FA disabled for user '${email}'.`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error disabling 2FA:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
|||||||
import { deleteClient } from "./commands/deleteClient";
|
import { deleteClient } from "./commands/deleteClient";
|
||||||
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||||
import { clearCertificates } from "./commands/clearCertificates";
|
import { clearCertificates } from "./commands/clearCertificates";
|
||||||
|
import { disableUser2fa } from "./commands/disableUser2fa";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
@@ -21,5 +22,6 @@ yargs(hideBin(process.argv))
|
|||||||
.command(deleteClient)
|
.command(deleteClient)
|
||||||
.command(generateOrgCaKeys)
|
.command(generateOrgCaKeys)
|
||||||
.command(clearCertificates)
|
.command(clearCertificates)
|
||||||
|
.command(disableUser2fa)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Възникна грешка при изтриване на връзката",
|
"shareErrorDeleteMessage": "Възникна грешка при изтриване на връзката",
|
||||||
"shareDeleted": "Връзката беше изтрита",
|
"shareDeleted": "Връзката беше изтрита",
|
||||||
"shareDeletedDescription": "Връзката беше премахната",
|
"shareDeletedDescription": "Връзката беше премахната",
|
||||||
|
"shareDelete": "Изтрийте споделената връзка",
|
||||||
|
"shareDeleteConfirm": "Потвърдете изтриването на споделената връзка",
|
||||||
|
"shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?",
|
||||||
|
"shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.",
|
||||||
"shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.",
|
"shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.",
|
||||||
"accessToken": "Достъп Токен",
|
"accessToken": "Достъп Токен",
|
||||||
"usageExamples": "Примери за използване",
|
"usageExamples": "Примери за използване",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "След като бъде премахнат, този потребител няма да има достъп до организацията. Винаги можете да го поканите отново по-късно, но той ще трябва да приеме отново поканата.",
|
"userMessageOrgRemove": "След като бъде премахнат, този потребител няма да има достъп до организацията. Винаги можете да го поканите отново по-късно, но той ще трябва да приеме отново поканата.",
|
||||||
"userRemoveOrgConfirm": "Потвърдете премахването на потребителя",
|
"userRemoveOrgConfirm": "Потвърдете премахването на потребителя",
|
||||||
"userRemoveOrg": "Премахване на потребителя от организацията",
|
"userRemoveOrg": "Премахване на потребителя от организацията",
|
||||||
|
"userQuestionOrgRemoveSelf": "Сигурни ли сте, че искате да премахнете себе си от тази организация?",
|
||||||
|
"userMessageOrgRemoveSelf": "Ще загубите достъп незабавно. Администратор може да ви покани отново по-късно, но ще трябва да приемете нова покана.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Потвърдете премахването на себе си",
|
||||||
|
"userRemoveOrgSelf": "Премахнете себе си от организацията",
|
||||||
|
"userRemoveOrgSelfWarning": "Ще загубите достъп до тази организация незабавно.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "ПРЕМАХНЕТЕ МЕ ОТ ОРГАНИЗАЦИЯТА",
|
||||||
"users": "Потребители",
|
"users": "Потребители",
|
||||||
"accessRoleMember": "Член",
|
"accessRoleMember": "Член",
|
||||||
"accessRoleOwner": "Собственик",
|
"accessRoleOwner": "Собственик",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Невалиден имейл адрес",
|
"emailInvalid": "Невалиден имейл адрес",
|
||||||
"inviteValidityDuration": "Моля, изберете продължителност",
|
"inviteValidityDuration": "Моля, изберете продължителност",
|
||||||
"accessRoleSelectPlease": "Моля, изберете роля",
|
"accessRoleSelectPlease": "Моля, изберете роля",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Премахване на административния ви достъп?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "След записване няма да имате повече администраторски права в тази организация. Друг администратор може да възстанови достъпа, ако е необходимо.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Премахнете административния ми достъп",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "ПРЕМАХНЕТЕ АДМИНИСТРАТИВНИЯ МИ ДОСТЪП",
|
||||||
|
"ownerMustRetainAdminRole": "Собственикът на организацията трябва да запази поне една администраторска роля.",
|
||||||
"usernameRequired": "Необходимо е потребителско име",
|
"usernameRequired": "Необходимо е потребителско име",
|
||||||
"idpSelectPlease": "Моля, изберете доставчик на идентичност",
|
"idpSelectPlease": "Моля, изберете доставчик на идентичност",
|
||||||
"idpGenericOidc": "Основен OAuth2/OIDC доставчик.",
|
"idpGenericOidc": "Основен OAuth2/OIDC доставчик.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
|
"targetNoOneDescription": "Добавянето на повече от една цел ще активира натоварването на баланса.",
|
||||||
"targetsSubmit": "Запазване на целите",
|
"targetsSubmit": "Запазване на целите",
|
||||||
"addTarget": "Добавете цел",
|
"addTarget": "Добавете цел",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Роунд Робин маршрутизирането няма да работи между сайтове, които не са свързани към един и същ възел, но автоматичното превключване ще работи.",
|
||||||
"targetErrorInvalidIp": "Невалиден IP адрес",
|
"targetErrorInvalidIp": "Невалиден IP адрес",
|
||||||
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
|
"targetErrorInvalidIpDescription": "Моля, въведете валиден IP адрес или име на хост",
|
||||||
"targetErrorInvalidPort": "Невалиден порт",
|
"targetErrorInvalidPort": "Невалиден порт",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Валидна парола",
|
"validPassword": "Валидна парола",
|
||||||
"validEmail": "Валиден имейл",
|
"validEmail": "Валиден имейл",
|
||||||
"validSSO": "Валидно SSO",
|
"validSSO": "Валидно SSO",
|
||||||
|
"view": "Преглед",
|
||||||
|
"configManaged": "Управлявана конфигурация",
|
||||||
"connectedClient": "Свързан клиент",
|
"connectedClient": "Свързан клиент",
|
||||||
"resourceBlocked": "Блокирани ресурси",
|
"resourceBlocked": "Блокирани ресурси",
|
||||||
"droppedByRule": "Прекратено от правило",
|
"droppedByRule": "Прекратено от правило",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.",
|
"streamingDatadogDescription": "Пресочвайте събития директно към вашият акаунт в Datadog. Очаквайте скоро.",
|
||||||
"streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.",
|
"streamingTypePickerDescription": "Изберете вид на дестинацията, за да започнете.",
|
||||||
"streamingFailedToLoad": "Неуспешно зареждане на дестинации",
|
"streamingLastSyncError": "Възникна грешка при последната синхронизация",
|
||||||
"streamingUnexpectedError": "Възникна неочаквана грешка.",
|
"streamingUnexpectedError": "Възникна неочаквана грешка.",
|
||||||
"streamingFailedToUpdate": "Неуспешно актуализиране на дестинация",
|
"streamingFailedToUpdate": "Неуспешно актуализиране на дестинация",
|
||||||
"streamingDeletedSuccess": "Дестинацията беше изтрита успешно",
|
"streamingDeletedSuccess": "Дестинацията беше изтрита успешно",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Редактиране на дестинацията",
|
"S3DestEditTitle": "Редактиране на дестинацията",
|
||||||
"S3DestAddTitle": "Добавете S3 дестинация",
|
"S3DestAddTitle": "Добавете S3 дестинация",
|
||||||
"S3DestEditDescription": "Актуализирайте конфигурацията за тази S3 дестинация за предаване на събития.",
|
"S3DestEditDescription": "Актуализирайте конфигурацията за тази S3 дестинация за предаване на събития.",
|
||||||
"S3DestAddDescription": "Конфигурирайте нов крайна точка на S3, за да получавате събития на вашата организация.",
|
"S3DestAddDescription": "Конфигурирайте ново хранилище Amazon S3 (или съвместимо с S3), за да получавате събития на вашата организация.",
|
||||||
|
"s3DestTabSettings": "Настройки",
|
||||||
|
"s3DestTabFormat": "Формат",
|
||||||
|
"s3DestNameLabel": "Име",
|
||||||
|
"s3DestNamePlaceholder": "Моята S3 дестинация",
|
||||||
|
"s3DestAccessKeyIdLabel": "Идентификатор на достъп за AWS Key ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Тайният ключ за достъп на AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Вашият таен ключ за достъп за AWS",
|
||||||
|
"s3DestRegionLabel": "AWS Регион",
|
||||||
|
"s3DestBucketLabel": "Име на хранилище",
|
||||||
|
"s3DestPrefixLabel": "Префикс на ключ (по избор)",
|
||||||
|
"s3DestPrefixDescription": "По избор пътеводен префикс, добавен към всеки обектен ключ. Обектите се съхраняват в {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Потребителски крайна точка (по избор)",
|
||||||
|
"s3DestEndpointDescription": "Заместете крайната точка на S3 за съвместимо с S3 хранилище като MinIO или Cloudflare R2. Оставете празно за стандартното AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip компресия",
|
||||||
|
"s3DestGzipDescription": "Компресирайте всеки качен обект с gzip. Намалява разходите за съхранение и размера на качването.",
|
||||||
|
"s3DestFormatTitle": "Формат на файл",
|
||||||
|
"s3DestFormatDescription": "Как събитията са сериализирани вътре във всеки качен обект.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Всеки обект е JSON масив от записи на събития. Съвместим с повечето аналитични инструменти.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Всеки обект съдържа един JSON запис на ред (форматиран JSON с нов ред). Съвместим с Athena, BigQuery и Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Всеки обект е RFC-4180 CSV файл с ред заглавие. Имената на колоните са извлечени от полетата на данните за събитията.",
|
||||||
|
"s3DestSaveChanges": "Запази промените",
|
||||||
|
"s3DestCreateDestination": "Създаване на дестинация",
|
||||||
|
"s3DestUpdatedSuccess": "Дестинацията е актуализирана успешно",
|
||||||
|
"s3DestCreatedSuccess": "Дестинацията е създадена успешно",
|
||||||
|
"s3DestUpdateFailed": "Неуспешно актуализиране на дестинацията",
|
||||||
|
"s3DestCreateFailed": "Неуспешно създаване на дестинация",
|
||||||
"datadogDestEditTitle": "Редактиране на дестинация",
|
"datadogDestEditTitle": "Редактиране на дестинация",
|
||||||
"datadogDestAddTitle": "Добавяне на Datadog дестинация",
|
"datadogDestAddTitle": "Добавяне на Datadog дестинация",
|
||||||
"datadogDestEditDescription": "Актуализирайте конфигурацията за тази Datadog дестинация за предаване на събития.",
|
"datadogDestEditDescription": "Актуализирайте конфигурацията за тази Datadog дестинация за предаване на събития.",
|
||||||
@@ -3174,7 +3219,7 @@
|
|||||||
"publicIpEndpoint": "Крайна точка",
|
"publicIpEndpoint": "Крайна точка",
|
||||||
"lastTriggeredAt": "Последен тригер",
|
"lastTriggeredAt": "Последен тригер",
|
||||||
"reject": "Отхвърляне",
|
"reject": "Отхвърляне",
|
||||||
"uptimeDaysAgo": "{count} days ago",
|
"uptimeDaysAgo": "преди {count} дни",
|
||||||
"uptimeToday": "Днес",
|
"uptimeToday": "Днес",
|
||||||
"uptimeNoDataAvailable": "Няма налични данни",
|
"uptimeNoDataAvailable": "Няма налични данни",
|
||||||
"uptimeSuffix": "време без прекъсване",
|
"uptimeSuffix": "време без прекъсване",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Došlo k chybě při odstraňování odkazu",
|
"shareErrorDeleteMessage": "Došlo k chybě při odstraňování odkazu",
|
||||||
"shareDeleted": "Odkaz odstraněn",
|
"shareDeleted": "Odkaz odstraněn",
|
||||||
"shareDeletedDescription": "Odkaz byl odstraněn",
|
"shareDeletedDescription": "Odkaz byl odstraněn",
|
||||||
|
"shareDelete": "Smazat odkaz ke sdílení",
|
||||||
|
"shareDeleteConfirm": "Potvrdit smazání odkazu ke sdílení",
|
||||||
|
"shareQuestionRemove": "Jste si jisti, že chcete smazat tento odkaz ke sdílení?",
|
||||||
|
"shareMessageRemove": "Jakmile bude smazán, odkaz přestane fungovat a všichni, kdo jej používají, ztratí přístup k prostředku.",
|
||||||
"shareTokenDescription": "Přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v záhlaví požadavku. Tyto údaje musí být předány klientovi na každé žádosti o ověřený přístup.",
|
"shareTokenDescription": "Přístupový token může být předán dvěma způsoby: jako parametr dotazu nebo v záhlaví požadavku. Tyto údaje musí být předány klientovi na každé žádosti o ověřený přístup.",
|
||||||
"accessToken": "Přístupový token",
|
"accessToken": "Přístupový token",
|
||||||
"usageExamples": "Příklady použití",
|
"usageExamples": "Příklady použití",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Po odstranění tohoto uživatele již nebude mít přístup k organizaci. Vždy je můžete znovu pozvat později, ale budou muset pozvání znovu přijmout.",
|
"userMessageOrgRemove": "Po odstranění tohoto uživatele již nebude mít přístup k organizaci. Vždy je můžete znovu pozvat později, ale budou muset pozvání znovu přijmout.",
|
||||||
"userRemoveOrgConfirm": "Potvrdit odebrání uživatele",
|
"userRemoveOrgConfirm": "Potvrdit odebrání uživatele",
|
||||||
"userRemoveOrg": "Odebrat uživatele z organizace",
|
"userRemoveOrg": "Odebrat uživatele z organizace",
|
||||||
|
"userQuestionOrgRemoveSelf": "Jste si jisti, že se chcete odstranit z této organizace?",
|
||||||
|
"userMessageOrgRemoveSelf": "Okamžitě ztratíte přístup. Administrátor vás může později znovu pozvat, ale budete muset přijmout nové pozvání.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Potvrdit odstranění sebe",
|
||||||
|
"userRemoveOrgSelf": "Odstranit se z organizace",
|
||||||
|
"userRemoveOrgSelfWarning": "Okamžitě ztratíte přístup k této organizaci.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "ODSTRANIT SE Z ORGANIZACE",
|
||||||
"users": "Uživatelé",
|
"users": "Uživatelé",
|
||||||
"accessRoleMember": "Člen",
|
"accessRoleMember": "Člen",
|
||||||
"accessRoleOwner": "Vlastník",
|
"accessRoleOwner": "Vlastník",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Neplatná e-mailová adresa",
|
"emailInvalid": "Neplatná e-mailová adresa",
|
||||||
"inviteValidityDuration": "Zvolte prosím dobu trvání",
|
"inviteValidityDuration": "Zvolte prosím dobu trvání",
|
||||||
"accessRoleSelectPlease": "Vyberte prosím roli",
|
"accessRoleSelectPlease": "Vyberte prosím roli",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Odebrat přístup správce?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Po uložení již nebudete mít oprávnění správce v této organizaci. Další administrátor vám může přístup obnovit, pokud bude potřeba.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Odebrat mé administrátorské oprávnění",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "ODEBRAT MÉ ADMINISTRÁTORSKÉ OPRÁVNĚNÍ",
|
||||||
|
"ownerMustRetainAdminRole": "Vlastník organizace musí zachovat alespoň jednu roli správce.",
|
||||||
"usernameRequired": "Uživatelské jméno je povinné",
|
"usernameRequired": "Uživatelské jméno je povinné",
|
||||||
"idpSelectPlease": "Vyberte poskytovatele identity",
|
"idpSelectPlease": "Vyberte poskytovatele identity",
|
||||||
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.",
|
"targetNoOneDescription": "Přidáním více než jednoho cíle se umožní vyvážení zatížení.",
|
||||||
"targetsSubmit": "Uložit cíle",
|
"targetsSubmit": "Uložit cíle",
|
||||||
"addTarget": "Add Target",
|
"addTarget": "Add Target",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round robin routing nebude fungovat mezi lokalitami, které nejsou připojeny ke stejnému uzlu, ale failover bude fungovat.",
|
||||||
"targetErrorInvalidIp": "Neplatná IP adresa",
|
"targetErrorInvalidIp": "Neplatná IP adresa",
|
||||||
"targetErrorInvalidIpDescription": "Zadejte prosím platnou IP adresu nebo název hostitele",
|
"targetErrorInvalidIpDescription": "Zadejte prosím platnou IP adresu nebo název hostitele",
|
||||||
"targetErrorInvalidPort": "Neplatný port",
|
"targetErrorInvalidPort": "Neplatný port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Platné heslo",
|
"validPassword": "Platné heslo",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Zobrazit",
|
||||||
|
"configManaged": "Správa konfigurace",
|
||||||
"connectedClient": "Připojený klient",
|
"connectedClient": "Připojený klient",
|
||||||
"resourceBlocked": "Zablokované zdroje",
|
"resourceBlocked": "Zablokované zdroje",
|
||||||
"droppedByRule": "Zrušeno pravidlem",
|
"droppedByRule": "Zrušeno pravidlem",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Přeposlat události přímo do vašeho účtu Datadog účtu. Brzy přijde.",
|
"streamingDatadogDescription": "Přeposlat události přímo do vašeho účtu Datadog účtu. Brzy přijde.",
|
||||||
"streamingTypePickerDescription": "Vyberte cílový typ pro začátek.",
|
"streamingTypePickerDescription": "Vyberte cílový typ pro začátek.",
|
||||||
"streamingFailedToLoad": "Nepodařilo se načíst destinace",
|
"streamingLastSyncError": "Došlo k chybě při poslední synchronizaci",
|
||||||
"streamingUnexpectedError": "Došlo k neočekávané chybě.",
|
"streamingUnexpectedError": "Došlo k neočekávané chybě.",
|
||||||
"streamingFailedToUpdate": "Nepodařilo se aktualizovat cíl",
|
"streamingFailedToUpdate": "Nepodařilo se aktualizovat cíl",
|
||||||
"streamingDeletedSuccess": "Cíl byl úspěšně odstraněn",
|
"streamingDeletedSuccess": "Cíl byl úspěšně odstraněn",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Upravit cíl",
|
"S3DestEditTitle": "Upravit cíl",
|
||||||
"S3DestAddTitle": "Přidat S3 cíl",
|
"S3DestAddTitle": "Přidat S3 cíl",
|
||||||
"S3DestEditDescription": "Aktualizujte konfiguraci tohoto S3 cíle pro streamování událostí.",
|
"S3DestEditDescription": "Aktualizujte konfiguraci tohoto S3 cíle pro streamování událostí.",
|
||||||
"S3DestAddDescription": "Konfigurujte nový S3 koncový bod pro přijímání událostí vaší organizace.",
|
"S3DestAddDescription": "Nakonfigurujte nový Amazon S3 (nebo S3-kompatibilní) bucket, aby přijímal události vaší organizace.",
|
||||||
|
"s3DestTabSettings": "Nastavení",
|
||||||
|
"s3DestTabFormat": "Formát",
|
||||||
|
"s3DestNameLabel": "Jméno",
|
||||||
|
"s3DestNamePlaceholder": "Moje cílové S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID přístupového klíče AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Tajný přístupový klíč AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Váš tajný přístupový klíč AWS",
|
||||||
|
"s3DestRegionLabel": "Oblast AWS",
|
||||||
|
"s3DestBucketLabel": "Název bucketu",
|
||||||
|
"s3DestPrefixLabel": "Předpona klíče (volitelné)",
|
||||||
|
"s3DestPrefixDescription": "Volitelná cesta předpony přidaná ke každému objektovému klíči. Objekty jsou uloženy na {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Vlastní koncový bod (volitelné)",
|
||||||
|
"s3DestEndpointDescription": "Přepište koncový bod S3 pro S3-kompatibilní úložiště, jako je MinIO nebo Cloudflare R2. Nechte prázdné pro standardní AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Komprese Gzip",
|
||||||
|
"s3DestGzipDescription": "Komprimujte každý nahraný objekt pomocí gzip. Snižuje náklady na uložení a velikost nahrávání.",
|
||||||
|
"s3DestFormatTitle": "Formát souboru",
|
||||||
|
"s3DestFormatDescription": "Jak jsou události serializovány v každém nahraném objektu.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Každý objekt je pole JSON záznamů událostí. Kompatibilní s většinou analytických nástrojů.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Každý objekt obsahuje jeden JSON záznam na řádku (newline-delimited JSON). Kompatibilní s Athena, BigQuery a Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Každý objekt je soubor CSV podle RFC-4180 s řádkem záhlaví. Názvy sloupců jsou odvozeny z polí dat událostí.",
|
||||||
|
"s3DestSaveChanges": "Uložit změny",
|
||||||
|
"s3DestCreateDestination": "Vytvořit destinaci",
|
||||||
|
"s3DestUpdatedSuccess": "Destinace úspěšně aktualizována",
|
||||||
|
"s3DestCreatedSuccess": "Destinace úspěšně vytvořena",
|
||||||
|
"s3DestUpdateFailed": "Aktualizace destinace se nezdařila",
|
||||||
|
"s3DestCreateFailed": "Vytvoření destinace se nezdařilo",
|
||||||
"datadogDestEditTitle": "Upravit cíl",
|
"datadogDestEditTitle": "Upravit cíl",
|
||||||
"datadogDestAddTitle": "Přidat Datadog cíl",
|
"datadogDestAddTitle": "Přidat Datadog cíl",
|
||||||
"datadogDestEditDescription": "Aktualizujte konfiguraci tohoto Datadog cíle pro streamování událostí.",
|
"datadogDestEditDescription": "Aktualizujte konfiguraci tohoto Datadog cíle pro streamování událostí.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Fehler beim Löschen des Links",
|
"shareErrorDeleteMessage": "Fehler beim Löschen des Links",
|
||||||
"shareDeleted": "Link gelöscht",
|
"shareDeleted": "Link gelöscht",
|
||||||
"shareDeletedDescription": "Der Link wurde gelöscht",
|
"shareDeletedDescription": "Der Link wurde gelöscht",
|
||||||
|
"shareDelete": "Freigabelink löschen",
|
||||||
|
"shareDeleteConfirm": "Löschen des Freigabelinks bestätigen",
|
||||||
|
"shareQuestionRemove": "Sind Sie sicher, dass Sie diesen Freigabelink löschen möchten?",
|
||||||
|
"shareMessageRemove": "Nach dem Löschen funktioniert der Link nicht mehr, und jeder, der ihn nutzt, verliert den Zugriff auf die Ressource.",
|
||||||
"shareTokenDescription": "Das Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Request-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.",
|
"shareTokenDescription": "Das Zugriffstoken kann auf zwei Arten übergeben werden: als Abfrageparameter oder in den Request-Headern. Diese müssen vom Client auf jeder Anfrage für authentifizierten Zugriff weitergegeben werden.",
|
||||||
"accessToken": "Zugangs-Token",
|
"accessToken": "Zugangs-Token",
|
||||||
"usageExamples": "Nutzungsbeispiele",
|
"usageExamples": "Nutzungsbeispiele",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Nach dem Entfernen hat dieser Benutzer keinen Zugriff mehr auf die Organisation. Sie können ihn später jederzeit wieder einladen, aber er muss die Einladung erneut annehmen.",
|
"userMessageOrgRemove": "Nach dem Entfernen hat dieser Benutzer keinen Zugriff mehr auf die Organisation. Sie können ihn später jederzeit wieder einladen, aber er muss die Einladung erneut annehmen.",
|
||||||
"userRemoveOrgConfirm": "Entfernen des Benutzers bestätigen",
|
"userRemoveOrgConfirm": "Entfernen des Benutzers bestätigen",
|
||||||
"userRemoveOrg": "Benutzer aus der Organisation entfernen",
|
"userRemoveOrg": "Benutzer aus der Organisation entfernen",
|
||||||
|
"userQuestionOrgRemoveSelf": "Sind Sie sicher, dass Sie sich aus dieser Organisation entfernen möchten?",
|
||||||
|
"userMessageOrgRemoveSelf": "Sie verlieren sofort den Zugriff. Ein Administrator kann Sie später erneut einladen, aber Sie müssen eine neue Einladung annehmen.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Entfernung bestätigen",
|
||||||
|
"userRemoveOrgSelf": "Sich selbst aus der Organisation entfernen",
|
||||||
|
"userRemoveOrgSelfWarning": "Sie verlieren sofort den Zugriff auf diese Organisation.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "ENTFERNUNG MICH SELBST AUS DER ORGANISATION",
|
||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"accessRoleMember": "Mitglied",
|
"accessRoleMember": "Mitglied",
|
||||||
"accessRoleOwner": "Eigentümer",
|
"accessRoleOwner": "Eigentümer",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Ungültige E-Mail-Adresse",
|
"emailInvalid": "Ungültige E-Mail-Adresse",
|
||||||
"inviteValidityDuration": "Bitte wählen Sie eine Dauer",
|
"inviteValidityDuration": "Bitte wählen Sie eine Dauer",
|
||||||
"accessRoleSelectPlease": "Bitte wählen Sie eine Rolle",
|
"accessRoleSelectPlease": "Bitte wählen Sie eine Rolle",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Möchten Sie Ihren Administratorzugriff entfernen?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Nach dem Speichern haben Sie keine Administratorrechte mehr in dieser Organisation. Ein anderer Administrator kann den Zugriff bei Bedarf wiederherstellen.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Meinen Administratorzugriff entfernen",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "NIMM MEINEN ADMIN-ZUGRIFF WEG",
|
||||||
|
"ownerMustRetainAdminRole": "Der Organisationsinhaber muss mindestens eine Administratorrolle behalten.",
|
||||||
"usernameRequired": "Benutzername ist erforderlich",
|
"usernameRequired": "Benutzername ist erforderlich",
|
||||||
"idpSelectPlease": "Bitte wählen Sie einen Identitätsanbieter",
|
"idpSelectPlease": "Bitte wählen Sie einen Identitätsanbieter",
|
||||||
"idpGenericOidc": "Generischer OAuth2/OIDC-Anbieter.",
|
"idpGenericOidc": "Generischer OAuth2/OIDC-Anbieter.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.",
|
"targetNoOneDescription": "Das Hinzufügen von mehr als einem Ziel aktiviert den Lastausgleich.",
|
||||||
"targetsSubmit": "Ziele speichern",
|
"targetsSubmit": "Ziele speichern",
|
||||||
"addTarget": "Ziel hinzufügen",
|
"addTarget": "Ziel hinzufügen",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round-Robin-Routing funktioniert nicht zwischen Standorten, die nicht mit demselben Knoten verbunden sind, aber Failover funktioniert.",
|
||||||
"targetErrorInvalidIp": "Ungültige IP-Adresse",
|
"targetErrorInvalidIp": "Ungültige IP-Adresse",
|
||||||
"targetErrorInvalidIpDescription": "Bitte geben Sie eine gültige IP-Adresse oder einen Hostnamen ein",
|
"targetErrorInvalidIpDescription": "Bitte geben Sie eine gültige IP-Adresse oder einen Hostnamen ein",
|
||||||
"targetErrorInvalidPort": "Ungültiger Port",
|
"targetErrorInvalidPort": "Ungültiger Port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Gültiges Passwort",
|
"validPassword": "Gültiges Passwort",
|
||||||
"validEmail": "Gültige E-Mail-Adresse",
|
"validEmail": "Gültige E-Mail-Adresse",
|
||||||
"validSSO": "Gültige SSO-Anmeldung",
|
"validSSO": "Gültige SSO-Anmeldung",
|
||||||
|
"view": "Ansehen",
|
||||||
|
"configManaged": "Konfiguration verwaltet",
|
||||||
"connectedClient": "Verbundenes Gerät",
|
"connectedClient": "Verbundenes Gerät",
|
||||||
"resourceBlocked": "Ressource blockiert",
|
"resourceBlocked": "Ressource blockiert",
|
||||||
"droppedByRule": "Abgelegt durch Regel",
|
"droppedByRule": "Abgelegt durch Regel",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Events direkt an Ihr Datadog Konto weiterleiten. Kommen Sie bald.",
|
"streamingDatadogDescription": "Events direkt an Ihr Datadog Konto weiterleiten. Kommen Sie bald.",
|
||||||
"streamingTypePickerDescription": "Wählen Sie einen Zieltyp aus, um loszulegen.",
|
"streamingTypePickerDescription": "Wählen Sie einen Zieltyp aus, um loszulegen.",
|
||||||
"streamingFailedToLoad": "Fehler beim Laden der Ziele",
|
"streamingLastSyncError": "Beim letzten Synchronisieren ist ein Fehler aufgetreten.",
|
||||||
"streamingUnexpectedError": "Ein unerwarteter Fehler ist aufgetreten.",
|
"streamingUnexpectedError": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||||
"streamingFailedToUpdate": "Fehler beim Aktualisieren des Ziels",
|
"streamingFailedToUpdate": "Fehler beim Aktualisieren des Ziels",
|
||||||
"streamingDeletedSuccess": "Ziel erfolgreich gelöscht",
|
"streamingDeletedSuccess": "Ziel erfolgreich gelöscht",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Ziel bearbeiten",
|
"S3DestEditTitle": "Ziel bearbeiten",
|
||||||
"S3DestAddTitle": "S3-Ziel hinzufügen",
|
"S3DestAddTitle": "S3-Ziel hinzufügen",
|
||||||
"S3DestEditDescription": "Konfiguration für dieses S3-Ereignis-Streamingziel aktualisieren.",
|
"S3DestEditDescription": "Konfiguration für dieses S3-Ereignis-Streamingziel aktualisieren.",
|
||||||
"S3DestAddDescription": "Neuen S3-Endpunkt konfigurieren, um die Ereignisse Ihrer Organisation zu erhalten.",
|
"S3DestAddDescription": "Konfigurieren Sie einen neuen Amazon S3 (oder S3-kompatiblen) Bucket, um die Ereignisse Ihrer Organisation zu empfangen.",
|
||||||
|
"s3DestTabSettings": "Einstellungen",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Name",
|
||||||
|
"s3DestNamePlaceholder": "Mein S3-Ziel",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS-Zugriffsschlüssel-ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS-Geheimzugriffsschlüssel",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Ihr AWS-Geheimzugriffsschlüssel",
|
||||||
|
"s3DestRegionLabel": "AWS-Region",
|
||||||
|
"s3DestBucketLabel": "Bucket-Name",
|
||||||
|
"s3DestPrefixLabel": "Schlüssel-Präfix (optional)",
|
||||||
|
"s3DestPrefixDescription": "Optionales Pfadpräfix, das jedem Objektschlüssel vorangestellt wird. Objekte werden unter {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename} gespeichert.",
|
||||||
|
"s3DestEndpointLabel": "Benutzerdefinierter Endpunkt (optional)",
|
||||||
|
"s3DestEndpointDescription": "Überschreiben Sie den S3-Endpunkt für S3-kompatiblen Speicher wie MinIO oder Cloudflare R2. Lassen Sie das Feld leer für standardmäßiges AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip-Komprimierung",
|
||||||
|
"s3DestGzipDescription": "Jedes hochgeladene Objekt mit Gzip komprimieren. Reduziert die Speicherkosten und die Upload-Größe.",
|
||||||
|
"s3DestFormatTitle": "Dateiformat",
|
||||||
|
"s3DestFormatDescription": "Wie Ereignisse in jedem hochgeladenen Objekt serialisiert werden.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Jedes Objekt ist ein JSON-Array von Ereignisdaten. Kompatibel mit den meisten Analysetools.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Jedes Objekt enthält einen JSON-Datensatz pro Zeile (newline-delimited JSON). Kompatibel mit Athena, BigQuery und Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Jedes Objekt ist eine RFC-4180 CSV-Datei mit einer Kopfzeile. Spaltennamen werden aus den Ereignisdatenfeldern abgeleitet.",
|
||||||
|
"s3DestSaveChanges": "Änderungen speichern",
|
||||||
|
"s3DestCreateDestination": "Ziel erstellen",
|
||||||
|
"s3DestUpdatedSuccess": "Ziel erfolgreich aktualisiert",
|
||||||
|
"s3DestCreatedSuccess": "Ziel erfolgreich erstellt",
|
||||||
|
"s3DestUpdateFailed": "Fehler beim Aktualisieren des Ziels",
|
||||||
|
"s3DestCreateFailed": "Fehler beim Erstellen des Ziels",
|
||||||
"datadogDestEditTitle": "Ziel bearbeiten",
|
"datadogDestEditTitle": "Ziel bearbeiten",
|
||||||
"datadogDestAddTitle": "Datadog-Ziel hinzufügen",
|
"datadogDestAddTitle": "Datadog-Ziel hinzufügen",
|
||||||
"datadogDestEditDescription": "Konfiguration für dieses Datadog-Ereignis-Streamingziel aktualisieren.",
|
"datadogDestEditDescription": "Konfiguration für dieses Datadog-Ereignis-Streamingziel aktualisieren.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "An error occurred deleting link",
|
"shareErrorDeleteMessage": "An error occurred deleting link",
|
||||||
"shareDeleted": "Link deleted",
|
"shareDeleted": "Link deleted",
|
||||||
"shareDeletedDescription": "The link has been deleted",
|
"shareDeletedDescription": "The link has been deleted",
|
||||||
|
"shareDelete": "Delete Share Link",
|
||||||
|
"shareDeleteConfirm": "Confirm Delete Share Link",
|
||||||
|
"shareQuestionRemove": "Are you sure you want to delete this share link?",
|
||||||
|
"shareMessageRemove": "Once deleted, the link will no longer work and anyone using it will lose access to the resource.",
|
||||||
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
"shareTokenDescription": "The access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
|
||||||
"accessToken": "Access Token",
|
"accessToken": "Access Token",
|
||||||
"usageExamples": "Usage Examples",
|
"usageExamples": "Usage Examples",
|
||||||
@@ -204,11 +208,33 @@
|
|||||||
"resourcesSearch": "Search resources...",
|
"resourcesSearch": "Search resources...",
|
||||||
"resourceAdd": "Add Resource",
|
"resourceAdd": "Add Resource",
|
||||||
"resourceErrorDelte": "Error deleting resource",
|
"resourceErrorDelte": "Error deleting resource",
|
||||||
|
"resourcePoliciesTitle": "Manage Resource Policies",
|
||||||
|
"resourcePoliciesAttachedResourcesColumnTitle": "Attached resources",
|
||||||
|
"resourcePoliciesAttachedResources": "{count} resource(s)",
|
||||||
|
"resourcePoliciesAttachedResourcesEmpty": "no resources",
|
||||||
|
"resourcePoliciesDescription": "Create and manage authentication policies to control access to your resources",
|
||||||
|
"resourcePoliciesSearch": "Search policies...",
|
||||||
|
"resourcePoliciesAdd": "Add Policy",
|
||||||
|
"resourcePoliciesDefaultBadgeText": "Default policy",
|
||||||
|
"resourcePoliciesCreate": "Create Resource Policy",
|
||||||
|
"resourcePoliciesCreateDescription": "Follow the steps below to create a new policy",
|
||||||
|
"resourcePolicyName": "Policy Name",
|
||||||
|
"resourcePolicyNameDescription": "Give this policy a name to identify it across your resources",
|
||||||
|
"resourcePolicyNamePlaceholder": "e.g. Internal Access Policy",
|
||||||
|
"resourcePoliciesSeeAll": "See All Policies",
|
||||||
|
"resourcePolicyAuthMethodAdd": "Add Authentication Method",
|
||||||
|
"resourcePolicyOtpEmailAdd": "Add OTP emails",
|
||||||
|
"resourcePolicyRulesAdd": "Add Rules",
|
||||||
|
"resourcePolicyAuthMethodsDescription": "Allow access to resources via additional auth methods",
|
||||||
|
"resourcePolicyUsersRolesDescription": "Configure which users and roles can visit associated resources",
|
||||||
|
"rulesResourcePolicyDescription": "Configure rules to control access resources associated to this policy",
|
||||||
"authentication": "Authentication",
|
"authentication": "Authentication",
|
||||||
"protected": "Protected",
|
"protected": "Protected",
|
||||||
"notProtected": "Not Protected",
|
"notProtected": "Not Protected",
|
||||||
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
||||||
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
||||||
|
"resourcePolicyMessageRemove": "Once removed, the resource policy will no longer be accessible. All resources associated with the resource will be unlinked and left without authentication.",
|
||||||
|
"resourcePolicyQuestionRemove": "Are you sure you want to remove the resource policy from the organization?",
|
||||||
"resourceHTTP": "HTTPS Resource",
|
"resourceHTTP": "HTTPS Resource",
|
||||||
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
||||||
"resourceRaw": "Raw TCP/UDP Resource",
|
"resourceRaw": "Raw TCP/UDP Resource",
|
||||||
@@ -249,6 +275,8 @@
|
|||||||
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
"resourceLearnRaw": "Learn how to configure TCP/UDP resources",
|
||||||
"resourceBack": "Back to Resources",
|
"resourceBack": "Back to Resources",
|
||||||
"resourceGoTo": "Go to Resource",
|
"resourceGoTo": "Go to Resource",
|
||||||
|
"resourcePolicyDelete": "Delete Resource Policy",
|
||||||
|
"resourcePolicyDeleteConfirm": "Confirm Delete Resource Policy",
|
||||||
"resourceDelete": "Delete Resource",
|
"resourceDelete": "Delete Resource",
|
||||||
"resourceDeleteConfirm": "Confirm Delete Resource",
|
"resourceDeleteConfirm": "Confirm Delete Resource",
|
||||||
"visibility": "Visibility",
|
"visibility": "Visibility",
|
||||||
@@ -261,6 +289,8 @@
|
|||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"resourceSettingDescription": "Configure the settings on the resource",
|
"resourceSettingDescription": "Configure the settings on the resource",
|
||||||
"resourceSetting": "{resourceName} Settings",
|
"resourceSetting": "{resourceName} Settings",
|
||||||
|
"resourcePolicySettingDescription": "Configure the settings on the resource policy",
|
||||||
|
"resourcePolicySetting": "{policyName} Settings",
|
||||||
"alwaysAllow": "Bypass Auth",
|
"alwaysAllow": "Bypass Auth",
|
||||||
"alwaysDeny": "Block Access",
|
"alwaysDeny": "Block Access",
|
||||||
"passToAuth": "Pass to Auth",
|
"passToAuth": "Pass to Auth",
|
||||||
@@ -523,6 +553,12 @@
|
|||||||
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
||||||
"userRemoveOrgConfirm": "Confirm Remove User",
|
"userRemoveOrgConfirm": "Confirm Remove User",
|
||||||
"userRemoveOrg": "Remove User from Organization",
|
"userRemoveOrg": "Remove User from Organization",
|
||||||
|
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
|
||||||
|
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
|
||||||
|
"userRemoveOrgSelf": "Remove yourself from the organization",
|
||||||
|
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"accessRoleMember": "Member",
|
"accessRoleMember": "Member",
|
||||||
"accessRoleOwner": "Owner",
|
"accessRoleOwner": "Owner",
|
||||||
@@ -531,6 +567,11 @@
|
|||||||
"emailInvalid": "Invalid email address",
|
"emailInvalid": "Invalid email address",
|
||||||
"inviteValidityDuration": "Please select a duration",
|
"inviteValidityDuration": "Please select a duration",
|
||||||
"accessRoleSelectPlease": "Please select a role",
|
"accessRoleSelectPlease": "Please select a role",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
|
||||||
|
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
|
||||||
"usernameRequired": "Username is required",
|
"usernameRequired": "Username is required",
|
||||||
"idpSelectPlease": "Please select an identity provider",
|
"idpSelectPlease": "Please select an identity provider",
|
||||||
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
"idpGenericOidc": "Generic OAuth2/OIDC provider.",
|
||||||
@@ -658,6 +699,7 @@
|
|||||||
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
||||||
"targetsSubmit": "Save Targets",
|
"targetsSubmit": "Save Targets",
|
||||||
"addTarget": "Add Target",
|
"addTarget": "Add Target",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round robin routing will not work between sites that are not connected to the same node, but failover will work.",
|
||||||
"targetErrorInvalidIp": "Invalid IP address",
|
"targetErrorInvalidIp": "Invalid IP address",
|
||||||
"targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname",
|
"targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname",
|
||||||
"targetErrorInvalidPort": "Invalid port",
|
"targetErrorInvalidPort": "Invalid port",
|
||||||
@@ -731,6 +773,16 @@
|
|||||||
"rulesNoOne": "No rules. Add a rule using the form.",
|
"rulesNoOne": "No rules. Add a rule using the form.",
|
||||||
"rulesOrder": "Rules are evaluated by priority in ascending order.",
|
"rulesOrder": "Rules are evaluated by priority in ascending order.",
|
||||||
"rulesSubmit": "Save Rules",
|
"rulesSubmit": "Save Rules",
|
||||||
|
"policyErrorCreate": "Error creating policy",
|
||||||
|
"policyErrorCreateDescription": "An error occurred when creating the policy",
|
||||||
|
"policyErrorCreateMessageDescription": "An unexpected error occurred",
|
||||||
|
"policyErrorUpdate": "Error updating policy",
|
||||||
|
"policyErrorUpdateDescription": "An error occurred when updating the policy",
|
||||||
|
"policyErrorUpdateMessageDescription": "An unexpected error occurred",
|
||||||
|
"policyCreatedSuccess": "Resource policy succesfully created",
|
||||||
|
"policyUpdatedSuccess": "Resource policy succesfully updated",
|
||||||
|
"authMethodsSave": "Save auth methods",
|
||||||
|
"rulesSave": "Save Rules",
|
||||||
"resourceErrorCreate": "Error creating resource",
|
"resourceErrorCreate": "Error creating resource",
|
||||||
"resourceErrorCreateDescription": "An error occurred when creating the resource",
|
"resourceErrorCreateDescription": "An error occurred when creating the resource",
|
||||||
"resourceErrorCreateMessage": "Error creating resource:",
|
"resourceErrorCreateMessage": "Error creating resource:",
|
||||||
@@ -794,6 +846,16 @@
|
|||||||
"pincodeAdd": "Add PIN Code",
|
"pincodeAdd": "Add PIN Code",
|
||||||
"pincodeRemove": "Remove PIN Code",
|
"pincodeRemove": "Remove PIN Code",
|
||||||
"resourceAuthMethods": "Authentication Methods",
|
"resourceAuthMethods": "Authentication Methods",
|
||||||
|
"resourcePolicyAuthMethodsEmpty": "No authentication method",
|
||||||
|
"resourcePolicyOtpEmpty": "No one time password",
|
||||||
|
"resourcePolicyReadOnly": "This policy is Read only",
|
||||||
|
"resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it on this page.",
|
||||||
|
"resourcePolicyTypeSave": "Save Resource type",
|
||||||
|
"resourcePolicySelect": "Select resource policy",
|
||||||
|
"resourcePolicySelectError": "Select a resource policy",
|
||||||
|
"resourcePolicyNotFound": "Policy not found",
|
||||||
|
"resourcePolicySearch": "Search policies",
|
||||||
|
"resourcePolicyRulesEmpty": "No authentication rules",
|
||||||
"resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods",
|
"resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods",
|
||||||
"resourceAuthSettingsSave": "Saved successfully",
|
"resourceAuthSettingsSave": "Saved successfully",
|
||||||
"resourceAuthSettingsSaveDescription": "Authentication settings have been saved",
|
"resourceAuthSettingsSaveDescription": "Authentication settings have been saved",
|
||||||
@@ -829,6 +891,12 @@
|
|||||||
"resourcePincodeSetupTitle": "Set Pincode",
|
"resourcePincodeSetupTitle": "Set Pincode",
|
||||||
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
|
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
|
||||||
"resourceRoleDescription": "Admins can always access this resource.",
|
"resourceRoleDescription": "Admins can always access this resource.",
|
||||||
|
"resourcePolicySelectTitle": "Resource Access Policy",
|
||||||
|
"resourcePolicySelectDescription": "Select the resource policy type for authentication",
|
||||||
|
"resourcePolicyInline": "Inline Resource Policy",
|
||||||
|
"resourcePolicyInlineDescription": "Access Policy scoped to only this resource",
|
||||||
|
"resourcePolicyShared": "Shared Resource Policy",
|
||||||
|
"resourcePolicySharedDescription": "Access Policy shared accross multiple resources",
|
||||||
"resourceUsersRoles": "Access Controls",
|
"resourceUsersRoles": "Access Controls",
|
||||||
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
||||||
"resourceUsersRolesSubmit": "Save Access Controls",
|
"resourceUsersRolesSubmit": "Save Access Controls",
|
||||||
@@ -1358,6 +1426,8 @@
|
|||||||
"sidebarResources": "Resources",
|
"sidebarResources": "Resources",
|
||||||
"sidebarProxyResources": "Public",
|
"sidebarProxyResources": "Public",
|
||||||
"sidebarClientResources": "Private",
|
"sidebarClientResources": "Private",
|
||||||
|
"sidebarPolicies": "Policies",
|
||||||
|
"sidebarResourcePolicies": "Resources",
|
||||||
"sidebarAccessControl": "Access Control",
|
"sidebarAccessControl": "Access Control",
|
||||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||||
"sidebarTeam": "Team",
|
"sidebarTeam": "Team",
|
||||||
@@ -2652,6 +2722,8 @@
|
|||||||
"validPassword": "Valid Password",
|
"validPassword": "Valid Password",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "View",
|
||||||
|
"configManaged": "Config Managed",
|
||||||
"connectedClient": "Connected Client",
|
"connectedClient": "Connected Client",
|
||||||
"resourceBlocked": "Resource Blocked",
|
"resourceBlocked": "Resource Blocked",
|
||||||
"droppedByRule": "Dropped by Rule",
|
"droppedByRule": "Dropped by Rule",
|
||||||
@@ -3062,7 +3134,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
|
"streamingDatadogDescription": "Forward events directly to your Datadog account.",
|
||||||
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
"streamingTypePickerDescription": "Choose a destination type to get started.",
|
||||||
"streamingFailedToLoad": "Failed to load destinations",
|
"streamingLastSyncError": "An error occurred on the last sync",
|
||||||
"streamingUnexpectedError": "An unexpected error occurred.",
|
"streamingUnexpectedError": "An unexpected error occurred.",
|
||||||
"streamingFailedToUpdate": "Failed to update destination",
|
"streamingFailedToUpdate": "Failed to update destination",
|
||||||
"streamingDeletedSuccess": "Destination deleted successfully",
|
"streamingDeletedSuccess": "Destination deleted successfully",
|
||||||
@@ -3079,7 +3151,34 @@
|
|||||||
"S3DestEditTitle": "Edit Destination",
|
"S3DestEditTitle": "Edit Destination",
|
||||||
"S3DestAddTitle": "Add S3 Destination",
|
"S3DestAddTitle": "Add S3 Destination",
|
||||||
"S3DestEditDescription": "Update the configuration for this S3 event streaming destination.",
|
"S3DestEditDescription": "Update the configuration for this S3 event streaming destination.",
|
||||||
"S3DestAddDescription": "Configure a new S3 endpoint to receive your organization's events.",
|
"S3DestAddDescription": "Configure a new Amazon S3 (or S3-compatible) bucket to receive your organization's events.",
|
||||||
|
"s3DestTabSettings": "Settings",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Name",
|
||||||
|
"s3DestNamePlaceholder": "My S3 destination",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS Access Key ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS Secret Access Key",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Your AWS secret access key",
|
||||||
|
"s3DestRegionLabel": "AWS Region",
|
||||||
|
"s3DestBucketLabel": "Bucket Name",
|
||||||
|
"s3DestPrefixLabel": "Key Prefix (optional)",
|
||||||
|
"s3DestPrefixDescription": "Optional path prefix prepended to every object key. Objects are stored at {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Custom Endpoint (optional)",
|
||||||
|
"s3DestEndpointDescription": "Override the S3 endpoint for S3-compatible storage such as MinIO or Cloudflare R2. Leave blank for standard AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip compression",
|
||||||
|
"s3DestGzipDescription": "Compress each uploaded object with gzip. Reduces storage costs and upload size.",
|
||||||
|
"s3DestFormatTitle": "File Format",
|
||||||
|
"s3DestFormatDescription": "How events are serialised inside each uploaded object.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Each object is a JSON array of event records. Compatible with most analytics tools.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Each object contains one JSON record per line (newline-delimited JSON). Compatible with Athena, BigQuery, and Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Each object is an RFC-4180 CSV file with a header row. Column names are derived from the event data fields.",
|
||||||
|
"s3DestSaveChanges": "Save Changes",
|
||||||
|
"s3DestCreateDestination": "Create Destination",
|
||||||
|
"s3DestUpdatedSuccess": "Destination updated successfully",
|
||||||
|
"s3DestCreatedSuccess": "Destination created successfully",
|
||||||
|
"s3DestUpdateFailed": "Failed to update destination",
|
||||||
|
"s3DestCreateFailed": "Failed to create destination",
|
||||||
"datadogDestEditTitle": "Edit Destination",
|
"datadogDestEditTitle": "Edit Destination",
|
||||||
"datadogDestAddTitle": "Add Datadog Destination",
|
"datadogDestAddTitle": "Add Datadog Destination",
|
||||||
"datadogDestEditDescription": "Update the configuration for this Datadog event streaming destination.",
|
"datadogDestEditDescription": "Update the configuration for this Datadog event streaming destination.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Se ha producido un error al eliminar el enlace",
|
"shareErrorDeleteMessage": "Se ha producido un error al eliminar el enlace",
|
||||||
"shareDeleted": "Enlace eliminado",
|
"shareDeleted": "Enlace eliminado",
|
||||||
"shareDeletedDescription": "El enlace ha sido eliminado",
|
"shareDeletedDescription": "El enlace ha sido eliminado",
|
||||||
|
"shareDelete": "Borrar Enlace Compartido",
|
||||||
|
"shareDeleteConfirm": "Confirmar Borrado del Enlace Compartido",
|
||||||
|
"shareQuestionRemove": "¿Está seguro de que desea borrar este enlace compartido?",
|
||||||
|
"shareMessageRemove": "Una vez borrado, el enlace dejará de funcionar y cualquier persona que lo use perderá acceso al recurso.",
|
||||||
"shareTokenDescription": "El token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.",
|
"shareTokenDescription": "El token de acceso puede ser pasado de dos maneras: como parámetro de consulta o en las cabeceras de solicitud. Estos deben ser pasados del cliente en cada solicitud de acceso autenticado.",
|
||||||
"accessToken": "Token de acceso",
|
"accessToken": "Token de acceso",
|
||||||
"usageExamples": "Ejemplos de uso",
|
"usageExamples": "Ejemplos de uso",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Una vez eliminado, este usuario ya no tendrá acceso a la organización. Siempre puede volver a invitarlos más tarde, pero tendrán que aceptar la invitación de nuevo.",
|
"userMessageOrgRemove": "Una vez eliminado, este usuario ya no tendrá acceso a la organización. Siempre puede volver a invitarlos más tarde, pero tendrán que aceptar la invitación de nuevo.",
|
||||||
"userRemoveOrgConfirm": "Confirmar eliminar usuario",
|
"userRemoveOrgConfirm": "Confirmar eliminar usuario",
|
||||||
"userRemoveOrg": "Eliminar usuario de la organización",
|
"userRemoveOrg": "Eliminar usuario de la organización",
|
||||||
|
"userQuestionOrgRemoveSelf": "¿Está seguro de que desea eliminarse de esta organización?",
|
||||||
|
"userMessageOrgRemoveSelf": "Perderá acceso inmediatamente. Un administrador puede invitarlo de nuevo más tarde, pero necesitará aceptar una nueva invitación.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirmar Eliminarme",
|
||||||
|
"userRemoveOrgSelf": "Eliminarse de la organización",
|
||||||
|
"userRemoveOrgSelfWarning": "Perderá acceso a esta organización inmediatamente.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "ELIMINARME DE LA ORGANIZACIÓN",
|
||||||
"users": "Usuarios",
|
"users": "Usuarios",
|
||||||
"accessRoleMember": "Miembro",
|
"accessRoleMember": "Miembro",
|
||||||
"accessRoleOwner": "Propietario",
|
"accessRoleOwner": "Propietario",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Dirección de correo inválida",
|
"emailInvalid": "Dirección de correo inválida",
|
||||||
"inviteValidityDuration": "Por favor, seleccione una duración",
|
"inviteValidityDuration": "Por favor, seleccione una duración",
|
||||||
"accessRoleSelectPlease": "Por favor, seleccione un rol",
|
"accessRoleSelectPlease": "Por favor, seleccione un rol",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "¿Eliminar su acceso de administrador?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Ya no tendrá permisos de administrador en esta organización después de guardar. Otro administrador puede restaurar el acceso si es necesario.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Eliminar Mi Acceso de Administrador",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "ELIMINAR MI ACCESO DE ADMINISTRADOR",
|
||||||
|
"ownerMustRetainAdminRole": "El propietario de la organización debe mantener al menos un rol de administrador.",
|
||||||
"usernameRequired": "Nombre de usuario requerido",
|
"usernameRequired": "Nombre de usuario requerido",
|
||||||
"idpSelectPlease": "Por favor, seleccione un proveedor de identidad",
|
"idpSelectPlease": "Por favor, seleccione un proveedor de identidad",
|
||||||
"idpGenericOidc": "Proveedor OAuth2/OIDC genérico.",
|
"idpGenericOidc": "Proveedor OAuth2/OIDC genérico.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.",
|
"targetNoOneDescription": "Si se añade más de un objetivo anterior se activará el balance de carga.",
|
||||||
"targetsSubmit": "Guardar objetivos",
|
"targetsSubmit": "Guardar objetivos",
|
||||||
"addTarget": "Añadir destino",
|
"addTarget": "Añadir destino",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "El enrutamiento de turnos no funcionará entre sitios que no están conectados al mismo nodo, pero el failover funcionará.",
|
||||||
"targetErrorInvalidIp": "Dirección IP inválida",
|
"targetErrorInvalidIp": "Dirección IP inválida",
|
||||||
"targetErrorInvalidIpDescription": "Por favor, introduzca una dirección IP válida o nombre de host",
|
"targetErrorInvalidIpDescription": "Por favor, introduzca una dirección IP válida o nombre de host",
|
||||||
"targetErrorInvalidPort": "Puerto inválido",
|
"targetErrorInvalidPort": "Puerto inválido",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Contraseña válida",
|
"validPassword": "Contraseña válida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Ver",
|
||||||
|
"configManaged": "Configuración Gestionada",
|
||||||
"connectedClient": "Cliente conectado",
|
"connectedClient": "Cliente conectado",
|
||||||
"resourceBlocked": "Recurso bloqueado",
|
"resourceBlocked": "Recurso bloqueado",
|
||||||
"droppedByRule": "Soltado por regla",
|
"droppedByRule": "Soltado por regla",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Reenviar eventos directamente a tu cuenta de Datadog. Próximamente.",
|
"streamingDatadogDescription": "Reenviar eventos directamente a tu cuenta de Datadog. Próximamente.",
|
||||||
"streamingTypePickerDescription": "Elija un tipo de destino para empezar.",
|
"streamingTypePickerDescription": "Elija un tipo de destino para empezar.",
|
||||||
"streamingFailedToLoad": "Error al cargar destinos",
|
"streamingLastSyncError": "Ocurrió un error en la última sincronización.",
|
||||||
"streamingUnexpectedError": "Se ha producido un error inesperado.",
|
"streamingUnexpectedError": "Se ha producido un error inesperado.",
|
||||||
"streamingFailedToUpdate": "Error al actualizar destino",
|
"streamingFailedToUpdate": "Error al actualizar destino",
|
||||||
"streamingDeletedSuccess": "Destino eliminado correctamente",
|
"streamingDeletedSuccess": "Destino eliminado correctamente",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Editar destino",
|
"S3DestEditTitle": "Editar destino",
|
||||||
"S3DestAddTitle": "Añadir destino S3",
|
"S3DestAddTitle": "Añadir destino S3",
|
||||||
"S3DestEditDescription": "Actualice la configuración para este destino de transmisión de eventos S3.",
|
"S3DestEditDescription": "Actualice la configuración para este destino de transmisión de eventos S3.",
|
||||||
"S3DestAddDescription": "Configure un nuevo punto final S3 para recibir los eventos de su organización.",
|
"S3DestAddDescription": "Configura un nuevo bucket de Amazon S3 (o compatible con S3) para recibir los eventos de tu organización.",
|
||||||
|
"s3DestTabSettings": "Ajustes",
|
||||||
|
"s3DestTabFormat": "Formato",
|
||||||
|
"s3DestNameLabel": "Nombre",
|
||||||
|
"s3DestNamePlaceholder": "Mi destino S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID de clave de acceso de AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Clave de acceso secreta de AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Tu clave de acceso secreta de AWS",
|
||||||
|
"s3DestRegionLabel": "Región de AWS",
|
||||||
|
"s3DestBucketLabel": "Nombre del bucket",
|
||||||
|
"s3DestPrefixLabel": "Prefijo clave (opcional)",
|
||||||
|
"s3DestPrefixDescription": "Prefijo de ruta opcional preanexado a cada clave de objeto. Los objetos se almacenan en {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Punto final personalizado (opcional)",
|
||||||
|
"s3DestEndpointDescription": "Sobrescribe el punto final de S3 para almacenamiento compatible con S3 como MinIO o Cloudflare R2. Deja en blanco para el estándar AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Compresión Gzip",
|
||||||
|
"s3DestGzipDescription": "Comprime cada objeto subido con gzip. Reduce costos de almacenamiento y tamaño de carga.",
|
||||||
|
"s3DestFormatTitle": "Formato de archivo",
|
||||||
|
"s3DestFormatDescription": "Cómo se serializan los eventos dentro de cada objeto cargado.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Cada objeto es un arreglo JSON de registros de eventos. Compatible con la mayoría de las herramientas de analítica.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Cada objeto contiene un registro JSON por línea (JSON delimitado por nueva línea). Compatible con Athena, BigQuery y Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Cada objeto es un archivo CSV conforme a RFC-4180 con una fila de encabezado. Los nombres de columna se derivan de los campos de datos del evento.",
|
||||||
|
"s3DestSaveChanges": "Guardar cambios",
|
||||||
|
"s3DestCreateDestination": "Crear destino",
|
||||||
|
"s3DestUpdatedSuccess": "Destino actualizado con éxito",
|
||||||
|
"s3DestCreatedSuccess": "Destino creado con éxito",
|
||||||
|
"s3DestUpdateFailed": "No se pudo actualizar el destino",
|
||||||
|
"s3DestCreateFailed": "No se pudo crear el destino",
|
||||||
"datadogDestEditTitle": "Editar destino",
|
"datadogDestEditTitle": "Editar destino",
|
||||||
"datadogDestAddTitle": "Añadir destino Datadog",
|
"datadogDestAddTitle": "Añadir destino Datadog",
|
||||||
"datadogDestEditDescription": "Actualice la configuración para este destino de transmisión de eventos Datadog.",
|
"datadogDestEditDescription": "Actualice la configuración para este destino de transmisión de eventos Datadog.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
|
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
|
||||||
"shareDeleted": "Lien supprimé",
|
"shareDeleted": "Lien supprimé",
|
||||||
"shareDeletedDescription": "Le lien a été supprimé",
|
"shareDeletedDescription": "Le lien a été supprimé",
|
||||||
|
"shareDelete": "Supprimer le lien de partage",
|
||||||
|
"shareDeleteConfirm": "Confirmer la suppression du lien de partage",
|
||||||
|
"shareQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce lien de partage ?",
|
||||||
|
"shareMessageRemove": "Une fois supprimé, le lien ne fonctionnera plus et toute personne l'utilisant perdra l'accès à la ressource.",
|
||||||
"shareTokenDescription": "Le jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
|
"shareTokenDescription": "Le jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
|
||||||
"accessToken": "Jeton d'accès",
|
"accessToken": "Jeton d'accès",
|
||||||
"usageExamples": "Exemples d'utilisation",
|
"usageExamples": "Exemples d'utilisation",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Une fois retiré, cet utilisateur n'aura plus accès à l'organisation. Vous pouvez toujours le réinviter plus tard, mais il devra accepter l'invitation à nouveau.",
|
"userMessageOrgRemove": "Une fois retiré, cet utilisateur n'aura plus accès à l'organisation. Vous pouvez toujours le réinviter plus tard, mais il devra accepter l'invitation à nouveau.",
|
||||||
"userRemoveOrgConfirm": "Confirmer la suppression de l'utilisateur",
|
"userRemoveOrgConfirm": "Confirmer la suppression de l'utilisateur",
|
||||||
"userRemoveOrg": "Retirer l'utilisateur de l'organisation",
|
"userRemoveOrg": "Retirer l'utilisateur de l'organisation",
|
||||||
|
"userQuestionOrgRemoveSelf": "Êtes-vous sûr de vouloir vous retirer de cette organisation ?",
|
||||||
|
"userMessageOrgRemoveSelf": "Vous perdrez immédiatement l'accès. Un administrateur pourra vous inviter à nouveau plus tard, mais vous devrez accepter une nouvelle invitation.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirmer la suppression de moi-même",
|
||||||
|
"userRemoveOrgSelf": "Se retirer de l'organisation",
|
||||||
|
"userRemoveOrgSelfWarning": "Vous perdrez immédiatement l'accès à cette organisation.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "SUPPRIMER MOI-MÊME DE L'ORG",
|
||||||
"users": "Utilisateurs",
|
"users": "Utilisateurs",
|
||||||
"accessRoleMember": "Membre",
|
"accessRoleMember": "Membre",
|
||||||
"accessRoleOwner": "Propriétaire",
|
"accessRoleOwner": "Propriétaire",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Adresse e-mail invalide",
|
"emailInvalid": "Adresse e-mail invalide",
|
||||||
"inviteValidityDuration": "Veuillez sélectionner une durée",
|
"inviteValidityDuration": "Veuillez sélectionner une durée",
|
||||||
"accessRoleSelectPlease": "Veuillez sélectionner un rôle",
|
"accessRoleSelectPlease": "Veuillez sélectionner un rôle",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Retirer votre accès administrateur ?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Vous n'aurez plus de droits d'administrateur dans cette organisation après avoir enregistré. Un autre administrateur pourra restaurer cet accès si nécessaire.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Retirer mon accès administrateur",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "RETIRER MON ACCÈS ADMIN",
|
||||||
|
"ownerMustRetainAdminRole": "Le propriétaire de l'organisation doit conserver au moins un rôle d'administrateur.",
|
||||||
"usernameRequired": "Le nom d'utilisateur est requis",
|
"usernameRequired": "Le nom d'utilisateur est requis",
|
||||||
"idpSelectPlease": "Veuillez sélectionner un fournisseur d'identité",
|
"idpSelectPlease": "Veuillez sélectionner un fournisseur d'identité",
|
||||||
"idpGenericOidc": "Fournisseur OAuth2/OIDC générique.",
|
"idpGenericOidc": "Fournisseur OAuth2/OIDC générique.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.",
|
"targetNoOneDescription": "L'ajout de plus d'une cible ci-dessus activera l'équilibrage de charge.",
|
||||||
"targetsSubmit": "Enregistrer les cibles",
|
"targetsSubmit": "Enregistrer les cibles",
|
||||||
"addTarget": "Ajouter une cible",
|
"addTarget": "Ajouter une cible",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Le routage en tourniquet n'opérera pas entre des sites qui ne sont pas connectés au même nœud, mais le basculement fonctionnera.",
|
||||||
"targetErrorInvalidIp": "Adresse IP invalide",
|
"targetErrorInvalidIp": "Adresse IP invalide",
|
||||||
"targetErrorInvalidIpDescription": "Veuillez entrer une adresse IP ou un nom d'hôte valide",
|
"targetErrorInvalidIpDescription": "Veuillez entrer une adresse IP ou un nom d'hôte valide",
|
||||||
"targetErrorInvalidPort": "Port invalide",
|
"targetErrorInvalidPort": "Port invalide",
|
||||||
@@ -1356,7 +1372,7 @@
|
|||||||
"sidebarSites": "Nœuds",
|
"sidebarSites": "Nœuds",
|
||||||
"sidebarApprovals": "Demandes d'approbation",
|
"sidebarApprovals": "Demandes d'approbation",
|
||||||
"sidebarResources": "Ressource",
|
"sidebarResources": "Ressource",
|
||||||
"sidebarProxyResources": "Publiques",
|
"sidebarProxyResources": "Publique",
|
||||||
"sidebarClientResources": "Privé",
|
"sidebarClientResources": "Privé",
|
||||||
"sidebarAccessControl": "Contrôle d'accès",
|
"sidebarAccessControl": "Contrôle d'accès",
|
||||||
"sidebarLogsAndAnalytics": "Journaux & Analytiques",
|
"sidebarLogsAndAnalytics": "Journaux & Analytiques",
|
||||||
@@ -2458,8 +2474,8 @@
|
|||||||
"manageUserDevicesDescription": "Voir et gérer les appareils que les utilisateurs utilisent pour se connecter en privé aux ressources",
|
"manageUserDevicesDescription": "Voir et gérer les appareils que les utilisateurs utilisent pour se connecter en privé aux ressources",
|
||||||
"downloadClientBannerTitle": "Télécharger le client Pangolin",
|
"downloadClientBannerTitle": "Télécharger le client Pangolin",
|
||||||
"downloadClientBannerDescription": "Téléchargez le client Pangolin pour votre système afin de vous connecter au réseau Pangolin et accéder aux ressources de manière privée.",
|
"downloadClientBannerDescription": "Téléchargez le client Pangolin pour votre système afin de vous connecter au réseau Pangolin et accéder aux ressources de manière privée.",
|
||||||
"manageMachineClients": "Gérer les machines",
|
"manageMachineClients": "Gérer les clients de la machine",
|
||||||
"manageMachineClientsDescription": "Créer et gérer les clients que les serveurs et systèmes utilisent pour se connecter en privé aux ressources",
|
"manageMachineClientsDescription": "Créer et gérer des clients que les serveurs et les systèmes utilisent pour se connecter en privé aux ressources",
|
||||||
"machineClientsBannerTitle": "Serveurs & Systèmes automatisés",
|
"machineClientsBannerTitle": "Serveurs & Systèmes automatisés",
|
||||||
"machineClientsBannerDescription": "Les clients de machine sont conçus pour les serveurs et les systèmes automatisés qui ne sont pas associés à un utilisateur spécifique. Ils s'authentifient avec un identifiant et une clé secrète, et peuvent être exécutés avec Pangolin CLI, Olm CLI ou Olm en tant que conteneur.",
|
"machineClientsBannerDescription": "Les clients de machine sont conçus pour les serveurs et les systèmes automatisés qui ne sont pas associés à un utilisateur spécifique. Ils s'authentifient avec un identifiant et une clé secrète, et peuvent être exécutés avec Pangolin CLI, Olm CLI ou Olm en tant que conteneur.",
|
||||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Mot de passe valide",
|
"validPassword": "Mot de passe valide",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Afficher",
|
||||||
|
"configManaged": "Configuration gérée",
|
||||||
"connectedClient": "Client connecté",
|
"connectedClient": "Client connecté",
|
||||||
"resourceBlocked": "Ressource bloquée",
|
"resourceBlocked": "Ressource bloquée",
|
||||||
"droppedByRule": "Abandonné par la règle",
|
"droppedByRule": "Abandonné par la règle",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Transférer des événements directement sur votre compte Datadog. Prochainement.",
|
"streamingDatadogDescription": "Transférer des événements directement sur votre compte Datadog. Prochainement.",
|
||||||
"streamingTypePickerDescription": "Choisissez un type de destination pour commencer.",
|
"streamingTypePickerDescription": "Choisissez un type de destination pour commencer.",
|
||||||
"streamingFailedToLoad": "Impossible de charger les destinations",
|
"streamingLastSyncError": "Une erreur s'est produite lors de la dernière synchronisation",
|
||||||
"streamingUnexpectedError": "Une erreur inattendue s'est produite.",
|
"streamingUnexpectedError": "Une erreur inattendue s'est produite.",
|
||||||
"streamingFailedToUpdate": "Impossible de mettre à jour la destination",
|
"streamingFailedToUpdate": "Impossible de mettre à jour la destination",
|
||||||
"streamingDeletedSuccess": "Destination supprimée avec succès",
|
"streamingDeletedSuccess": "Destination supprimée avec succès",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Modifier la destination",
|
"S3DestEditTitle": "Modifier la destination",
|
||||||
"S3DestAddTitle": "Ajouter une destination S3",
|
"S3DestAddTitle": "Ajouter une destination S3",
|
||||||
"S3DestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements S3.",
|
"S3DestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements S3.",
|
||||||
"S3DestAddDescription": "Configurer un nouveau point de terminaison S3 pour recevoir les événements de votre organisation.",
|
"S3DestAddDescription": "Configurez un nouveau bucket Amazon S3 (ou compatible S3) pour recevoir les événements de votre organisation.",
|
||||||
|
"s3DestTabSettings": "Réglages",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Nom",
|
||||||
|
"s3DestNamePlaceholder": "Ma destination S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID de clé d'accès AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Clé d'accès secrète AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Votre clé d'accès secrète AWS",
|
||||||
|
"s3DestRegionLabel": "Région AWS",
|
||||||
|
"s3DestBucketLabel": "Nom du bucket",
|
||||||
|
"s3DestPrefixLabel": "Préfixe clé (facultatif)",
|
||||||
|
"s3DestPrefixDescription": "Préfixe de chemin facultatif préfixé à chaque clé d'objet. Les objets sont stockés à {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Point de terminaison personnalisé (facultatif)",
|
||||||
|
"s3DestEndpointDescription": "Modifiez le point de terminaison S3 pour un stockage compatible S3 tel que MinIO ou Cloudflare R2. Laissez vide pour l'AWS S3 standard.",
|
||||||
|
"s3DestGzipLabel": "Compression Gzip",
|
||||||
|
"s3DestGzipDescription": "Compressez chaque objet téléchargé avec gzip. Réduit les coûts de stockage et la taille de téléchargement.",
|
||||||
|
"s3DestFormatTitle": "Format de fichier",
|
||||||
|
"s3DestFormatDescription": "Comment les événements sont sérialisés dans chaque objet téléchargé.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Chaque objet est un tableau JSON des enregistrements d'événements. Compatible avec la plupart des outils d'analyse.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Chaque objet contient un enregistrement JSON par ligne (JSON délimité par saut de ligne). Compatible avec Athena, BigQuery et Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Chaque objet est un fichier CSV RFC-4180 avec une ligne d'en-tête. Les noms de colonne sont dérivés des champs de données de l'événement.",
|
||||||
|
"s3DestSaveChanges": "Enregistrer les modifications",
|
||||||
|
"s3DestCreateDestination": "Créer une destination",
|
||||||
|
"s3DestUpdatedSuccess": "Destination mise à jour avec succès",
|
||||||
|
"s3DestCreatedSuccess": "Destination créée avec succès",
|
||||||
|
"s3DestUpdateFailed": "Échec de la mise à jour de la destination",
|
||||||
|
"s3DestCreateFailed": "Échec de la création de la destination",
|
||||||
"datadogDestEditTitle": "Modifier la destination",
|
"datadogDestEditTitle": "Modifier la destination",
|
||||||
"datadogDestAddTitle": "Ajouter une destination Datadog",
|
"datadogDestAddTitle": "Ajouter une destination Datadog",
|
||||||
"datadogDestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements Datadog.",
|
"datadogDestEditDescription": "Mettre à jour la configuration de cette destination de diffusion d'événements Datadog.",
|
||||||
@@ -3154,7 +3199,6 @@
|
|||||||
"healthCheckTabAdvanced": "Avancé",
|
"healthCheckTabAdvanced": "Avancé",
|
||||||
"healthCheckStrategyNotAvailable": "Cette stratégie n'est pas disponible. Veuillez contacter le service commercial pour activer cette fonctionnalité.",
|
"healthCheckStrategyNotAvailable": "Cette stratégie n'est pas disponible. Veuillez contacter le service commercial pour activer cette fonctionnalité.",
|
||||||
"uptime30d": "Disponibilité (30j)",
|
"uptime30d": "Disponibilité (30j)",
|
||||||
"uptimeNoData": "Aucune donnée",
|
|
||||||
"idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité",
|
"idpAddActionCreateNew": "Créer un nouveau fournisseur d'identité",
|
||||||
"idpAddActionImportFromOrg": "Importer d'une autre organisation",
|
"idpAddActionImportFromOrg": "Importer d'une autre organisation",
|
||||||
"idpImportDialogTitle": "Importer le fournisseur d'identité",
|
"idpImportDialogTitle": "Importer le fournisseur d'identité",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione del link",
|
"shareErrorDeleteMessage": "Si è verificato un errore durante l'eliminazione del link",
|
||||||
"shareDeleted": "Link eliminato",
|
"shareDeleted": "Link eliminato",
|
||||||
"shareDeletedDescription": "Il link è stato eliminato",
|
"shareDeletedDescription": "Il link è stato eliminato",
|
||||||
|
"shareDelete": "Elimina Link di Condivisione",
|
||||||
|
"shareDeleteConfirm": "Conferma Eliminazione Link di Condivisione",
|
||||||
|
"shareQuestionRemove": "Sei sicuro di voler eliminare questo link di condivisione?",
|
||||||
|
"shareMessageRemove": "Una volta eliminato, il link non funzionerà più e chiunque lo utilizzi perderà l'accesso alla risorsa.",
|
||||||
"shareTokenDescription": "Il token di accesso può essere passato in due modi: come parametro di interrogazione o nelle intestazioni della richiesta. Questi devono essere passati dal client su ogni richiesta di accesso autenticato.",
|
"shareTokenDescription": "Il token di accesso può essere passato in due modi: come parametro di interrogazione o nelle intestazioni della richiesta. Questi devono essere passati dal client su ogni richiesta di accesso autenticato.",
|
||||||
"accessToken": "Token Di Accesso",
|
"accessToken": "Token Di Accesso",
|
||||||
"usageExamples": "Esempi Di Utilizzo",
|
"usageExamples": "Esempi Di Utilizzo",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Una volta rimosso questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
"userMessageOrgRemove": "Una volta rimosso questo utente non avrà più accesso all'organizzazione. Puoi sempre reinvitarlo in seguito, ma dovrà accettare nuovamente l'invito.",
|
||||||
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
"userRemoveOrgConfirm": "Conferma Rimozione Utente",
|
||||||
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
"userRemoveOrg": "Rimuovi Utente dall'Organizzazione",
|
||||||
|
"userQuestionOrgRemoveSelf": "Sei sicuro di voler rimuovere te stesso da questa organizzazione?",
|
||||||
|
"userMessageOrgRemoveSelf": "Perderai immediatamente l'accesso. Un amministratore può invitarti nuovamente in seguito, ma dovrai accettare un nuovo invito.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Conferma Rimozione Me Stesso",
|
||||||
|
"userRemoveOrgSelf": "Rimuoviti dall'organizzazione",
|
||||||
|
"userRemoveOrgSelfWarning": "Perderai immediatamente l'accesso a questa organizzazione.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "RIMUOVITI DALL'ORGANIZZAZIONE",
|
||||||
"users": "Utenti",
|
"users": "Utenti",
|
||||||
"accessRoleMember": "Membro",
|
"accessRoleMember": "Membro",
|
||||||
"accessRoleOwner": "Proprietario",
|
"accessRoleOwner": "Proprietario",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Indirizzo email non valido",
|
"emailInvalid": "Indirizzo email non valido",
|
||||||
"inviteValidityDuration": "Seleziona una durata",
|
"inviteValidityDuration": "Seleziona una durata",
|
||||||
"accessRoleSelectPlease": "Seleziona un ruolo",
|
"accessRoleSelectPlease": "Seleziona un ruolo",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Rimuovere il tuo accesso amministrativo?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Non avrai più i permessi di amministratore in questa organizzazione dopo il salvataggio. Un altro amministratore può ripristinare l'accesso se necessario.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Rimuovere il Mio Accesso Amministrativo",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "RIMUOVERE IL MIO ACCESSO AMMINISTRATIVO",
|
||||||
|
"ownerMustRetainAdminRole": "Il proprietario dell'organizzazione deve mantenere almeno un ruolo di amministratore.",
|
||||||
"usernameRequired": "Username richiesto",
|
"usernameRequired": "Username richiesto",
|
||||||
"idpSelectPlease": "Seleziona un provider di identità",
|
"idpSelectPlease": "Seleziona un provider di identità",
|
||||||
"idpGenericOidc": "Provider OAuth2/OIDC generico.",
|
"idpGenericOidc": "Provider OAuth2/OIDC generico.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.",
|
"targetNoOneDescription": "L'aggiunta di più di un target abiliterà il bilanciamento del carico.",
|
||||||
"targetsSubmit": "Salva Target",
|
"targetsSubmit": "Salva Target",
|
||||||
"addTarget": "Aggiungi Target",
|
"addTarget": "Aggiungi Target",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Il routing round robin non funzionerà tra siti che non sono connessi allo stesso nodo, ma il failover funzionerà.",
|
||||||
"targetErrorInvalidIp": "Indirizzo IP non valido",
|
"targetErrorInvalidIp": "Indirizzo IP non valido",
|
||||||
"targetErrorInvalidIpDescription": "Inserisci un indirizzo IP o un hostname valido",
|
"targetErrorInvalidIpDescription": "Inserisci un indirizzo IP o un hostname valido",
|
||||||
"targetErrorInvalidPort": "Porta non valida",
|
"targetErrorInvalidPort": "Porta non valida",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Password Valida",
|
"validPassword": "Password Valida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Visualizza",
|
||||||
|
"configManaged": "Gestione Configurazione",
|
||||||
"connectedClient": "Cliente Connesso",
|
"connectedClient": "Cliente Connesso",
|
||||||
"resourceBlocked": "Risorsa Bloccata",
|
"resourceBlocked": "Risorsa Bloccata",
|
||||||
"droppedByRule": "Eliminato dalla regola",
|
"droppedByRule": "Eliminato dalla regola",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Inoltra gli eventi direttamente al tuo account Datadog. In arrivo.",
|
"streamingDatadogDescription": "Inoltra gli eventi direttamente al tuo account Datadog. In arrivo.",
|
||||||
"streamingTypePickerDescription": "Scegli un tipo di destinazione per iniziare.",
|
"streamingTypePickerDescription": "Scegli un tipo di destinazione per iniziare.",
|
||||||
"streamingFailedToLoad": "Impossibile caricare le destinazioni",
|
"streamingLastSyncError": "Si è verificato un errore durante l'ultima sincronizzazione",
|
||||||
"streamingUnexpectedError": "Si è verificato un errore imprevisto.",
|
"streamingUnexpectedError": "Si è verificato un errore imprevisto.",
|
||||||
"streamingFailedToUpdate": "Impossibile aggiornare la destinazione",
|
"streamingFailedToUpdate": "Impossibile aggiornare la destinazione",
|
||||||
"streamingDeletedSuccess": "Destinazione eliminata con successo",
|
"streamingDeletedSuccess": "Destinazione eliminata con successo",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Modifica Destinazione",
|
"S3DestEditTitle": "Modifica Destinazione",
|
||||||
"S3DestAddTitle": "Aggiungi Destinazione S3",
|
"S3DestAddTitle": "Aggiungi Destinazione S3",
|
||||||
"S3DestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi S3.",
|
"S3DestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi S3.",
|
||||||
"S3DestAddDescription": "Configura un nuovo endpoint S3 per ricevere gli eventi della tua organizzazione.",
|
"S3DestAddDescription": "Configura un nuovo bucket Amazon S3 (o compatibile con S3) per ricevere gli eventi della tua organizzazione.",
|
||||||
|
"s3DestTabSettings": "Impostazioni",
|
||||||
|
"s3DestTabFormat": "Formato",
|
||||||
|
"s3DestNameLabel": "Nome",
|
||||||
|
"s3DestNamePlaceholder": "La mia destinazione S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID Chiave Accesso AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Chiave Segreta Accesso AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "La tua chiave segreta di accesso AWS",
|
||||||
|
"s3DestRegionLabel": "Regione AWS",
|
||||||
|
"s3DestBucketLabel": "Nome Bucket",
|
||||||
|
"s3DestPrefixLabel": "Prefisso Chiave (facoltativo)",
|
||||||
|
"s3DestPrefixDescription": "Prefisso percorso facoltativo anteposto a ogni chiave oggetto. Gli oggetti vengono archiviati in {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Endpoint personalizzato (facoltativo)",
|
||||||
|
"s3DestEndpointDescription": "Sostituisci l'endpoint S3 per lo storage compatibile con S3 come MinIO o Cloudflare R2. Lasciare vuoto per l'AWS S3 standard.",
|
||||||
|
"s3DestGzipLabel": "Compressione Gzip",
|
||||||
|
"s3DestGzipDescription": "Comprimi ogni oggetto caricato con gzip. Riduce i costi di archiviazione e la dimensione di caricamento.",
|
||||||
|
"s3DestFormatTitle": "Formato del File",
|
||||||
|
"s3DestFormatDescription": "Come gli eventi sono serializzati all'interno di ciascun oggetto caricato.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Ogni oggetto è un array JSON di record di eventi. Compatibile con la maggior parte degli strumenti analitici.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Ogni oggetto contiene un record JSON per linea (JSON delimitato da newline). Compatibile con Athena, BigQuery e Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "\"CSV\"",
|
||||||
|
"s3DestFormatCsvDescription": "Ogni oggetto è un file CSV RFC-4180 con una riga di intestazione. I nomi delle colonne sono derivati dai campi dei dati degli eventi.",
|
||||||
|
"s3DestSaveChanges": "Salva modifiche",
|
||||||
|
"s3DestCreateDestination": "Crea destinazione",
|
||||||
|
"s3DestUpdatedSuccess": "Destinazione aggiornata con successo",
|
||||||
|
"s3DestCreatedSuccess": "Destinazione creata con successo",
|
||||||
|
"s3DestUpdateFailed": "Aggiornamento della destinazione fallito",
|
||||||
|
"s3DestCreateFailed": "Creazione della destinazione fallita",
|
||||||
"datadogDestEditTitle": "Modifica Destinazione",
|
"datadogDestEditTitle": "Modifica Destinazione",
|
||||||
"datadogDestAddTitle": "Aggiungi Destinazione Datadog",
|
"datadogDestAddTitle": "Aggiungi Destinazione Datadog",
|
||||||
"datadogDestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi Datadog.",
|
"datadogDestEditDescription": "Aggiorna la configurazione per questa destinazione di streaming eventi Datadog.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "링크 삭제 중 오류가 발생했습니다.",
|
"shareErrorDeleteMessage": "링크 삭제 중 오류가 발생했습니다.",
|
||||||
"shareDeleted": "링크가 삭제되었습니다.",
|
"shareDeleted": "링크가 삭제되었습니다.",
|
||||||
"shareDeletedDescription": "링크가 삭제되었습니다.",
|
"shareDeletedDescription": "링크가 삭제되었습니다.",
|
||||||
|
"shareDelete": "공유 링크 삭제",
|
||||||
|
"shareDeleteConfirm": "공유 링크 삭제 확인",
|
||||||
|
"shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?",
|
||||||
|
"shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.",
|
||||||
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.",
|
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.",
|
||||||
"accessToken": "액세스 토큰",
|
"accessToken": "액세스 토큰",
|
||||||
"usageExamples": "사용 예",
|
"usageExamples": "사용 예",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.",
|
"userMessageOrgRemove": "이 사용자가 제거되면 더 이상 조직에 접근할 수 없습니다. 나중에 다시 초대할 수 있지만, 초대를 다시 수락해야 합니다.",
|
||||||
"userRemoveOrgConfirm": "사용자 제거 확인",
|
"userRemoveOrgConfirm": "사용자 제거 확인",
|
||||||
"userRemoveOrg": "조직에서 사용자 제거",
|
"userRemoveOrg": "조직에서 사용자 제거",
|
||||||
|
"userQuestionOrgRemoveSelf": "이 조직에서 자신을 제거하시겠습니까?",
|
||||||
|
"userMessageOrgRemoveSelf": "귀하는 즉시 접근 권한을 잃게 됩니다. 관리자가 나중에 다시 초대할 수 있지만, 새 초대를 수락해야 합니다.",
|
||||||
|
"userRemoveOrgConfirmSelf": "내 제거 확인",
|
||||||
|
"userRemoveOrgSelf": "조직에서 자신을 제거하십시오",
|
||||||
|
"userRemoveOrgSelfWarning": "귀하는 이 조직에 대한 접근 권한을 즉시 상실합니다.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "조직에서 나를 제거",
|
||||||
"users": "사용자",
|
"users": "사용자",
|
||||||
"accessRoleMember": "회원",
|
"accessRoleMember": "회원",
|
||||||
"accessRoleOwner": "소유자",
|
"accessRoleOwner": "소유자",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "유효하지 않은 이메일 주소입니다.",
|
"emailInvalid": "유효하지 않은 이메일 주소입니다.",
|
||||||
"inviteValidityDuration": "지속 시간을 선택하십시오.",
|
"inviteValidityDuration": "지속 시간을 선택하십시오.",
|
||||||
"accessRoleSelectPlease": "역할을 선택하세요",
|
"accessRoleSelectPlease": "역할을 선택하세요",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "관리자 권한을 제거하시겠습니까?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "저장 후 이 조직에 대한 관리자 권한이 없어집니다. 필요한 경우 다른 관리자가 접근 권한을 복구할 수 있습니다.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "내 관리자 권한 제거",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "내 관리자 권한 제거",
|
||||||
|
"ownerMustRetainAdminRole": "조직 소유자는 최소한 하나의 관리자 역할을 유지해야 합니다.",
|
||||||
"usernameRequired": "사용자 이름은 필수입니다.",
|
"usernameRequired": "사용자 이름은 필수입니다.",
|
||||||
"idpSelectPlease": "신원 제공자를 선택하십시오",
|
"idpSelectPlease": "신원 제공자를 선택하십시오",
|
||||||
"idpGenericOidc": "일반 OAuth2/OIDC 공급자.",
|
"idpGenericOidc": "일반 OAuth2/OIDC 공급자.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
"targetNoOneDescription": "위에 하나 이상의 대상을 추가하면 로드 밸런싱이 활성화됩니다.",
|
||||||
"targetsSubmit": "대상 저장",
|
"targetsSubmit": "대상 저장",
|
||||||
"addTarget": "대상 추가",
|
"addTarget": "대상 추가",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "라운드 로빈 라우팅은 동일한 노드에 연결되지 않은 사이트 간에는 작동하지 않으나, 대체 라우팅은 작동합니다.",
|
||||||
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
|
"targetErrorInvalidIp": "유효하지 않은 IP 주소",
|
||||||
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
|
"targetErrorInvalidIpDescription": "유효한 IP 주소 또는 호스트 이름을 입력하세요.",
|
||||||
"targetErrorInvalidPort": "유효하지 않은 포트",
|
"targetErrorInvalidPort": "유효하지 않은 포트",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "유효한 비밀번호",
|
"validPassword": "유효한 비밀번호",
|
||||||
"validEmail": "유효한 이메일",
|
"validEmail": "유효한 이메일",
|
||||||
"validSSO": "유효한 SSO",
|
"validSSO": "유효한 SSO",
|
||||||
|
"view": "보기",
|
||||||
|
"configManaged": "구성 관리됨",
|
||||||
"connectedClient": "연결된 클라이언트",
|
"connectedClient": "연결된 클라이언트",
|
||||||
"resourceBlocked": "리소스 차단됨",
|
"resourceBlocked": "리소스 차단됨",
|
||||||
"droppedByRule": "룰에 의해 드롭됨",
|
"droppedByRule": "룰에 의해 드롭됨",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "데이터독",
|
"streamingDatadogTitle": "데이터독",
|
||||||
"streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.",
|
"streamingDatadogDescription": "이벤트를 직접 Datadog 계정으로 전달합니다. 곧 제공됩니다.",
|
||||||
"streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.",
|
"streamingTypePickerDescription": "목표 유형을 선택하여 시작합니다.",
|
||||||
"streamingFailedToLoad": "대상 로드에 실패했습니다",
|
"streamingLastSyncError": "마지막 동기화에서 오류가 발생했습니다.",
|
||||||
"streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.",
|
"streamingUnexpectedError": "예기치 않은 오류가 발생했습니다.",
|
||||||
"streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다",
|
"streamingFailedToUpdate": "대상지를 업데이트하는 데 실패했습니다",
|
||||||
"streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다",
|
"streamingDeletedSuccess": "대상지가 성공적으로 삭제되었습니다",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "대상지 수정",
|
"S3DestEditTitle": "대상지 수정",
|
||||||
"S3DestAddTitle": "S3 대상지 추가",
|
"S3DestAddTitle": "S3 대상지 추가",
|
||||||
"S3DestEditDescription": "이 S3 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
"S3DestEditDescription": "이 S3 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
||||||
"S3DestAddDescription": "조직의 이벤트를 받기 위한 새로운 S3 엔드포인트를 구성하세요.",
|
"S3DestAddDescription": "조직의 이벤트를 수신할 새로운 Amazon S3(또는 S3 호환) 버킷을 구성하세요.",
|
||||||
|
"s3DestTabSettings": "설정",
|
||||||
|
"s3DestTabFormat": "형식",
|
||||||
|
"s3DestNameLabel": "이름",
|
||||||
|
"s3DestNamePlaceholder": "내 S3 대상",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS 액세스 키 ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS 비밀 액세스 키",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "귀하의 AWS 비밀 액세스 키",
|
||||||
|
"s3DestRegionLabel": "AWS 지역",
|
||||||
|
"s3DestBucketLabel": "버킷 이름",
|
||||||
|
"s3DestPrefixLabel": "키 접두사(선택 사항)",
|
||||||
|
"s3DestPrefixDescription": "하나의 객체 키 앞에 붙이는 선택적 경로 접두사입니다. 객체는 {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}에 저장됩니다.",
|
||||||
|
"s3DestEndpointLabel": "사용자 정의 엔드포인트(선택 사항)",
|
||||||
|
"s3DestEndpointDescription": "MinIO 또는 Cloudflare R2와 같은 S3 호환 저장소에 대한 S3 엔드포인트를 재정의합니다. 표준 AWS S3의 경우 비워 두십시오.",
|
||||||
|
"s3DestGzipLabel": "Gzip 압축",
|
||||||
|
"s3DestGzipDescription": "각 업로드된 객체를 gzip으로 압축합니다. 저장 비용과 업로드 크기를 줄입니다.",
|
||||||
|
"s3DestFormatTitle": "파일 형식",
|
||||||
|
"s3DestFormatDescription": "업로드된 각 객체 내에서 이벤트가 직렬화되는 방식입니다.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "각 객체는 이벤트 기록의 JSON 배열입니다. 대부분의 분석 도구와 호환됩니다.",
|
||||||
|
"s3DestFormatNdjsonDescription": "각 객체는 한 줄당 하나의 JSON 레코드를 포함합니다(새 줄로 구분된 JSON). Athena, BigQuery, Spark와 호환됩니다.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "각 객체는 헤더 행이 있는 RFC-4180 CSV 파일입니다. 열 이름은 이벤트 데이터 필드에서 파생됩니다.",
|
||||||
|
"s3DestSaveChanges": "변경 사항 저장",
|
||||||
|
"s3DestCreateDestination": "대상 생성",
|
||||||
|
"s3DestUpdatedSuccess": "대상이 성공적으로 업데이트되었습니다",
|
||||||
|
"s3DestCreatedSuccess": "대상이 성공적으로 생성되었습니다",
|
||||||
|
"s3DestUpdateFailed": "대상 업데이트에 실패했습니다",
|
||||||
|
"s3DestCreateFailed": "대상 생성에 실패했습니다",
|
||||||
"datadogDestEditTitle": "대상지 수정",
|
"datadogDestEditTitle": "대상지 수정",
|
||||||
"datadogDestAddTitle": "Datadog 대상지 추가",
|
"datadogDestAddTitle": "Datadog 대상지 추가",
|
||||||
"datadogDestEditDescription": "이 Datadog 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
"datadogDestEditDescription": "이 Datadog 이벤트 스트리밍 대상지의 구성을 업데이트하세요.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "En feil oppstod ved sletting av lenke",
|
"shareErrorDeleteMessage": "En feil oppstod ved sletting av lenke",
|
||||||
"shareDeleted": "Lenke slettet",
|
"shareDeleted": "Lenke slettet",
|
||||||
"shareDeletedDescription": "Lenken har blitt slettet",
|
"shareDeletedDescription": "Lenken har blitt slettet",
|
||||||
|
"shareDelete": "Slett delingslenke",
|
||||||
|
"shareDeleteConfirm": "Bekreft sletting av delingslenke",
|
||||||
|
"shareQuestionRemove": "Er du sikker på at du vil slette denne delingslenken?",
|
||||||
|
"shareMessageRemove": "Når slettet, vil lenken ikke lenger fungere, og alle som bruker den vil miste tilgang til ressursen.",
|
||||||
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en spørringsparameter eller i forespørselsoverskriftene. Disse må sendes fra klienten på hver forespørsel om autentisert tilgang.",
|
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en spørringsparameter eller i forespørselsoverskriftene. Disse må sendes fra klienten på hver forespørsel om autentisert tilgang.",
|
||||||
"accessToken": "Tilgangsnøkkel",
|
"accessToken": "Tilgangsnøkkel",
|
||||||
"usageExamples": "Brukseksempler",
|
"usageExamples": "Brukseksempler",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.",
|
"userMessageOrgRemove": "Når denne brukeren er fjernet, vil de ikke lenger ha tilgang til organisasjonen. Du kan alltid invitere dem på nytt senere, men de vil måtte godta invitasjonen på nytt.",
|
||||||
"userRemoveOrgConfirm": "Bekreft fjerning av bruker",
|
"userRemoveOrgConfirm": "Bekreft fjerning av bruker",
|
||||||
"userRemoveOrg": "Fjern bruker fra organisasjon",
|
"userRemoveOrg": "Fjern bruker fra organisasjon",
|
||||||
|
"userQuestionOrgRemoveSelf": "Er du sikker på at du vil fjerne deg selv fra denne organisasjonen?",
|
||||||
|
"userMessageOrgRemoveSelf": "Du vil miste tilgang umiddelbart. En administrator kan invitere deg igjen senere, men du må godta en ny invitasjon.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Bekreft fjerning av meg selv",
|
||||||
|
"userRemoveOrgSelf": "Fjern deg selv fra organisasjonen",
|
||||||
|
"userRemoveOrgSelfWarning": "Du vil miste tilgangen til denne organisasjonen umiddelbart.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "FJERN MEG SELV FRA ORG",
|
||||||
"users": "Brukere",
|
"users": "Brukere",
|
||||||
"accessRoleMember": "Medlem",
|
"accessRoleMember": "Medlem",
|
||||||
"accessRoleOwner": "Eier",
|
"accessRoleOwner": "Eier",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Ugyldig e-postadresse",
|
"emailInvalid": "Ugyldig e-postadresse",
|
||||||
"inviteValidityDuration": "Vennligst velg en varighet",
|
"inviteValidityDuration": "Vennligst velg en varighet",
|
||||||
"accessRoleSelectPlease": "Vennligst velg en rolle",
|
"accessRoleSelectPlease": "Vennligst velg en rolle",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Fjern din administratoradgang?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Du vil ikke lenger ha administratorrettigheter i denne organisasjonen etter lagring. En annen administrator kan gjenopprette tilgang hvis nødvendig.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Fjern min administratoradgang",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "FJERN MIN ADMINISTRATORADGANG",
|
||||||
|
"ownerMustRetainAdminRole": "Organisasjonseier må beholde minst én administratorrolle.",
|
||||||
"usernameRequired": "Brukernavn er påkrevd",
|
"usernameRequired": "Brukernavn er påkrevd",
|
||||||
"idpSelectPlease": "Vennligst velg en identitetsleverandør",
|
"idpSelectPlease": "Vennligst velg en identitetsleverandør",
|
||||||
"idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.",
|
"idpGenericOidc": "Generisk OAuth2/OIDC-leverandør.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.",
|
"targetNoOneDescription": "Å legge til mer enn ett mål ovenfor vil aktivere lastbalansering.",
|
||||||
"targetsSubmit": "Lagre mål",
|
"targetsSubmit": "Lagre mål",
|
||||||
"addTarget": "Legg til mål",
|
"addTarget": "Legg til mål",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Rundkjøringrutefordeling vil ikke fungere mellom steder som ikke er koblet til samme node, men failover vil fungere.",
|
||||||
"targetErrorInvalidIp": "Ugyldig IP-adresse",
|
"targetErrorInvalidIp": "Ugyldig IP-adresse",
|
||||||
"targetErrorInvalidIpDescription": "Skriv inn en gyldig IP-adresse eller vertsnavn",
|
"targetErrorInvalidIpDescription": "Skriv inn en gyldig IP-adresse eller vertsnavn",
|
||||||
"targetErrorInvalidPort": "Ugyldig port",
|
"targetErrorInvalidPort": "Ugyldig port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Gyldig passord",
|
"validPassword": "Gyldig passord",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Vis",
|
||||||
|
"configManaged": "Konfigurasjon administrert",
|
||||||
"connectedClient": "Tilkoblet klient",
|
"connectedClient": "Tilkoblet klient",
|
||||||
"resourceBlocked": "Ressurs blokkert",
|
"resourceBlocked": "Ressurs blokkert",
|
||||||
"droppedByRule": "Legg i regelen",
|
"droppedByRule": "Legg i regelen",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.",
|
"streamingDatadogDescription": "Videresend arrangementer direkte til din Datadog-konto. Kommer snart.",
|
||||||
"streamingTypePickerDescription": "Velg en måltype for å komme i gang.",
|
"streamingTypePickerDescription": "Velg en måltype for å komme i gang.",
|
||||||
"streamingFailedToLoad": "Kan ikke laste inn destinasjoner",
|
"streamingLastSyncError": "Det oppstod en feil under siste synkronisering",
|
||||||
"streamingUnexpectedError": "En uventet feil oppstod.",
|
"streamingUnexpectedError": "En uventet feil oppstod.",
|
||||||
"streamingFailedToUpdate": "Kunne ikke oppdatere destinasjon",
|
"streamingFailedToUpdate": "Kunne ikke oppdatere destinasjon",
|
||||||
"streamingDeletedSuccess": "Målet ble slettet",
|
"streamingDeletedSuccess": "Målet ble slettet",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Rediger destinasjon",
|
"S3DestEditTitle": "Rediger destinasjon",
|
||||||
"S3DestAddTitle": "Legg til S3 destinasjon",
|
"S3DestAddTitle": "Legg til S3 destinasjon",
|
||||||
"S3DestEditDescription": "Oppdatere konfigurasjonen for denne S3-hendelsesstrømmingsdestinasjonen.",
|
"S3DestEditDescription": "Oppdatere konfigurasjonen for denne S3-hendelsesstrømmingsdestinasjonen.",
|
||||||
"S3DestAddDescription": "Konfigurer et nytt S3-endepunkt for å motta organisasjonens hendelser.",
|
"S3DestAddDescription": "Konfigurer en ny Amazon S3 (eller S3-kompatibel) bucket for å motta din organisasjons hendelser.",
|
||||||
|
"s3DestTabSettings": "Innstillinger",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Navn",
|
||||||
|
"s3DestNamePlaceholder": "Min S3-destinasjon",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS tilgangsnøkkel-ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS hemmelige tilgangsnøkkel",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Din AWS secret access key",
|
||||||
|
"s3DestRegionLabel": "AWS-region",
|
||||||
|
"s3DestBucketLabel": "Bucket-navn",
|
||||||
|
"s3DestPrefixLabel": "Nøkkelprefiks (valgfritt)",
|
||||||
|
"s3DestPrefixDescription": "Valgfritt bane-prefiks lagt til hver objektnøkkel. Objekter er lagret på {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Egendefinert endepunkt (valgfritt)",
|
||||||
|
"s3DestEndpointDescription": "Overstyr S3-endepunktet for S3-kompatibel lagring som MinIO eller Cloudflare R2. La stå tomt for standard AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip-komprimering",
|
||||||
|
"s3DestGzipDescription": "Komprimer hvert opplastede objekt med gzip. Reduserer lagringskostnader og opplastingsstørrelse.",
|
||||||
|
"s3DestFormatTitle": "Filformat",
|
||||||
|
"s3DestFormatDescription": "Hvordan hendelser er serialisert inni hvert opplastede objekt.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Hvert objekt er et JSON-array av hendelsesposter. Kompatibel med de fleste analyseverktøy.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Hvert objekt inneholder en JSON-post per linje (nylinje-delt JSON). Kompatibel med Athena, BigQuery, og Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Hvert objekt er en RFC-4180 CSV-fil med en overskriftsrad. Kolonnenavn er avledet fra hendelsesdatafeltene.",
|
||||||
|
"s3DestSaveChanges": "Lagre endringer",
|
||||||
|
"s3DestCreateDestination": "Opprett destinasjon",
|
||||||
|
"s3DestUpdatedSuccess": "Destinasjon oppdatert vellykket",
|
||||||
|
"s3DestCreatedSuccess": "Destinasjon opprettet vellykket",
|
||||||
|
"s3DestUpdateFailed": "Kunne ikke oppdatere destinasjon",
|
||||||
|
"s3DestCreateFailed": "Kunne ikke opprette destinasjon",
|
||||||
"datadogDestEditTitle": "Rediger destinasjon",
|
"datadogDestEditTitle": "Rediger destinasjon",
|
||||||
"datadogDestAddTitle": "Legg til Datadog destinasjon",
|
"datadogDestAddTitle": "Legg til Datadog destinasjon",
|
||||||
"datadogDestEditDescription": "Oppdatere konfigurasjonen for denne Datadog-hendelsesstrømmingsdestinasjonen.",
|
"datadogDestEditDescription": "Oppdatere konfigurasjonen for denne Datadog-hendelsesstrømmingsdestinasjonen.",
|
||||||
@@ -3174,7 +3219,7 @@
|
|||||||
"publicIpEndpoint": "Endepunkt",
|
"publicIpEndpoint": "Endepunkt",
|
||||||
"lastTriggeredAt": "Siste utløste",
|
"lastTriggeredAt": "Siste utløste",
|
||||||
"reject": "Avvis",
|
"reject": "Avvis",
|
||||||
"uptimeDaysAgo": "{count} days ago",
|
"uptimeDaysAgo": "{count} dager siden",
|
||||||
"uptimeToday": "I dag",
|
"uptimeToday": "I dag",
|
||||||
"uptimeNoDataAvailable": "Ingen data tilgjengelig",
|
"uptimeNoDataAvailable": "Ingen data tilgjengelig",
|
||||||
"uptimeSuffix": "oppetid",
|
"uptimeSuffix": "oppetid",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Fout opgetreden tijdens het verwijderen link",
|
"shareErrorDeleteMessage": "Fout opgetreden tijdens het verwijderen link",
|
||||||
"shareDeleted": "Link verwijderd",
|
"shareDeleted": "Link verwijderd",
|
||||||
"shareDeletedDescription": "De link is verwijderd",
|
"shareDeletedDescription": "De link is verwijderd",
|
||||||
|
"shareDelete": "Verwijder Deel Link",
|
||||||
|
"shareDeleteConfirm": "Bevestig verwijdering van Deel Link",
|
||||||
|
"shareQuestionRemove": "Weet u zeker dat u deze deel link wilt verwijderen?",
|
||||||
|
"shareMessageRemove": "Zodra verwijderd, zal de link niet meer werken en zal iedereen die het gebruikt de toegang tot de bron verliezen.",
|
||||||
"shareTokenDescription": "De toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de aanvraagheaders. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.",
|
"shareTokenDescription": "De toegangstoken kan op twee manieren worden doorgegeven: als queryparameter of in de aanvraagheaders. Deze moeten worden doorgegeven van de client op elk verzoek voor geverifieerde toegang.",
|
||||||
"accessToken": "Toegangs-token",
|
"accessToken": "Toegangs-token",
|
||||||
"usageExamples": "Voorbeelden van gebruik",
|
"usageExamples": "Voorbeelden van gebruik",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Eenmaal verwijderd, heeft deze gebruiker geen toegang meer tot de organisatie. Je kunt ze later altijd opnieuw uitnodigen, maar ze zullen de uitnodiging opnieuw moeten accepteren.",
|
"userMessageOrgRemove": "Eenmaal verwijderd, heeft deze gebruiker geen toegang meer tot de organisatie. Je kunt ze later altijd opnieuw uitnodigen, maar ze zullen de uitnodiging opnieuw moeten accepteren.",
|
||||||
"userRemoveOrgConfirm": "Bevestig verwijderen gebruiker",
|
"userRemoveOrgConfirm": "Bevestig verwijderen gebruiker",
|
||||||
"userRemoveOrg": "Gebruiker uit organisatie verwijderen",
|
"userRemoveOrg": "Gebruiker uit organisatie verwijderen",
|
||||||
|
"userQuestionOrgRemoveSelf": "Weet u zeker dat u zichzelf uit deze organisatie wilt verwijderen?",
|
||||||
|
"userMessageOrgRemoveSelf": "U verliest onmiddellijk toegang. Een beheerder kan u later opnieuw uitnodigen, maar u moet een nieuwe uitnodiging accepteren.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Bevestig Verwijder Mijn Persoon",
|
||||||
|
"userRemoveOrgSelf": "Verwijder uzelf uit de organisatie",
|
||||||
|
"userRemoveOrgSelfWarning": "U verliest onmiddellijk toegang tot deze organisatie.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "VERWIJDER MIJ UIT ORGANISATIE",
|
||||||
"users": "Gebruikers",
|
"users": "Gebruikers",
|
||||||
"accessRoleMember": "Lid",
|
"accessRoleMember": "Lid",
|
||||||
"accessRoleOwner": "Eigenaar",
|
"accessRoleOwner": "Eigenaar",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Ongeldig e-mailadres",
|
"emailInvalid": "Ongeldig e-mailadres",
|
||||||
"inviteValidityDuration": "Selecteer een tijdsduur",
|
"inviteValidityDuration": "Selecteer een tijdsduur",
|
||||||
"accessRoleSelectPlease": "Selecteer een rol",
|
"accessRoleSelectPlease": "Selecteer een rol",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Uw beheerderstoegang verwijderen?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "U zult na het opslaan geen beheerdersrechten meer hebben in deze organisatie. Een andere beheerder kan de toegang indien nodig herstellen.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Verwijder Mijn Beheerderstoegang",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "VERWIJDER MIJN BEHEERDERSTOEGANG",
|
||||||
|
"ownerMustRetainAdminRole": "De organisatie-eigenaar moet minstens één beheerdersrol behouden.",
|
||||||
"usernameRequired": "Gebruikersnaam is verplicht",
|
"usernameRequired": "Gebruikersnaam is verplicht",
|
||||||
"idpSelectPlease": "Selecteer een identiteitsprovider",
|
"idpSelectPlease": "Selecteer een identiteitsprovider",
|
||||||
"idpGenericOidc": "Algemene OAuth2/OIDC provider.",
|
"idpGenericOidc": "Algemene OAuth2/OIDC provider.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.",
|
"targetNoOneDescription": "Het toevoegen van meer dan één doel hierboven zal de load balancering mogelijk maken.",
|
||||||
"targetsSubmit": "Doelstellingen opslaan",
|
"targetsSubmit": "Doelstellingen opslaan",
|
||||||
"addTarget": "Doelwit toevoegen",
|
"addTarget": "Doelwit toevoegen",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round-robin routering werkt niet tussen locaties die niet met hetzelfde knooppunt zijn verbonden, maar failover werkt wel.",
|
||||||
"targetErrorInvalidIp": "Ongeldig IP-adres",
|
"targetErrorInvalidIp": "Ongeldig IP-adres",
|
||||||
"targetErrorInvalidIpDescription": "Voer een geldig IP-adres of hostnaam in",
|
"targetErrorInvalidIpDescription": "Voer een geldig IP-adres of hostnaam in",
|
||||||
"targetErrorInvalidPort": "Ongeldige poort",
|
"targetErrorInvalidPort": "Ongeldige poort",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Geldig wachtwoord",
|
"validPassword": "Geldig wachtwoord",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Bekijk",
|
||||||
|
"configManaged": "Configuratie Beheerd",
|
||||||
"connectedClient": "Verbonden Client",
|
"connectedClient": "Verbonden Client",
|
||||||
"resourceBlocked": "Bron geblokkeerd",
|
"resourceBlocked": "Bron geblokkeerd",
|
||||||
"droppedByRule": "Achtergelaten door regel",
|
"droppedByRule": "Achtergelaten door regel",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Stuur gebeurtenissen rechtstreeks door naar je Datadog account. Binnenkort beschikbaar.",
|
"streamingDatadogDescription": "Stuur gebeurtenissen rechtstreeks door naar je Datadog account. Binnenkort beschikbaar.",
|
||||||
"streamingTypePickerDescription": "Kies een bestemmingstype om te beginnen.",
|
"streamingTypePickerDescription": "Kies een bestemmingstype om te beginnen.",
|
||||||
"streamingFailedToLoad": "Laden van bestemmingen mislukt",
|
"streamingLastSyncError": "Er is een fout opgetreden bij de laatste synchronisatie",
|
||||||
"streamingUnexpectedError": "Er is een onverwachte fout opgetreden.",
|
"streamingUnexpectedError": "Er is een onverwachte fout opgetreden.",
|
||||||
"streamingFailedToUpdate": "Bijwerken bestemming mislukt",
|
"streamingFailedToUpdate": "Bijwerken bestemming mislukt",
|
||||||
"streamingDeletedSuccess": "Bestemming succesvol verwijderd",
|
"streamingDeletedSuccess": "Bestemming succesvol verwijderd",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Bestemming bewerken",
|
"S3DestEditTitle": "Bestemming bewerken",
|
||||||
"S3DestAddTitle": "S3-bestemming toevoegen",
|
"S3DestAddTitle": "S3-bestemming toevoegen",
|
||||||
"S3DestEditDescription": "Werk de configuratie bij voor deze S3-gebeurtenisstreamingbestemming.",
|
"S3DestEditDescription": "Werk de configuratie bij voor deze S3-gebeurtenisstreamingbestemming.",
|
||||||
"S3DestAddDescription": "Configureer een nieuw S3-eindpunt om de gebeurtenissen van uw organisatie te ontvangen.",
|
"S3DestAddDescription": "Configureer een nieuwe Amazon S3 (of S3-compatibele) bucket om de gebeurtenissen van uw organisatie te ontvangen.",
|
||||||
|
"s3DestTabSettings": "Instellingen",
|
||||||
|
"s3DestTabFormat": "Formaat",
|
||||||
|
"s3DestNameLabel": "Naam",
|
||||||
|
"s3DestNamePlaceholder": "Mijn S3-bestemming",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS-toegangssleutel-ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS Geheime Toegangssleutel",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Uw AWS geheime toegangssleutel",
|
||||||
|
"s3DestRegionLabel": "AWS-regio",
|
||||||
|
"s3DestBucketLabel": "Bucketnaam",
|
||||||
|
"s3DestPrefixLabel": "Sleutelvoorvoegsel (optioneel)",
|
||||||
|
"s3DestPrefixDescription": "Optioneel padvoorvoegsel dat aan elke object sleutel wordt toegevoegd. Objecten worden opgeslagen op {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Aangepast Eindpunt (optioneel)",
|
||||||
|
"s3DestEndpointDescription": "Overschrijf het S3-eindpunt voor S3-compatibele opslag zoals MinIO of Cloudflare R2. Laat leeg voor standaard AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Gzip-compressie",
|
||||||
|
"s3DestGzipDescription": "Comprimeer elk geüpload object met gzip. Verlaagt opslagkosten en uploadgrootte.",
|
||||||
|
"s3DestFormatTitle": "Bestandsformaat",
|
||||||
|
"s3DestFormatDescription": "Hoe gebeurtenissen binnen elk geüpload object worden geserialiseerd.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Elk object is een JSON-array van gebeurtenisrecords. Compatibel met de meeste analysetools.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Elk object bevat één JSON-record per regel (nieuwregel-gescheiden JSON). Compatibel met Athena, BigQuery en Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Elk object is een RFC-4180 CSV-bestand met een kopregel. Kolomnamen zijn afgeleid van de gebeurtenis gegevensvelden.",
|
||||||
|
"s3DestSaveChanges": "Wijzigingen opslaan",
|
||||||
|
"s3DestCreateDestination": "Bestemming maken",
|
||||||
|
"s3DestUpdatedSuccess": "Bestemming succesvol bijgewerkt",
|
||||||
|
"s3DestCreatedSuccess": "Bestemming succesvol gecreëerd",
|
||||||
|
"s3DestUpdateFailed": "Bijwerken bestemming mislukt",
|
||||||
|
"s3DestCreateFailed": "Aanmaken bestemming mislukt",
|
||||||
"datadogDestEditTitle": "Bestemming bewerken",
|
"datadogDestEditTitle": "Bestemming bewerken",
|
||||||
"datadogDestAddTitle": "Datadog-bestemming toevoegen",
|
"datadogDestAddTitle": "Datadog-bestemming toevoegen",
|
||||||
"datadogDestEditDescription": "Werk de configuratie bij voor deze Datadog-gebeurtenisstreamingbestemming.",
|
"datadogDestEditDescription": "Werk de configuratie bij voor deze Datadog-gebeurtenisstreamingbestemming.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Wystąpił błąd podczas usuwania linku",
|
"shareErrorDeleteMessage": "Wystąpił błąd podczas usuwania linku",
|
||||||
"shareDeleted": "Link usunięty",
|
"shareDeleted": "Link usunięty",
|
||||||
"shareDeletedDescription": "Link został usunięty",
|
"shareDeletedDescription": "Link został usunięty",
|
||||||
|
"shareDelete": "Usuń link udostępniania",
|
||||||
|
"shareDeleteConfirm": "Potwierdź usunięcie linku udostępniania",
|
||||||
|
"shareQuestionRemove": "Czy na pewno chcesz usunąć ten link udostępniania?",
|
||||||
|
"shareMessageRemove": "Po usunięciu, link przestanie działać i wszyscy korzystający z niego stracą dostęp do zasobu.",
|
||||||
"shareTokenDescription": "Token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.",
|
"shareTokenDescription": "Token dostępu może być przekazywany na dwa sposoby: jako parametr zapytania lub w nagłówkach żądania. Muszą być przekazywane z klienta na każde żądanie uwierzytelnionego dostępu.",
|
||||||
"accessToken": "Token dostępu",
|
"accessToken": "Token dostępu",
|
||||||
"usageExamples": "Przykłady użycia",
|
"usageExamples": "Przykłady użycia",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Po usunięciu ten użytkownik nie będzie miał już dostępu do organizacji. Zawsze możesz ponownie go zaprosić później, ale będzie musiał ponownie zaakceptować zaproszenie.",
|
"userMessageOrgRemove": "Po usunięciu ten użytkownik nie będzie miał już dostępu do organizacji. Zawsze możesz ponownie go zaprosić później, ale będzie musiał ponownie zaakceptować zaproszenie.",
|
||||||
"userRemoveOrgConfirm": "Potwierdź usunięcie użytkownika",
|
"userRemoveOrgConfirm": "Potwierdź usunięcie użytkownika",
|
||||||
"userRemoveOrg": "Usuń użytkownika z organizacji",
|
"userRemoveOrg": "Usuń użytkownika z organizacji",
|
||||||
|
"userQuestionOrgRemoveSelf": "Czy na pewno chcesz usunąć się z tej organizacji?",
|
||||||
|
"userMessageOrgRemoveSelf": "Stracisz dostęp natychmiastowo. Administrator może cię ponownie zaprosić, ale będziesz musiał przyjąć nowe zaproszenie.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Potwierdź usunięcie siebie",
|
||||||
|
"userRemoveOrgSelf": "Usuń siebie z organizacji",
|
||||||
|
"userRemoveOrgSelfWarning": "Natychmiast stracisz dostęp do tej organizacji.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "USUŃ SIEBIE Z ORGANIZACJI",
|
||||||
"users": "Użytkownicy",
|
"users": "Użytkownicy",
|
||||||
"accessRoleMember": "Członek",
|
"accessRoleMember": "Członek",
|
||||||
"accessRoleOwner": "Właściciel",
|
"accessRoleOwner": "Właściciel",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Nieprawidłowy adres e-mail",
|
"emailInvalid": "Nieprawidłowy adres e-mail",
|
||||||
"inviteValidityDuration": "Proszę wybrać okres ważności",
|
"inviteValidityDuration": "Proszę wybrać okres ważności",
|
||||||
"accessRoleSelectPlease": "Proszę wybrać rolę",
|
"accessRoleSelectPlease": "Proszę wybrać rolę",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Usunąć dostęp administratora?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Po zapisaniu nie będziesz już posiadał uprawnień administratora w tej organizacji. Inny administrator może przywrócić dostęp, jeśli to konieczne.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Usuń mój dostęp administratora",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "USUŃ MÓJ DOSTĘP ADMINISTRATORA",
|
||||||
|
"ownerMustRetainAdminRole": "Właściciel organizacji musi zachować co najmniej jedną rolę administratora.",
|
||||||
"usernameRequired": "Nazwa użytkownika jest wymagana",
|
"usernameRequired": "Nazwa użytkownika jest wymagana",
|
||||||
"idpSelectPlease": "Proszę wybrać dostawcę tożsamości",
|
"idpSelectPlease": "Proszę wybrać dostawcę tożsamości",
|
||||||
"idpGenericOidc": "Ogólny dostawca OAuth2/OIDC.",
|
"idpGenericOidc": "Ogólny dostawca OAuth2/OIDC.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.",
|
"targetNoOneDescription": "Dodanie więcej niż jednego celu powyżej włączy równoważenie obciążenia.",
|
||||||
"targetsSubmit": "Zapisz cele",
|
"targetsSubmit": "Zapisz cele",
|
||||||
"addTarget": "Dodaj cel",
|
"addTarget": "Dodaj cel",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Trasowanie round-robin nie będzie działać między witrynami, które nie są połączone z tym samym węzłem, ale przełączanie awaryjne będzie działać.",
|
||||||
"targetErrorInvalidIp": "Nieprawidłowy adres IP",
|
"targetErrorInvalidIp": "Nieprawidłowy adres IP",
|
||||||
"targetErrorInvalidIpDescription": "Wprowadź prawidłowy adres IP lub nazwę hosta",
|
"targetErrorInvalidIpDescription": "Wprowadź prawidłowy adres IP lub nazwę hosta",
|
||||||
"targetErrorInvalidPort": "Nieprawidłowy port",
|
"targetErrorInvalidPort": "Nieprawidłowy port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Prawidłowe hasło",
|
"validPassword": "Prawidłowe hasło",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Zobacz",
|
||||||
|
"configManaged": "Konfiguracja zarządzana",
|
||||||
"connectedClient": "Połączony Klient",
|
"connectedClient": "Połączony Klient",
|
||||||
"resourceBlocked": "Zasób zablokowany",
|
"resourceBlocked": "Zasób zablokowany",
|
||||||
"droppedByRule": "Upuszczone przez regułę",
|
"droppedByRule": "Upuszczone przez regułę",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Przekaż wydarzenia bezpośrednio do Twojego konta Datadog. Już wkrótce.",
|
"streamingDatadogDescription": "Przekaż wydarzenia bezpośrednio do Twojego konta Datadog. Już wkrótce.",
|
||||||
"streamingTypePickerDescription": "Wybierz typ docelowy, aby rozpocząć.",
|
"streamingTypePickerDescription": "Wybierz typ docelowy, aby rozpocząć.",
|
||||||
"streamingFailedToLoad": "Nie udało się załadować miejsc docelowych",
|
"streamingLastSyncError": "Wystąpił błąd podczas ostatniej synchronizacji",
|
||||||
"streamingUnexpectedError": "Wystąpił nieoczekiwany błąd.",
|
"streamingUnexpectedError": "Wystąpił nieoczekiwany błąd.",
|
||||||
"streamingFailedToUpdate": "Nie udało się zaktualizować miejsca docelowego",
|
"streamingFailedToUpdate": "Nie udało się zaktualizować miejsca docelowego",
|
||||||
"streamingDeletedSuccess": "Cel usunięty pomyślnie",
|
"streamingDeletedSuccess": "Cel usunięty pomyślnie",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Edytuj Miejsce Docelowe",
|
"S3DestEditTitle": "Edytuj Miejsce Docelowe",
|
||||||
"S3DestAddTitle": "Dodaj Miejsce Docelowe S3",
|
"S3DestAddTitle": "Dodaj Miejsce Docelowe S3",
|
||||||
"S3DestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń S3.",
|
"S3DestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń S3.",
|
||||||
"S3DestAddDescription": "Skonfiguruj nowy punkt końcowy S3, aby odbierać zdarzenia Twojej organizacji.",
|
"S3DestAddDescription": "Skonfiguruj nowy zasobnik Amazon S3 (lub zgodny z S3), aby otrzymywać zdarzenia twojej organizacji.",
|
||||||
|
"s3DestTabSettings": "Ustawienia",
|
||||||
|
"s3DestTabFormat": "Format",
|
||||||
|
"s3DestNameLabel": "Nazwa",
|
||||||
|
"s3DestNamePlaceholder": "Moje miejsce docelowe S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS Access Key ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS Secret Access Key",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Twój AWS Secret Access Key",
|
||||||
|
"s3DestRegionLabel": "Region AWS",
|
||||||
|
"s3DestBucketLabel": "Nazwa kubła",
|
||||||
|
"s3DestPrefixLabel": "Prefiks klucza (opcjonalnie)",
|
||||||
|
"s3DestPrefixDescription": "Opcjonalny prefiks ścieżki dołączony do każdego klucza obiektu. Obiekty są przechowywane w {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Niestandardowy punkt końcowy (opcjonalnie)",
|
||||||
|
"s3DestEndpointDescription": "Nadpisz punkt końcowy S3 dla zgodnego przechowywania danych, takiego jak MinIO lub Cloudflare R2. Pozostaw puste dla standardowego AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Kompresja Gzip",
|
||||||
|
"s3DestGzipDescription": "Skompresuj każdy przesłany obiekt za pomocą gzip. Zmniejsza koszty przechowywania i rozmiar przesyłu.",
|
||||||
|
"s3DestFormatTitle": "Format pliku",
|
||||||
|
"s3DestFormatDescription": "Jak zdarzenia są serializowane w każdym przesłanym obiekcie.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Każdy obiekt to tablica JSON z rekordami zdarzeń. Zgodne z większością narzędzi analitycznych.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Każdy obiekt zawiera jeden rekord JSON na linię (nowa linia-dzielone JSON). Zgodne z Athena, BigQuery i Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Każdy obiekt to plik CSV zgodny z RFC-4180 z wierszem nagłówka. Nazwy kolumn pochodzą z pól danych zdarzeń.",
|
||||||
|
"s3DestSaveChanges": "Zapisz zmiany",
|
||||||
|
"s3DestCreateDestination": "Utwórz miejsce docelowe",
|
||||||
|
"s3DestUpdatedSuccess": "Miejsce docelowe zaktualizowane pomyślnie",
|
||||||
|
"s3DestCreatedSuccess": "Miejsce docelowe utworzone pomyślnie",
|
||||||
|
"s3DestUpdateFailed": "Nie udało się zaktualizować miejsca docelowego",
|
||||||
|
"s3DestCreateFailed": "Nie udało się utworzyć miejsca docelowego",
|
||||||
"datadogDestEditTitle": "Edytuj Miejsce Docelowe",
|
"datadogDestEditTitle": "Edytuj Miejsce Docelowe",
|
||||||
"datadogDestAddTitle": "Dodaj Miejsce Docelowe Datadog",
|
"datadogDestAddTitle": "Dodaj Miejsce Docelowe Datadog",
|
||||||
"datadogDestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń Datadog.",
|
"datadogDestEditDescription": "Zaktualizuj konfigurację dla tego miejsca docelowego strumieniowego zdarzeń Datadog.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Ocorreu um erro ao apagar o link",
|
"shareErrorDeleteMessage": "Ocorreu um erro ao apagar o link",
|
||||||
"shareDeleted": "Link excluído",
|
"shareDeleted": "Link excluído",
|
||||||
"shareDeletedDescription": "O link foi eliminado",
|
"shareDeletedDescription": "O link foi eliminado",
|
||||||
|
"shareDelete": "Excluir Link de Compartilhamento",
|
||||||
|
"shareDeleteConfirm": "Confirmar Exclusão de Link de Compartilhamento",
|
||||||
|
"shareQuestionRemove": "Tem certeza de que deseja excluir este link de compartilhamento?",
|
||||||
|
"shareMessageRemove": "Uma vez excluído, o link não funcionará mais e qualquer pessoa que o utilizar perderá o acesso ao recurso.",
|
||||||
"shareTokenDescription": "O token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.",
|
"shareTokenDescription": "O token de acesso pode ser passado de duas maneiras: como um parâmetro de consulta ou nos cabeçalhos da solicitação. Estes devem ser passados do cliente em todas as solicitações para acesso autenticado.",
|
||||||
"accessToken": "Token de acesso",
|
"accessToken": "Token de acesso",
|
||||||
"usageExamples": "Exemplos de uso",
|
"usageExamples": "Exemplos de uso",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Uma vez removido, este utilizador não terá mais acesso à organização. Você sempre pode reconvidá-lo depois, mas eles precisarão aceitar o convite novamente.",
|
"userMessageOrgRemove": "Uma vez removido, este utilizador não terá mais acesso à organização. Você sempre pode reconvidá-lo depois, mas eles precisarão aceitar o convite novamente.",
|
||||||
"userRemoveOrgConfirm": "Confirmar Remoção do Usuário",
|
"userRemoveOrgConfirm": "Confirmar Remoção do Usuário",
|
||||||
"userRemoveOrg": "Remover Usuário da Organização",
|
"userRemoveOrg": "Remover Usuário da Organização",
|
||||||
|
"userQuestionOrgRemoveSelf": "Tem certeza de que deseja se remover desta organização?",
|
||||||
|
"userMessageOrgRemoveSelf": "Você perderá o acesso imediatamente. Um administrador poderá convidá-lo novamente mais tarde, mas você precisará aceitar um novo convite.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Confirmar a Remoção de Mim Mesmo",
|
||||||
|
"userRemoveOrgSelf": "Remover-se da organização",
|
||||||
|
"userRemoveOrgSelfWarning": "Você perderá o acesso a esta organização imediatamente.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "REMOVER-ME DA ORG",
|
||||||
"users": "Utilizadores",
|
"users": "Utilizadores",
|
||||||
"accessRoleMember": "Membro",
|
"accessRoleMember": "Membro",
|
||||||
"accessRoleOwner": "Proprietário",
|
"accessRoleOwner": "Proprietário",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Endereço de email inválido",
|
"emailInvalid": "Endereço de email inválido",
|
||||||
"inviteValidityDuration": "Por favor, selecione uma duração",
|
"inviteValidityDuration": "Por favor, selecione uma duração",
|
||||||
"accessRoleSelectPlease": "Por favor, selecione uma função",
|
"accessRoleSelectPlease": "Por favor, selecione uma função",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Remover seu acesso de administrador?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Você não terá mais permissões de administrador nesta organização após salvar. Outro administrador pode restaurar seu acesso, se necessário.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Remover Meu Acesso de Administrador",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "REMOVER MEU ACESSO DE ADMIN",
|
||||||
|
"ownerMustRetainAdminRole": "O proprietário da organização deve manter pelo menos um papel de administrador.",
|
||||||
"usernameRequired": "Nome de utilizador é obrigatório",
|
"usernameRequired": "Nome de utilizador é obrigatório",
|
||||||
"idpSelectPlease": "Por favor, selecione um provedor de identidade",
|
"idpSelectPlease": "Por favor, selecione um provedor de identidade",
|
||||||
"idpGenericOidc": "Provedor genérico OAuth2/OIDC.",
|
"idpGenericOidc": "Provedor genérico OAuth2/OIDC.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.",
|
"targetNoOneDescription": "Adicionar mais de um alvo acima habilitará o balanceamento de carga.",
|
||||||
"targetsSubmit": "Guardar Alvos",
|
"targetsSubmit": "Guardar Alvos",
|
||||||
"addTarget": "Adicionar Alvo",
|
"addTarget": "Adicionar Alvo",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "O roteamento round robin não funcionará entre sites que não estão conectados ao mesmo nó, mas o failover funcionará.",
|
||||||
"targetErrorInvalidIp": "Endereço IP inválido",
|
"targetErrorInvalidIp": "Endereço IP inválido",
|
||||||
"targetErrorInvalidIpDescription": "Por favor, insira um endereço IP ou nome de host válido",
|
"targetErrorInvalidIpDescription": "Por favor, insira um endereço IP ou nome de host válido",
|
||||||
"targetErrorInvalidPort": "Porta inválida",
|
"targetErrorInvalidPort": "Porta inválida",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Senha válida",
|
"validPassword": "Senha válida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Visualizar",
|
||||||
|
"configManaged": "Configuração Gerenciada",
|
||||||
"connectedClient": "Cliente Conectado",
|
"connectedClient": "Cliente Conectado",
|
||||||
"resourceBlocked": "Recurso bloqueado",
|
"resourceBlocked": "Recurso bloqueado",
|
||||||
"droppedByRule": "Derrubado pela regra",
|
"droppedByRule": "Derrubado pela regra",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Encaminha eventos diretamente para a sua conta no Datadog. Em breve.",
|
"streamingDatadogDescription": "Encaminha eventos diretamente para a sua conta no Datadog. Em breve.",
|
||||||
"streamingTypePickerDescription": "Escolha um tipo de destino para começar.",
|
"streamingTypePickerDescription": "Escolha um tipo de destino para começar.",
|
||||||
"streamingFailedToLoad": "Falha ao carregar destinos",
|
"streamingLastSyncError": "Ocorreu um erro na última sincronização",
|
||||||
"streamingUnexpectedError": "Ocorreu um erro inesperado.",
|
"streamingUnexpectedError": "Ocorreu um erro inesperado.",
|
||||||
"streamingFailedToUpdate": "Falha ao atualizar destino",
|
"streamingFailedToUpdate": "Falha ao atualizar destino",
|
||||||
"streamingDeletedSuccess": "Destino apagado com sucesso",
|
"streamingDeletedSuccess": "Destino apagado com sucesso",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Editar Destino",
|
"S3DestEditTitle": "Editar Destino",
|
||||||
"S3DestAddTitle": "Adicionar Destino S3",
|
"S3DestAddTitle": "Adicionar Destino S3",
|
||||||
"S3DestEditDescription": "Atualize a configuração para este destino de streaming de eventos S3.",
|
"S3DestEditDescription": "Atualize a configuração para este destino de streaming de eventos S3.",
|
||||||
"S3DestAddDescription": "Configure um novo endpoint S3 para receber os eventos da sua organização.",
|
"S3DestAddDescription": "Configure um novo bucket Amazon S3 (ou compatível com S3) para receber os eventos da sua organização.",
|
||||||
|
"s3DestTabSettings": "Configurações",
|
||||||
|
"s3DestTabFormat": "Formato",
|
||||||
|
"s3DestNameLabel": "Nome",
|
||||||
|
"s3DestNamePlaceholder": "Meu destino S3",
|
||||||
|
"s3DestAccessKeyIdLabel": "ID da Chave de Acesso AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Chave de Acesso Secreta AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Sua chave de acesso secreta AWS",
|
||||||
|
"s3DestRegionLabel": "Região AWS",
|
||||||
|
"s3DestBucketLabel": "Nome do Bucket",
|
||||||
|
"s3DestPrefixLabel": "Prefixo da Chave (opcional)",
|
||||||
|
"s3DestPrefixDescription": "Prefixo de caminho opcional adicionado a cada chave de objeto. Os objetos são armazenados em {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Endpoint Personalizado (opcional)",
|
||||||
|
"s3DestEndpointDescription": "Substitua o endpoint S3 por armazenamento compatível com S3, como MinIO ou Cloudflare R2. Deixe em branco para o padrão AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Compressão Gzip",
|
||||||
|
"s3DestGzipDescription": "Comprime cada objeto carregado com gzip. Reduz custos de armazenamento e tamanho de upload.",
|
||||||
|
"s3DestFormatTitle": "Formato de Arquivo",
|
||||||
|
"s3DestFormatDescription": "Como os eventos são serializados dentro de cada objeto carregado.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Cada objeto é um array JSON de registros de eventos. Compatível com a maioria das ferramentas de análise.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Cada objeto contém um registro JSON por linha (JSON delimitado por nova linha). Compatível com Athena, BigQuery e Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Cada objeto é um arquivo CSV RFC-4180 com uma linha de cabeçalho. Nomes de colunas são derivados dos campos de dados do evento.",
|
||||||
|
"s3DestSaveChanges": "Salvar Alterações",
|
||||||
|
"s3DestCreateDestination": "Criar Destino",
|
||||||
|
"s3DestUpdatedSuccess": "Destino atualizado com sucesso",
|
||||||
|
"s3DestCreatedSuccess": "Destino criado com sucesso",
|
||||||
|
"s3DestUpdateFailed": "Falha ao atualizar destino",
|
||||||
|
"s3DestCreateFailed": "Falha ao criar destino",
|
||||||
"datadogDestEditTitle": "Editar Destino",
|
"datadogDestEditTitle": "Editar Destino",
|
||||||
"datadogDestAddTitle": "Adicionar Destino Datadog",
|
"datadogDestAddTitle": "Adicionar Destino Datadog",
|
||||||
"datadogDestEditDescription": "Atualize a configuração para este destino de streaming de eventos Datadog.",
|
"datadogDestEditDescription": "Atualize a configuração para este destino de streaming de eventos Datadog.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Произошла ошибка при удалении ссылки",
|
"shareErrorDeleteMessage": "Произошла ошибка при удалении ссылки",
|
||||||
"shareDeleted": "Ссылка удалена",
|
"shareDeleted": "Ссылка удалена",
|
||||||
"shareDeletedDescription": "Ссылка была успешно удалена",
|
"shareDeletedDescription": "Ссылка была успешно удалена",
|
||||||
|
"shareDelete": "Удалить общую ссылку",
|
||||||
|
"shareDeleteConfirm": "Подтвердите удаление общей ссылки",
|
||||||
|
"shareQuestionRemove": "Вы уверены, что хотите удалить эту общую ссылку?",
|
||||||
|
"shareMessageRemove": "После удаления ссылка перестанет работать, и все, кто ее использует, потеряют доступ к ресурсу.",
|
||||||
"shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.",
|
"shareTokenDescription": "Токен доступа может быть передан двумя способами: как параметр запроса или в заголовках запроса. Они должны быть переданы от клиента по каждому запросу для аутентифицированного доступа.",
|
||||||
"accessToken": "Токен доступа",
|
"accessToken": "Токен доступа",
|
||||||
"usageExamples": "Примеры использования",
|
"usageExamples": "Примеры использования",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "После удаления этот пользователь больше не будет иметь доступ к организации. Вы всегда можете пригласить его заново, но ему нужно будет снова принять приглашение.",
|
"userMessageOrgRemove": "После удаления этот пользователь больше не будет иметь доступ к организации. Вы всегда можете пригласить его заново, но ему нужно будет снова принять приглашение.",
|
||||||
"userRemoveOrgConfirm": "Подтвердить удаление пользователя",
|
"userRemoveOrgConfirm": "Подтвердить удаление пользователя",
|
||||||
"userRemoveOrg": "Удалить пользователя из организации",
|
"userRemoveOrg": "Удалить пользователя из организации",
|
||||||
|
"userQuestionOrgRemoveSelf": "Вы уверены, что хотите удалить себя из этой организации?",
|
||||||
|
"userMessageOrgRemoveSelf": "Вы немедленно потеряете доступ. Администратор сможет снова пригласить вас позже, но вам нужно будет принять новое приглашение.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Подтвердите удаление себя",
|
||||||
|
"userRemoveOrgSelf": "Удалите себя из организации",
|
||||||
|
"userRemoveOrgSelfWarning": "Вы немедленно потеряете доступ к этой организации.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "Удалить себя из организации",
|
||||||
"users": "Пользователи",
|
"users": "Пользователи",
|
||||||
"accessRoleMember": "Участник",
|
"accessRoleMember": "Участник",
|
||||||
"accessRoleOwner": "Владелец",
|
"accessRoleOwner": "Владелец",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Неверный адрес Email",
|
"emailInvalid": "Неверный адрес Email",
|
||||||
"inviteValidityDuration": "Пожалуйста, выберите продолжительность",
|
"inviteValidityDuration": "Пожалуйста, выберите продолжительность",
|
||||||
"accessRoleSelectPlease": "Пожалуйста, выберите роль",
|
"accessRoleSelectPlease": "Пожалуйста, выберите роль",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Удалить доступ администратора?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "После сохранения у вас больше не будет прав администратора в этой организации. Другой администратор может восстановить доступ, если это необходимо.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Удалить мой доступ администратора",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "УДАЛИТЬ МОЙ ДОСТУП АДМИНИСТРАТОРА",
|
||||||
|
"ownerMustRetainAdminRole": "Владелец организации должен сохранить по крайней мере одну роль администратора.",
|
||||||
"usernameRequired": "Имя пользователя обязательно",
|
"usernameRequired": "Имя пользователя обязательно",
|
||||||
"idpSelectPlease": "Пожалуйста, выберите Identity Provider",
|
"idpSelectPlease": "Пожалуйста, выберите Identity Provider",
|
||||||
"idpGenericOidc": "Обычный OAuth2/OIDC provider.",
|
"idpGenericOidc": "Обычный OAuth2/OIDC provider.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
|
"targetNoOneDescription": "Добавление более одной цели выше включит балансировку нагрузки.",
|
||||||
"targetsSubmit": "Сохранить цели",
|
"targetsSubmit": "Сохранить цели",
|
||||||
"addTarget": "Добавить цель",
|
"addTarget": "Добавить цель",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Роутинг с балансировкой нагрузки не будет работать между сайтами, не подключенными к одному и тому же узлу, но подмена будет работать.",
|
||||||
"targetErrorInvalidIp": "Неверный IP-адрес",
|
"targetErrorInvalidIp": "Неверный IP-адрес",
|
||||||
"targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста",
|
"targetErrorInvalidIpDescription": "Пожалуйста, введите действительный IP адрес или имя хоста",
|
||||||
"targetErrorInvalidPort": "Неверный порт",
|
"targetErrorInvalidPort": "Неверный порт",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Допустимый пароль",
|
"validPassword": "Допустимый пароль",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "Просмотр",
|
||||||
|
"configManaged": "Конфигурация управляется",
|
||||||
"connectedClient": "Подключенный клиент",
|
"connectedClient": "Подключенный клиент",
|
||||||
"resourceBlocked": "Ресурс заблокирован",
|
"resourceBlocked": "Ресурс заблокирован",
|
||||||
"droppedByRule": "Отброшено по правилам",
|
"droppedByRule": "Отброшено по правилам",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.",
|
"streamingDatadogDescription": "Перенаправлять события непосредственно на ваш аккаунт в Datadog. Скоро будет доступно.",
|
||||||
"streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.",
|
"streamingTypePickerDescription": "Выберите тип назначения, чтобы начать.",
|
||||||
"streamingFailedToLoad": "Не удалось загрузить места назначения",
|
"streamingLastSyncError": "Во время последней синхронизации произошла ошибка",
|
||||||
"streamingUnexpectedError": "Произошла непредвиденная ошибка.",
|
"streamingUnexpectedError": "Произошла непредвиденная ошибка.",
|
||||||
"streamingFailedToUpdate": "Не удалось обновить место назначения",
|
"streamingFailedToUpdate": "Не удалось обновить место назначения",
|
||||||
"streamingDeletedSuccess": "Адрес назначения успешно удален",
|
"streamingDeletedSuccess": "Адрес назначения успешно удален",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Редактировать пункт назначения",
|
"S3DestEditTitle": "Редактировать пункт назначения",
|
||||||
"S3DestAddTitle": "Добавить S3 пункт назначения",
|
"S3DestAddTitle": "Добавить S3 пункт назначения",
|
||||||
"S3DestEditDescription": "Обновите конфигурацию для этого S3 пункта назначения потоковых событий.",
|
"S3DestEditDescription": "Обновите конфигурацию для этого S3 пункта назначения потоковых событий.",
|
||||||
"S3DestAddDescription": "Настройте новую S3 конечную точку для получения событий вашей организации.",
|
"S3DestAddDescription": "Настройте новый Amazon S3 (или совместимое S3) хранилище для получения событий вашей организации.",
|
||||||
|
"s3DestTabSettings": "Настройки",
|
||||||
|
"s3DestTabFormat": "Формат",
|
||||||
|
"s3DestNameLabel": "Имя",
|
||||||
|
"s3DestNamePlaceholder": "Моя S3 конечная точка",
|
||||||
|
"s3DestAccessKeyIdLabel": "Идентификатор ключа доступа AWS",
|
||||||
|
"s3DestSecretAccessKeyLabel": "Секретный ключ доступа AWS",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "Ваш секретный ключ доступа AWS",
|
||||||
|
"s3DestRegionLabel": "Регион AWS",
|
||||||
|
"s3DestBucketLabel": "Имя хранилища",
|
||||||
|
"s3DestPrefixLabel": "Префикс ключа (по желанию)",
|
||||||
|
"s3DestPrefixDescription": "Необязательный префикс пути, добавляется к каждому ключу объекта. Объекты хранятся в {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}.",
|
||||||
|
"s3DestEndpointLabel": "Пользовательская конечная точка (по желанию)",
|
||||||
|
"s3DestEndpointDescription": "Переопределите конечную точку S3 для совместимого хранилища, такого как MinIO или Cloudflare R2. Оставьте пустым для стандартного AWS S3.",
|
||||||
|
"s3DestGzipLabel": "Сжатие Gzip",
|
||||||
|
"s3DestGzipDescription": "Сжимайте каждый загруженный объект с помощью gzip. Уменьшает стоимость хранения и размер загрузки.",
|
||||||
|
"s3DestFormatTitle": "Формат файла",
|
||||||
|
"s3DestFormatDescription": "Как события сериализуются внутри каждого загруженного объекта.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Каждый объект — это JSON массив записей событий. Совместим с большинством аналитических инструментов.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Каждый объект содержит одну запись JSON на строку (JSON, разделённый новой строкой). Совместим с Athena, BigQuery и Spark.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Каждый объект представляет собой CSV файл по стандарту RFC-4180 с заголовочной строкой. Имена столбцов выведены из полей данных событий.",
|
||||||
|
"s3DestSaveChanges": "Сохранить изменения",
|
||||||
|
"s3DestCreateDestination": "Создать конечную точку",
|
||||||
|
"s3DestUpdatedSuccess": "Конечная точка успешно обновлена",
|
||||||
|
"s3DestCreatedSuccess": "Конечная точка успешно создана",
|
||||||
|
"s3DestUpdateFailed": "Не удалось обновить конечную точку",
|
||||||
|
"s3DestCreateFailed": "Не удалось создать конечную точку",
|
||||||
"datadogDestEditTitle": "Редактировать пункт назначения",
|
"datadogDestEditTitle": "Редактировать пункт назначения",
|
||||||
"datadogDestAddTitle": "Добавить пункт назначения Datadog",
|
"datadogDestAddTitle": "Добавить пункт назначения Datadog",
|
||||||
"datadogDestEditDescription": "Обновите конфигурацию для этого пункта назначения потоковых событий Datadog.",
|
"datadogDestEditDescription": "Обновите конфигурацию для этого пункта назначения потоковых событий Datadog.",
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "Bağlantı silinirken bir hata oluştu",
|
"shareErrorDeleteMessage": "Bağlantı silinirken bir hata oluştu",
|
||||||
"shareDeleted": "Bağlantı silindi",
|
"shareDeleted": "Bağlantı silindi",
|
||||||
"shareDeletedDescription": "Bağlantı silindi",
|
"shareDeletedDescription": "Bağlantı silindi",
|
||||||
|
"shareDelete": "Paylaşım Bağlantısını Sil",
|
||||||
|
"shareDeleteConfirm": "Paylaşım Bağlantısının Silinmesini Onayla",
|
||||||
|
"shareQuestionRemove": "Bu paylaşım bağlantısını silmek istediğinizden emin misiniz?",
|
||||||
|
"shareMessageRemove": "Silindikten sonra, bağlantı artık çalışmayacak ve kullanan herkes kaynağa erişimini kaybedecek.",
|
||||||
"shareTokenDescription": "Erişim jetonunuz iki şekilde iletilebilir: sorgu parametresi olarak veya istek başlıklarında. Kimlik doğrulanmış erişim için her istekten müşteri tarafından iletilmelidir.",
|
"shareTokenDescription": "Erişim jetonunuz iki şekilde iletilebilir: sorgu parametresi olarak veya istek başlıklarında. Kimlik doğrulanmış erişim için her istekten müşteri tarafından iletilmelidir.",
|
||||||
"accessToken": "Erişim Jetonu",
|
"accessToken": "Erişim Jetonu",
|
||||||
"usageExamples": "Kullanım Örnekleri",
|
"usageExamples": "Kullanım Örnekleri",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "Kaldırıldığında, bu kullanıcı organizasyona artık erişim sağlayamayacak. Kullanıcı tekrar davet edilebilir, ancak daveti kabul etmesi gerekecek.",
|
"userMessageOrgRemove": "Kaldırıldığında, bu kullanıcı organizasyona artık erişim sağlayamayacak. Kullanıcı tekrar davet edilebilir, ancak daveti kabul etmesi gerekecek.",
|
||||||
"userRemoveOrgConfirm": "Kullanıcıyı Kaldırmayı Onayla",
|
"userRemoveOrgConfirm": "Kullanıcıyı Kaldırmayı Onayla",
|
||||||
"userRemoveOrg": "Kullanıcıyı Organizasyondan Kaldır",
|
"userRemoveOrg": "Kullanıcıyı Organizasyondan Kaldır",
|
||||||
|
"userQuestionOrgRemoveSelf": "Bu organizasyondan kendinizi kaldırmak istediğinizden emin misiniz?",
|
||||||
|
"userMessageOrgRemoveSelf": "Erişiminizi hemen kaybedeceksiniz. Bir yönetici daha sonra sizi tekrar davet edebilir, ancak yeni bir daveti kabul etmeniz gerekecek.",
|
||||||
|
"userRemoveOrgConfirmSelf": "Kendimi Kaldırmayı Onayla",
|
||||||
|
"userRemoveOrgSelf": "Kendinizi organizasyondan kaldırın",
|
||||||
|
"userRemoveOrgSelfWarning": "Bu organizasyona erişiminizi anında kaybedeceksiniz.",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "KENDİMİ ORGANİZASYONDAN KALDIR",
|
||||||
"users": "Kullanıcılar",
|
"users": "Kullanıcılar",
|
||||||
"accessRoleMember": "Üye",
|
"accessRoleMember": "Üye",
|
||||||
"accessRoleOwner": "Sahip",
|
"accessRoleOwner": "Sahip",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "Geçersiz e-posta adresi",
|
"emailInvalid": "Geçersiz e-posta adresi",
|
||||||
"inviteValidityDuration": "Lütfen bir süre seçin",
|
"inviteValidityDuration": "Lütfen bir süre seçin",
|
||||||
"accessRoleSelectPlease": "Lütfen bir rol seçin",
|
"accessRoleSelectPlease": "Lütfen bir rol seçin",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "Yönetici erişiminizi kaldırmak istiyor musunuz?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "Kaydettikten sonra, bu organizasyonda artık yönetici izinleriniz olmayacak. Gerekirse başka bir yönetici erişimi geri yükleyebilir.",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "Yönetici Erişimi Kaldır",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "YÖNETİCİ ERİŞİMİMİ KALDIR",
|
||||||
|
"ownerMustRetainAdminRole": "Organizasyon sahibi en az bir yönetici rolü bulundurmalıdır.",
|
||||||
"usernameRequired": "Kullanıcı adı gereklidir",
|
"usernameRequired": "Kullanıcı adı gereklidir",
|
||||||
"idpSelectPlease": "Lütfen bir kimlik sağlayıcı seçin",
|
"idpSelectPlease": "Lütfen bir kimlik sağlayıcı seçin",
|
||||||
"idpGenericOidc": "Genel OAuth2/OIDC sağlayıcısı.",
|
"idpGenericOidc": "Genel OAuth2/OIDC sağlayıcısı.",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
|
"targetNoOneDescription": "Yukarıdaki birden fazla hedef ekleyerek yük dengeleme etkinleştirilecektir.",
|
||||||
"targetsSubmit": "Hedefleri Kaydet",
|
"targetsSubmit": "Hedefleri Kaydet",
|
||||||
"addTarget": "Hedef Ekle",
|
"addTarget": "Hedef Ekle",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "Round robin yönlendirme, aynı düğüme bağlı olmayan siteler arasında çalışmayacaktır, ancak failover çalışacaktır.",
|
||||||
"targetErrorInvalidIp": "Geçersiz IP adresi",
|
"targetErrorInvalidIp": "Geçersiz IP adresi",
|
||||||
"targetErrorInvalidIpDescription": "Lütfen geçerli bir IP adresi veya host adı girin",
|
"targetErrorInvalidIpDescription": "Lütfen geçerli bir IP adresi veya host adı girin",
|
||||||
"targetErrorInvalidPort": "Geçersiz port",
|
"targetErrorInvalidPort": "Geçersiz port",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "Geçerli Şifre",
|
"validPassword": "Geçerli Şifre",
|
||||||
"validEmail": "Geçerli E-posta",
|
"validEmail": "Geçerli E-posta",
|
||||||
"validSSO": "Geçerli SSO",
|
"validSSO": "Geçerli SSO",
|
||||||
|
"view": "Görüntüle",
|
||||||
|
"configManaged": "Yapılandırma Yönetildi",
|
||||||
"connectedClient": "Bağlı İstemci",
|
"connectedClient": "Bağlı İstemci",
|
||||||
"resourceBlocked": "Kaynak Engellendi",
|
"resourceBlocked": "Kaynak Engellendi",
|
||||||
"droppedByRule": "Kurallara Göre Çıkartıldı",
|
"droppedByRule": "Kurallara Göre Çıkartıldı",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "Olayları doğrudan Datadog hesabınıza iletin. Yakında gelicek.",
|
"streamingDatadogDescription": "Olayları doğrudan Datadog hesabınıza iletin. Yakında gelicek.",
|
||||||
"streamingTypePickerDescription": "Başlamak için bir hedef türü seçin.",
|
"streamingTypePickerDescription": "Başlamak için bir hedef türü seçin.",
|
||||||
"streamingFailedToLoad": "Hedefler yüklenemedi",
|
"streamingLastSyncError": "Son senkronizasyonda bir hata oluştu",
|
||||||
"streamingUnexpectedError": "Beklenmeyen bir hata oluştu.",
|
"streamingUnexpectedError": "Beklenmeyen bir hata oluştu.",
|
||||||
"streamingFailedToUpdate": "Hedef güncellenemedi",
|
"streamingFailedToUpdate": "Hedef güncellenemedi",
|
||||||
"streamingDeletedSuccess": "Hedef başarıyla silindi",
|
"streamingDeletedSuccess": "Hedef başarıyla silindi",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "Hedefi Düzenle",
|
"S3DestEditTitle": "Hedefi Düzenle",
|
||||||
"S3DestAddTitle": "S3 Hedefi Ekle",
|
"S3DestAddTitle": "S3 Hedefi Ekle",
|
||||||
"S3DestEditDescription": "Bu S3 olay akışı hedefi için yapılandırmayı güncelleyin.",
|
"S3DestEditDescription": "Bu S3 olay akışı hedefi için yapılandırmayı güncelleyin.",
|
||||||
"S3DestAddDescription": "Kuruluşunuzun olaylarını almak için yeni bir S3 uç noktası yapılandırın.",
|
"S3DestAddDescription": "Kuruluşunuzun etkinliklerini almak için yeni bir Amazon S3 (veya S3-uyumlu) kovası yapılandırın.",
|
||||||
|
"s3DestTabSettings": "Ayarlar",
|
||||||
|
"s3DestTabFormat": "Biçim",
|
||||||
|
"s3DestNameLabel": "Ad",
|
||||||
|
"s3DestNamePlaceholder": "Benim S3 hedefim",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS Erişim Anahtar Kimliği",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS Gizli Erişim Anahtarı",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "AWS gizli erişim anahtarınız",
|
||||||
|
"s3DestRegionLabel": "AWS Bölgesi",
|
||||||
|
"s3DestBucketLabel": "Kova Adı",
|
||||||
|
"s3DestPrefixLabel": "Anahtar Ön Eki (isteğe bağlı)",
|
||||||
|
"s3DestPrefixDescription": "Her nesne anahtarının önüne eklenen isteğe bağlı yol öneki. Nesneler {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename} konumunda saklanır.",
|
||||||
|
"s3DestEndpointLabel": "Özel Uç Nokta (isteğe bağlı)",
|
||||||
|
"s3DestEndpointDescription": "MinIO veya Cloudflare R2 gibi S3-uyumlu depolama için S3 uç noktasını geçersiz kılın. Standart AWS S3 için boş bırakın.",
|
||||||
|
"s3DestGzipLabel": "Gzip sıkıştırması",
|
||||||
|
"s3DestGzipDescription": "Her yüklü nesneyi gzip ile sıkıştırın. Depolama maliyetlerini ve yükleme boyutunu azaltır.",
|
||||||
|
"s3DestFormatTitle": "Dosya Biçimi",
|
||||||
|
"s3DestFormatDescription": "Etkinliklerin her yüklendiği nesne içinde nasıl serileştirildiği.",
|
||||||
|
"s3DestFormatJsonArrayDescription": "Her nesne bir olay kayıtlarının JSON dizisidir. Çoğu analiz aracıyla uyumludur.",
|
||||||
|
"s3DestFormatNdjsonDescription": "Her nesne satır başına bir JSON kaydı içerir (yeni satır ile ayrılmış JSON). Athena, BigQuery ve Spark ile uyumludur.",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "Her nesne, bir başlık satırı ile birlikte RFC-4180 CSV dosyasıdır. Sütun isimleri olay verileri alanlarından türetilmiştir.",
|
||||||
|
"s3DestSaveChanges": "Değişiklikleri Kaydet",
|
||||||
|
"s3DestCreateDestination": "Hedef Oluştur",
|
||||||
|
"s3DestUpdatedSuccess": "Hedef başarıyla güncellendi",
|
||||||
|
"s3DestCreatedSuccess": "Hedef başarıyla oluşturuldu",
|
||||||
|
"s3DestUpdateFailed": "Hedef güncellenemedi",
|
||||||
|
"s3DestCreateFailed": "Hedef oluşturulamadı",
|
||||||
"datadogDestEditTitle": "Hedefi Düzenle",
|
"datadogDestEditTitle": "Hedefi Düzenle",
|
||||||
"datadogDestAddTitle": "Datadog Hedefi Ekle",
|
"datadogDestAddTitle": "Datadog Hedefi Ekle",
|
||||||
"datadogDestEditDescription": "Bu Datadog olay akışı hedefi için yapılandırmayı güncelleyin.",
|
"datadogDestEditDescription": "Bu Datadog olay akışı hedefi için yapılandırmayı güncelleyin.",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"trialActive": "免费试用中",
|
"trialActive": "免费试用中",
|
||||||
"trialExpired": "试用到期",
|
"trialExpired": "试用到期",
|
||||||
"trialHasEnded": "您的试用已结束。",
|
"trialHasEnded": "您的试用已结束。",
|
||||||
"trialDaysRemaining": "{count, plural, one {# day remaining} other {# days remaining}}",
|
"trialDaysRemaining": "{count, plural, other {# 天剩余}}",
|
||||||
"trialDaysLeftShort": "试用期剩余 {days} 天",
|
"trialDaysLeftShort": "试用期剩余 {days} 天",
|
||||||
"trialGoToBilling": "转到账单页面",
|
"trialGoToBilling": "转到账单页面",
|
||||||
"subscriptionViolationViewBilling": "查看计费",
|
"subscriptionViolationViewBilling": "查看计费",
|
||||||
@@ -156,6 +156,10 @@
|
|||||||
"shareErrorDeleteMessage": "删除链接时出错",
|
"shareErrorDeleteMessage": "删除链接时出错",
|
||||||
"shareDeleted": "链接已删除",
|
"shareDeleted": "链接已删除",
|
||||||
"shareDeletedDescription": "链接已删除",
|
"shareDeletedDescription": "链接已删除",
|
||||||
|
"shareDelete": "删除共享链接",
|
||||||
|
"shareDeleteConfirm": "确认删除共享链接",
|
||||||
|
"shareQuestionRemove": "您确定要删除这个共享链接吗?",
|
||||||
|
"shareMessageRemove": "删除后,该链接将不再可用,使用它的任何人将失去对资源的访问权限。",
|
||||||
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
|
"shareTokenDescription": "访问令牌可以通过两种方式传递:作为查询参数或请求标题。 每次验证访问请求都必须从客户端传递。",
|
||||||
"accessToken": "访问令牌",
|
"accessToken": "访问令牌",
|
||||||
"usageExamples": "用法示例",
|
"usageExamples": "用法示例",
|
||||||
@@ -303,7 +307,7 @@
|
|||||||
"accessUserManage": "管理用户",
|
"accessUserManage": "管理用户",
|
||||||
"accessUsersDescription": "邀请和管理访问此组织的用户",
|
"accessUsersDescription": "邀请和管理访问此组织的用户",
|
||||||
"accessUsersSearch": "搜索用户...",
|
"accessUsersSearch": "搜索用户...",
|
||||||
"accessUsersRoleFilterCount": "{count, plural, one {# role} other {# roles}}",
|
"accessUsersRoleFilterCount": "{count, plural, other {# 角色}}",
|
||||||
"accessUsersRoleFilterClear": "清除角色过滤器",
|
"accessUsersRoleFilterClear": "清除角色过滤器",
|
||||||
"accessUserCreate": "创建用户",
|
"accessUserCreate": "创建用户",
|
||||||
"accessUserRemove": "删除用户",
|
"accessUserRemove": "删除用户",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"userMessageOrgRemove": "一旦删除,这个用户将不再能够访问组织。 你总是可以稍后重新邀请他们,但他们需要再次接受邀请。",
|
"userMessageOrgRemove": "一旦删除,这个用户将不再能够访问组织。 你总是可以稍后重新邀请他们,但他们需要再次接受邀请。",
|
||||||
"userRemoveOrgConfirm": "确认删除用户",
|
"userRemoveOrgConfirm": "确认删除用户",
|
||||||
"userRemoveOrg": "从组织中删除用户",
|
"userRemoveOrg": "从组织中删除用户",
|
||||||
|
"userQuestionOrgRemoveSelf": "你确定要将自己从这个组织中移除吗?",
|
||||||
|
"userMessageOrgRemoveSelf": "你将立即失去访问权限。管理员稍后可以再次邀请你,但你需要接受新的邀请。",
|
||||||
|
"userRemoveOrgConfirmSelf": "确认删除我自己",
|
||||||
|
"userRemoveOrgSelf": "将自己从组织中移除",
|
||||||
|
"userRemoveOrgSelfWarning": "你将立即失去对此组织的访问权限。",
|
||||||
|
"userRemoveOrgConfirmPhraseSelf": "从组织中移除我自己",
|
||||||
"users": "用户",
|
"users": "用户",
|
||||||
"accessRoleMember": "成员",
|
"accessRoleMember": "成员",
|
||||||
"accessRoleOwner": "所有者",
|
"accessRoleOwner": "所有者",
|
||||||
@@ -531,6 +541,11 @@
|
|||||||
"emailInvalid": "无效的电子邮件地址",
|
"emailInvalid": "无效的电子邮件地址",
|
||||||
"inviteValidityDuration": "请选择持续时间",
|
"inviteValidityDuration": "请选择持续时间",
|
||||||
"accessRoleSelectPlease": "请选择一个角色",
|
"accessRoleSelectPlease": "请选择一个角色",
|
||||||
|
"removeOwnAdminRoleConfirmTitle": "移除你的管理员权限?",
|
||||||
|
"removeOwnAdminRoleConfirmDescription": "保存后,你将不再拥有该组织的管理员权限。如果需要,其他管理员可以恢复访问。",
|
||||||
|
"removeOwnAdminRoleConfirmButton": "移除我的管理员访问权限",
|
||||||
|
"removeOwnAdminRoleConfirmPhrase": "移除我的管理员访问",
|
||||||
|
"ownerMustRetainAdminRole": "组织所有者必须保留至少一个管理员角色。",
|
||||||
"usernameRequired": "必须输入用户名",
|
"usernameRequired": "必须输入用户名",
|
||||||
"idpSelectPlease": "请选择身份提供商",
|
"idpSelectPlease": "请选择身份提供商",
|
||||||
"idpGenericOidc": "通用的 OAuth2/OIDC 提供商。",
|
"idpGenericOidc": "通用的 OAuth2/OIDC 提供商。",
|
||||||
@@ -658,6 +673,7 @@
|
|||||||
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
|
"targetNoOneDescription": "在上面添加多个目标将启用负载平衡。",
|
||||||
"targetsSubmit": "保存目标",
|
"targetsSubmit": "保存目标",
|
||||||
"addTarget": "添加目标",
|
"addTarget": "添加目标",
|
||||||
|
"proxyMultiSiteRoundRobinNodeHelp": "轮询路由在未连接到相同节点的站点之间将不起作用,但故障转移会生效。",
|
||||||
"targetErrorInvalidIp": "无效的 IP 地址",
|
"targetErrorInvalidIp": "无效的 IP 地址",
|
||||||
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
|
"targetErrorInvalidIpDescription": "请输入有效的IP地址或主机名",
|
||||||
"targetErrorInvalidPort": "无效的端口",
|
"targetErrorInvalidPort": "无效的端口",
|
||||||
@@ -1499,7 +1515,7 @@
|
|||||||
"alertingGraphCanvasTitle": "规则流程",
|
"alertingGraphCanvasTitle": "规则流程",
|
||||||
"alertingGraphCanvasDescription": "源、触发器和操作的视觉概况。选择一个节点,在面板上进行编辑。",
|
"alertingGraphCanvasDescription": "源、触发器和操作的视觉概况。选择一个节点,在面板上进行编辑。",
|
||||||
"alertingNodeNotConfigured": "尚未配置",
|
"alertingNodeNotConfigured": "尚未配置",
|
||||||
"alertingNodeActionsCount": "{count, plural, one {# action} other {# actions}}",
|
"alertingNodeActionsCount": "{count, plural, other {# 操作}}",
|
||||||
"alertingNodeRoleSource": "来源",
|
"alertingNodeRoleSource": "来源",
|
||||||
"alertingNodeRoleTrigger": "触发",
|
"alertingNodeRoleTrigger": "触发",
|
||||||
"alertingNodeRoleAction": "行为",
|
"alertingNodeRoleAction": "行为",
|
||||||
@@ -2051,7 +2067,7 @@
|
|||||||
"createInternalResourceDialogName": "名称",
|
"createInternalResourceDialogName": "名称",
|
||||||
"createInternalResourceDialogSite": "站点",
|
"createInternalResourceDialogSite": "站点",
|
||||||
"selectSite": "选择站点...",
|
"selectSite": "选择站点...",
|
||||||
"multiSitesSelectorSitesCount": "{count, plural, one {# site} other {# sites}}",
|
"multiSitesSelectorSitesCount": "{count, plural, other {# 个网站}}",
|
||||||
"noSitesFound": "未找到站点。",
|
"noSitesFound": "未找到站点。",
|
||||||
"createInternalResourceDialogProtocol": "协议",
|
"createInternalResourceDialogProtocol": "协议",
|
||||||
"createInternalResourceDialogTcp": "TCP",
|
"createInternalResourceDialogTcp": "TCP",
|
||||||
@@ -2652,6 +2668,8 @@
|
|||||||
"validPassword": "有效密码",
|
"validPassword": "有效密码",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"view": "查看",
|
||||||
|
"configManaged": "配置已管理",
|
||||||
"connectedClient": "已连接客户端",
|
"connectedClient": "已连接客户端",
|
||||||
"resourceBlocked": "资源被阻止",
|
"resourceBlocked": "资源被阻止",
|
||||||
"droppedByRule": "被规则删除",
|
"droppedByRule": "被规则删除",
|
||||||
@@ -3062,7 +3080,7 @@
|
|||||||
"streamingDatadogTitle": "Datadog",
|
"streamingDatadogTitle": "Datadog",
|
||||||
"streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。",
|
"streamingDatadogDescription": "直接转发事件到您的Datadog 帐户。即将推出。",
|
||||||
"streamingTypePickerDescription": "选择要开始的目标类型。",
|
"streamingTypePickerDescription": "选择要开始的目标类型。",
|
||||||
"streamingFailedToLoad": "加载目的地失败",
|
"streamingLastSyncError": "最后一次同步时发生错误",
|
||||||
"streamingUnexpectedError": "发生意外错误.",
|
"streamingUnexpectedError": "发生意外错误.",
|
||||||
"streamingFailedToUpdate": "更新目标失败",
|
"streamingFailedToUpdate": "更新目标失败",
|
||||||
"streamingDeletedSuccess": "目标删除成功",
|
"streamingDeletedSuccess": "目标删除成功",
|
||||||
@@ -3079,7 +3097,34 @@
|
|||||||
"S3DestEditTitle": "编辑目的地",
|
"S3DestEditTitle": "编辑目的地",
|
||||||
"S3DestAddTitle": "添加 S3 目的地",
|
"S3DestAddTitle": "添加 S3 目的地",
|
||||||
"S3DestEditDescription": "更新此 S3 事件流目的地的配置。",
|
"S3DestEditDescription": "更新此 S3 事件流目的地的配置。",
|
||||||
"S3DestAddDescription": "配置新的 S3 终端以接收您的组织事件。",
|
"S3DestAddDescription": "配置一个新的 Amazon S3(或兼容 S3 的)存储桶以接收您的组织事件。",
|
||||||
|
"s3DestTabSettings": "设置",
|
||||||
|
"s3DestTabFormat": "格式",
|
||||||
|
"s3DestNameLabel": "名称",
|
||||||
|
"s3DestNamePlaceholder": "我的 S3 目的地",
|
||||||
|
"s3DestAccessKeyIdLabel": "AWS 访问密钥 ID",
|
||||||
|
"s3DestSecretAccessKeyLabel": "AWS 秘密访问密钥",
|
||||||
|
"s3DestSecretAccessKeyPlaceholder": "您的 AWS 密钥",
|
||||||
|
"s3DestRegionLabel": "AWS 地区",
|
||||||
|
"s3DestBucketLabel": "存储桶名称",
|
||||||
|
"s3DestPrefixLabel": "密钥前缀(可选)",
|
||||||
|
"s3DestPrefixDescription": "每个对象密钥前加的可选路径前缀。对象存储在 {prefix}/{logType}/{YYYY}/{MM}/{DD}/{filename}。",
|
||||||
|
"s3DestEndpointLabel": "自定义端点(可选)",
|
||||||
|
"s3DestEndpointDescription": "替代 S3 端点用于 MinIO 或 Cloudflare R2 等兼容 S3 的存储。标准 AWS S3 留空。",
|
||||||
|
"s3DestGzipLabel": "Gzip 压缩",
|
||||||
|
"s3DestGzipDescription": "使用 gzip 压缩每个上传的对象。减少存储成本和上传大小。",
|
||||||
|
"s3DestFormatTitle": "文件格式",
|
||||||
|
"s3DestFormatDescription": "事件在每个上传对象内的序列化方式。",
|
||||||
|
"s3DestFormatJsonArrayDescription": "每个对象是事件记录的 JSON 数组。兼容大多数分析工具。",
|
||||||
|
"s3DestFormatNdjsonDescription": "每个对象每行包含一个 JSON 记录(换行分隔的 JSON)。兼容 Athena、BigQuery 和 Spark。",
|
||||||
|
"s3DestFormatCsvTitle": "CSV",
|
||||||
|
"s3DestFormatCsvDescription": "每个对象是带有标题行的 RFC-4180 CSV 文件。列名来自事件数据字段。",
|
||||||
|
"s3DestSaveChanges": "保存更改",
|
||||||
|
"s3DestCreateDestination": "创建目的地",
|
||||||
|
"s3DestUpdatedSuccess": "目的地更新成功",
|
||||||
|
"s3DestCreatedSuccess": "目的地创建成功",
|
||||||
|
"s3DestUpdateFailed": "更新目的地失败",
|
||||||
|
"s3DestCreateFailed": "创建目的地失败",
|
||||||
"datadogDestEditTitle": "编辑目的地",
|
"datadogDestEditTitle": "编辑目的地",
|
||||||
"datadogDestAddTitle": "添加 Datadog 目的地",
|
"datadogDestAddTitle": "添加 Datadog 目的地",
|
||||||
"datadogDestEditDescription": "更新此 Datadog 事件流目的地的配置。",
|
"datadogDestEditDescription": "更新此 Datadog 事件流目的地的配置。",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { and, eq, inArray } from "drizzle-orm";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export enum ActionsEnum {
|
export enum ActionsEnum {
|
||||||
createOrgUser = "createOrgUser",
|
createOrgUser = "createOrgUser",
|
||||||
@@ -152,7 +153,21 @@ export enum ActionsEnum {
|
|||||||
createHealthCheck = "createHealthCheck",
|
createHealthCheck = "createHealthCheck",
|
||||||
updateHealthCheck = "updateHealthCheck",
|
updateHealthCheck = "updateHealthCheck",
|
||||||
deleteHealthCheck = "deleteHealthCheck",
|
deleteHealthCheck = "deleteHealthCheck",
|
||||||
listHealthChecks = "listHealthChecks"
|
listHealthChecks = "listHealthChecks",
|
||||||
|
listResourcePolicies = "listResourcePolicies",
|
||||||
|
getResourcePolicy = "getResourcePolicy",
|
||||||
|
createResourcePolicy = "createResourcePolicy",
|
||||||
|
updateResourcePolicy = "updateResourcePolicy",
|
||||||
|
deleteResourcePolicy = "deleteResourcePolicy",
|
||||||
|
listResourcePolicyRoles = "listResourcePolicyRoles",
|
||||||
|
setResourcePolicyRoles = "setResourcePolicyRoles",
|
||||||
|
listResourcePolicyUsers = "listResourcePolicyUsers",
|
||||||
|
setResourcePolicyUsers = "setResourcePolicyUsers",
|
||||||
|
setResourcePolicyPassword = "setResourcePolicyPassword",
|
||||||
|
setResourcePolicyPincode = "setResourcePolicyPincode",
|
||||||
|
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||||
|
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||||
|
setResourcePolicyRules = "setResourcePolicyRules"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
@@ -185,6 +200,23 @@ export async function checkUserActionPermission(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no direct permission, check role-based permission (any of user's roles)
|
||||||
|
const roleActionPermission = await db
|
||||||
|
.select()
|
||||||
|
.from(roleActions)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleActions.actionId, actionId),
|
||||||
|
inArray(roleActions.roleId, userOrgRoleIds),
|
||||||
|
eq(roleActions.orgId, req.userOrgId!)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (roleActionPermission.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the user has direct permission for the action in the current org
|
// Check if the user has direct permission for the action in the current org
|
||||||
const userActionPermission = await db
|
const userActionPermission = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -202,20 +234,7 @@ export async function checkUserActionPermission(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no direct permission, check role-based permission (any of user's roles)
|
return false;
|
||||||
const roleActionPermission = await db
|
|
||||||
.select()
|
|
||||||
.from(roleActions)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(roleActions.actionId, actionId),
|
|
||||||
inArray(roleActions.roleId, userOrgRoleIds),
|
|
||||||
eq(roleActions.orgId, req.userOrgId!)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return roleActionPermission.length > 0;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error checking user action permission:", error);
|
console.error("Error checking user action permission:", error);
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { clients, db, resources, siteResources } from "@server/db";
|
import {
|
||||||
|
clients,
|
||||||
|
db,
|
||||||
|
resourcePolicies,
|
||||||
|
resources,
|
||||||
|
siteResources
|
||||||
|
} from "@server/db";
|
||||||
import { randomInt } from "crypto";
|
import { randomInt } from "crypto";
|
||||||
import { exitNodes, sites } from "@server/db";
|
import { exitNodes, sites } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
@@ -107,6 +113,35 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUniqueResourcePolicyName(
|
||||||
|
orgId: string
|
||||||
|
): Promise<string> {
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = generateName();
|
||||||
|
const policyCount = await db
|
||||||
|
.select({
|
||||||
|
niceId: resourcePolicies.niceId,
|
||||||
|
orgId: resourcePolicies.orgId
|
||||||
|
})
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourcePolicies.niceId, name),
|
||||||
|
eq(resourcePolicies.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (policyCount.length === 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUniqueSiteResourceName(
|
export async function getUniqueSiteResourceName(
|
||||||
orgId: string
|
orgId: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function createDb() {
|
|||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
export const primaryDb = db.$primary;
|
export const primaryDb = db.$primary as typeof db; // is this typeof a problem - techincally they are different types
|
||||||
export type Transaction = Parameters<
|
export type Transaction = Parameters<
|
||||||
Parameters<(typeof db)["transaction"]>[0]
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
>[0];
|
>[0];
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ export const connectionAuditLog = pgTable(
|
|||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
|
clientEndpoint: text("clientEndpoint"),
|
||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
@@ -439,6 +440,8 @@ export const eventStreamingDestinations = pgTable(
|
|||||||
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
||||||
config: text("config").notNull(), // JSON string with the configuration for the destination
|
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
lastError: text("lastError"), // last send error message, null if healthy
|
||||||
|
lastErrorAt: bigint("lastErrorAt", { mode: "number" }), // epoch ms of last error, null if healthy
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ export const sites = pgTable("sites", {
|
|||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
resourceId: serial("resourceId").primaryKey(),
|
resourceId: serial("resourceId").primaryKey(),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId").references(
|
||||||
|
() => resourcePolicies.resourcePolicyId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
|
||||||
|
() => resourcePolicies.resourcePolicyId,
|
||||||
|
{
|
||||||
|
onDelete: "restrict"
|
||||||
|
}
|
||||||
|
),
|
||||||
resourceGuid: varchar("resourceGuid", { length: 36 })
|
resourceGuid: varchar("resourceGuid", { length: 36 })
|
||||||
.unique()
|
.unique()
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -196,9 +206,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId").references(() => sites.siteId, {
|
siteId: integer("siteId")
|
||||||
onDelete: "cascade"
|
.references(() => sites.siteId, {
|
||||||
}).notNull(),
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
name: varchar("name"),
|
name: varchar("name"),
|
||||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||||
hcPath: varchar("hcPath"),
|
hcPath: varchar("hcPath"),
|
||||||
@@ -521,6 +533,38 @@ export const userResources = pgTable("userResources", {
|
|||||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const rolePolicies = pgTable("rolePolicies", {
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userPolicies = pgTable("userPolicies", {
|
||||||
|
userId: varchar("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyWhiteList = pgTable("resourcePolicyWhitelist", {
|
||||||
|
whitelistId: serial("id").primaryKey(),
|
||||||
|
email: varchar("email").notNull(),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
export const userInvites = pgTable("userInvites", {
|
export const userInvites = pgTable("userInvites", {
|
||||||
inviteId: varchar("inviteId").primaryKey(),
|
inviteId: varchar("inviteId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -586,6 +630,40 @@ export const resourceHeaderAuthExtendedCompatibility = pgTable(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const resourcePolicyPincode = pgTable("resourcePolicyPincode", {
|
||||||
|
pincodeId: serial("pincodeId").primaryKey(),
|
||||||
|
pincodeHash: varchar("pincodeHash").notNull(),
|
||||||
|
digitLength: integer("digitLength").notNull(),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyPassword = pgTable("resourcePolicyPassword", {
|
||||||
|
passwordId: serial("passwordId").primaryKey(),
|
||||||
|
passwordHash: varchar("passwordHash").notNull(),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyHeaderAuth = pgTable("resourcePolicyHeaderAuth", {
|
||||||
|
headerAuthId: serial("headerAuthId").primaryKey(),
|
||||||
|
headerAuthHash: varchar("headerAuthHash").notNull(),
|
||||||
|
extendedCompatibility: boolean("extendedCompatibility")
|
||||||
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -679,6 +757,43 @@ export const resourceRules = pgTable("resourceRules", {
|
|||||||
value: varchar("value").notNull()
|
value: varchar("value").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyRules = pgTable("resourcePolicyRules", {
|
||||||
|
ruleId: serial("ruleId").primaryKey(),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
priority: integer("priority").notNull(),
|
||||||
|
action: varchar("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
|
||||||
|
match: varchar("match").$type<"CIDR" | "PATH" | "IP">().notNull(),
|
||||||
|
value: varchar("value").notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicies = pgTable("resourcePolicies", {
|
||||||
|
resourcePolicyId: serial("resourcePolicyId").primaryKey(),
|
||||||
|
sso: boolean("sso").notNull().default(true),
|
||||||
|
applyRules: boolean("applyRules").notNull().default(false),
|
||||||
|
scope: varchar("scope")
|
||||||
|
.$type<"global" | "resource">()
|
||||||
|
.notNull()
|
||||||
|
.default("global"),
|
||||||
|
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
idpId: integer("idpId").references(() => idp.idpId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
niceId: text("niceId").notNull(),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
});
|
||||||
|
|
||||||
export const supporterKey = pgTable("supporterKey", {
|
export const supporterKey = pgTable("supporterKey", {
|
||||||
keyId: serial("keyId").primaryKey(),
|
keyId: serial("keyId").primaryKey(),
|
||||||
key: varchar("key").notNull(),
|
key: varchar("key").notNull(),
|
||||||
@@ -1097,19 +1212,30 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
|
|||||||
complete: boolean("complete").notNull().default(false)
|
complete: boolean("complete").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const statusHistory = pgTable("statusHistory", {
|
export const statusHistory = pgTable(
|
||||||
id: serial("id").primaryKey(),
|
"statusHistory",
|
||||||
entityType: varchar("entityType").notNull(),
|
{
|
||||||
entityId: integer("entityId").notNull(),
|
id: serial("id").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
entityType: varchar("entityType").notNull(),
|
||||||
.notNull()
|
entityId: integer("entityId").notNull(),
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
orgId: varchar("orgId")
|
||||||
status: varchar("status").notNull(),
|
.notNull()
|
||||||
timestamp: integer("timestamp").notNull(),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
}, (table) => [
|
status: varchar("status").notNull(),
|
||||||
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
timestamp: integer("timestamp").notNull()
|
||||||
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
},
|
||||||
]);
|
(table) => [
|
||||||
|
index("idx_statusHistory_entity").on(
|
||||||
|
table.entityType,
|
||||||
|
table.entityId,
|
||||||
|
table.timestamp
|
||||||
|
),
|
||||||
|
index("idx_statusHistory_org_timestamp").on(
|
||||||
|
table.orgId,
|
||||||
|
table.timestamp
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
@@ -1179,3 +1305,6 @@ export type RoundTripMessageTracker = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type Network = InferSelectModel<typeof networks>;
|
export type Network = InferSelectModel<typeof networks>;
|
||||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||||
|
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||||
|
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||||
|
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||||
|
|||||||
@@ -17,10 +17,13 @@ import {
|
|||||||
resourceHeaderAuth,
|
resourceHeaderAuth,
|
||||||
ResourceHeaderAuth,
|
ResourceHeaderAuth,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
|
resourcePolicyRules,
|
||||||
resources,
|
resources,
|
||||||
roleResources,
|
roleResources,
|
||||||
|
rolePolicies,
|
||||||
sessions,
|
sessions,
|
||||||
userResources,
|
userResources,
|
||||||
|
userPolicies,
|
||||||
users,
|
users,
|
||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
resourceHeaderAuthExtendedCompatibility
|
resourceHeaderAuthExtendedCompatibility
|
||||||
@@ -154,58 +157,126 @@ export async function getRoleName(roleId: number): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if role has access to resource
|
* Check if role has access to resource (direct or via resource policy)
|
||||||
*/
|
*/
|
||||||
export async function getRoleResourceAccess(
|
export async function getRoleResourceAccess(
|
||||||
resourceId: number,
|
resourceId: number,
|
||||||
roleIds: number[]
|
roleIds: number[]
|
||||||
) {
|
) {
|
||||||
const roleResourceAccess = await db
|
const [direct, viaPolicies] = await Promise.all([
|
||||||
.select()
|
db
|
||||||
.from(roleResources)
|
.select()
|
||||||
.where(
|
.from(roleResources)
|
||||||
and(
|
.where(
|
||||||
eq(roleResources.resourceId, resourceId),
|
and(
|
||||||
inArray(roleResources.roleId, roleIds)
|
eq(roleResources.resourceId, resourceId),
|
||||||
|
inArray(roleResources.roleId, roleIds)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
roleId: rolePolicies.roleId,
|
||||||
|
resourcePolicyId: rolePolicies.resourcePolicyId
|
||||||
|
})
|
||||||
|
.from(rolePolicies)
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(resources.resourcePolicyId, rolePolicies.resourcePolicyId)
|
||||||
)
|
)
|
||||||
);
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.resourceId, resourceId),
|
||||||
|
inArray(rolePolicies.roleId, roleIds)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
return roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
const combined = [...direct, ...viaPolicies];
|
||||||
|
return combined.length > 0 ? combined : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has direct access to resource
|
* Check if user has access to resource (direct or via resource policy)
|
||||||
*/
|
*/
|
||||||
export async function getUserResourceAccess(
|
export async function getUserResourceAccess(
|
||||||
userId: string,
|
userId: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
) {
|
) {
|
||||||
const userResourceAccess = await db
|
const [direct, viaPolicies] = await Promise.all([
|
||||||
.select()
|
db
|
||||||
.from(userResources)
|
.select()
|
||||||
.where(
|
.from(userResources)
|
||||||
and(
|
.where(
|
||||||
eq(userResources.userId, userId),
|
and(
|
||||||
eq(userResources.resourceId, resourceId)
|
eq(userResources.userId, userId),
|
||||||
|
eq(userResources.resourceId, resourceId)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
.limit(1),
|
||||||
.limit(1);
|
db
|
||||||
|
.select({
|
||||||
|
userId: userPolicies.userId,
|
||||||
|
resourcePolicyId: userPolicies.resourcePolicyId
|
||||||
|
})
|
||||||
|
.from(userPolicies)
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(resources.resourcePolicyId, userPolicies.resourcePolicyId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.resourceId, resourceId),
|
||||||
|
eq(userPolicies.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
]);
|
||||||
|
|
||||||
return userResourceAccess.length > 0 ? userResourceAccess[0] : null;
|
return direct[0] ?? viaPolicies[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get resource rules for a given resource
|
* Get resource rules for a given resource (direct and via resource policy)
|
||||||
*/
|
*/
|
||||||
export async function getResourceRules(
|
export async function getResourceRules(
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceRule[]> {
|
): Promise<ResourceRule[]> {
|
||||||
const rules = await db
|
const [directRules, policyRules] = await Promise.all([
|
||||||
.select()
|
db
|
||||||
.from(resourceRules)
|
.select()
|
||||||
.where(eq(resourceRules.resourceId, resourceId));
|
.from(resourceRules)
|
||||||
|
.where(eq(resourceRules.resourceId, resourceId)),
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
ruleId: resourcePolicyRules.ruleId,
|
||||||
|
resourceId: sql<number>`${resourceId}`,
|
||||||
|
enabled: resourcePolicyRules.enabled,
|
||||||
|
priority: resourcePolicyRules.priority,
|
||||||
|
action: resourcePolicyRules.action,
|
||||||
|
match: resourcePolicyRules.match,
|
||||||
|
value: resourcePolicyRules.value
|
||||||
|
})
|
||||||
|
.from(resourcePolicyRules)
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(
|
||||||
|
resources.resourcePolicyId,
|
||||||
|
resourcePolicyRules.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(eq(resources.resourceId, resourceId))
|
||||||
|
]);
|
||||||
|
|
||||||
return rules;
|
const maxDirectPriority = directRules.reduce(
|
||||||
|
(max, r) => Math.max(max, r.priority),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const offsetPolicyRules = policyRules.map((r) => ({
|
||||||
|
...r,
|
||||||
|
priority: maxDirectPriority + r.priority
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...directRules, ...offsetPolicyRules] as ResourceRule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ export const connectionAuditLog = sqliteTable(
|
|||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
|
clientEndpoint: text("clientEndpoint"),
|
||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
@@ -445,6 +446,8 @@ export const eventStreamingDestinations = sqliteTable(
|
|||||||
enabled: integer("enabled", { mode: "boolean" })
|
enabled: integer("enabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
|
lastError: text("lastError"), // last send error message, null if healthy
|
||||||
|
lastErrorAt: integer("lastErrorAt"), // epoch ms of last error, null if healthy
|
||||||
createdAt: integer("createdAt").notNull(),
|
createdAt: integer("createdAt").notNull(),
|
||||||
updatedAt: integer("updatedAt").notNull()
|
updatedAt: integer("updatedAt").notNull()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,16 @@ export const sites = sqliteTable("sites", {
|
|||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId").references(
|
||||||
|
() => resourcePolicies.resourcePolicyId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
|
||||||
|
() => resourcePolicies.resourcePolicyId,
|
||||||
|
{
|
||||||
|
onDelete: "restrict"
|
||||||
|
}
|
||||||
|
),
|
||||||
resourceGuid: text("resourceGuid", { length: 36 })
|
resourceGuid: text("resourceGuid", { length: 36 })
|
||||||
.unique()
|
.unique()
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -219,9 +229,11 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId").references(() => sites.siteId, {
|
siteId: integer("siteId")
|
||||||
onDelete: "cascade"
|
.references(() => sites.siteId, {
|
||||||
}).notNull(),
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -909,6 +921,47 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
|||||||
headerAuthHash: text("headerAuthHash").notNull()
|
headerAuthHash: text("headerAuthHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyPincode = sqliteTable("resourcePolicyPincode", {
|
||||||
|
pincodeId: integer("pincodeId").primaryKey({ autoIncrement: true }),
|
||||||
|
pincodeHash: text("pincodeHash").notNull(),
|
||||||
|
digitLength: integer("digitLength").notNull(),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyPassword = sqliteTable("resourcePolicyPassword", {
|
||||||
|
passwordId: integer("passwordId").primaryKey({ autoIncrement: true }),
|
||||||
|
passwordHash: text("passwordHash").notNull(),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyHeaderAuth = sqliteTable(
|
||||||
|
"resourcePolicyHeaderAuth",
|
||||||
|
{
|
||||||
|
headerAuthId: integer("headerAuthId").primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
|
headerAuthHash: text("headerAuthHash").notNull(),
|
||||||
|
extendedCompatibility: integer("extendedCompatibility", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
|
export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
|
||||||
"resourceHeaderAuthExtendedCompatibility",
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
{
|
{
|
||||||
@@ -1023,6 +1076,77 @@ export const resourceRules = sqliteTable("resourceRules", {
|
|||||||
value: text("value").notNull()
|
value: text("value").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const rolePolicies = sqliteTable("rolePolicies", {
|
||||||
|
roleId: integer("roleId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userPolicies = sqliteTable("userPolicies", {
|
||||||
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyWhiteList = sqliteTable("resourcePolicyWhitelist", {
|
||||||
|
whitelistId: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
email: text("email").notNull(),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicyRules = sqliteTable("resourcePolicyRules", {
|
||||||
|
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
|
||||||
|
resourcePolicyId: integer("resourcePolicyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resourcePolicies.resourcePolicyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
|
priority: integer("priority").notNull(),
|
||||||
|
action: text("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
|
||||||
|
match: text("match").$type<"CIDR" | "PATH" | "IP">().notNull(),
|
||||||
|
value: text("value").notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourcePolicies = sqliteTable("resourcePolicies", {
|
||||||
|
resourcePolicyId: integer("resourcePolicyId").primaryKey(),
|
||||||
|
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
|
||||||
|
applyRules: integer("applyRules", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
scope: text("scope")
|
||||||
|
.$type<"global" | "resource">()
|
||||||
|
.notNull()
|
||||||
|
.default("global"),
|
||||||
|
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
niceId: text("niceId").notNull(),
|
||||||
|
idpId: integer("idpId").references(() => idp.idpId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
});
|
||||||
|
|
||||||
export const supporterKey = sqliteTable("supporterKey", {
|
export const supporterKey = sqliteTable("supporterKey", {
|
||||||
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
|
||||||
key: text("key").notNull(),
|
key: text("key").notNull(),
|
||||||
@@ -1196,19 +1320,30 @@ export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
|
|||||||
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
|
complete: integer("complete", { mode: "boolean" }).notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const statusHistory = sqliteTable("statusHistory", {
|
export const statusHistory = sqliteTable(
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
"statusHistory",
|
||||||
entityType: text("entityType").notNull(), // "site" | "healthCheck"
|
{
|
||||||
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
orgId: text("orgId")
|
entityType: text("entityType").notNull(), // "site" | "healthCheck"
|
||||||
.notNull()
|
entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
orgId: text("orgId")
|
||||||
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
|
.notNull()
|
||||||
timestamp: integer("timestamp").notNull(), // unix epoch seconds
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
}, (table) => [
|
status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks
|
||||||
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
timestamp: integer("timestamp").notNull() // unix epoch seconds
|
||||||
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
},
|
||||||
]);
|
(table) => [
|
||||||
|
index("idx_statusHistory_entity").on(
|
||||||
|
table.entityType,
|
||||||
|
table.entityId,
|
||||||
|
table.timestamp
|
||||||
|
),
|
||||||
|
index("idx_statusHistory_org_timestamp").on(
|
||||||
|
table.orgId,
|
||||||
|
table.timestamp
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
@@ -1278,3 +1413,6 @@ export type RoundTripMessageTracker = InferSelectModel<
|
|||||||
typeof roundTripMessageTracker
|
typeof roundTripMessageTracker
|
||||||
>;
|
>;
|
||||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||||
|
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
||||||
|
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||||
|
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export enum TierFeature {
|
|||||||
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
|
||||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||||
AlertingRules = "alertingRules",
|
AlertingRules = "alertingRules",
|
||||||
WildcardSubdomain = "wildcardSubdomain"
|
WildcardSubdomain = "wildcardSubdomain",
|
||||||
|
ResourcePolicies = "resourcePolicies"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
@@ -66,5 +67,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
||||||
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
||||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"]
|
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
|
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"]
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -162,9 +162,10 @@ export const HeaderSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Schema for individual resource
|
// Schema for individual resource
|
||||||
export const ResourceSchema = z
|
export const PublicResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
policy: z.string().optional(),
|
||||||
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
protocol: z.enum(["http", "tcp", "udp"]).optional(),
|
||||||
ssl: z.boolean().optional(),
|
ssl: z.boolean().optional(),
|
||||||
scheme: z.enum(["http", "https"]).optional(),
|
scheme: z.enum(["http", "https"]).optional(),
|
||||||
@@ -340,7 +341,8 @@ export const ResourceSchema = z
|
|||||||
if (parts.includes("*", 1)) return false; // no further wildcards
|
if (parts.includes("*", 1)) return false; // no further wildcards
|
||||||
if (parts.length < 3) return false; // need at least *.label.tld
|
if (parts.length < 3) return false; // need at least *.label.tld
|
||||||
|
|
||||||
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
const labelRegex =
|
||||||
|
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
||||||
return parts.slice(1).every((label) => labelRegex.test(label));
|
return parts.slice(1).every((label) => labelRegex.test(label));
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -354,7 +356,7 @@ export function isTargetsOnlyResource(resource: any): boolean {
|
|||||||
return Object.keys(resource).length === 1 && resource.targets;
|
return Object.keys(resource).length === 1 && resource.targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClientResourceSchema = z
|
export const PrivateResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
mode: z.enum(["host", "cidr", "http"]),
|
mode: z.enum(["host", "cidr", "http"]),
|
||||||
@@ -435,19 +437,19 @@ export const ClientResourceSchema = z
|
|||||||
export const ConfigSchema = z
|
export const ConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
"proxy-resources": z
|
"proxy-resources": z
|
||||||
.record(z.string(), ResourceSchema)
|
.record(z.string(), PublicResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
"public-resources": z
|
"public-resources": z
|
||||||
.record(z.string(), ResourceSchema)
|
.record(z.string(), PublicResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
"client-resources": z
|
"client-resources": z
|
||||||
.record(z.string(), ClientResourceSchema)
|
.record(z.string(), PrivateResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
"private-resources": z
|
"private-resources": z
|
||||||
.record(z.string(), ClientResourceSchema)
|
.record(z.string(), PrivateResourceSchema)
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
sites: z.record(z.string(), SiteSchema).optional().prefault({})
|
||||||
@@ -472,10 +474,13 @@ export const ConfigSchema = z
|
|||||||
}
|
}
|
||||||
|
|
||||||
return data as {
|
return data as {
|
||||||
"proxy-resources": Record<string, z.infer<typeof ResourceSchema>>;
|
"proxy-resources": Record<
|
||||||
|
string,
|
||||||
|
z.infer<typeof PublicResourceSchema>
|
||||||
|
>;
|
||||||
"client-resources": Record<
|
"client-resources": Record<
|
||||||
string,
|
string,
|
||||||
z.infer<typeof ClientResourceSchema>
|
z.infer<typeof PrivateResourceSchema>
|
||||||
>;
|
>;
|
||||||
sites: Record<string, z.infer<typeof SiteSchema>>;
|
sites: Record<string, z.infer<typeof SiteSchema>>;
|
||||||
};
|
};
|
||||||
@@ -614,5 +619,5 @@ export const ConfigSchema = z
|
|||||||
// Type inference from the schema
|
// Type inference from the schema
|
||||||
export type Site = z.infer<typeof SiteSchema>;
|
export type Site = z.infer<typeof SiteSchema>;
|
||||||
export type Target = z.infer<typeof TargetSchema>;
|
export type Target = z.infer<typeof TargetSchema>;
|
||||||
export type Resource = z.infer<typeof ResourceSchema>;
|
export type Resource = z.infer<typeof PublicResourceSchema>;
|
||||||
export type Config = z.infer<typeof ConfigSchema>;
|
export type Config = z.infer<typeof ConfigSchema>;
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import { tierMatrix } from "./billing/tierMatrix";
|
|||||||
|
|
||||||
export async function calculateUserClientsForOrgs(
|
export async function calculateUserClientsForOrgs(
|
||||||
userId: string,
|
userId: string,
|
||||||
trx?: Transaction
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const execute = async (transaction: Transaction) => {
|
const execute = async (transaction: Transaction | typeof db) => {
|
||||||
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
|
||||||
const adminRoleCache = new Map<
|
const adminRoleCache = new Map<
|
||||||
string,
|
string,
|
||||||
@@ -437,7 +437,7 @@ export async function calculateUserClientsForOrgs(
|
|||||||
|
|
||||||
async function cleanupOrphanedClients(
|
async function cleanupOrphanedClients(
|
||||||
userId: string,
|
userId: string,
|
||||||
trx: Transaction,
|
trx: Transaction | typeof db,
|
||||||
userOrgIds: string[] = []
|
userOrgIds: string[] = []
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Find all OLM clients for this user that should be deleted
|
// Find all OLM clients for this user that should be deleted
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.18.3";
|
export const APP_VERSION = "1.18.4";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function computeBuckets(
|
|||||||
let totalDowntime = 0;
|
let totalDowntime = 0;
|
||||||
|
|
||||||
for (let d = 0; d < days; d++) {
|
for (let d = 0; d < days; d++) {
|
||||||
const dayStartSec = todayMidnightSec - (days - d) * 86400;
|
const dayStartSec = todayMidnightSec - (days - 1 - d) * 86400;
|
||||||
const dayEndSec = dayStartSec + 86400;
|
const dayEndSec = dayStartSec + 86400;
|
||||||
|
|
||||||
const dayEvents = events.filter(
|
const dayEvents = events.filter(
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ export * from "./verifySiteResourceAccess";
|
|||||||
export * from "./logActionAudit";
|
export * from "./logActionAudit";
|
||||||
export * from "./verifyOlmAccess";
|
export * from "./verifyOlmAccess";
|
||||||
export * from "./verifyLimits";
|
export * from "./verifyLimits";
|
||||||
|
export * from "./verifyResourcePolicyAccess";
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ export * from "./verifyApiKeyClientAccess";
|
|||||||
export * from "./verifyApiKeySiteResourceAccess";
|
export * from "./verifyApiKeySiteResourceAccess";
|
||||||
export * from "./verifyApiKeyIdpAccess";
|
export * from "./verifyApiKeyIdpAccess";
|
||||||
export * from "./verifyApiKeyDomainAccess";
|
export * from "./verifyApiKeyDomainAccess";
|
||||||
|
export * from "./verifyApiKeyResourcePolicyAccess";
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { resourcePolicies, apiKeyOrg } from "@server/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
export async function verifyApiKeyResourcePolicyAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const resourcePolicyId =
|
||||||
|
req.params.resourcePolicyId ||
|
||||||
|
req.body.resourcePolicyId ||
|
||||||
|
req.query.resourcePolicyId;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Retrieve the resource policy
|
||||||
|
const [policy] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Resource policy with ID ${resourcePolicyId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any resource policy in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!policy.orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
`Resource policy with ID ${resourcePolicyId} does not have an organization ID`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the API key is linked to the resource policy's organization
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
const apiKeyOrgResult = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, policy.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (apiKeyOrgResult.length > 0) {
|
||||||
|
req.apiKeyOrg = apiKeyOrgResult[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying resource policy access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
server/middlewares/verifyResourcePolicyAccess.ts
Normal file
127
server/middlewares/verifyResourcePolicyAccess.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { resourcePolicies, userOrgs } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
|
||||||
|
export async function verifyResourcePolicyAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const resourcePolicyIdStr =
|
||||||
|
req.params?.resourcePolicyId ||
|
||||||
|
req.body?.resourcePolicyId ||
|
||||||
|
req.query?.resourcePolicyId;
|
||||||
|
const niceId = req.params?.niceId || req.body?.niceId || req.query?.niceId;
|
||||||
|
const orgId = req.params?.orgId || req.body?.orgId || req.query?.orgId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let policy: typeof resourcePolicies.$inferSelect | null = null;
|
||||||
|
|
||||||
|
if (orgId && niceId) {
|
||||||
|
const [policyRes] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourcePolicies.niceId, niceId),
|
||||||
|
eq(resourcePolicies.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
policy = policyRes ?? null;
|
||||||
|
} else {
|
||||||
|
const resourcePolicyId = parseInt(resourcePolicyIdStr);
|
||||||
|
if (isNaN(resourcePolicyId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid resource policy ID"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const [policyRes] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||||
|
.limit(1);
|
||||||
|
policy = policyRes ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Resource policy with ID ${resourcePolicyIdStr ?? niceId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
const userOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.userId, userId),
|
||||||
|
eq(userOrgs.orgId, policy.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.userOrg = userOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg || req.userOrg.orgId !== policy.orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.userOrgRoleIds = await getUserOrgRoleIds(
|
||||||
|
req.userOrg.userId,
|
||||||
|
policy.orgId
|
||||||
|
);
|
||||||
|
req.userOrgId = policy.orgId;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying resource policy access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ export function verifyUserCanSetUserOrgRoles() {
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have permission perform this action"
|
"User does not have permission to set user organization roles"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export enum OpenAPITags {
|
|||||||
Org = "Organization",
|
Org = "Organization",
|
||||||
PublicResource = "Public Resource",
|
PublicResource = "Public Resource",
|
||||||
PrivateResource = "Private Resource",
|
PrivateResource = "Private Resource",
|
||||||
|
Policy = "Policy",
|
||||||
Role = "Role",
|
Role = "Role",
|
||||||
User = "User",
|
User = "User",
|
||||||
Invitation = "User Invitation",
|
Invitation = "User Invitation",
|
||||||
|
|||||||
@@ -485,6 +485,133 @@ async function syncAcmeCertsFromHttp(endpoint: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function storeCertForDomain(
|
||||||
|
domain: string,
|
||||||
|
certPem: string,
|
||||||
|
keyPem: string,
|
||||||
|
validatedX509: crypto.X509Certificate
|
||||||
|
): Promise<void> {
|
||||||
|
const wildcard = domain.startsWith("*.");
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(certificates)
|
||||||
|
.where(eq(certificates.domain, domain))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let oldCertPem: string | null = null;
|
||||||
|
let oldKeyPem: string | null = null;
|
||||||
|
|
||||||
|
if (existing.length > 0 && existing[0].certFile) {
|
||||||
|
try {
|
||||||
|
const storedCertPem = decrypt(
|
||||||
|
existing[0].certFile,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const wildcardUnchanged = existing[0].wildcard === wildcard;
|
||||||
|
if (storedCertPem === certPem && wildcardUnchanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
oldCertPem = storedCertPem;
|
||||||
|
if (existing[0].keyFile) {
|
||||||
|
try {
|
||||||
|
oldKeyPem = decrypt(
|
||||||
|
existing[0].keyFile,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
} catch (keyErr) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expiresAt: number | null = null;
|
||||||
|
try {
|
||||||
|
expiresAt = Math.floor(
|
||||||
|
new Date(validatedX509.validTo).getTime() / 1000
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedCert = encrypt(
|
||||||
|
certPem,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const encryptedKey = encrypt(keyPem, config.getRawConfig().server.secret!);
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const domainId = await findDomainId(domain);
|
||||||
|
if (domainId) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: updating existing certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.update(certificates)
|
||||||
|
.set({
|
||||||
|
certFile: encryptedCert,
|
||||||
|
keyFile: encryptedKey,
|
||||||
|
status: "valid",
|
||||||
|
expiresAt,
|
||||||
|
updatedAt: now,
|
||||||
|
wildcard,
|
||||||
|
...(domainId !== null && { domainId })
|
||||||
|
})
|
||||||
|
.where(eq(certificates.domain, domain));
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: updated certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
|
||||||
|
await pushCertUpdateToAffectedNewts(
|
||||||
|
domain,
|
||||||
|
domainId,
|
||||||
|
oldCertPem,
|
||||||
|
oldKeyPem
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: inserting new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
await db.insert(certificates).values({
|
||||||
|
domain,
|
||||||
|
domainId,
|
||||||
|
certFile: encryptedCert,
|
||||||
|
keyFile: encryptedKey,
|
||||||
|
status: "valid",
|
||||||
|
expiresAt,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
wildcard
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: inserted new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
|
||||||
|
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function findAcmeJsonFiles(dirPath: string): string[] {
|
function findAcmeJsonFiles(dirPath: string): string[] {
|
||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
let entries: fs.Dirent[];
|
let entries: fs.Dirent[];
|
||||||
@@ -575,18 +702,16 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const cert of allCerts) {
|
for (const cert of allCerts) {
|
||||||
const domain = cert?.domain?.main;
|
const mainDomain = cert?.domain?.main;
|
||||||
|
|
||||||
if (!domain || typeof domain !== "string") {
|
if (!mainDomain || typeof mainDomain !== "string") {
|
||||||
logger.debug(`acmeCertSync: skipping cert with missing domain`);
|
logger.debug(`acmeCertSync: skipping cert with missing domain`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { wildcard } = detectWildcard(domain, cert.domain?.sans);
|
|
||||||
|
|
||||||
if (!cert.certificate || !cert.key) {
|
if (!cert.certificate || !cert.key) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - empty certificate or key field`
|
`acmeCertSync: skipping cert for ${mainDomain} - empty certificate or key field`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -598,14 +723,14 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
keyPem = Buffer.from(cert.key, "base64").toString("utf8");
|
keyPem = Buffer.from(cert.key, "base64").toString("utf8");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - failed to base64-decode cert/key: ${err}`
|
`acmeCertSync: skipping cert for ${mainDomain} - failed to base64-decode cert/key: ${err}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!certPem.trim() || !keyPem.trim()) {
|
if (!certPem.trim() || !keyPem.trim()) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - blank PEM after base64 decode`
|
`acmeCertSync: skipping cert for ${mainDomain} - blank PEM after base64 decode`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -616,7 +741,7 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
const firstCertPemForValidation = extractFirstCert(certPem);
|
const firstCertPemForValidation = extractFirstCert(certPem);
|
||||||
if (!firstCertPemForValidation) {
|
if (!firstCertPemForValidation) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - no PEM certificate block found`
|
`acmeCertSync: skipping cert for ${mainDomain} - no PEM certificate block found`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -628,7 +753,7 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - invalid X.509 certificate: ${err}`
|
`acmeCertSync: skipping cert for ${mainDomain} - invalid X.509 certificate: ${err}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -638,139 +763,40 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
|||||||
crypto.createPrivateKey(keyPem);
|
crypto.createPrivateKey(keyPem);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`acmeCertSync: skipping cert for ${domain} - invalid private key: ${err}`
|
`acmeCertSync: skipping cert for ${mainDomain} - invalid private key: ${err}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if cert already exists in DB
|
// Collect all domains covered by this cert: main + every SAN.
|
||||||
const existing = await db
|
// Each domain gets its own row in the certificates table so that
|
||||||
.select()
|
// lookups by any hostname on the cert succeed independently.
|
||||||
.from(certificates)
|
const allDomains = new Set<string>([mainDomain]);
|
||||||
.where(and(eq(certificates.domain, domain)))
|
if (Array.isArray(cert.domain?.sans)) {
|
||||||
.limit(1);
|
for (const san of cert.domain.sans) {
|
||||||
|
if (typeof san === "string" && san.trim()) {
|
||||||
let oldCertPem: string | null = null;
|
allDomains.add(san.trim());
|
||||||
let oldKeyPem: string | null = null;
|
|
||||||
|
|
||||||
if (existing.length > 0 && existing[0].certFile) {
|
|
||||||
try {
|
|
||||||
const storedCertPem = decrypt(
|
|
||||||
existing[0].certFile,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
const wildcardUnchanged = existing[0].wildcard === wildcard;
|
|
||||||
if (storedCertPem === certPem && wildcardUnchanged) {
|
|
||||||
// logger.debug(
|
|
||||||
// `acmeCertSync: cert for ${domain} is unchanged, skipping`
|
|
||||||
// );
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
// Cert has changed; capture old values so we can send a correct
|
|
||||||
// update message to the newt after the DB write.
|
|
||||||
oldCertPem = storedCertPem;
|
|
||||||
if (existing[0].keyFile) {
|
|
||||||
try {
|
|
||||||
oldKeyPem = decrypt(
|
|
||||||
existing[0].keyFile,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
} catch (keyErr) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Decryption failure means we should proceed with the update
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse cert expiry from the validated X.509 certificate
|
logger.debug(
|
||||||
let expiresAt: number | null = null;
|
`acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}`
|
||||||
try {
|
|
||||||
expiresAt = Math.floor(
|
|
||||||
new Date(validatedX509.validTo).getTime() / 1000
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptedCert = encrypt(
|
|
||||||
certPem,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
);
|
||||||
const encryptedKey = encrypt(
|
|
||||||
keyPem,
|
|
||||||
config.getRawConfig().server.secret!
|
|
||||||
);
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
const domainId = await findDomainId(domain);
|
for (const domain of allDomains) {
|
||||||
if (domainId) {
|
try {
|
||||||
logger.debug(
|
await storeCertForDomain(
|
||||||
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
|
domain,
|
||||||
);
|
certPem,
|
||||||
} else {
|
keyPem,
|
||||||
logger.debug(
|
validatedX509
|
||||||
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
|
);
|
||||||
);
|
} catch (err) {
|
||||||
}
|
logger.error(
|
||||||
|
`acmeCertSync: error storing cert for domain "${domain}": ${err}`
|
||||||
if (existing.length > 0) {
|
);
|
||||||
logger.debug(
|
}
|
||||||
`acmeCertSync: updating existing certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.update(certificates)
|
|
||||||
.set({
|
|
||||||
certFile: encryptedCert,
|
|
||||||
keyFile: encryptedKey,
|
|
||||||
status: "valid",
|
|
||||||
expiresAt,
|
|
||||||
updatedAt: now,
|
|
||||||
wildcard,
|
|
||||||
...(domainId !== null && { domainId })
|
|
||||||
})
|
|
||||||
.where(eq(certificates.domain, domain));
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: updated certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
|
|
||||||
await pushCertUpdateToAffectedNewts(
|
|
||||||
domain,
|
|
||||||
domainId,
|
|
||||||
oldCertPem,
|
|
||||||
oldKeyPem
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: inserting new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
await db.insert(certificates).values({
|
|
||||||
domain,
|
|
||||||
domainId,
|
|
||||||
certFile: encryptedCert,
|
|
||||||
keyFile: encryptedKey,
|
|
||||||
status: "valid",
|
|
||||||
expiresAt,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
wildcard
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`acmeCertSync: inserted new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
|
||||||
);
|
|
||||||
|
|
||||||
// For a brand-new cert, push to any SSL resources that were waiting for it
|
|
||||||
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ export class PrivateConfig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process.env.BRANDING_HIDE_POWERED_BY =
|
||||||
|
this.rawPrivateConfig.branding?.hide_powered_by === true ||
|
||||||
|
this.rawPrivateConfig.branding?.resource_auth_page
|
||||||
|
?.hide_powered_by === true
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
|
||||||
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
||||||
this.rawPrivateConfig.branding?.login_page?.subtitle_text || "";
|
this.rawPrivateConfig.branding?.login_page?.subtitle_text || "";
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export interface ConnectionLogRecord {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
siteId: number;
|
siteId: number;
|
||||||
clientId: number | null;
|
clientId: number | null;
|
||||||
|
clientEndpoint: string | null;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
sourceAddr: string;
|
sourceAddr: string;
|
||||||
destAddr: string;
|
destAddr: string;
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ import {
|
|||||||
LOG_TYPES,
|
LOG_TYPES,
|
||||||
LogEvent,
|
LogEvent,
|
||||||
DestinationFailureState,
|
DestinationFailureState,
|
||||||
HttpConfig
|
HttpConfig,
|
||||||
|
S3Config
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { LogDestinationProvider } from "./providers/LogDestinationProvider";
|
import { LogDestinationProvider } from "./providers/LogDestinationProvider";
|
||||||
import { HttpLogDestination } from "./providers/HttpLogDestination";
|
import { HttpLogDestination } from "./providers/HttpLogDestination";
|
||||||
|
import { S3LogDestination } from "./providers/S3LogDestination";
|
||||||
import type { EventStreamingDestination } from "@server/db";
|
import type { EventStreamingDestination } from "@server/db";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -72,11 +74,11 @@ const MAX_CATCHUP_BATCHES = 20;
|
|||||||
* After the last entry the max value is re-used.
|
* After the last entry the max value is re-used.
|
||||||
*/
|
*/
|
||||||
const BACKOFF_SCHEDULE_MS = [
|
const BACKOFF_SCHEDULE_MS = [
|
||||||
60_000, // 1 min (failure 1)
|
60_000, // 1 min (failure 1)
|
||||||
2 * 60_000, // 2 min (failure 2)
|
2 * 60_000, // 2 min (failure 2)
|
||||||
5 * 60_000, // 5 min (failure 3)
|
5 * 60_000, // 5 min (failure 3)
|
||||||
10 * 60_000, // 10 min (failure 4)
|
10 * 60_000, // 10 min (failure 4)
|
||||||
30 * 60_000 // 30 min (failure 5+)
|
30 * 60_000 // 30 min (failure 5+)
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,7 +206,10 @@ export class LogStreamingManager {
|
|||||||
this.pollTimer = null;
|
this.pollTimer = null;
|
||||||
this.runPoll()
|
this.runPoll()
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
logger.error("LogStreamingManager: unexpected poll error", err)
|
logger.error(
|
||||||
|
"LogStreamingManager: unexpected poll error",
|
||||||
|
err
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (this.isRunning) {
|
if (this.isRunning) {
|
||||||
@@ -275,10 +280,13 @@ export class LogStreamingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt and parse config – skip destination if either step fails
|
// Decrypt and parse config – skip destination if either step fails
|
||||||
let configFromDb: HttpConfig;
|
let configFromDb: unknown;
|
||||||
try {
|
try {
|
||||||
const decryptedConfig = decrypt(dest.config, config.getRawConfig().server.secret!);
|
const decryptedConfig = decrypt(
|
||||||
configFromDb = JSON.parse(decryptedConfig) as HttpConfig;
|
dest.config,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
configFromDb = JSON.parse(decryptedConfig);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`,
|
`LogStreamingManager: destination ${dest.destinationId} has invalid or undecryptable config`,
|
||||||
@@ -305,6 +313,7 @@ export class LogStreamingManager {
|
|||||||
if (enabledTypes.length === 0) return;
|
if (enabledTypes.length === 0) return;
|
||||||
|
|
||||||
let anyFailure = false;
|
let anyFailure = false;
|
||||||
|
let firstError: string | null = null;
|
||||||
|
|
||||||
for (const logType of enabledTypes) {
|
for (const logType of enabledTypes) {
|
||||||
if (!this.isRunning) break;
|
if (!this.isRunning) break;
|
||||||
@@ -312,6 +321,10 @@ export class LogStreamingManager {
|
|||||||
await this.processLogType(dest, provider, logType);
|
await this.processLogType(dest, provider, logType);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
anyFailure = true;
|
anyFailure = true;
|
||||||
|
if (firstError === null) {
|
||||||
|
firstError =
|
||||||
|
err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
`LogStreamingManager: failed to process "${logType}" logs ` +
|
`LogStreamingManager: failed to process "${logType}" logs ` +
|
||||||
`for destination ${dest.destinationId}`,
|
`for destination ${dest.destinationId}`,
|
||||||
@@ -322,6 +335,10 @@ export class LogStreamingManager {
|
|||||||
|
|
||||||
if (anyFailure) {
|
if (anyFailure) {
|
||||||
this.recordFailure(dest.destinationId);
|
this.recordFailure(dest.destinationId);
|
||||||
|
await this.setDestinationError(
|
||||||
|
dest.destinationId,
|
||||||
|
firstError ?? "Unknown error"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Any success resets the failure/back-off state
|
// Any success resets the failure/back-off state
|
||||||
if (this.failures.has(dest.destinationId)) {
|
if (this.failures.has(dest.destinationId)) {
|
||||||
@@ -330,6 +347,7 @@ export class LogStreamingManager {
|
|||||||
`LogStreamingManager: destination ${dest.destinationId} recovered`
|
`LogStreamingManager: destination ${dest.destinationId} recovered`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await this.clearDestinationError(dest.destinationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +380,10 @@ export class LogStreamingManager {
|
|||||||
.from(eventStreamingCursors)
|
.from(eventStreamingCursors)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(eventStreamingCursors.destinationId, dest.destinationId),
|
eq(
|
||||||
|
eventStreamingCursors.destinationId,
|
||||||
|
dest.destinationId
|
||||||
|
),
|
||||||
eq(eventStreamingCursors.logType, logType)
|
eq(eventStreamingCursors.logType, logType)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -431,9 +452,7 @@ export class LogStreamingManager {
|
|||||||
|
|
||||||
if (rows.length === 0) break;
|
if (rows.length === 0) break;
|
||||||
|
|
||||||
const events = rows.map((row) =>
|
const events = rows.map((row) => this.rowToLogEvent(logType, row));
|
||||||
this.rowToLogEvent(logType, row)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Throws on failure – caught by the caller which applies back-off
|
// Throws on failure – caught by the caller which applies back-off
|
||||||
await provider.send(events);
|
await provider.send(events);
|
||||||
@@ -677,8 +696,7 @@ export class LogStreamingManager {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgId =
|
const orgId = typeof row.orgId === "string" ? row.orgId : "";
|
||||||
typeof row.orgId === "string" ? row.orgId : "";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -708,6 +726,8 @@ export class LogStreamingManager {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case "http":
|
case "http":
|
||||||
return new HttpLogDestination(config as HttpConfig);
|
return new HttpLogDestination(config as HttpConfig);
|
||||||
|
case "s3":
|
||||||
|
return new S3LogDestination(config as S3Config);
|
||||||
// Future providers:
|
// Future providers:
|
||||||
// case "datadog": return new DatadogLogDestination(config as DatadogConfig);
|
// case "datadog": return new DatadogLogDestination(config as DatadogConfig);
|
||||||
default:
|
default:
|
||||||
@@ -749,6 +769,45 @@ export class LogStreamingManager {
|
|||||||
// DB helpers
|
// DB helpers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private async setDestinationError(
|
||||||
|
destinationId: number,
|
||||||
|
errorMessage: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Truncate to 1000 chars so it fits comfortably in the text column.
|
||||||
|
const truncated = errorMessage.slice(0, 1000);
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.update(eventStreamingDestinations)
|
||||||
|
.set({ lastError: truncated, lastErrorAt: Date.now() })
|
||||||
|
.where(
|
||||||
|
eq(eventStreamingDestinations.destinationId, destinationId)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`LogStreamingManager: could not persist error status for destination ${destinationId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearDestinationError(destinationId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Only update if there is actually an error stored, to avoid
|
||||||
|
// unnecessary writes on every successful poll cycle.
|
||||||
|
await db
|
||||||
|
.update(eventStreamingDestinations)
|
||||||
|
.set({ lastError: null, lastErrorAt: null })
|
||||||
|
.where(
|
||||||
|
eq(eventStreamingDestinations.destinationId, destinationId)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`LogStreamingManager: could not clear error status for destination ${destinationId}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async loadEnabledDestinations(): Promise<
|
private async loadEnabledDestinations(): Promise<
|
||||||
EventStreamingDestination[]
|
EventStreamingDestination[]
|
||||||
> {
|
> {
|
||||||
|
|||||||
279
server/private/lib/logStreaming/providers/S3LogDestination.ts
Normal file
279
server/private/lib/logStreaming/providers/S3LogDestination.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
import { gzip as gzipCallback } from "zlib";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { LogEvent, S3Config, S3PayloadFormat } from "../types";
|
||||||
|
import { LogDestinationProvider } from "./LogDestinationProvider";
|
||||||
|
|
||||||
|
const gzipAsync = promisify(gzipCallback);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constants
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Maximum time (ms) to wait for a single S3 PutObject response. */
|
||||||
|
const REQUEST_TIMEOUT_MS = 60_000;
|
||||||
|
|
||||||
|
/** Default payload format when none is specified in the config. */
|
||||||
|
const DEFAULT_FORMAT: S3PayloadFormat = "json_array";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// S3LogDestination
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forwards a batch of log events to an S3-compatible object store by
|
||||||
|
* uploading a single object per `send()` call.
|
||||||
|
*
|
||||||
|
* **Object key layout**
|
||||||
|
* ```
|
||||||
|
* {prefix}/{logType}/{YYYY}/{MM}/{DD}/{HH}-{mm}-{ss}-{uuid}.{ext}[.gz]
|
||||||
|
* ```
|
||||||
|
* - `prefix` – from `config.prefix` (default: empty – key starts at logType)
|
||||||
|
* - `logType` – one of "request", "action", "access", "connection"
|
||||||
|
* - Date components are derived from the upload time (UTC)
|
||||||
|
* - `ext` – `json` | `ndjson` | `csv`
|
||||||
|
* - `.gz` – appended when `config.gzip` is true
|
||||||
|
*
|
||||||
|
* **Payload formats** (controlled by `config.format`):
|
||||||
|
* - `json_array` (default) – body is a JSON array of event objects.
|
||||||
|
* - `ndjson` – one JSON object per line (newline-delimited).
|
||||||
|
* - `csv` – RFC-4180 CSV with a header row; columns are the
|
||||||
|
* union of all field names in the batch's event data.
|
||||||
|
*
|
||||||
|
* **Compression**: when `config.gzip` is `true` the body is gzip-compressed
|
||||||
|
* before upload and `Content-Encoding: gzip` is set on the object.
|
||||||
|
*
|
||||||
|
* **Custom endpoint**: set `config.endpoint` to target any S3-compatible
|
||||||
|
* storage service (e.g. MinIO, Cloudflare R2).
|
||||||
|
*/
|
||||||
|
export class S3LogDestination implements LogDestinationProvider {
|
||||||
|
readonly type = "s3";
|
||||||
|
|
||||||
|
private readonly config: S3Config;
|
||||||
|
|
||||||
|
constructor(config: S3Config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// LogDestinationProvider implementation
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
async send(events: LogEvent[]): Promise<void> {
|
||||||
|
if (events.length === 0) return;
|
||||||
|
|
||||||
|
const format = this.config.format ?? DEFAULT_FORMAT;
|
||||||
|
const useGzip = this.config.gzip ?? false;
|
||||||
|
const logType = events[0].logType;
|
||||||
|
|
||||||
|
const rawBody = this.serialize(events, format);
|
||||||
|
const bodyBuffer = Buffer.from(rawBody, "utf-8");
|
||||||
|
|
||||||
|
let uploadBody: Buffer;
|
||||||
|
let contentEncoding: string | undefined;
|
||||||
|
|
||||||
|
if (useGzip) {
|
||||||
|
uploadBody = (await gzipAsync(bodyBuffer)) as Buffer;
|
||||||
|
contentEncoding = "gzip";
|
||||||
|
} else {
|
||||||
|
uploadBody = bodyBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.buildObjectKey(logType, format, useGzip);
|
||||||
|
const contentType = this.contentType(format);
|
||||||
|
|
||||||
|
const clientConfig: ConstructorParameters<typeof S3Client>[0] = {
|
||||||
|
region: this.config.region,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: this.config.accessKeyId,
|
||||||
|
secretAccessKey: this.config.secretAccessKey
|
||||||
|
},
|
||||||
|
requestHandler: {
|
||||||
|
requestTimeout: REQUEST_TIMEOUT_MS
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.config.endpoint?.trim()) {
|
||||||
|
clientConfig.endpoint = this.config.endpoint.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new S3Client(clientConfig);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: this.config.bucket,
|
||||||
|
Key: key,
|
||||||
|
Body: uploadBody,
|
||||||
|
ContentType: contentType,
|
||||||
|
...(contentEncoding
|
||||||
|
? { ContentEncoding: contentEncoding }
|
||||||
|
: {})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(
|
||||||
|
`S3LogDestination: failed to upload object "${key}" ` +
|
||||||
|
`to bucket "${this.config.bucket}" – ${msg}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a unique S3 object key for the given log type and format.
|
||||||
|
* Keys are partitioned by logType and date so they can be queried or
|
||||||
|
* lifecycle-managed independently.
|
||||||
|
*/
|
||||||
|
private buildObjectKey(
|
||||||
|
logType: string,
|
||||||
|
format: S3PayloadFormat,
|
||||||
|
gzip: boolean
|
||||||
|
): string {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getUTCFullYear();
|
||||||
|
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(now.getUTCDate()).padStart(2, "0");
|
||||||
|
const hh = String(now.getUTCHours()).padStart(2, "0");
|
||||||
|
const mm = String(now.getUTCMinutes()).padStart(2, "0");
|
||||||
|
const ss = String(now.getUTCSeconds()).padStart(2, "0");
|
||||||
|
const uid = randomUUID();
|
||||||
|
|
||||||
|
const ext =
|
||||||
|
format === "csv" ? "csv" : format === "ndjson" ? "ndjson" : "json";
|
||||||
|
const fileName = `${hh}-${mm}-${ss}-${uid}.${ext}${gzip ? ".gz" : ""}`;
|
||||||
|
|
||||||
|
const rawPrefix = (this.config.prefix ?? "").trim().replace(/\/+$/, "");
|
||||||
|
const parts = [
|
||||||
|
rawPrefix,
|
||||||
|
logType,
|
||||||
|
`${year}/${month}/${day}`,
|
||||||
|
fileName
|
||||||
|
].filter((p) => p !== "");
|
||||||
|
|
||||||
|
return parts.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private contentType(format: S3PayloadFormat): string {
|
||||||
|
switch (format) {
|
||||||
|
case "csv":
|
||||||
|
return "text/csv; charset=utf-8";
|
||||||
|
case "ndjson":
|
||||||
|
return "application/x-ndjson";
|
||||||
|
default:
|
||||||
|
return "application/json";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private serialize(events: LogEvent[], format: S3PayloadFormat): string {
|
||||||
|
switch (format) {
|
||||||
|
case "json_array":
|
||||||
|
return JSON.stringify(events.map(toPayload));
|
||||||
|
case "ndjson":
|
||||||
|
return events
|
||||||
|
.map((e) => JSON.stringify(toPayload(e)))
|
||||||
|
.join("\n");
|
||||||
|
case "csv":
|
||||||
|
return toCsv(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Payload helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function toPayload(event: LogEvent): unknown {
|
||||||
|
return {
|
||||||
|
event: event.logType,
|
||||||
|
timestamp: new Date(event.timestamp * 1000).toISOString(),
|
||||||
|
data: event.data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a batch of events to RFC-4180 CSV.
|
||||||
|
*
|
||||||
|
* The column set is the union of `event`, `timestamp`, and all keys present in
|
||||||
|
* `event.data` across the batch, preserving insertion order. Values that
|
||||||
|
* contain commas, double-quotes, or newlines are quoted and escaped.
|
||||||
|
*/
|
||||||
|
function toCsv(events: LogEvent[]): string {
|
||||||
|
if (events.length === 0) return "";
|
||||||
|
|
||||||
|
// Collect all unique data keys in stable order
|
||||||
|
const keySet = new LinkedSet<string>();
|
||||||
|
keySet.add("event");
|
||||||
|
keySet.add("timestamp");
|
||||||
|
for (const e of events) {
|
||||||
|
for (const k of Object.keys(e.data)) {
|
||||||
|
keySet.add(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const headers = keySet.toArray();
|
||||||
|
|
||||||
|
const rows: string[] = [headers.map(csvEscape).join(",")];
|
||||||
|
|
||||||
|
for (const e of events) {
|
||||||
|
const flat: Record<string, unknown> = {
|
||||||
|
event: e.logType,
|
||||||
|
timestamp: new Date(e.timestamp * 1000).toISOString(),
|
||||||
|
...e.data
|
||||||
|
};
|
||||||
|
rows.push(
|
||||||
|
headers.map((h) => csvEscape(flattenValue(flat[h]))).join(",")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flatten a value to a plain string suitable for a CSV cell. */
|
||||||
|
function flattenValue(value: unknown): string {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
if (typeof value === "object") return JSON.stringify(value);
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RFC-4180 CSV escaping. */
|
||||||
|
function csvEscape(value: string): string {
|
||||||
|
if (/[",\n\r]/.test(value)) {
|
||||||
|
return `"${value.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Minimal ordered set (preserves insertion order, deduplicates)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class LinkedSet<T> {
|
||||||
|
private readonly map = new Map<T, true>();
|
||||||
|
|
||||||
|
add(value: T): void {
|
||||||
|
this.map.set(value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): T[] {
|
||||||
|
return Array.from(this.map.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,6 +107,40 @@ export interface HttpConfig {
|
|||||||
bodyTemplate?: string;
|
bodyTemplate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// S3 destination configuration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls how the batch of events is serialised into each S3 object.
|
||||||
|
*
|
||||||
|
* - `json_array` – `[{…}, {…}]` – default; each object is a JSON array.
|
||||||
|
* - `ndjson` – `{…}\n{…}` – newline-delimited JSON, one object per line.
|
||||||
|
* - `csv` – RFC-4180 CSV with a header row derived from the event fields.
|
||||||
|
*/
|
||||||
|
export type S3PayloadFormat = "json_array" | "ndjson" | "csv";
|
||||||
|
|
||||||
|
export interface S3Config {
|
||||||
|
/** Human-readable label for the destination */
|
||||||
|
name: string;
|
||||||
|
/** AWS Access Key ID */
|
||||||
|
accessKeyId: string;
|
||||||
|
/** AWS Secret Access Key */
|
||||||
|
secretAccessKey: string;
|
||||||
|
/** AWS region (e.g. "us-east-1") */
|
||||||
|
region: string;
|
||||||
|
/** Target S3 bucket name */
|
||||||
|
bucket: string;
|
||||||
|
/** Optional key prefix – appended before the auto-generated path */
|
||||||
|
prefix?: string;
|
||||||
|
/** Override the S3 endpoint for S3-compatible storage (e.g. MinIO, R2) */
|
||||||
|
endpoint?: string;
|
||||||
|
/** How events are serialised into each object. Defaults to "json_array". */
|
||||||
|
format: S3PayloadFormat;
|
||||||
|
/** Whether to gzip-compress the object before upload. */
|
||||||
|
gzip: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Per-destination per-log-type cursor (reflects the DB table)
|
// Per-destination per-log-type cursor (reflects the DB table)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const privateConfigSchema = z
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
hide_auth_layout_footer: z.boolean().optional().default(false),
|
hide_auth_layout_footer: z.boolean().optional().default(false),
|
||||||
|
hide_powered_by: z.boolean().optional(),
|
||||||
login_page: z
|
login_page: z
|
||||||
.object({
|
.object({
|
||||||
subtitle_text: z.string().optional()
|
subtitle_text: z.string().optional()
|
||||||
|
|||||||
@@ -124,15 +124,11 @@ function getWhere(data: Q) {
|
|||||||
data.clientId
|
data.clientId
|
||||||
? eq(connectionAuditLog.clientId, data.clientId)
|
? eq(connectionAuditLog.clientId, data.clientId)
|
||||||
: undefined,
|
: undefined,
|
||||||
data.siteId
|
data.siteId ? eq(connectionAuditLog.siteId, data.siteId) : undefined,
|
||||||
? eq(connectionAuditLog.siteId, data.siteId)
|
|
||||||
: undefined,
|
|
||||||
data.siteResourceId
|
data.siteResourceId
|
||||||
? eq(connectionAuditLog.siteResourceId, data.siteResourceId)
|
? eq(connectionAuditLog.siteResourceId, data.siteResourceId)
|
||||||
: undefined,
|
: undefined,
|
||||||
data.userId
|
data.userId ? eq(connectionAuditLog.userId, data.userId) : undefined
|
||||||
? eq(connectionAuditLog.userId, data.userId)
|
|
||||||
: undefined
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +140,7 @@ export function queryConnection(data: Q) {
|
|||||||
orgId: connectionAuditLog.orgId,
|
orgId: connectionAuditLog.orgId,
|
||||||
siteId: connectionAuditLog.siteId,
|
siteId: connectionAuditLog.siteId,
|
||||||
clientId: connectionAuditLog.clientId,
|
clientId: connectionAuditLog.clientId,
|
||||||
|
clientEndpoint: connectionAuditLog.clientEndpoint,
|
||||||
userId: connectionAuditLog.userId,
|
userId: connectionAuditLog.userId,
|
||||||
sourceAddr: connectionAuditLog.sourceAddr,
|
sourceAddr: connectionAuditLog.sourceAddr,
|
||||||
destAddr: connectionAuditLog.destAddr,
|
destAddr: connectionAuditLog.destAddr,
|
||||||
@@ -203,10 +200,7 @@ async function enrichWithDetails(
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Fetch resource details from main database
|
// Fetch resource details from main database
|
||||||
const resourceMap = new Map<
|
const resourceMap = new Map<number, { name: string; niceId: string }>();
|
||||||
number,
|
|
||||||
{ name: string; niceId: string }
|
|
||||||
>();
|
|
||||||
if (siteResourceIds.length > 0) {
|
if (siteResourceIds.length > 0) {
|
||||||
const resourceDetails = await primaryDb
|
const resourceDetails = await primaryDb
|
||||||
.select({
|
.select({
|
||||||
@@ -268,10 +262,7 @@ async function enrichWithDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch user details from main database
|
// Fetch user details from main database
|
||||||
const userMap = new Map<
|
const userMap = new Map<string, { email: string | null }>();
|
||||||
string,
|
|
||||||
{ email: string | null }
|
|
||||||
>();
|
|
||||||
if (userIds.length > 0) {
|
if (userIds.length > 0) {
|
||||||
const userDetails = await primaryDb
|
const userDetails = await primaryDb
|
||||||
.select({
|
.select({
|
||||||
@@ -290,29 +281,25 @@ async function enrichWithDetails(
|
|||||||
return logs.map((log) => ({
|
return logs.map((log) => ({
|
||||||
...log,
|
...log,
|
||||||
resourceName: log.siteResourceId
|
resourceName: log.siteResourceId
|
||||||
? resourceMap.get(log.siteResourceId)?.name ?? null
|
? (resourceMap.get(log.siteResourceId)?.name ?? null)
|
||||||
: null,
|
: null,
|
||||||
resourceNiceId: log.siteResourceId
|
resourceNiceId: log.siteResourceId
|
||||||
? resourceMap.get(log.siteResourceId)?.niceId ?? null
|
? (resourceMap.get(log.siteResourceId)?.niceId ?? null)
|
||||||
: null,
|
|
||||||
siteName: log.siteId
|
|
||||||
? siteMap.get(log.siteId)?.name ?? null
|
|
||||||
: null,
|
: null,
|
||||||
|
siteName: log.siteId ? (siteMap.get(log.siteId)?.name ?? null) : null,
|
||||||
siteNiceId: log.siteId
|
siteNiceId: log.siteId
|
||||||
? siteMap.get(log.siteId)?.niceId ?? null
|
? (siteMap.get(log.siteId)?.niceId ?? null)
|
||||||
: null,
|
: null,
|
||||||
clientName: log.clientId
|
clientName: log.clientId
|
||||||
? clientMap.get(log.clientId)?.name ?? null
|
? (clientMap.get(log.clientId)?.name ?? null)
|
||||||
: null,
|
: null,
|
||||||
clientNiceId: log.clientId
|
clientNiceId: log.clientId
|
||||||
? clientMap.get(log.clientId)?.niceId ?? null
|
? (clientMap.get(log.clientId)?.niceId ?? null)
|
||||||
: null,
|
: null,
|
||||||
clientType: log.clientId
|
clientType: log.clientId
|
||||||
? clientMap.get(log.clientId)?.type ?? null
|
? (clientMap.get(log.clientId)?.type ?? null)
|
||||||
: null,
|
: null,
|
||||||
userEmail: log.userId
|
userEmail: log.userId ? (userMap.get(log.userId)?.email ?? null) : null
|
||||||
? userMap.get(log.userId)?.email ?? null
|
|
||||||
: null
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,4 +508,4 @@ export async function queryConnectionAuditLogs(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export type ListEventStreamingDestinationsResponse = {
|
|||||||
type: string;
|
type: string;
|
||||||
config: string;
|
config: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
lastError: string | null;
|
||||||
|
lastErrorAt: number | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
sendConnectionLogs: boolean;
|
sendConnectionLogs: boolean;
|
||||||
@@ -79,7 +81,8 @@ async function query(orgId: string, limit: number, offset: number) {
|
|||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/event-streaming-destination",
|
path: "/org/{orgId}/event-streaming-destination",
|
||||||
description: "List all event streaming destinations for a specific organization.",
|
description:
|
||||||
|
"List all event streaming destinations for a specific organization.",
|
||||||
tags: [OpenAPITags.Org],
|
tags: [OpenAPITags.Org],
|
||||||
request: {
|
request: {
|
||||||
query: querySchema,
|
query: querySchema,
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
|
|||||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||||
import * as alertRule from "#private/routers/alertRule";
|
import * as alertRule from "#private/routers/alertRule";
|
||||||
import * as healthChecks from "#private/routers/healthChecks";
|
import * as healthChecks from "#private/routers/healthChecks";
|
||||||
|
import * as resource from "#private/routers/resource";
|
||||||
|
import * as policy from "#private/routers/policy";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -44,7 +46,8 @@ import {
|
|||||||
verifyUserCanSetUserOrgRoles,
|
verifyUserCanSetUserOrgRoles,
|
||||||
verifySiteProvisioningKeyAccess,
|
verifySiteProvisioningKeyAccess,
|
||||||
verifyIsLoggedInUser,
|
verifyIsLoggedInUser,
|
||||||
verifyAdmin
|
verifyAdmin,
|
||||||
|
verifyResourcePolicyAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import {
|
import {
|
||||||
@@ -382,6 +385,39 @@ authenticated.get(
|
|||||||
approval.countApprovals
|
approval.countApprovals
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/resource-policy/:resourcePolicyId",
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.deleteResourcePolicy),
|
||||||
|
logActionAudit(ActionsEnum.deleteResourcePolicy),
|
||||||
|
policy.deleteResourcePolicy
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/resource-policies",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.listResourcePolicies),
|
||||||
|
logActionAudit(ActionsEnum.listResourcePolicies),
|
||||||
|
policy.listResourcePolicies
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/resource-policy",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyValidSubscription(tierMatrix.resourcePolicies),
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.createResourcePolicy),
|
||||||
|
logActionAudit(ActionsEnum.createResourcePolicy),
|
||||||
|
policy.createResourcePolicy
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/approvals/:approvalId",
|
"/org/:orgId/approvals/:approvalId",
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
|
|||||||
@@ -45,8 +45,11 @@ import {
|
|||||||
users,
|
users,
|
||||||
userOrgs,
|
userOrgs,
|
||||||
roleResources,
|
roleResources,
|
||||||
|
rolePolicies,
|
||||||
userResources,
|
userResources,
|
||||||
|
userPolicies,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
|
resourcePolicyRules,
|
||||||
userOrgRoles,
|
userOrgRoles,
|
||||||
roles
|
roles
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
@@ -430,7 +433,10 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Decrypt and save key file
|
// Decrypt and save key file
|
||||||
const decryptedKey = decrypt(cert.keyFile!, config.getRawConfig().server.secret!);
|
const decryptedKey = decrypt(
|
||||||
|
cert.keyFile!,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
|
||||||
// Return only the certificate data without org information
|
// Return only the certificate data without org information
|
||||||
return {
|
return {
|
||||||
@@ -531,7 +537,10 @@ hybridRouter.get(
|
|||||||
wildcardCandidates.length > 0
|
wildcardCandidates.length > 0
|
||||||
? and(
|
? and(
|
||||||
eq(resources.wildcard, true),
|
eq(resources.wildcard, true),
|
||||||
inArray(resources.fullDomain, wildcardCandidates)
|
inArray(
|
||||||
|
resources.fullDomain,
|
||||||
|
wildcardCandidates
|
||||||
|
)
|
||||||
)
|
)
|
||||||
: sql`false`
|
: sql`false`
|
||||||
)
|
)
|
||||||
@@ -545,10 +554,10 @@ hybridRouter.get(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
result &&
|
result &&
|
||||||
await checkExitNodeOrg(
|
(await checkExitNodeOrg(
|
||||||
remoteExitNode.exitNodeId,
|
remoteExitNode.exitNodeId,
|
||||||
result.resources.orgId
|
result.resources.orgId
|
||||||
)
|
))
|
||||||
) {
|
) {
|
||||||
// If the exit node is not allowed for the org, return an error
|
// If the exit node is not allowed for the org, return an error
|
||||||
return next(
|
return next(
|
||||||
@@ -1132,22 +1141,43 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleResourceAccess = await db
|
const [direct, viaPolicies] = await Promise.all([
|
||||||
.select()
|
db
|
||||||
.from(roleResources)
|
.select()
|
||||||
.where(
|
.from(roleResources)
|
||||||
and(
|
.where(
|
||||||
eq(roleResources.resourceId, resourceId),
|
and(
|
||||||
eq(roleResources.roleId, roleId)
|
eq(roleResources.resourceId, resourceId),
|
||||||
|
eq(roleResources.roleId, roleId)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
.limit(1),
|
||||||
.limit(1);
|
db
|
||||||
|
.select({
|
||||||
|
roleId: rolePolicies.roleId,
|
||||||
|
resourcePolicyId: rolePolicies.resourcePolicyId
|
||||||
|
})
|
||||||
|
.from(rolePolicies)
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(
|
||||||
|
resources.resourcePolicyId,
|
||||||
|
rolePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.resourceId, resourceId),
|
||||||
|
eq(rolePolicies.roleId, roleId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
]);
|
||||||
|
|
||||||
const result =
|
const result = direct[0] ?? viaPolicies[0] ?? null;
|
||||||
roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
|
|
||||||
|
|
||||||
return response<typeof roleResources.$inferSelect | null>(res, {
|
return response<typeof roleResources.$inferSelect | null>(res, {
|
||||||
data: result,
|
data: result as any,
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: result
|
message: result
|
||||||
@@ -1222,21 +1252,44 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleResourceAccess = await db
|
const [direct, viaPolicies] = await Promise.all([
|
||||||
.select({
|
db
|
||||||
resourceId: roleResources.resourceId,
|
.select({
|
||||||
roleId: roleResources.roleId
|
resourceId: roleResources.resourceId,
|
||||||
})
|
roleId: roleResources.roleId
|
||||||
.from(roleResources)
|
})
|
||||||
.where(
|
.from(roleResources)
|
||||||
and(
|
.where(
|
||||||
eq(roleResources.resourceId, resourceId),
|
and(
|
||||||
inArray(roleResources.roleId, roleIds)
|
eq(roleResources.resourceId, resourceId),
|
||||||
)
|
inArray(roleResources.roleId, roleIds)
|
||||||
);
|
)
|
||||||
|
),
|
||||||
|
roleIds.length > 0
|
||||||
|
? db
|
||||||
|
.select({
|
||||||
|
resourceId: sql<number>`${resourceId}`,
|
||||||
|
roleId: rolePolicies.roleId
|
||||||
|
})
|
||||||
|
.from(rolePolicies)
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(
|
||||||
|
resources.resourcePolicyId,
|
||||||
|
rolePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.resourceId, resourceId),
|
||||||
|
inArray(rolePolicies.roleId, roleIds)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: Promise.resolve([])
|
||||||
|
]);
|
||||||
|
|
||||||
const result =
|
const combined = [...direct, ...viaPolicies];
|
||||||
roleResourceAccess.length > 0 ? roleResourceAccess : null;
|
const result = combined.length > 0 ? combined : null;
|
||||||
|
|
||||||
return response<{ resourceId: number; roleId: number }[] | null>(
|
return response<{ resourceId: number; roleId: number }[] | null>(
|
||||||
res,
|
res,
|
||||||
@@ -1397,10 +1450,45 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules = await db
|
const [directRules, policyRules] = await Promise.all([
|
||||||
.select()
|
db
|
||||||
.from(resourceRules)
|
.select()
|
||||||
.where(eq(resourceRules.resourceId, resourceId));
|
.from(resourceRules)
|
||||||
|
.where(eq(resourceRules.resourceId, resourceId)),
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
ruleId: resourcePolicyRules.ruleId,
|
||||||
|
resourceId: sql<number>`${resourceId}`,
|
||||||
|
enabled: resourcePolicyRules.enabled,
|
||||||
|
priority: resourcePolicyRules.priority,
|
||||||
|
action: resourcePolicyRules.action,
|
||||||
|
match: resourcePolicyRules.match,
|
||||||
|
value: resourcePolicyRules.value
|
||||||
|
})
|
||||||
|
.from(resourcePolicyRules)
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(
|
||||||
|
resources.resourcePolicyId,
|
||||||
|
resourcePolicyRules.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(eq(resources.resourceId, resourceId))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const maxDirectPriority = directRules.reduce(
|
||||||
|
(max, r) => Math.max(max, r.priority),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const offsetPolicyRules = policyRules.map((r) => ({
|
||||||
|
...r,
|
||||||
|
priority: maxDirectPriority + r.priority
|
||||||
|
}));
|
||||||
|
|
||||||
|
const rules = [
|
||||||
|
...directRules,
|
||||||
|
...offsetPolicyRules
|
||||||
|
] as (typeof resourceRules.$inferSelect)[];
|
||||||
|
|
||||||
// backward compatibility: COUNTRY -> GEOIP
|
// backward compatibility: COUNTRY -> GEOIP
|
||||||
// TODO: remove this after a few versions once all exit nodes are updated
|
// TODO: remove this after a few versions once all exit nodes are updated
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db } from "@server/db";
|
import { clientSitesAssociationsCache, db } from "@server/db";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { sites, Newt, clients, orgs } from "@server/db";
|
import { sites, Newt, clients, orgs } from "@server/db";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
@@ -146,7 +146,11 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
|||||||
// each unique sourceAddr + the org's CIDR suffix and do a targeted IN query.
|
// each unique sourceAddr + the org's CIDR suffix and do a targeted IN query.
|
||||||
const ipToClient = new Map<
|
const ipToClient = new Map<
|
||||||
string,
|
string,
|
||||||
{ clientId: number; userId: string | null }
|
{
|
||||||
|
clientId: number;
|
||||||
|
userId: string | null;
|
||||||
|
clientEndpoint: string | null;
|
||||||
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
if (cidrSuffix) {
|
if (cidrSuffix) {
|
||||||
@@ -172,9 +176,21 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
|||||||
.select({
|
.select({
|
||||||
clientId: clients.clientId,
|
clientId: clients.clientId,
|
||||||
userId: clients.userId,
|
userId: clients.userId,
|
||||||
subnet: clients.subnet
|
subnet: clients.subnet,
|
||||||
|
clientEndpoint: clientSitesAssociationsCache.endpoint
|
||||||
})
|
})
|
||||||
.from(clients)
|
.from(clients)
|
||||||
|
.leftJoin(
|
||||||
|
// this should be one to one
|
||||||
|
clientSitesAssociationsCache,
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
clients.clientId,
|
||||||
|
clientSitesAssociationsCache.clientId
|
||||||
|
),
|
||||||
|
eq(clientSitesAssociationsCache.siteId, newt.siteId)
|
||||||
|
)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(clients.orgId, orgId),
|
eq(clients.orgId, orgId),
|
||||||
@@ -189,7 +205,8 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
|||||||
);
|
);
|
||||||
ipToClient.set(ip, {
|
ipToClient.set(ip, {
|
||||||
clientId: c.clientId,
|
clientId: c.clientId,
|
||||||
userId: c.userId
|
userId: c.userId,
|
||||||
|
clientEndpoint: c.clientEndpoint
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,6 +251,7 @@ export const handleConnectionLogMessage: MessageHandler = async (context) => {
|
|||||||
orgId,
|
orgId,
|
||||||
siteId: newt.siteId,
|
siteId: newt.siteId,
|
||||||
clientId: clientInfo?.clientId ?? null,
|
clientId: clientInfo?.clientId ?? null,
|
||||||
|
clientEndpoint: clientInfo?.clientEndpoint ?? null,
|
||||||
userId: clientInfo?.userId ?? null,
|
userId: clientInfo?.userId ?? null,
|
||||||
sourceAddr: session.sourceAddr,
|
sourceAddr: session.sourceAddr,
|
||||||
destAddr: session.destAddr,
|
destAddr: session.destAddr,
|
||||||
|
|||||||
417
server/private/routers/policy/createResourcePolicy.ts
Normal file
417
server/private/routers/policy/createResourcePolicy.ts
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { hashPassword } from "@server/auth/password";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
idp,
|
||||||
|
idpOrg,
|
||||||
|
orgs,
|
||||||
|
resourcePolicies,
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
resourcePolicyPassword,
|
||||||
|
resourcePolicyPincode,
|
||||||
|
resourcePolicyRules,
|
||||||
|
resourcePolicyWhiteList,
|
||||||
|
rolePolicies,
|
||||||
|
roles,
|
||||||
|
userOrgs,
|
||||||
|
userPolicies,
|
||||||
|
users,
|
||||||
|
type ResourcePolicy
|
||||||
|
} from "@server/db";
|
||||||
|
import { getUniqueResourcePolicyName } from "@server/db/names";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import {
|
||||||
|
isValidCIDR,
|
||||||
|
isValidIP,
|
||||||
|
isValidUrlGlobPattern
|
||||||
|
} from "@server/lib/validators";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { and, eq, inArray, type InferInsertModel } from "drizzle-orm";
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import z from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const createResourcePolicyParamsSchema = z.strictObject({
|
||||||
|
orgId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
const ruleSchema = z.strictObject({
|
||||||
|
action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["ACCEPT", "DROP", "PASS"],
|
||||||
|
description: "rule action"
|
||||||
|
}),
|
||||||
|
match: z.enum(["CIDR", "IP", "PATH"]).openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["CIDR", "IP", "PATH"],
|
||||||
|
description: "rule match"
|
||||||
|
}),
|
||||||
|
value: z.string().min(1),
|
||||||
|
priority: z.int().openapi({
|
||||||
|
type: "integer",
|
||||||
|
description: "Rule priority"
|
||||||
|
}),
|
||||||
|
enabled: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const createResourcePolicyBodySchema = z.strictObject({
|
||||||
|
name: z.string().min(1).max(255),
|
||||||
|
// Access control
|
||||||
|
sso: z.boolean().default(true),
|
||||||
|
skipToIdpId: z
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.openapi({ type: "integer" }),
|
||||||
|
roleIds: z
|
||||||
|
.array(z.string().transform(Number).pipe(z.int().positive()))
|
||||||
|
.optional()
|
||||||
|
.default([]),
|
||||||
|
userIds: z.array(z.string()).optional().default([]),
|
||||||
|
// auth methods
|
||||||
|
password: z.string().min(4).max(100).nullable().optional(),
|
||||||
|
pincode: z
|
||||||
|
.string()
|
||||||
|
.regex(/^\d{6}$/)
|
||||||
|
.or(z.null())
|
||||||
|
.optional(),
|
||||||
|
headerAuth: z
|
||||||
|
.object({
|
||||||
|
user: z.string().min(4).max(100),
|
||||||
|
password: z.string().min(4).max(100),
|
||||||
|
extendedCompatibility: z.boolean()
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
// email OTP
|
||||||
|
emailWhitelistEnabled: z.boolean().optional().default(false),
|
||||||
|
emails: z
|
||||||
|
.array(
|
||||||
|
z.email().or(
|
||||||
|
z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
|
||||||
|
error: "Invalid email address. Wildcard (*) must be the entire local part."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.max(50)
|
||||||
|
.transform((v) => v.map((e) => e.toLowerCase()))
|
||||||
|
.optional()
|
||||||
|
.default([]),
|
||||||
|
// rules
|
||||||
|
applyRules: z.boolean().default(false),
|
||||||
|
rules: z.array(ruleSchema).optional().default([])
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/org/{orgId}/resource-policy",
|
||||||
|
description: "Create a resource policy.",
|
||||||
|
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: createResourcePolicyParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: createResourcePolicyBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createResourcePolicy(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Validate request params
|
||||||
|
const parsedParams = createResourcePolicyParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
if (req.user && req.userOrgRoleIds?.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the org
|
||||||
|
const org = await db
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (org.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Organization with ID ${orgId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = createResourcePolicyBodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
sso,
|
||||||
|
userIds,
|
||||||
|
roleIds,
|
||||||
|
skipToIdpId,
|
||||||
|
applyRules,
|
||||||
|
emailWhitelistEnabled,
|
||||||
|
password,
|
||||||
|
pincode,
|
||||||
|
headerAuth,
|
||||||
|
emails,
|
||||||
|
rules
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
|
// Check if Identity provider in `skipToIdpId` exists
|
||||||
|
if (skipToIdpId) {
|
||||||
|
const [provider] = await db
|
||||||
|
.select()
|
||||||
|
.from(idp)
|
||||||
|
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
|
||||||
|
.where(and(eq(idp.idpId, skipToIdpId), eq(idpOrg.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Identity provider not found in this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminRole = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (adminRole.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRoles = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(inArray(roles.roleId, roleIds)));
|
||||||
|
|
||||||
|
const hasAdminRole = existingRoles.some((role) => role.isAdmin);
|
||||||
|
|
||||||
|
if (hasAdminRole) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Admin role cannot be assigned to resource policy"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUsers = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(userOrgs.userId, users.userId))
|
||||||
|
.where(
|
||||||
|
and(eq(userOrgs.orgId, orgId), inArray(users.userId, userIds))
|
||||||
|
);
|
||||||
|
|
||||||
|
const niceId = await getUniqueResourcePolicyName(orgId);
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid CIDR provided"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (rule.match === "IP" && !isValidIP(rule.value)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided")
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
rule.match === "PATH" &&
|
||||||
|
!isValidUrlGlobPattern(rule.value)
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid URL glob pattern provided"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = await db.transaction(async (trx) => {
|
||||||
|
const [newPolicy] = await trx
|
||||||
|
.insert(resourcePolicies)
|
||||||
|
.values({
|
||||||
|
niceId,
|
||||||
|
orgId,
|
||||||
|
name,
|
||||||
|
sso,
|
||||||
|
idpId: skipToIdpId,
|
||||||
|
applyRules,
|
||||||
|
emailWhitelistEnabled
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const rolesToAdd = [
|
||||||
|
{
|
||||||
|
roleId: adminRole[0].roleId,
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId
|
||||||
|
}
|
||||||
|
] satisfies InferInsertModel<typeof rolePolicies>[];
|
||||||
|
|
||||||
|
rolesToAdd.push(
|
||||||
|
...existingRoles.map((role) => ({
|
||||||
|
roleId: role.roleId,
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(rolePolicies).values(rolesToAdd);
|
||||||
|
|
||||||
|
const usersToAdd: InferInsertModel<typeof userPolicies>[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.user &&
|
||||||
|
!req.userOrgRoleIds?.includes(adminRole[0].roleId)
|
||||||
|
) {
|
||||||
|
// make sure the user can access the policy
|
||||||
|
usersToAdd.push({
|
||||||
|
userId: req.user?.userId!,
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
usersToAdd.push(
|
||||||
|
...existingUsers.map(({ user }) => ({
|
||||||
|
userId: user.userId,
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (usersToAdd.length > 0) {
|
||||||
|
await trx.insert(userPolicies).values(usersToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
const passwordHash = await hashPassword(password);
|
||||||
|
|
||||||
|
await trx.insert(resourcePolicyPassword).values({
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId,
|
||||||
|
passwordHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pincode) {
|
||||||
|
const pincodeHash = await hashPassword(pincode);
|
||||||
|
|
||||||
|
await trx.insert(resourcePolicyPincode).values({
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId,
|
||||||
|
pincodeHash,
|
||||||
|
digitLength: 6
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerAuth) {
|
||||||
|
const headerAuthHash = await hashPassword(
|
||||||
|
Buffer.from(
|
||||||
|
`${headerAuth.user}:${headerAuth.password}`
|
||||||
|
).toString("base64")
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId,
|
||||||
|
headerAuthHash,
|
||||||
|
extendedCompatibility: headerAuth.extendedCompatibility
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailWhitelistEnabled && emails.length > 0) {
|
||||||
|
await trx.insert(resourcePolicyWhiteList).values(
|
||||||
|
emails.map((email) => ({
|
||||||
|
email,
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.length > 0) {
|
||||||
|
await trx.insert(resourcePolicyRules).values(
|
||||||
|
rules.map((rule) => ({
|
||||||
|
resourcePolicyId: newPolicy.resourcePolicyId,
|
||||||
|
...rule
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPolicy;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to create policy"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response<ResourcePolicy>(res, {
|
||||||
|
data: policy,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "resource policy created successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
107
server/private/routers/policy/deleteResourcePolicy.ts
Normal file
107
server/private/routers/policy/deleteResourcePolicy.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db, resourcePolicies, resources } 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 } from "drizzle-orm";
|
||||||
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import z from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
// Define Zod schema for request parameters validation
|
||||||
|
const deleteResourcePolicySchema = z.strictObject({
|
||||||
|
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "delete",
|
||||||
|
path: "/resource-policy/{resourcePolicyId}",
|
||||||
|
description: "Delete a resource policy.",
|
||||||
|
tags: [OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: deleteResourcePolicySchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function deleteResourcePolicy(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = deleteResourcePolicySchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourcePolicyId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [existingResource] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||||
|
|
||||||
|
if (!existingResource) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Resource Policy with ID ${resourcePolicyId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAffectedResources = await db.$count(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(eq(resources.resourcePolicyId, resourcePolicyId))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (totalAffectedResources > 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
`Cannot delete Policy '${existingResource.name}' as it's being used by at least one resource`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete policy
|
||||||
|
await db
|
||||||
|
.delete(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resource Policy deleted successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
server/private/routers/policy/index.ts
Normal file
16
server/private/routers/policy/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./createResourcePolicy";
|
||||||
|
export * from "./listResourcePolicies";
|
||||||
|
export * from "./deleteResourcePolicy";
|
||||||
271
server/private/routers/policy/listResourcePolicies.ts
Normal file
271
server/private/routers/policy/listResourcePolicies.ts
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
resourcePolicies,
|
||||||
|
resources,
|
||||||
|
rolePolicies,
|
||||||
|
userPolicies
|
||||||
|
} from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import type {
|
||||||
|
ListResourcePoliciesResponse,
|
||||||
|
ResourcePolicyWithResources
|
||||||
|
} from "@server/routers/resource/types";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { and, asc, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const listResourcePoliciesParamsSchema = z.strictObject({
|
||||||
|
orgId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
const listResourcePoliciesSchema = z.object({
|
||||||
|
pageSize: z.coerce
|
||||||
|
.number<string>() // for prettier formatting
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.optional()
|
||||||
|
.catch(20)
|
||||||
|
.default(20)
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
default: 20,
|
||||||
|
description: "Number of items per page"
|
||||||
|
}),
|
||||||
|
page: z.coerce
|
||||||
|
.number<string>() // for prettier formatting
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.optional()
|
||||||
|
.catch(1)
|
||||||
|
.default(1)
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
default: 1,
|
||||||
|
description: "Page number to retrieve"
|
||||||
|
}),
|
||||||
|
query: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
function queryResourcePoliciesBase() {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
resourcePolicyId: resourcePolicies.resourcePolicyId,
|
||||||
|
name: resourcePolicies.name,
|
||||||
|
niceId: resourcePolicies.niceId,
|
||||||
|
orgId: resourcePolicies.orgId
|
||||||
|
})
|
||||||
|
.from(resourcePolicies);
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/resource-policies",
|
||||||
|
description: "List resource policies for an organization.",
|
||||||
|
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: z.object({
|
||||||
|
orgId: z.string()
|
||||||
|
}),
|
||||||
|
query: listResourcePoliciesSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function listResourcePolicies(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedQuery = listResourcePoliciesSchema.safeParse(req.query);
|
||||||
|
if (!parsedQuery.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromZodError(parsedQuery.error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { page, pageSize, query } = parsedQuery.data;
|
||||||
|
|
||||||
|
const parsedParams = listResourcePoliciesParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromZodError(parsedParams.error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId =
|
||||||
|
parsedParams.data.orgId ||
|
||||||
|
req.userOrg?.orgId ||
|
||||||
|
req.apiKeyOrg?.orgId;
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let accessibleResourcePolicies: Array<{ resourcePolicyId: number }>;
|
||||||
|
if (req.user) {
|
||||||
|
accessibleResourcePolicies = await db
|
||||||
|
.select({
|
||||||
|
resourcePolicyId: sql<number>`COALESCE(${userPolicies.resourcePolicyId}, ${rolePolicies.resourcePolicyId})`
|
||||||
|
})
|
||||||
|
.from(userPolicies)
|
||||||
|
.fullJoin(
|
||||||
|
rolePolicies,
|
||||||
|
eq(
|
||||||
|
userPolicies.resourcePolicyId,
|
||||||
|
rolePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
eq(userPolicies.userId, req.user!.userId),
|
||||||
|
inArray(rolePolicies.roleId, req.userOrgRoleIds || [])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
accessibleResourcePolicies = await db
|
||||||
|
.select({
|
||||||
|
resourcePolicyId: resourcePolicies.resourcePolicyId
|
||||||
|
})
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.orgId, orgId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessibleResourceIds = accessibleResourcePolicies.map(
|
||||||
|
(resource) => resource.resourcePolicyId
|
||||||
|
);
|
||||||
|
|
||||||
|
const conditions = [
|
||||||
|
and(
|
||||||
|
inArray(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
accessibleResourceIds
|
||||||
|
),
|
||||||
|
eq(resourcePolicies.orgId, orgId),
|
||||||
|
eq(resourcePolicies.scope, "global")
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
like(
|
||||||
|
sql`LOWER(${resourcePolicies.name})`,
|
||||||
|
"%" + query.toLowerCase() + "%"
|
||||||
|
),
|
||||||
|
like(
|
||||||
|
sql`LOWER(${resourcePolicies.niceId})`,
|
||||||
|
"%" + query.toLowerCase() + "%"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseQuery = queryResourcePoliciesBase().where(and(...conditions));
|
||||||
|
|
||||||
|
// we need to add `as` so that drizzle filters the result as a subquery
|
||||||
|
const countQuery = db.$count(baseQuery.as("filtered_policies"));
|
||||||
|
|
||||||
|
const [rows, totalCount] = await Promise.all([
|
||||||
|
baseQuery
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(pageSize * (page - 1))
|
||||||
|
.orderBy(asc(resourcePolicies.resourcePolicyId)),
|
||||||
|
countQuery
|
||||||
|
]);
|
||||||
|
|
||||||
|
const attachedResources =
|
||||||
|
rows.length === 0
|
||||||
|
? []
|
||||||
|
: await db
|
||||||
|
.select({
|
||||||
|
resourceId: resources.resourceId,
|
||||||
|
name: resources.name,
|
||||||
|
fullDomain: resources.fullDomain,
|
||||||
|
resourcePolicyId: resources.resourcePolicyId
|
||||||
|
})
|
||||||
|
.from(resources)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
resources.resourcePolicyId,
|
||||||
|
rows.map((row) => row.resourcePolicyId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// avoids TS issues with reduce/never[]
|
||||||
|
const map = new Map<number, ResourcePolicyWithResources>();
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
let entry = map.get(row.resourcePolicyId);
|
||||||
|
if (!entry) {
|
||||||
|
entry = {
|
||||||
|
...row,
|
||||||
|
resources: []
|
||||||
|
};
|
||||||
|
map.set(row.resourcePolicyId, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.resources = attachedResources.filter(
|
||||||
|
(r) => r.resourcePolicyId === entry?.resourcePolicyId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const policiesList = Array.from(map.values());
|
||||||
|
|
||||||
|
return response<ListResourcePoliciesResponse>(res, {
|
||||||
|
data: {
|
||||||
|
policies: policiesList,
|
||||||
|
pagination: {
|
||||||
|
total: totalCount,
|
||||||
|
pageSize,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resources retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { clients, db } from "@server/db";
|
import { clients, db, primaryDb, Client } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -98,15 +98,6 @@ export async function addUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser[0].isOwner) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Cannot change the role of the owner of the organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleExists = await db
|
const roleExists = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
@@ -122,8 +113,12 @@ export async function addUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let newUserRole: { userId: string; orgId: string; roleId: number } | null =
|
let newUserRole: {
|
||||||
null;
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
roleId: number;
|
||||||
|
} | null = null;
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const inserted = await trx
|
const inserted = await trx
|
||||||
.insert(userOrgRoles)
|
.insert(userOrgRoles)
|
||||||
@@ -149,11 +144,19 @@ export async function addUserRole(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const orgClient of orgClients) {
|
orgClientsToRebuild = orgClients;
|
||||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const orgClient of orgClientsToRebuild) {
|
||||||
|
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: newUserRole ?? { userId, orgId: role.orgId, roleId },
|
data: newUserRole ?? { userId, orgId: role.orgId, roleId },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { db } from "@server/db";
|
import { db, primaryDb, Client } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles, clients } from "@server/db";
|
import { userOrgRoles, userOrgs, roles, clients } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -98,11 +98,11 @@ export async function removeUserRole(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
if (existingUser.isOwner && role.isAdmin === true) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"Cannot change the roles of the owner of the organization"
|
"Cannot remove the administrator role from the organization owner"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -129,6 +129,7 @@ export async function removeUserRole(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
.delete(userOrgRoles)
|
.delete(userOrgRoles)
|
||||||
@@ -150,11 +151,19 @@ export async function removeUserRole(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const orgClient of orgClients) {
|
orgClientsToRebuild = orgClients;
|
||||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const orgClient of orgClientsToRebuild) {
|
||||||
|
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations for client ${orgClient.clientId} after removing role: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: { userId, orgId: role.orgId, roleId },
|
data: { userId, orgId: role.orgId, roleId },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { clients, db } from "@server/db";
|
import { clients, db, primaryDb, Client } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -87,17 +87,8 @@ export async function setUserOrgRoles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Cannot change the roles of the owner of the organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgRoles = await db
|
const orgRoles = await db
|
||||||
.select({ roleId: roles.roleId })
|
.select({ roleId: roles.roleId, isAdmin: roles.isAdmin })
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -115,6 +106,19 @@ export async function setUserOrgRoles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingUser.isOwner) {
|
||||||
|
const hasAdminRole = orgRoles.some((r) => r.isAdmin === true);
|
||||||
|
if (!hasAdminRole) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"The organization owner must retain an administrator role"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
.delete(userOrgRoles)
|
.delete(userOrgRoles)
|
||||||
@@ -142,11 +146,19 @@ export async function setUserOrgRoles(
|
|||||||
and(eq(clients.userId, userId), eq(clients.orgId, orgId))
|
and(eq(clients.userId, userId), eq(clients.orgId, orgId))
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const orgClient of orgClients) {
|
orgClientsToRebuild = orgClients;
|
||||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const orgClient of orgClientsToRebuild) {
|
||||||
|
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations for client ${orgClient.clientId} after setting roles: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: { userId, orgId, roleIds: uniqueRoleIds },
|
data: { userId, orgId, roleIds: uniqueRoleIds },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export type QueryConnectionAuditLogResponse = {
|
|||||||
orgId: string | null;
|
orgId: string | null;
|
||||||
siteId: number | null;
|
siteId: number | null;
|
||||||
clientId: number | null;
|
clientId: number | null;
|
||||||
|
clientEndpoint: string | null;
|
||||||
userId: string | null;
|
userId: string | null;
|
||||||
sourceAddr: string;
|
sourceAddr: string;
|
||||||
destAddr: string;
|
destAddr: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, orgs, userOrgs, users } from "@server/db";
|
import { db, orgs, userOrgs, users, primaryDb } from "@server/db";
|
||||||
import { eq, and, inArray, not } from "drizzle-orm";
|
import { eq, and, inArray, not } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -218,13 +218,18 @@ export async function deleteMyAccount(
|
|||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx.delete(users).where(eq(users.userId, userId));
|
await trx.delete(users).where(eq(users.userId, userId));
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
|
||||||
// loop through the other orgs and decrement the count
|
// loop through the other orgs and decrement the count
|
||||||
for (const userOrg of otherOrgsTheUserWasIn) {
|
for (const userOrg of otherOrgsTheUserWasIn) {
|
||||||
await usageService.add(userOrg.orgId, FeatureId.USERS, -1, trx);
|
await usageService.add(userOrg.orgId, FeatureId.USERS, -1, trx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after deleting account for user ${userId}: ${e}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invalidateSession(session.sessionId);
|
await invalidateSession(session.sessionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -671,7 +671,8 @@ export async function verifyResourceSession(
|
|||||||
resourceData.org
|
resourceData.org
|
||||||
);
|
);
|
||||||
|
|
||||||
localCache.set(userAccessCacheKey, allowedUserData, 5);
|
// this is query intensive so let it cache a little longer
|
||||||
|
localCache.set(userAccessCacheKey, allowedUserData, 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -1003,11 +1004,7 @@ async function checkRules(
|
|||||||
isIpInCidr(clientIp, rule.value)
|
isIpInCidr(clientIp, rule.value)
|
||||||
) {
|
) {
|
||||||
return rule.action as any;
|
return rule.action as any;
|
||||||
} else if (
|
} else if (clientIp && rule.match == "IP" && clientIp == rule.value) {
|
||||||
clientIp &&
|
|
||||||
rule.match == "IP" &&
|
|
||||||
clientIp == rule.value
|
|
||||||
) {
|
|
||||||
return rule.action as any;
|
return rule.action as any;
|
||||||
} else if (
|
} else if (
|
||||||
path &&
|
path &&
|
||||||
@@ -1015,10 +1012,7 @@ async function checkRules(
|
|||||||
isPathAllowed(rule.value, path)
|
isPathAllowed(rule.value, path)
|
||||||
) {
|
) {
|
||||||
return rule.action as any;
|
return rule.action as any;
|
||||||
} else if (
|
} else if (clientIp && rule.match == "COUNTRY") {
|
||||||
clientIp &&
|
|
||||||
rule.match == "COUNTRY"
|
|
||||||
) {
|
|
||||||
// COUNTRY=ALL should not affect local/private/CGNAT addresses.
|
// COUNTRY=ALL should not affect local/private/CGNAT addresses.
|
||||||
if (
|
if (
|
||||||
rule.value.toUpperCase() === "ALL" &&
|
rule.value.toUpperCase() === "ALL" &&
|
||||||
@@ -1030,10 +1024,7 @@ async function checkRules(
|
|||||||
if (await isIpInGeoIP(ipCC, rule.value)) {
|
if (await isIpInGeoIP(ipCC, rule.value)) {
|
||||||
return rule.action as any;
|
return rule.action as any;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (clientIp && rule.match == "ASN") {
|
||||||
clientIp &&
|
|
||||||
rule.match == "ASN"
|
|
||||||
) {
|
|
||||||
// ASN=ALL/AS0 should not affect local/private/CGNAT addresses.
|
// ASN=ALL/AS0 should not affect local/private/CGNAT addresses.
|
||||||
if (
|
if (
|
||||||
(rule.value.toUpperCase() === "ALL" ||
|
(rule.value.toUpperCase() === "ALL" ||
|
||||||
@@ -1272,11 +1263,15 @@ export async function isIpInRegion(
|
|||||||
if (region.id === checkRegionCode) {
|
if (region.id === checkRegionCode) {
|
||||||
for (const subregion of region.includes) {
|
for (const subregion of region.includes) {
|
||||||
if (subregion.countries.includes(upperCode)) {
|
if (subregion.countries.includes(upperCode)) {
|
||||||
logger.debug(`Country ${upperCode} is in region ${region.id} (${region.name})`);
|
logger.debug(
|
||||||
|
`Country ${upperCode} is in region ${region.id} (${region.name})`
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug(`Country ${upperCode} is not in region ${region.id} (${region.name})`);
|
logger.debug(
|
||||||
|
`Country ${upperCode} is not in region ${region.id} (${region.name})`
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1284,10 +1279,14 @@ export async function isIpInRegion(
|
|||||||
for (const subregion of region.includes) {
|
for (const subregion of region.includes) {
|
||||||
if (subregion.id === checkRegionCode) {
|
if (subregion.id === checkRegionCode) {
|
||||||
if (subregion.countries.includes(upperCode)) {
|
if (subregion.countries.includes(upperCode)) {
|
||||||
logger.debug(`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`);
|
logger.debug(
|
||||||
|
`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
logger.debug(`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`);
|
logger.debug(
|
||||||
|
`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, primaryDb } from "@server/db";
|
||||||
import {
|
import {
|
||||||
roles,
|
roles,
|
||||||
Client,
|
Client,
|
||||||
@@ -92,7 +92,10 @@ export async function createClient(
|
|||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
|
if (
|
||||||
|
req.user &&
|
||||||
|
(!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)
|
||||||
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||||
);
|
);
|
||||||
@@ -198,7 +201,10 @@ export async function createClient(
|
|||||||
|
|
||||||
if (!randomExitNode) {
|
if (!randomExitNode) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.NOT_FOUND, `No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`)
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,10 +262,18 @@ export async function createClient(
|
|||||||
clientId: newClient.clientId,
|
clientId: newClient.clientId,
|
||||||
dateCreated: moment().toISOString()
|
dateCreated: moment().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(newClient, trx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (newClient) {
|
||||||
|
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations after creating client: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response<CreateClientResponse>(res, {
|
return response<CreateClientResponse>(res, {
|
||||||
data: newClient,
|
data: newClient,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, primaryDb } from "@server/db";
|
||||||
import {
|
import {
|
||||||
roles,
|
roles,
|
||||||
Client,
|
Client,
|
||||||
@@ -237,10 +237,18 @@ export async function createUserClient(
|
|||||||
userId,
|
userId,
|
||||||
clientId: newClient.clientId
|
clientId: newClient.clientId
|
||||||
});
|
});
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(newClient, trx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (newClient) {
|
||||||
|
rebuildClientAssociationsFromClient(newClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations after creating user client: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response<CreateClientAndOlmResponse>(res, {
|
return response<CreateClientAndOlmResponse>(res, {
|
||||||
data: newClient,
|
data: newClient,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, olms } from "@server/db";
|
import { db, olms, primaryDb, Client, Olm } from "@server/db";
|
||||||
import { clients, clientSitesAssociationsCache } from "@server/db";
|
import { clients, clientSitesAssociationsCache } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -71,14 +71,17 @@ export async function deleteClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deletedClient: Client | undefined;
|
||||||
|
let olm: Olm | undefined;
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// Then delete the client itself
|
// Then delete the client itself
|
||||||
const [deletedClient] = await trx
|
[deletedClient] = await trx
|
||||||
.delete(clients)
|
.delete(clients)
|
||||||
.where(eq(clients.clientId, clientId))
|
.where(eq(clients.clientId, clientId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const [olm] = await trx
|
[olm] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(olms)
|
.from(olms)
|
||||||
.where(eq(olms.clientId, clientId))
|
.where(eq(olms.clientId, clientId))
|
||||||
@@ -88,14 +91,29 @@ export async function deleteClient(
|
|||||||
if (!client.userId && client.olmId) {
|
if (!client.userId && client.olmId) {
|
||||||
await trx.delete(olms).where(eq(olms.olmId, client.olmId));
|
await trx.delete(olms).where(eq(olms.olmId, client.olmId));
|
||||||
}
|
}
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
|
||||||
|
|
||||||
if (olm) {
|
|
||||||
await sendTerminateClient(deletedClient.clientId, OlmErrorCodes.TERMINATED_DELETED, olm.olmId); // the olmId needs to be provided because it cant look it up after deletion
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (deletedClient) {
|
||||||
|
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations after deleting client ${clientId}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (olm) {
|
||||||
|
sendTerminateClient(
|
||||||
|
deletedClient.clientId,
|
||||||
|
OlmErrorCodes.TERMINATED_DELETED,
|
||||||
|
olm.olmId
|
||||||
|
).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to send terminate message for client ${deletedClient?.clientId} after deleting client ${clientId}: ${e}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import config from "@server/lib/config";
|
|||||||
import * as site from "./site";
|
import * as site from "./site";
|
||||||
import * as org from "./org";
|
import * as org from "./org";
|
||||||
import * as resource from "./resource";
|
import * as resource from "./resource";
|
||||||
|
import * as policy from "./policy";
|
||||||
import * as domain from "./domain";
|
import * as domain from "./domain";
|
||||||
import * as target from "./target";
|
import * as target from "./target";
|
||||||
import * as user from "./user";
|
import * as user from "./user";
|
||||||
@@ -42,7 +43,8 @@ import {
|
|||||||
verifyUserIsOrgOwner,
|
verifyUserIsOrgOwner,
|
||||||
verifySiteResourceAccess,
|
verifySiteResourceAccess,
|
||||||
verifyOlmAccess,
|
verifyOlmAccess,
|
||||||
verifyLimits
|
verifyLimits,
|
||||||
|
verifyResourcePolicyAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
@@ -103,7 +105,6 @@ authenticated.put(
|
|||||||
site.createSite
|
site.createSite
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/sites",
|
"/org/:orgId/sites",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -540,6 +541,7 @@ authenticated.get(
|
|||||||
verifyUserHasAction(ActionsEnum.getResource),
|
verifyUserHasAction(ActionsEnum.getResource),
|
||||||
resource.getResource
|
resource.getResource
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/resource/:resourceId",
|
"/resource/:resourceId",
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
@@ -646,6 +648,29 @@ authenticated.post(
|
|||||||
logActionAudit(ActionsEnum.updateRole),
|
logActionAudit(ActionsEnum.updateRole),
|
||||||
role.updateRole
|
role.updateRole
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/resource-policy/:niceId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getResourcePolicy),
|
||||||
|
policy.getResourcePolicy
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/resource/:resourceId/policies",
|
||||||
|
verifyResourceAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getResourcePolicy),
|
||||||
|
resource.getResourcePolicies
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId",
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateResourcePolicy),
|
||||||
|
policy.updateResourcePolicy
|
||||||
|
);
|
||||||
|
|
||||||
// authenticated.get(
|
// authenticated.get(
|
||||||
// "/role/:roleId",
|
// "/role/:roleId",
|
||||||
// verifyRoleAccess,
|
// verifyRoleAccess,
|
||||||
@@ -697,6 +722,59 @@ authenticated.post(
|
|||||||
resource.setResourceUsers
|
resource.setResourceUsers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/access-control",
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourcePolicyUsers),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyUsers),
|
||||||
|
policy.setResourcePolicyAccessControl
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/password",
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourcePolicyPassword),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyPassword),
|
||||||
|
policy.setResourcePolicyPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/pincode",
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourcePolicyPincode),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyPincode),
|
||||||
|
policy.setResourcePolicyPincode
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/header-auth",
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||||
|
policy.setResourcePolicyHeaderAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/whitelist",
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourcePolicyWhitelist),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyWhitelist),
|
||||||
|
policy.setResourcePolicyWhitelist
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/rules",
|
||||||
|
verifyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourcePolicyRules),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyRules),
|
||||||
|
policy.setResourcePolicyRules
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
`/resource/:resourceId/password`,
|
`/resource/:resourceId/password`,
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as site from "./site";
|
|||||||
import * as org from "./org";
|
import * as org from "./org";
|
||||||
import * as blueprints from "./blueprints";
|
import * as blueprints from "./blueprints";
|
||||||
import * as resource from "./resource";
|
import * as resource from "./resource";
|
||||||
|
import * as policy from "./policy";
|
||||||
import * as domain from "./domain";
|
import * as domain from "./domain";
|
||||||
import * as target from "./target";
|
import * as target from "./target";
|
||||||
import * as user from "./user";
|
import * as user from "./user";
|
||||||
@@ -29,7 +30,9 @@ import {
|
|||||||
verifyApiKeySiteResourceAccess,
|
verifyApiKeySiteResourceAccess,
|
||||||
verifyApiKeySetResourceClients,
|
verifyApiKeySetResourceClients,
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyApiKeyDomainAccess
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyUserHasAction
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
@@ -459,6 +462,20 @@ authenticated.get(
|
|||||||
resource.getResource
|
resource.getResource
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/resource-policy/:resourcePolicyId",
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
|
||||||
|
policy.getResourcePolicy
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/resource/:resourceId/policies",
|
||||||
|
verifyApiKeyResourceAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
|
||||||
|
resource.getResourcePolicies
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/resource/:resourceId",
|
"/resource/:resourceId",
|
||||||
verifyApiKeyResourceAccess,
|
verifyApiKeyResourceAccess,
|
||||||
@@ -468,6 +485,13 @@ authenticated.post(
|
|||||||
resource.updateResource
|
resource.updateResource
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId",
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
|
||||||
|
policy.updateResourcePolicy
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/resource/:resourceId",
|
"/resource/:resourceId",
|
||||||
verifyApiKeyResourceAccess,
|
verifyApiKeyResourceAccess,
|
||||||
@@ -619,6 +643,63 @@ authenticated.post(
|
|||||||
resource.setResourceUsers
|
resource.setResourceUsers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/access-control",
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyApiKeyRoleAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourcePolicyUsers),
|
||||||
|
verifyUserHasAction(ActionsEnum.setResourcePolicyRoles),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyUsers),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyRoles),
|
||||||
|
policy.setResourcePolicyAccessControl
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/password",
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyPassword),
|
||||||
|
policy.setResourcePolicyPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/pincode",
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyPincode),
|
||||||
|
policy.setResourcePolicyPincode
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/header-auth",
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth),
|
||||||
|
policy.setResourcePolicyHeaderAuth
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/whitelist",
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyWhitelist),
|
||||||
|
policy.setResourcePolicyWhitelist
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/resource-policy/:resourcePolicyId/rules",
|
||||||
|
verifyApiKeyResourcePolicyAccess,
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules),
|
||||||
|
logActionAudit(ActionsEnum.setResourcePolicyRules),
|
||||||
|
policy.setResourcePolicyRules
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/resource/:resourceId/roles/add",
|
"/resource/:resourceId/roles/add",
|
||||||
verifyApiKeyResourceAccess,
|
verifyApiKeyResourceAccess,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { db, olms } from "@server/db";
|
import { db, olms, primaryDb } from "@server/db";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -81,16 +81,19 @@ export async function createUserOlm(
|
|||||||
|
|
||||||
const secretHash = await hashPassword(secret);
|
const secretHash = await hashPassword(secret);
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.insert(olms).values({
|
||||||
await trx.insert(olms).values({
|
olmId: olmId,
|
||||||
olmId: olmId,
|
userId,
|
||||||
userId,
|
name,
|
||||||
name,
|
secretHash,
|
||||||
secretHash,
|
dateCreated: moment().toISOString()
|
||||||
dateCreated: moment().toISOString()
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
|
console.error(
|
||||||
|
"Error calculating user clients after creating olm:",
|
||||||
|
e
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response<CreateOlmResponse>(res, {
|
return response<CreateOlmResponse>(res, {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { Client, db } from "@server/db";
|
import { Client, db, Olm, primaryDb } from "@server/db";
|
||||||
import { olms, clients, clientSitesAssociationsCache } from "@server/db";
|
import { olms, clients, clientSitesAssociationsCache } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -49,6 +49,7 @@ export async function deleteUserOlm(
|
|||||||
|
|
||||||
const { olmId } = parsedParams.data;
|
const { olmId } = parsedParams.data;
|
||||||
|
|
||||||
|
let deletedClient: Client | undefined;
|
||||||
// Delete associated clients and the OLM in a transaction
|
// Delete associated clients and the OLM in a transaction
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// Find all clients associated with this OLM
|
// Find all clients associated with this OLM
|
||||||
@@ -57,7 +58,6 @@ export async function deleteUserOlm(
|
|||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.olmId, olmId));
|
.where(eq(clients.olmId, olmId));
|
||||||
|
|
||||||
let deletedClient: Client | null = null;
|
|
||||||
// Delete all associated clients
|
// Delete all associated clients
|
||||||
if (associatedClients.length > 0) {
|
if (associatedClients.length > 0) {
|
||||||
[deletedClient] = await trx
|
[deletedClient] = await trx
|
||||||
@@ -67,23 +67,28 @@ export async function deleteUserOlm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finally, delete the OLM itself
|
// Finally, delete the OLM itself
|
||||||
const [olm] = await trx
|
await trx.delete(olms).where(eq(olms.olmId, olmId)).returning();
|
||||||
.delete(olms)
|
|
||||||
.where(eq(olms.olmId, olmId))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (deletedClient) {
|
|
||||||
await rebuildClientAssociationsFromClient(deletedClient, trx);
|
|
||||||
if (olm) {
|
|
||||||
await sendTerminateClient(
|
|
||||||
deletedClient.clientId,
|
|
||||||
OlmErrorCodes.TERMINATED_DELETED,
|
|
||||||
olm.olmId
|
|
||||||
); // the olmId needs to be provided because it cant look it up after deletion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (deletedClient) {
|
||||||
|
rebuildClientAssociationsFromClient(deletedClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client-site associations after deleting OLM ${olmId}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
sendTerminateClient(
|
||||||
|
deletedClient.clientId,
|
||||||
|
OlmErrorCodes.TERMINATED_DELETED,
|
||||||
|
olmId
|
||||||
|
).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to send terminate message for client ${deletedClient?.clientId} after deleting OLM ${olmId}: ${e}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ import { canCompress } from "@server/lib/clientVersionChecks";
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||||
logger.info("Handling register olm message!");
|
logger.info("[handleOlmRegisterMessage] Handling register olm message");
|
||||||
const { message, client: c, sendToClient } = context;
|
const { message, client: c, sendToClient } = context;
|
||||||
const olm = c as Olm;
|
const olm = c as Olm;
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
if (!olm) {
|
if (!olm) {
|
||||||
logger.warn("Olm not found");
|
logger.warn("[handleOlmRegisterMessage] Olm not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,16 +46,19 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
} = message.data;
|
} = message.data;
|
||||||
|
|
||||||
if (!olm.clientId) {
|
if (!olm.clientId) {
|
||||||
logger.warn("Olm client ID not found");
|
logger.warn("[handleOlmRegisterMessage] Olm client ID not found");
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_ID_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_ID_NOT_FOUND, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Handling fingerprint insertion for olm register...", {
|
logger.debug(
|
||||||
olmId: olm.olmId,
|
"[handleOlmRegisterMessage] Handling fingerprint insertion for olm register...",
|
||||||
fingerprint,
|
{
|
||||||
postures
|
olmId: olm.olmId,
|
||||||
});
|
fingerprint,
|
||||||
|
postures
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
||||||
|
|
||||||
@@ -85,14 +88,17 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
logger.warn("Client ID not found");
|
logger.warn("[handleOlmRegisterMessage] Client not found", {
|
||||||
|
clientId: olm.clientId
|
||||||
|
});
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_NOT_FOUND, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.blocked) {
|
if (client.blocked) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Client ${client.clientId} is blocked. Ignoring register.`
|
`[handleOlmRegisterMessage] Client ${client.clientId} is blocked. Ignoring register.`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_BLOCKED, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_BLOCKED, olm.olmId);
|
||||||
return;
|
return;
|
||||||
@@ -100,7 +106,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
if (client.approvalState == "pending") {
|
if (client.approvalState == "pending") {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Client ${client.clientId} approval is pending. Ignoring register.`
|
`[handleOlmRegisterMessage] Client ${client.clientId} approval is pending. Ignoring register.`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_PENDING, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_PENDING, olm.olmId);
|
||||||
return;
|
return;
|
||||||
@@ -128,14 +135,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
logger.warn("Org not found");
|
logger.warn("[handleOlmRegisterMessage] Org not found", {
|
||||||
|
orgId: client.orgId
|
||||||
|
});
|
||||||
sendOlmError(OlmErrorCodes.ORG_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.ORG_NOT_FOUND, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
if (!olm.userId) {
|
if (!olm.userId) {
|
||||||
logger.warn("Olm has no user ID");
|
logger.warn("[handleOlmRegisterMessage] Olm has no user ID", {
|
||||||
|
orgId: client.orgId
|
||||||
|
});
|
||||||
sendOlmError(OlmErrorCodes.USER_ID_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.USER_ID_NOT_FOUND, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -143,12 +154,18 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
const { session: userSession, user } =
|
const { session: userSession, user } =
|
||||||
await validateSessionToken(userToken);
|
await validateSessionToken(userToken);
|
||||||
if (!userSession || !user) {
|
if (!userSession || !user) {
|
||||||
logger.warn("Invalid user session for olm register");
|
logger.warn(
|
||||||
|
"[handleOlmRegisterMessage] Invalid user session for olm register",
|
||||||
|
{ orgId: client.orgId }
|
||||||
|
);
|
||||||
sendOlmError(OlmErrorCodes.INVALID_USER_SESSION, olm.olmId);
|
sendOlmError(OlmErrorCodes.INVALID_USER_SESSION, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (user.userId !== olm.userId) {
|
if (user.userId !== olm.userId) {
|
||||||
logger.warn("User ID mismatch for olm register");
|
logger.warn(
|
||||||
|
"[handleOlmRegisterMessage] User ID mismatch for olm register",
|
||||||
|
{ orgId: client.orgId }
|
||||||
|
);
|
||||||
sendOlmError(OlmErrorCodes.USER_ID_MISMATCH, olm.olmId);
|
sendOlmError(OlmErrorCodes.USER_ID_MISMATCH, olm.olmId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -163,11 +180,15 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
sessionId // this is the user token passed in the message
|
sessionId // this is the user token passed in the message
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("Policy check result:", policyCheck);
|
logger.debug("[handleOlmRegisterMessage] Policy check result", {
|
||||||
|
orgId: client.orgId,
|
||||||
|
policyCheck
|
||||||
|
});
|
||||||
|
|
||||||
if (policyCheck?.error) {
|
if (policyCheck?.error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`
|
`[handleOlmRegisterMessage] Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
||||||
return;
|
return;
|
||||||
@@ -175,7 +196,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
|
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant password age for org ${orgId}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(
|
sendOlmError(
|
||||||
OlmErrorCodes.ORG_ACCESS_POLICY_PASSWORD_EXPIRED,
|
OlmErrorCodes.ORG_ACCESS_POLICY_PASSWORD_EXPIRED,
|
||||||
@@ -186,7 +208,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
policyCheck.policies?.maxSessionLength?.compliant === false
|
policyCheck.policies?.maxSessionLength?.compliant === false
|
||||||
) {
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}`
|
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant session length for org ${orgId}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(
|
sendOlmError(
|
||||||
OlmErrorCodes.ORG_ACCESS_POLICY_SESSION_EXPIRED,
|
OlmErrorCodes.ORG_ACCESS_POLICY_SESSION_EXPIRED,
|
||||||
@@ -195,7 +218,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
} else if (policyCheck.policies?.requiredTwoFactor === false) {
|
} else if (policyCheck.policies?.requiredTwoFactor === false) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}`
|
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not have 2FA enabled for org ${orgId}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(
|
sendOlmError(
|
||||||
OlmErrorCodes.ORG_ACCESS_POLICY_2FA_REQUIRED,
|
OlmErrorCodes.ORG_ACCESS_POLICY_2FA_REQUIRED,
|
||||||
@@ -204,7 +228,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
} else if (!policyCheck.allowed) {
|
} else if (!policyCheck.allowed) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`
|
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
||||||
return;
|
return;
|
||||||
@@ -226,29 +251,39 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
sitesCountResult.length > 0 ? sitesCountResult[0].count : 0;
|
||||||
|
|
||||||
// Prepare an array to store site configurations
|
// Prepare an array to store site configurations
|
||||||
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
logger.debug(
|
||||||
|
`[handleOlmRegisterMessage] Found ${sitesCount} sites for client ${client.clientId}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
|
);
|
||||||
|
|
||||||
let jitMode = false;
|
let jitMode = false;
|
||||||
if (sitesCount > 250 && build == "saas") {
|
if (sitesCount > 250 && build == "saas") {
|
||||||
// THIS IS THE MAX ON THE BUSINESS TIER
|
// THIS IS THE MAX ON THE BUSINESS TIER
|
||||||
// we have too many sites
|
// we have too many sites
|
||||||
// If we have too many sites we need to drop into fully JIT mode by not sending any of the sites
|
// If we have too many sites we need to drop into fully JIT mode by not sending any of the sites
|
||||||
logger.info("Too many sites (%d), dropping into JIT mode", sitesCount);
|
logger.info(
|
||||||
|
`[handleOlmRegisterMessage] Too many sites (${sitesCount}), dropping into JIT mode`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
|
);
|
||||||
jitMode = true;
|
jitMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
`[handleOlmRegisterMessage] Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
logger.warn("Public key not provided");
|
logger.warn("[handleOlmRegisterMessage] Public key not provided", {
|
||||||
|
orgId: client.orgId
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.pubKey !== publicKey || client.archived) {
|
if (client.pubKey !== publicKey || client.archived) {
|
||||||
logger.info(
|
logger.info(
|
||||||
"Public key mismatch. Updating public key and clearing session info..."
|
"[handleOlmRegisterMessage] Public key mismatch. Updating public key and clearing session info...",
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
// Update the client's public key
|
// Update the client's public key
|
||||||
await db
|
await db
|
||||||
@@ -274,12 +309,13 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||||
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
|
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
|
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
|
||||||
|
{ orgId: client.orgId }
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: its important that the client here is the old client and the public key is the new key
|
// NOTE: its important that the client here is the old client and the public key is the new key
|
||||||
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
||||||
client,
|
client,
|
||||||
publicKey,
|
publicKey,
|
||||||
|
|||||||
231
server/routers/policy/getResourcePolicy.ts
Normal file
231
server/routers/policy/getResourcePolicy.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import {
|
||||||
|
db,
|
||||||
|
idp,
|
||||||
|
resourcePolicyRules,
|
||||||
|
resourcePolicies,
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
resourcePolicyPassword,
|
||||||
|
resourcePolicyPincode,
|
||||||
|
resourcePolicyWhiteList,
|
||||||
|
rolePolicies,
|
||||||
|
roles,
|
||||||
|
userPolicies,
|
||||||
|
users
|
||||||
|
} 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 { and, eq, isNull, not, or, type SQL } from "drizzle-orm";
|
||||||
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import z from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const getResourcePolicySchema = z
|
||||||
|
.strictObject({
|
||||||
|
niceId: z.string(),
|
||||||
|
orgId: z.string()
|
||||||
|
})
|
||||||
|
.or(
|
||||||
|
z.strictObject({
|
||||||
|
resourcePolicyId: z.coerce
|
||||||
|
.number<string>()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
description: "Resource policy ID"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function queryResourcePolicy(
|
||||||
|
params: z.infer<typeof getResourcePolicySchema>
|
||||||
|
) {
|
||||||
|
const conditions: SQL<unknown>[] = [];
|
||||||
|
if ("resourcePolicyId" in params) {
|
||||||
|
conditions.push(
|
||||||
|
eq(resourcePolicies.resourcePolicyId, params.resourcePolicyId)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
conditions.push(
|
||||||
|
eq(resourcePolicies.niceId, params.niceId),
|
||||||
|
eq(resourcePolicies.orgId, params.orgId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [res] = await db
|
||||||
|
.select({
|
||||||
|
resourcePolicyId: resourcePolicies.resourcePolicyId,
|
||||||
|
sso: resourcePolicies.sso,
|
||||||
|
applyRules: resourcePolicies.applyRules,
|
||||||
|
emailWhitelistEnabled: resourcePolicies.emailWhitelistEnabled,
|
||||||
|
idpId: resourcePolicies.idpId,
|
||||||
|
niceId: resourcePolicies.niceId,
|
||||||
|
name: resourcePolicies.name,
|
||||||
|
passwordId: resourcePolicyPassword.passwordId,
|
||||||
|
pincodeId: resourcePolicyPincode.pincodeId,
|
||||||
|
headerAuth: {
|
||||||
|
id: resourcePolicyHeaderAuth.headerAuthId,
|
||||||
|
extendedCompability:
|
||||||
|
resourcePolicyHeaderAuth.extendedCompatibility
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPassword,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPassword.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPincode,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPincode.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
eq(
|
||||||
|
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!res) return null;
|
||||||
|
|
||||||
|
const policyUsers = await db
|
||||||
|
.select({
|
||||||
|
userId: userPolicies.userId,
|
||||||
|
email: users.email,
|
||||||
|
name: users.name,
|
||||||
|
username: users.username,
|
||||||
|
type: users.type,
|
||||||
|
idpName: idp.name
|
||||||
|
})
|
||||||
|
.from(userPolicies)
|
||||||
|
.innerJoin(users, eq(userPolicies.userId, users.userId))
|
||||||
|
.leftJoin(idp, eq(idp.idpId, users.idpId))
|
||||||
|
.where(eq(userPolicies.resourcePolicyId, res.resourcePolicyId));
|
||||||
|
|
||||||
|
const policyRoles = await db
|
||||||
|
.select({
|
||||||
|
roleId: rolePolicies.roleId,
|
||||||
|
name: roles.name
|
||||||
|
})
|
||||||
|
.from(rolePolicies)
|
||||||
|
.innerJoin(
|
||||||
|
roles,
|
||||||
|
and(
|
||||||
|
eq(rolePolicies.roleId, roles.roleId),
|
||||||
|
or(isNull(roles.isAdmin), not(roles.isAdmin))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(eq(rolePolicies.resourcePolicyId, res.resourcePolicyId));
|
||||||
|
|
||||||
|
const policyEmailWhiteList = await db
|
||||||
|
.select({
|
||||||
|
whiteListId: resourcePolicyWhiteList.whitelistId,
|
||||||
|
email: resourcePolicyWhiteList.email
|
||||||
|
})
|
||||||
|
.from(resourcePolicyWhiteList)
|
||||||
|
.where(
|
||||||
|
eq(resourcePolicyWhiteList.resourcePolicyId, res.resourcePolicyId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const policyRules = await db
|
||||||
|
.select({
|
||||||
|
ruleId: resourcePolicyRules.ruleId,
|
||||||
|
enabled: resourcePolicyRules.enabled,
|
||||||
|
priority: resourcePolicyRules.priority,
|
||||||
|
action: resourcePolicyRules.action,
|
||||||
|
match: resourcePolicyRules.match,
|
||||||
|
value: resourcePolicyRules.value
|
||||||
|
})
|
||||||
|
.from(resourcePolicyRules)
|
||||||
|
.where(eq(resourcePolicyRules.resourcePolicyId, res.resourcePolicyId));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
roles: policyRoles,
|
||||||
|
users: policyUsers,
|
||||||
|
emailWhiteList: policyEmailWhiteList,
|
||||||
|
rules: policyRules
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetResourcePolicyResponse = NonNullable<
|
||||||
|
Awaited<ReturnType<typeof queryResourcePolicy>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/org/{orgId}/resource-policy/{niceId}",
|
||||||
|
description:
|
||||||
|
"Get a resource policy by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
|
||||||
|
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: z.object({
|
||||||
|
orgId: z.string(),
|
||||||
|
niceId: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/resource-policy/{resourcePolicyId}",
|
||||||
|
description: "Get a resource policy by its resourcePolicyId.",
|
||||||
|
tags: [OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: z.object({
|
||||||
|
resourcePolicyId: z.number()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getResourcePolicy(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = getResourcePolicySchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const policy = await queryResourcePolicy(parsedParams.data);
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Resource policy not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<GetResourcePolicyResponse>(res, {
|
||||||
|
data: policy,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resource Policy retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
server/routers/policy/index.ts
Normal file
8
server/routers/policy/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export * from "./getResourcePolicy";
|
||||||
|
export * from "./updateResourcePolicy";
|
||||||
|
export * from "./setResourcePolicyAccessControl";
|
||||||
|
export * from "./setResourcePolicyPassword";
|
||||||
|
export * from "./setResourcePolicyPincode";
|
||||||
|
export * from "./setResourcePolicyHeaderAuth";
|
||||||
|
export * from "./setResourcePolicyWhitelist";
|
||||||
|
export * from "./setResourcePolicyRules";
|
||||||
237
server/routers/policy/setResourcePolicyAccessControl.ts
Normal file
237
server/routers/policy/setResourcePolicyAccessControl.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
idp,
|
||||||
|
idpOrg,
|
||||||
|
resourcePolicies,
|
||||||
|
rolePolicies,
|
||||||
|
roles,
|
||||||
|
userOrgs,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
|
import { userPolicies } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const setResourcePolicyAcccessControlBodySchema = z.strictObject({
|
||||||
|
sso: z.boolean(),
|
||||||
|
userIds: z.array(z.string()),
|
||||||
|
roleIds: z.array(z.int().positive()).openapi({
|
||||||
|
type: "array"
|
||||||
|
}),
|
||||||
|
skipToIdpId: z.int().positive().optional().nullable().openapi({
|
||||||
|
type: "integer",
|
||||||
|
description: "Page number to retrieve"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const setResourcePolicyAccessControlParamsSchema = z.strictObject({
|
||||||
|
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/resource-policy/{resourceId}/access-control",
|
||||||
|
description:
|
||||||
|
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
|
||||||
|
tags: [OpenAPITags.Policy, OpenAPITags.User],
|
||||||
|
request: {
|
||||||
|
params: setResourcePolicyAccessControlParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: setResourcePolicyAcccessControlBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setResourcePolicyAccessControl(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedBody = setResourcePolicyAcccessControlBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userIds, roleIds, sso, skipToIdpId: idpId } = parsedBody.data;
|
||||||
|
|
||||||
|
const parsedParams =
|
||||||
|
setResourcePolicyAccessControlParamsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourcePolicyId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [policy] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Resource policy not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Identity provider in `skipToIdpId` exists
|
||||||
|
if (idpId) {
|
||||||
|
const [provider] = await db
|
||||||
|
.select()
|
||||||
|
.from(idp)
|
||||||
|
.innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId))
|
||||||
|
.where(
|
||||||
|
and(eq(idp.idpId, idpId), eq(idpOrg.orgId, policy.orgId))
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Identity provider not found in this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any of the roleIds are admin roles
|
||||||
|
const rolesToCheck = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(roles.roleId, roleIds),
|
||||||
|
eq(roles.orgId, policy.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasAdminRole = rolesToCheck.some((role) => role.isAdmin);
|
||||||
|
|
||||||
|
if (hasAdminRole) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Admin role cannot be assigned to resources"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all admin role IDs for this org to exclude from deletion
|
||||||
|
const adminRoles = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, policy.orgId)));
|
||||||
|
const adminRoleIds = adminRoles.map((role) => role.roleId);
|
||||||
|
|
||||||
|
const existingUsers = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(userOrgs.userId, users.userId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.orgId, policy.orgId),
|
||||||
|
inArray(users.userId, userIds)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingRoles = await db
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roles.orgId, policy.orgId),
|
||||||
|
inArray(roles.roleId, roleIds)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
// Update SSO status
|
||||||
|
await trx
|
||||||
|
.update(resourcePolicies)
|
||||||
|
.set({
|
||||||
|
sso,
|
||||||
|
idpId
|
||||||
|
})
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||||
|
|
||||||
|
// Update roles
|
||||||
|
if (adminRoleIds.length > 0) {
|
||||||
|
await trx.delete(rolePolicies).where(
|
||||||
|
and(
|
||||||
|
eq(rolePolicies.resourcePolicyId, resourcePolicyId),
|
||||||
|
ne(rolePolicies.roleId, adminRoleIds[0]) // delete all but the admin role
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await trx
|
||||||
|
.delete(rolePolicies)
|
||||||
|
.where(eq(rolePolicies.resourcePolicyId, resourcePolicyId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rolesToAdd = existingRoles.map(({ roleId }) => ({
|
||||||
|
roleId,
|
||||||
|
resourcePolicyId
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (rolesToAdd.length > 0) {
|
||||||
|
await trx.insert(rolePolicies).values(rolesToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update users
|
||||||
|
await trx
|
||||||
|
.delete(userPolicies)
|
||||||
|
.where(eq(userPolicies.resourcePolicyId, resourcePolicyId));
|
||||||
|
|
||||||
|
const usersToAdd = existingUsers.map(({ user }) => ({
|
||||||
|
userId: user.userId,
|
||||||
|
resourcePolicyId: resourcePolicyId
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (usersToAdd.length > 0) {
|
||||||
|
await trx.insert(userPolicies).values(usersToAdd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resource policy succesfully updated",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
server/routers/policy/setResourcePolicyHeaderAuth.ts
Normal file
117
server/routers/policy/setResourcePolicyHeaderAuth.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, resourcePolicyHeaderAuth } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { response } from "@server/lib/response";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { hashPassword } from "@server/auth/password";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const setResourcePolicyHeaderAuthParamsSchema = z.object({
|
||||||
|
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
const setResourcePolicyHeaderAuthBodySchema = z.strictObject({
|
||||||
|
headerAuth: z
|
||||||
|
.object({
|
||||||
|
user: z.string().min(4).max(100),
|
||||||
|
password: z.string().min(4).max(100),
|
||||||
|
extendedCompatibility: z.boolean()
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/resource-policy/{resourcePolicyId}/header-auth",
|
||||||
|
description:
|
||||||
|
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.",
|
||||||
|
tags: [OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: setResourcePolicyHeaderAuthParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: setResourcePolicyHeaderAuthBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setResourcePolicyHeaderAuth(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = setResourcePolicyHeaderAuthParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = setResourcePolicyHeaderAuthBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourcePolicyId } = parsedParams.data;
|
||||||
|
const { headerAuth } = parsedBody.data;
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyHeaderAuth)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||||
|
resourcePolicyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (headerAuth !== null) {
|
||||||
|
const headerAuthHash = await hashPassword(
|
||||||
|
Buffer.from(
|
||||||
|
`${headerAuth.user}:${headerAuth.password}`
|
||||||
|
).toString("base64")
|
||||||
|
);
|
||||||
|
|
||||||
|
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||||
|
resourcePolicyId,
|
||||||
|
headerAuthHash,
|
||||||
|
extendedCompatibility: headerAuth.extendedCompatibility
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Header Authentication set successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
server/routers/policy/setResourcePolicyPassword.ts
Normal file
106
server/routers/policy/setResourcePolicyPassword.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { resourcePolicyPassword } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { response } from "@server/lib/response";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { hashPassword } from "@server/auth/password";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const setResourcePolicyPasswordParamsSchema = z.object({
|
||||||
|
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
const setResourcePolicyPasswordBodySchema = z.strictObject({
|
||||||
|
password: z.string().min(4).max(100).nullable()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/resource-policy/{resourcePolicyId}/password",
|
||||||
|
description:
|
||||||
|
"Set the password for a resource policy. Setting the password to null will remove it.",
|
||||||
|
tags: [OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: setResourcePolicyPasswordParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: setResourcePolicyPasswordBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setResourcePolicyPassword(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = setResourcePolicyPasswordParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = setResourcePolicyPasswordBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourcePolicyId } = parsedParams.data;
|
||||||
|
const { password } = parsedBody.data;
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyPassword)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicyPassword.resourcePolicyId,
|
||||||
|
resourcePolicyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
const passwordHash = await hashPassword(password);
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.insert(resourcePolicyPassword)
|
||||||
|
.values({ resourcePolicyId, passwordHash });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resource policy password set successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
server/routers/policy/setResourcePolicyPincode.ts
Normal file
106
server/routers/policy/setResourcePolicyPincode.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { resourcePolicyPincode } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { response } from "@server/lib/response";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { hashPassword } from "@server/auth/password";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const setResourcePolicyPincodeParamsSchema = z.object({
|
||||||
|
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
const setResourcePolicyPincodeBodySchema = z.strictObject({
|
||||||
|
pincode: z
|
||||||
|
.string()
|
||||||
|
.regex(/^\d{6}$/)
|
||||||
|
.or(z.null())
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/resource-policy/{resourcePolicyId}/pincode",
|
||||||
|
description:
|
||||||
|
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it.",
|
||||||
|
tags: [OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: setResourcePolicyPincodeParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: setResourcePolicyPincodeBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setResourcePolicyPincode(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = setResourcePolicyPincodeParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = setResourcePolicyPincodeBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourcePolicyId } = parsedParams.data;
|
||||||
|
const { pincode } = parsedBody.data;
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyPincode)
|
||||||
|
.where(
|
||||||
|
eq(resourcePolicyPincode.resourcePolicyId, resourcePolicyId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pincode) {
|
||||||
|
const pincodeHash = await hashPassword(pincode);
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.insert(resourcePolicyPincode)
|
||||||
|
.values({ resourcePolicyId, pincodeHash, digitLength: 6 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resource policy PIN code set successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
167
server/routers/policy/setResourcePolicyRules.ts
Normal file
167
server/routers/policy/setResourcePolicyRules.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, resourcePolicyRules, resourcePolicies } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import {
|
||||||
|
isValidCIDR,
|
||||||
|
isValidIP,
|
||||||
|
isValidUrlGlobPattern
|
||||||
|
} from "@server/lib/validators";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const ruleSchema = z.strictObject({
|
||||||
|
action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["ACCEPT", "DROP", "PASS"],
|
||||||
|
description: "rule action"
|
||||||
|
}),
|
||||||
|
match: z.enum(["CIDR", "IP", "PATH"]).openapi({
|
||||||
|
type: "string",
|
||||||
|
enum: ["CIDR", "IP", "PATH"],
|
||||||
|
description: "rule match"
|
||||||
|
}),
|
||||||
|
value: z.string().min(1),
|
||||||
|
priority: z.int().openapi({
|
||||||
|
type: "integer",
|
||||||
|
description: "Rule priority"
|
||||||
|
}),
|
||||||
|
enabled: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const setResourcePolicyRulesBodySchema = z.strictObject({
|
||||||
|
applyRules: z.boolean(),
|
||||||
|
rules: z.array(ruleSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const setResourcePolicyRulesParamsSchema = z.strictObject({
|
||||||
|
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/resource-policy/{resourcePolicyId}/rules",
|
||||||
|
description:
|
||||||
|
"Set all rules for a resource policy at once. This will replace all existing rules.",
|
||||||
|
tags: [OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: setResourcePolicyRulesParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: setResourcePolicyRulesBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setResourcePolicyRules(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = setResourcePolicyRulesParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = setResourcePolicyRulesBodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourcePolicyId } = parsedParams.data;
|
||||||
|
const { applyRules, rules } = parsedBody.data;
|
||||||
|
|
||||||
|
const [policy] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Resource policy not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid CIDR provided"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (rule.match === "IP" && !isValidIP(rule.value)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided")
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
rule.match === "PATH" &&
|
||||||
|
!isValidUrlGlobPattern(rule.value)
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid URL glob pattern provided"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx
|
||||||
|
.update(resourcePolicies)
|
||||||
|
.set({ applyRules })
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyRules)
|
||||||
|
.where(
|
||||||
|
eq(resourcePolicyRules.resourcePolicyId, resourcePolicyId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rules.length > 0) {
|
||||||
|
await trx.insert(resourcePolicyRules).values(
|
||||||
|
rules.map((rule) => ({
|
||||||
|
resourcePolicyId,
|
||||||
|
...rule
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resource policy rules set successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
server/routers/policy/setResourcePolicyWhitelist.ts
Normal file
132
server/routers/policy/setResourcePolicyWhitelist.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, resourcePolicies, resourcePolicyWhiteList } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
|
const setResourcePolicyWhitelistBodySchema = z.strictObject({
|
||||||
|
emailWhitelistEnabled: z.boolean(),
|
||||||
|
emails: z
|
||||||
|
.array(
|
||||||
|
z.email().or(
|
||||||
|
z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
|
||||||
|
error: "Invalid email address. Wildcard (*) must be the entire local part."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.max(50)
|
||||||
|
.transform((v) => v.map((e) => e.toLowerCase()))
|
||||||
|
});
|
||||||
|
|
||||||
|
const setResourcePolicyWhitelistParamsSchema = z.strictObject({
|
||||||
|
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/resource-policy/{resourcePolicyId}/whitelist",
|
||||||
|
description:
|
||||||
|
"Set email whitelist for a resource policy. This will replace all existing emails.",
|
||||||
|
tags: [OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: setResourcePolicyWhitelistParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: setResourcePolicyWhitelistBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function setResourcePolicyWhitelist(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedBody = setResourcePolicyWhitelistBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedParams = setResourcePolicyWhitelistParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourcePolicyId } = parsedParams.data;
|
||||||
|
const { emailWhitelistEnabled, emails } = parsedBody.data;
|
||||||
|
|
||||||
|
const [policy] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Resource policy not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx
|
||||||
|
.update(resourcePolicies)
|
||||||
|
.set({ emailWhitelistEnabled })
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||||
|
|
||||||
|
// delete all whitelist emails
|
||||||
|
await trx
|
||||||
|
.delete(resourcePolicyWhiteList)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicyWhiteList.resourcePolicyId,
|
||||||
|
resourcePolicyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emailWhitelistEnabled && emails.length > 0) {
|
||||||
|
await trx.insert(resourcePolicyWhiteList).values(
|
||||||
|
emails.map((email) => ({
|
||||||
|
email,
|
||||||
|
resourcePolicyId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Whitelist set for resource policy successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
157
server/routers/policy/updateResourcePolicy.ts
Normal file
157
server/routers/policy/updateResourcePolicy.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import z from "zod";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { db, orgs, resourcePolicies, type ResourcePolicy } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
|
||||||
|
const updateResourcePolicyParamsSchema = z.strictObject({
|
||||||
|
resourcePolicyId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateResourcePolicyBodySchema = z.strictObject({
|
||||||
|
name: z.string().min(1).max(255).optional(),
|
||||||
|
niceId: z.string().min(1).max(255).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "put",
|
||||||
|
path: "/resource-policy/{resourcePolicyId}",
|
||||||
|
description: "Update a resource policy.",
|
||||||
|
tags: [OpenAPITags.Org, OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: updateResourcePolicyParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: updateResourcePolicyBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function updateResourcePolicy(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const parsedParams = updateResourcePolicyParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.user && req.userOrgRoleIds?.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourcePolicyId } = parsedParams.data;
|
||||||
|
const [result] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
|
||||||
|
.leftJoin(orgs, eq(resourcePolicies.orgId, orgs.orgId));
|
||||||
|
|
||||||
|
const policy = result?.resourcePolicies;
|
||||||
|
const org = result?.orgs;
|
||||||
|
|
||||||
|
if (!policy || !org) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Resource Policy with ID ${resourcePolicyId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = updateResourcePolicyBodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = parsedBody.data;
|
||||||
|
|
||||||
|
if (updateData.niceId) {
|
||||||
|
const [existingPolicy] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourcePolicies.niceId, updateData.niceId),
|
||||||
|
eq(resourcePolicies.orgId, policy.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
existingPolicy &&
|
||||||
|
existingPolicy.resourcePolicyId !== policy.resourcePolicyId
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
`A resource policy with niceId "${updateData.niceId}" already exists`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedPolicy = await db.transaction(async (trx) => {
|
||||||
|
const [updated] = await trx
|
||||||
|
.update(resourcePolicies)
|
||||||
|
.set({
|
||||||
|
...updateData
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
policy.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updatedPolicy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to update policy"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<ResourcePolicy>(res, {
|
||||||
|
data: updatedPolicy,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resource policy updated successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, domainNamespaces, loginPage } from "@server/db";
|
import { build } from "@server/build";
|
||||||
import {
|
import {
|
||||||
domains,
|
db,
|
||||||
orgDomains,
|
loginPage,
|
||||||
orgs,
|
orgs,
|
||||||
Resource,
|
Resource,
|
||||||
resources,
|
resources,
|
||||||
|
resourcePolicies,
|
||||||
roleResources,
|
roleResources,
|
||||||
|
rolePolicies,
|
||||||
roles,
|
roles,
|
||||||
userResources
|
userPolicies,
|
||||||
|
userResources,
|
||||||
|
domainNamespaces
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -20,13 +24,18 @@ import logger from "@server/logger";
|
|||||||
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
|
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { build } from "@server/build";
|
|
||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import { getUniqueResourceName } from "@server/db/names";
|
import {
|
||||||
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
validateAndConstructDomain,
|
||||||
|
checkWildcardDomainConflict
|
||||||
|
} from "@server/lib/domainUtils";
|
||||||
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
import { isSubscribed } from "#dynamic/lib/isSubscribed";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import {
|
||||||
|
getUniqueResourceName,
|
||||||
|
getUniqueResourcePolicyName
|
||||||
|
} from "@server/db/names";
|
||||||
|
|
||||||
const createResourceParamsSchema = z.strictObject({
|
const createResourceParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -311,8 +320,46 @@ async function createHttpResource(
|
|||||||
let resource: Resource | undefined;
|
let resource: Resource | undefined;
|
||||||
|
|
||||||
const niceId = await getUniqueResourceName(orgId);
|
const niceId = await getUniqueResourceName(orgId);
|
||||||
|
const policyNiceId = await getUniqueResourcePolicyName(orgId);
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
|
const adminRole = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (adminRole.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [defaultPolicy] = await trx
|
||||||
|
.insert(resourcePolicies)
|
||||||
|
.values({
|
||||||
|
niceId: policyNiceId,
|
||||||
|
orgId,
|
||||||
|
name: `default policy for ${niceId}`,
|
||||||
|
sso: true,
|
||||||
|
scope: "resource"
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// make this policy visible by the admin role
|
||||||
|
await trx.insert(rolePolicies).values({
|
||||||
|
roleId: adminRole[0].roleId,
|
||||||
|
resourcePolicyId: defaultPolicy.resourcePolicyId
|
||||||
|
});
|
||||||
|
|
||||||
|
// make this policy visible by the current user
|
||||||
|
if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) {
|
||||||
|
await trx.insert(userPolicies).values({
|
||||||
|
userId: req.user?.userId!,
|
||||||
|
resourcePolicyId: defaultPolicy.resourcePolicyId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newResource = await trx
|
const newResource = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
.values({
|
.values({
|
||||||
@@ -328,22 +375,11 @@ async function createHttpResource(
|
|||||||
stickySession: stickySession,
|
stickySession: stickySession,
|
||||||
postAuthPath: postAuthPath,
|
postAuthPath: postAuthPath,
|
||||||
wildcard,
|
wildcard,
|
||||||
health: "unknown"
|
health: "unknown",
|
||||||
|
defaultResourcePolicyId: defaultPolicy.resourcePolicyId
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const adminRole = await db
|
|
||||||
.select()
|
|
||||||
.from(roles)
|
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (adminRole.length === 0) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await trx.insert(roleResources).values({
|
await trx.insert(roleResources).values({
|
||||||
roleId: adminRole[0].roleId,
|
roleId: adminRole[0].roleId,
|
||||||
resourceId: newResource[0].resourceId
|
resourceId: newResource[0].resourceId
|
||||||
@@ -369,7 +405,7 @@ async function createHttpResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (build != "oss") {
|
if (build !== "oss") {
|
||||||
await createCertificate(domainId, fullDomain, db);
|
await createCertificate(domainId, fullDomain, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,22 +446,10 @@ async function createRawResource(
|
|||||||
let resource: Resource | undefined;
|
let resource: Resource | undefined;
|
||||||
|
|
||||||
const niceId = await getUniqueResourceName(orgId);
|
const niceId = await getUniqueResourceName(orgId);
|
||||||
|
const policyNiceId = await getUniqueResourcePolicyName(orgId);
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const newResource = await trx
|
const adminRole = await trx
|
||||||
.insert(resources)
|
|
||||||
.values({
|
|
||||||
niceId,
|
|
||||||
orgId,
|
|
||||||
name,
|
|
||||||
http,
|
|
||||||
protocol,
|
|
||||||
proxyPort
|
|
||||||
// enableProxy
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
const adminRole = await db
|
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
@@ -437,6 +461,44 @@ async function createRawResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [defaultPolicy] = await trx
|
||||||
|
.insert(resourcePolicies)
|
||||||
|
.values({
|
||||||
|
niceId: policyNiceId,
|
||||||
|
orgId,
|
||||||
|
name: `default policy for ${niceId}`,
|
||||||
|
sso: true,
|
||||||
|
scope: "resource"
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// make this policy visible by the admin role
|
||||||
|
await trx.insert(rolePolicies).values({
|
||||||
|
roleId: adminRole[0].roleId,
|
||||||
|
resourcePolicyId: defaultPolicy.resourcePolicyId
|
||||||
|
});
|
||||||
|
|
||||||
|
// make this policy visible by the current user
|
||||||
|
if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) {
|
||||||
|
await trx.insert(userPolicies).values({
|
||||||
|
userId: req.user?.userId!,
|
||||||
|
resourcePolicyId: defaultPolicy.resourcePolicyId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newResource = await trx
|
||||||
|
.insert(resources)
|
||||||
|
.values({
|
||||||
|
niceId,
|
||||||
|
orgId,
|
||||||
|
name,
|
||||||
|
http,
|
||||||
|
protocol,
|
||||||
|
proxyPort,
|
||||||
|
defaultResourcePolicyId: defaultPolicy.resourcePolicyId
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
await trx.insert(roleResources).values({
|
await trx.insert(roleResources).values({
|
||||||
roleId: adminRole[0].roleId,
|
roleId: adminRole[0].roleId,
|
||||||
resourceId: newResource[0].resourceId
|
resourceId: newResource[0].resourceId
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db, targetHealthCheck } from "@server/db";
|
|
||||||
import { newts, resources, sites, targets } from "@server/db";
|
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
newts,
|
||||||
|
resourcePolicies,
|
||||||
|
resources,
|
||||||
|
sites,
|
||||||
|
targetHealthCheck,
|
||||||
|
targets
|
||||||
|
} from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { addPeer } from "../gerbil/peers";
|
|
||||||
import { removeTargets } from "../newt/targets";
|
|
||||||
import { getAllowedIps } from "../target/helpers";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { removeTargets } from "../newt/targets";
|
||||||
|
|
||||||
// Define Zod schema for request parameters validation
|
// Define Zod schema for request parameters validation
|
||||||
const deleteResourceSchema = z.strictObject({
|
const deleteResourceSchema = z.strictObject({
|
||||||
@@ -113,6 +118,18 @@ export async function deleteResource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also delete default resource policy
|
||||||
|
if (deletedResource.defaultResourcePolicyId) {
|
||||||
|
await db
|
||||||
|
.delete(resourcePolicies)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
deletedResource.defaultResourcePolicyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
resourceHeaderAuth,
|
resourcePolicies,
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
resourcePolicyHeaderAuth,
|
||||||
resourcePassword,
|
resourcePolicyPassword,
|
||||||
resourcePincode,
|
resourcePolicyPincode,
|
||||||
resources
|
resources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, or } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -60,64 +60,53 @@ export async function getResourceAuthInfo(
|
|||||||
|
|
||||||
const isGuidInteger = /^\d+$/.test(resourceGuid);
|
const isGuidInteger = /^\d+$/.test(resourceGuid);
|
||||||
|
|
||||||
|
const buildQuery = (whereClause: ReturnType<typeof eq>) =>
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicies,
|
||||||
|
or(
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
resources.resourcePolicyId
|
||||||
|
),
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
resources.defaultResourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPincode,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPincode.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPassword,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPassword.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
eq(
|
||||||
|
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(whereClause)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
const [result] =
|
const [result] =
|
||||||
isGuidInteger && build === "saas"
|
isGuidInteger && build === "saas"
|
||||||
? await db
|
? await buildQuery(
|
||||||
.select()
|
eq(resources.resourceId, Number(resourceGuid))
|
||||||
.from(resources)
|
)
|
||||||
.leftJoin(
|
: await buildQuery(eq(resources.resourceGuid, resourceGuid));
|
||||||
resourcePincode,
|
|
||||||
eq(resourcePincode.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourcePassword,
|
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
|
|
||||||
.leftJoin(
|
|
||||||
resourceHeaderAuth,
|
|
||||||
eq(
|
|
||||||
resourceHeaderAuth.resourceId,
|
|
||||||
resources.resourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
|
||||||
eq(
|
|
||||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
|
||||||
resources.resourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(eq(resources.resourceId, Number(resourceGuid)))
|
|
||||||
.limit(1)
|
|
||||||
: await db
|
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.leftJoin(
|
|
||||||
resourcePincode,
|
|
||||||
eq(resourcePincode.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourcePassword,
|
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
|
|
||||||
.leftJoin(
|
|
||||||
resourceHeaderAuth,
|
|
||||||
eq(
|
|
||||||
resourceHeaderAuth.resourceId,
|
|
||||||
resources.resourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
|
||||||
eq(
|
|
||||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
|
||||||
resources.resourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(eq(resources.resourceGuid, resourceGuid))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const resource = result?.resources;
|
const resource = result?.resources;
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
@@ -126,11 +115,10 @@ export async function getResourceAuthInfo(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pincode = result?.resourcePincode;
|
const policy = result?.resourcePolicies;
|
||||||
const password = result?.resourcePassword;
|
const pincode = result?.resourcePolicyPincode;
|
||||||
const headerAuth = result?.resourceHeaderAuth;
|
const password = result?.resourcePolicyPassword;
|
||||||
const headerAuthExtendedCompatibility =
|
const headerAuth = result?.resourcePolicyHeaderAuth;
|
||||||
result?.resourceHeaderAuthExtendedCompatibility;
|
|
||||||
|
|
||||||
const url = resource.fullDomain
|
const url = resource.fullDomain
|
||||||
? `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
|
? `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
|
||||||
@@ -146,13 +134,13 @@ export async function getResourceAuthInfo(
|
|||||||
pincode: pincode !== null,
|
pincode: pincode !== null,
|
||||||
headerAuth: headerAuth !== null,
|
headerAuth: headerAuth !== null,
|
||||||
headerAuthExtendedCompatibility:
|
headerAuthExtendedCompatibility:
|
||||||
headerAuthExtendedCompatibility !== null,
|
headerAuth?.extendedCompatibility ?? false,
|
||||||
sso: resource.sso,
|
sso: policy?.sso ?? false,
|
||||||
blockAccess: resource.blockAccess,
|
blockAccess: resource.blockAccess,
|
||||||
url: url ?? "",
|
url: url ?? "",
|
||||||
wildcard: resource.wildcard ?? false,
|
wildcard: resource.wildcard ?? false,
|
||||||
fullDomain: resource.fullDomain,
|
fullDomain: resource.fullDomain,
|
||||||
whitelist: resource.emailWhitelistEnabled,
|
whitelist: policy?.emailWhitelistEnabled ?? false,
|
||||||
skipToIdpId: resource.skipToIdpId,
|
skipToIdpId: resource.skipToIdpId,
|
||||||
orgId: resource.orgId,
|
orgId: resource.orgId,
|
||||||
postAuthPath: resource.postAuthPath ?? null
|
postAuthPath: resource.postAuthPath ?? null
|
||||||
|
|||||||
109
server/routers/resource/getResourcePolicies.ts
Normal file
109
server/routers/resource/getResourcePolicies.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { db, resources } from "@server/db";
|
||||||
|
import {
|
||||||
|
queryResourcePolicy,
|
||||||
|
type GetResourcePolicyResponse
|
||||||
|
} from "@server/routers/policy/getResourcePolicy";
|
||||||
|
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 } from "drizzle-orm";
|
||||||
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import z from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const getResourcePoliciesParamsSchema = z.strictObject({
|
||||||
|
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GetResourcePoliciesResponse = {
|
||||||
|
defaultPolicy: GetResourcePolicyResponse;
|
||||||
|
sharedPolicy: GetResourcePolicyResponse | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get",
|
||||||
|
path: "/resource/{resourceId}/policies",
|
||||||
|
description: "Get the inline and shared policies associated with a resource.",
|
||||||
|
tags: [OpenAPITags.PublicResource, OpenAPITags.Policy],
|
||||||
|
request: {
|
||||||
|
params: getResourcePoliciesParamsSchema
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getResourcePolicies(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = getResourcePoliciesParamsSchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resourceId } = parsedParams.data;
|
||||||
|
|
||||||
|
const [resource] = await db
|
||||||
|
.select({
|
||||||
|
defaultResourcePolicyId: resources.defaultResourcePolicyId,
|
||||||
|
resourcePolicyId: resources.resourcePolicyId
|
||||||
|
})
|
||||||
|
.from(resources)
|
||||||
|
.where(eq(resources.resourceId, resourceId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resource.defaultResourcePolicyId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Resource has no default policy"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [defaultPolicy, sharedPolicy] = await Promise.all([
|
||||||
|
queryResourcePolicy({
|
||||||
|
resourcePolicyId: resource.defaultResourcePolicyId
|
||||||
|
}),
|
||||||
|
resource.resourcePolicyId
|
||||||
|
? queryResourcePolicy({
|
||||||
|
resourcePolicyId: resource.resourcePolicyId
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response<GetResourcePoliciesResponse>(res, {
|
||||||
|
data: {
|
||||||
|
defaultPolicy:
|
||||||
|
// the policy will always be non nullable
|
||||||
|
defaultPolicy as unknown as GetResourcePolicyResponse,
|
||||||
|
sharedPolicy
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Resource policies retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,3 +33,4 @@ export * from "./removeUserFromResource";
|
|||||||
export * from "./listAllResourceNames";
|
export * from "./listAllResourceNames";
|
||||||
export * from "./removeEmailFromResourceWhitelist";
|
export * from "./removeEmailFromResourceWhitelist";
|
||||||
export * from "./getStatusHistory";
|
export * from "./getStatusHistory";
|
||||||
|
export * from "./getResourcePolicies";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
resourceHeaderAuth,
|
resourcePolicies,
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
resourcePolicyHeaderAuth,
|
||||||
resourcePassword,
|
resourcePolicyPassword,
|
||||||
resourcePincode,
|
resourcePolicyPincode,
|
||||||
resources,
|
resources,
|
||||||
roleResources,
|
roleResources,
|
||||||
sites,
|
sites,
|
||||||
@@ -163,10 +163,10 @@ function queryResourcesBase() {
|
|||||||
name: resources.name,
|
name: resources.name,
|
||||||
ssl: resources.ssl,
|
ssl: resources.ssl,
|
||||||
fullDomain: resources.fullDomain,
|
fullDomain: resources.fullDomain,
|
||||||
passwordId: resourcePassword.passwordId,
|
passwordId: resourcePolicyPassword.passwordId,
|
||||||
sso: resources.sso,
|
sso: resourcePolicies.sso,
|
||||||
pincodeId: resourcePincode.pincodeId,
|
pincodeId: resourcePolicyPincode.pincodeId,
|
||||||
whitelist: resources.emailWhitelistEnabled,
|
whitelist: resourcePolicies.emailWhitelistEnabled,
|
||||||
http: resources.http,
|
http: resources.http,
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol,
|
||||||
proxyPort: resources.proxyPort,
|
proxyPort: resources.proxyPort,
|
||||||
@@ -174,29 +174,45 @@ function queryResourcesBase() {
|
|||||||
domainId: resources.domainId,
|
domainId: resources.domainId,
|
||||||
niceId: resources.niceId,
|
niceId: resources.niceId,
|
||||||
wildcard: resources.wildcard,
|
wildcard: resources.wildcard,
|
||||||
headerAuthId: resourceHeaderAuth.headerAuthId,
|
health: resources.health,
|
||||||
headerAuthExtendedCompatibilityId:
|
headerAuthId: resourcePolicyHeaderAuth.headerAuthId,
|
||||||
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
|
headerAuthExtendedCompatibility:
|
||||||
health: resources.health
|
resourcePolicyHeaderAuth.extendedCompatibility
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePassword,
|
resourcePolicies,
|
||||||
eq(resourcePassword.resourceId, resources.resourceId)
|
or(
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
resources.resourcePolicyId
|
||||||
|
),
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
resources.defaultResourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourcePincode,
|
resourcePolicyPassword,
|
||||||
eq(resourcePincode.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourceHeaderAuth,
|
|
||||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
|
||||||
eq(
|
eq(
|
||||||
resourceHeaderAuthExtendedCompatibility.resourceId,
|
resourcePolicyPassword.resourcePolicyId,
|
||||||
resources.resourceId
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyPincode,
|
||||||
|
eq(
|
||||||
|
resourcePolicyPincode.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
resourcePolicyHeaderAuth,
|
||||||
|
eq(
|
||||||
|
resourcePolicyHeaderAuth.resourcePolicyId,
|
||||||
|
resourcePolicies.resourcePolicyId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
||||||
@@ -206,10 +222,10 @@ function queryResourcesBase() {
|
|||||||
)
|
)
|
||||||
.groupBy(
|
.groupBy(
|
||||||
resources.resourceId,
|
resources.resourceId,
|
||||||
resourcePassword.passwordId,
|
resourcePolicies.resourcePolicyId,
|
||||||
resourcePincode.pincodeId,
|
resourcePolicyPassword.passwordId,
|
||||||
resourceHeaderAuth.headerAuthId,
|
resourcePolicyPincode.pincodeId,
|
||||||
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId
|
resourcePolicyHeaderAuth.headerAuthId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,21 +371,21 @@ export async function listResources(
|
|||||||
case "protected":
|
case "protected":
|
||||||
conditions.push(
|
conditions.push(
|
||||||
or(
|
or(
|
||||||
eq(resources.sso, true),
|
eq(resourcePolicies.sso, true),
|
||||||
eq(resources.emailWhitelistEnabled, true),
|
eq(resourcePolicies.emailWhitelistEnabled, true),
|
||||||
not(isNull(resourceHeaderAuth.headerAuthId)),
|
not(isNull(resourcePolicyHeaderAuth.headerAuthId)),
|
||||||
not(isNull(resourcePincode.pincodeId)),
|
not(isNull(resourcePolicyPincode.pincodeId)),
|
||||||
not(isNull(resourcePassword.passwordId))
|
not(isNull(resourcePolicyPassword.passwordId))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "not_protected":
|
case "not_protected":
|
||||||
conditions.push(
|
conditions.push(
|
||||||
not(eq(resources.sso, true)),
|
not(eq(resourcePolicies.sso, true)),
|
||||||
not(eq(resources.emailWhitelistEnabled, true)),
|
not(eq(resourcePolicies.emailWhitelistEnabled, true)),
|
||||||
isNull(resourceHeaderAuth.headerAuthId),
|
isNull(resourcePolicyHeaderAuth.headerAuthId),
|
||||||
isNull(resourcePincode.pincodeId),
|
isNull(resourcePolicyPincode.pincodeId),
|
||||||
isNull(resourcePassword.passwordId)
|
isNull(resourcePolicyPassword.passwordId)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -446,9 +462,9 @@ export async function listResources(
|
|||||||
ssl: row.ssl,
|
ssl: row.ssl,
|
||||||
fullDomain: row.fullDomain,
|
fullDomain: row.fullDomain,
|
||||||
passwordId: row.passwordId,
|
passwordId: row.passwordId,
|
||||||
sso: row.sso,
|
sso: row.sso ?? false,
|
||||||
pincodeId: row.pincodeId,
|
pincodeId: row.pincodeId,
|
||||||
whitelist: row.whitelist,
|
whitelist: row.whitelist ?? false,
|
||||||
http: row.http,
|
http: row.http,
|
||||||
protocol: row.protocol,
|
protocol: row.protocol,
|
||||||
proxyPort: row.proxyPort,
|
proxyPort: row.proxyPort,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import type { Resource, ResourcePolicy } from "@server/db";
|
||||||
|
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||||
|
|
||||||
export type GetMaintenanceInfoResponse = {
|
export type GetMaintenanceInfoResponse = {
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -8,3 +11,19 @@ export type GetMaintenanceInfoResponse = {
|
|||||||
maintenanceMessage: string | null;
|
maintenanceMessage: string | null;
|
||||||
maintenanceEstimatedTime: string | null;
|
maintenanceEstimatedTime: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AttachedResource = Pick<
|
||||||
|
Resource,
|
||||||
|
"resourceId" | "name" | "fullDomain"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ResourcePolicyWithResources = Pick<
|
||||||
|
ResourcePolicy,
|
||||||
|
"resourcePolicyId" | "niceId" | "name" | "orgId"
|
||||||
|
> & {
|
||||||
|
resources: Array<AttachedResource>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListResourcePoliciesResponse = PaginatedResponse<{
|
||||||
|
policies: Array<ResourcePolicyWithResources>;
|
||||||
|
}>;
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, domainNamespaces, loginPage } from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
domainNamespaces,
|
||||||
|
loginPage,
|
||||||
|
resourceHeaderAuth,
|
||||||
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
|
resourcePassword,
|
||||||
|
resourcePincode,
|
||||||
|
resourceRules,
|
||||||
|
resourceWhitelist
|
||||||
|
} from "@server/db";
|
||||||
import {
|
import {
|
||||||
domains,
|
domains,
|
||||||
Org,
|
Org,
|
||||||
orgDomains,
|
orgDomains,
|
||||||
orgs,
|
orgs,
|
||||||
Resource,
|
Resource,
|
||||||
|
resourcePolicies,
|
||||||
resources
|
resources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, and, ne } from "drizzle-orm";
|
import { eq, and, ne } from "drizzle-orm";
|
||||||
@@ -24,7 +35,10 @@ import {
|
|||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
import { OpenAPITags } from "@server/openApi";
|
import { OpenAPITags } from "@server/openApi";
|
||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils";
|
import {
|
||||||
|
validateAndConstructDomain,
|
||||||
|
checkWildcardDomainConflict
|
||||||
|
} from "@server/lib/domainUtils";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
@@ -68,7 +82,8 @@ const updateHttpResourceBodySchema = z
|
|||||||
maintenanceTitle: z.string().max(255).nullable().optional(),
|
maintenanceTitle: z.string().max(255).nullable().optional(),
|
||||||
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
||||||
maintenanceEstimatedTime: z.string().max(100).nullable().optional(),
|
maintenanceEstimatedTime: z.string().max(100).nullable().optional(),
|
||||||
postAuthPath: z.string().nullable().optional()
|
postAuthPath: z.string().nullable().optional(),
|
||||||
|
resourcePolicyId: z.number().nullable().optional()
|
||||||
})
|
})
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
@@ -165,7 +180,8 @@ const updateRawResourceBodySchema = z
|
|||||||
stickySession: z.boolean().optional(),
|
stickySession: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
proxyProtocol: z.boolean().optional(),
|
proxyProtocol: z.boolean().optional(),
|
||||||
proxyProtocolVersion: z.int().min(1).optional()
|
proxyProtocolVersion: z.int().min(1).optional(),
|
||||||
|
resourcePolicyId: z.number().nullable().optional()
|
||||||
})
|
})
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
@@ -301,6 +317,42 @@ async function updateHttpResource(
|
|||||||
|
|
||||||
const updateData = parsedBody.data;
|
const updateData = parsedBody.data;
|
||||||
|
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
|
resource.orgId,
|
||||||
|
tierMatrix.wildcardSubdomain
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateData.resourcePolicyId != null) {
|
||||||
|
if (!isLicensed) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Resource policies are not supported on your current plan. Please upgrade to access this feature."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existingPolicy] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
updateData.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingPolicy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Resource policy with ID ${updateData.resourcePolicyId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updateData.niceId) {
|
if (updateData.niceId) {
|
||||||
const [existingResource] = await db
|
const [existingResource] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -326,10 +378,6 @@ async function updateHttpResource(
|
|||||||
|
|
||||||
// Wildcard subdomains are a paid feature
|
// Wildcard subdomains are a paid feature
|
||||||
if (updateData.subdomain && updateData.subdomain.includes("*")) {
|
if (updateData.subdomain && updateData.subdomain.includes("*")) {
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
|
||||||
resource.orgId,
|
|
||||||
tierMatrix.wildcardSubdomain
|
|
||||||
);
|
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -474,10 +522,6 @@ async function updateHttpResource(
|
|||||||
headers = null;
|
headers = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
|
||||||
resource.orgId,
|
|
||||||
tierMatrix.maintencePage
|
|
||||||
);
|
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
updateData.maintenanceModeEnabled = undefined;
|
updateData.maintenanceModeEnabled = undefined;
|
||||||
updateData.maintenanceModeType = undefined;
|
updateData.maintenanceModeType = undefined;
|
||||||
@@ -535,38 +579,122 @@ async function updateRawResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateData = parsedBody.data;
|
const updateData = parsedBody.data;
|
||||||
|
let updatedResource: Resource | null = null;
|
||||||
|
|
||||||
if (updateData.niceId) {
|
const [existingResource] = await db
|
||||||
const [existingResource] = await db
|
.select()
|
||||||
.select()
|
.from(resources)
|
||||||
.from(resources)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(resources.niceId, updateData.niceId),
|
|
||||||
eq(resources.orgId, resource.orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
existingResource &&
|
|
||||||
existingResource.resourceId !== resource.resourceId
|
|
||||||
) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.CONFLICT,
|
|
||||||
`A resource with niceId "${updateData.niceId}" already exists`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedResource = await db
|
|
||||||
.update(resources)
|
|
||||||
.set(updateData)
|
|
||||||
.where(eq(resources.resourceId, resource.resourceId))
|
.where(eq(resources.resourceId, resource.resourceId))
|
||||||
.returning();
|
.limit(1);
|
||||||
|
|
||||||
if (updatedResource.length === 0) {
|
await db.transaction(async (trx) => {
|
||||||
|
if (updateData.resourcePolicyId != null) {
|
||||||
|
const [existingPolicy] = await trx
|
||||||
|
.select()
|
||||||
|
.from(resourcePolicies)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePolicies.resourcePolicyId,
|
||||||
|
updateData.resourcePolicyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingPolicy) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Resource policy with ID ${updateData.resourcePolicyId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we are in an inline policy and we need to clear out the old tables
|
||||||
|
await Promise.all([
|
||||||
|
trx
|
||||||
|
.delete(resourcePassword)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePassword.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
),
|
||||||
|
trx
|
||||||
|
.delete(resourcePincode)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourcePincode.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
),
|
||||||
|
trx
|
||||||
|
.delete(resourceHeaderAuth)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourceHeaderAuth.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
),
|
||||||
|
trx
|
||||||
|
.delete(resourceHeaderAuthExtendedCompatibility)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
),
|
||||||
|
trx
|
||||||
|
.delete(resourceWhitelist)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourceWhitelist.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
trx
|
||||||
|
.delete(resourceRules)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourceRules.resourceId,
|
||||||
|
existingResource.resourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateData.niceId) {
|
||||||
|
const [existingResourceConflict] = await trx
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.niceId, updateData.niceId),
|
||||||
|
eq(resources.orgId, resource.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
existingResourceConflict &&
|
||||||
|
existingResourceConflict.resourceId !== resource.resourceId
|
||||||
|
) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
`A resource with niceId "${updateData.niceId}" already exists`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[updatedResource] = await trx
|
||||||
|
.update(resources)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(resources.resourceId, resource.resourceId))
|
||||||
|
.returning();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updatedResource) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
@@ -576,7 +704,7 @@ async function updateRawResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: updatedResource[0],
|
data: updatedResource,
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Non-http Resource updated successfully",
|
message: "Non-http Resource updated successfully",
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const listSitesSchema = z.object({
|
|||||||
page: z.coerce
|
page: z.coerce
|
||||||
.number<string>() // for prettier formatting
|
.number<string>() // for prettier formatting
|
||||||
.int()
|
.int()
|
||||||
.min(0)
|
.positive()
|
||||||
.optional()
|
.optional()
|
||||||
.catch(1)
|
.catch(1)
|
||||||
.default(1)
|
.default(1)
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
clients,
|
clients,
|
||||||
clientSiteResources,
|
clientSiteResources,
|
||||||
siteResources,
|
siteResources,
|
||||||
apiKeyOrg
|
apiKeyOrg,
|
||||||
|
primaryDb
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -220,8 +221,12 @@ export async function batchAddClientToSiteResources(
|
|||||||
siteResourceId: siteResource.siteResourceId
|
siteResourceId: siteResource.siteResourceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(client, trx);
|
rebuildClientAssociationsFromClient(client, primaryDb).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations after batch adding site resources for client ${clientId}: ${e}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
SiteResource,
|
SiteResource,
|
||||||
siteResources,
|
siteResources,
|
||||||
sites,
|
sites,
|
||||||
userSiteResources
|
userSiteResources,
|
||||||
|
primaryDb
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { getUniqueSiteResourceName } from "@server/db/names";
|
import { getUniqueSiteResourceName } from "@server/db/names";
|
||||||
import {
|
import {
|
||||||
@@ -519,12 +520,10 @@ export async function createSiteResource(
|
|||||||
// own transaction so it always executes on the primary — avoiding any
|
// own transaction so it always executes on the primary — avoiding any
|
||||||
// replica-lag issues while still allowing the HTTP response to return
|
// replica-lag issues while still allowing the HTTP response to return
|
||||||
// early.
|
// early.
|
||||||
db.transaction(async (trx) => {
|
rebuildClientAssociationsFromSiteResource(
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
newSiteResource!,
|
||||||
newSiteResource!,
|
primaryDb
|
||||||
trx
|
).catch((err) => {
|
||||||
);
|
|
||||||
}).catch((err) => {
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
`Error rebuilding client associations for site resource ${newSiteResource!.siteResourceId}:`,
|
||||||
err
|
err
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, newts, sites } from "@server/db";
|
import { db, newts, primaryDb, sites } from "@server/db";
|
||||||
import { siteResources } from "@server/db";
|
import { siteResources } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -73,12 +73,10 @@ export async function deleteSiteResource(
|
|||||||
// own transaction so it always executes on the primary — avoiding any
|
// own transaction so it always executes on the primary — avoiding any
|
||||||
// replica-lag issues while still allowing the HTTP response to return
|
// replica-lag issues while still allowing the HTTP response to return
|
||||||
// early.
|
// early.
|
||||||
db.transaction(async (trx) => {
|
rebuildClientAssociationsFromSiteResource(
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
removedSiteResource,
|
||||||
removedSiteResource,
|
primaryDb
|
||||||
trx
|
).catch((err) => {
|
||||||
);
|
|
||||||
}).catch((err) => {
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
`Error rebuilding client associations for site resource ${removedSiteResource!.siteResourceId}:`,
|
||||||
err
|
err
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, orgs } from "@server/db";
|
import { db, orgs, primaryDb } from "@server/db";
|
||||||
import { roles, userInviteRoles, userInvites, userOrgs, users } from "@server/db";
|
import {
|
||||||
|
roles,
|
||||||
|
userInviteRoles,
|
||||||
|
userInvites,
|
||||||
|
userOrgs,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -146,9 +152,7 @@ export async function acceptInvite(
|
|||||||
.from(userInviteRoles)
|
.from(userInviteRoles)
|
||||||
.where(eq(userInviteRoles.inviteId, inviteId));
|
.where(eq(userInviteRoles.inviteId, inviteId));
|
||||||
|
|
||||||
const inviteRoleIds = [
|
const inviteRoleIds = [...new Set(inviteRoleRows.map((r) => r.roleId))];
|
||||||
...new Set(inviteRoleRows.map((r) => r.roleId))
|
|
||||||
];
|
|
||||||
if (inviteRoleIds.length === 0) {
|
if (inviteRoleIds.length === 0) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -193,13 +197,19 @@ export async function acceptInvite(
|
|||||||
.delete(userInvites)
|
.delete(userInvites)
|
||||||
.where(eq(userInvites.inviteId, inviteId));
|
.where(eq(userInvites.inviteId, inviteId));
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(existingUser[0].userId, trx);
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}`
|
`User ${existingUser[0].userId} accepted invite to org ${existingInvite.orgId}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
calculateUserClientsForOrgs(existingUser[0].userId, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after accepting invite for user ${existingUser[0].userId}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return response<AcceptInviteResponse>(res, {
|
return response<AcceptInviteResponse>(res, {
|
||||||
data: { accepted: true, orgId: existingInvite.orgId },
|
data: { accepted: true, orgId: existingInvite.orgId },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { clients, db } from "@server/db";
|
import { clients, db, primaryDb, Client } from "@server/db";
|
||||||
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
import { userOrgRoles, userOrgs, roles } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -88,11 +88,11 @@ export async function addUserRoleLegacy(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.isOwner) {
|
if (existingUser.isOwner && role.isAdmin !== true) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"Cannot change the role of the owner of the organization"
|
"The organization owner must retain an administrator role"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -112,6 +112,8 @@ export async function addUserRoleLegacy(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let orgClientsToRebuild: Client[] = [];
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx
|
await trx
|
||||||
.delete(userOrgRoles)
|
.delete(userOrgRoles)
|
||||||
@@ -138,11 +140,19 @@ export async function addUserRoleLegacy(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const orgClient of orgClients) {
|
orgClientsToRebuild = orgClients;
|
||||||
await rebuildClientAssociationsFromClient(orgClient, trx);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const orgClient of orgClientsToRebuild) {
|
||||||
|
rebuildClientAssociationsFromClient(orgClient, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to rebuild client associations for client ${orgClient.clientId} after adding role: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: { ...existingUser, roleId },
|
data: { ...existingUser, roleId },
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, primaryDb } from "@server/db";
|
||||||
import { users } from "@server/db";
|
import { users } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -53,8 +53,12 @@ export async function adminRemoveUser(
|
|||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await trx.delete(users).where(eq(users.userId, userId));
|
await trx.delete(users).where(eq(users.userId, userId));
|
||||||
|
});
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after removing user ${userId}: ${e}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { db, orgs } from "@server/db";
|
import { db, orgs, primaryDb } from "@server/db";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db";
|
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db";
|
||||||
import { generateId } from "@server/auth/sessions/app";
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
@@ -34,8 +34,7 @@ const bodySchema = z
|
|||||||
roleId: z.number().int().positive().optional()
|
roleId: z.number().int().positive().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(d) =>
|
(d) => (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
|
||||||
(d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
|
|
||||||
{ message: "roleIds or roleId is required", path: ["roleIds"] }
|
{ message: "roleIds or roleId is required", path: ["roleIds"] }
|
||||||
)
|
)
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
@@ -100,8 +99,14 @@ export async function createOrgUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const { username, email, name, type, idpId, roleIds: uniqueRoleIds } =
|
const {
|
||||||
parsedBody.data;
|
username,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
idpId,
|
||||||
|
roleIds: uniqueRoleIds
|
||||||
|
} = parsedBody.data;
|
||||||
|
|
||||||
if (build == "saas") {
|
if (build == "saas") {
|
||||||
const usage = await usageService.getUsage(orgId, FeatureId.USERS);
|
const usage = await usageService.getUsage(orgId, FeatureId.USERS);
|
||||||
@@ -232,6 +237,7 @@ export async function createOrgUser(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let userIdForClients: string | undefined;
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const [existingUser] = await trx
|
const [existingUser] = await trx
|
||||||
.select()
|
.select()
|
||||||
@@ -270,7 +276,7 @@ export async function createOrgUser(
|
|||||||
{
|
{
|
||||||
orgId,
|
orgId,
|
||||||
userId: existingUser.userId,
|
userId: existingUser.userId,
|
||||||
autoProvisioned: false,
|
autoProvisioned: false
|
||||||
},
|
},
|
||||||
uniqueRoleIds,
|
uniqueRoleIds,
|
||||||
trx
|
trx
|
||||||
@@ -292,20 +298,30 @@ export async function createOrgUser(
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
await assignUserToOrg(
|
await assignUserToOrg(
|
||||||
org,
|
org,
|
||||||
{
|
{
|
||||||
orgId,
|
orgId,
|
||||||
userId: newUser.userId,
|
userId: newUser.userId,
|
||||||
autoProvisioned: false,
|
autoProvisioned: false
|
||||||
},
|
},
|
||||||
uniqueRoleIds,
|
uniqueRoleIds,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
userIdForClients = userId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (userIdForClients) {
|
||||||
|
calculateUserClientsForOrgs(userIdForClients, primaryDb).catch(
|
||||||
|
(e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after creating org user: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
createHttpError(HttpCode.BAD_REQUEST, "User type is required")
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ export async function queryUser(orgId: string, userId: string) {
|
|||||||
.from(userOrgRoles)
|
.from(userOrgRoles)
|
||||||
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
|
||||||
eq(userOrgRoles.userId, userId),
|
|
||||||
eq(userOrgRoles.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAdmin = roleRows.some((r) => r.isAdmin);
|
const isAdmin = roleRows.some((r) => r.isAdmin);
|
||||||
@@ -61,7 +58,8 @@ export async function queryUser(orgId: string, userId: string) {
|
|||||||
roleIds: roleRows.map((r) => r.roleId),
|
roleIds: roleRows.map((r) => r.roleId),
|
||||||
roles: roleRows.map((r) => ({
|
roles: roleRows.map((r) => ({
|
||||||
roleId: r.roleId,
|
roleId: r.roleId,
|
||||||
name: r.roleName ?? ""
|
name: r.roleName ?? "",
|
||||||
|
isAdmin: r.isAdmin === true
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -146,7 +144,7 @@ export async function getOrgUser(
|
|||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
"User does not have permission perform this action"
|
"User does not have permission to get organization user details"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
siteResources,
|
siteResources,
|
||||||
sites,
|
sites,
|
||||||
UserOrg,
|
UserOrg,
|
||||||
userSiteResources
|
userSiteResources,
|
||||||
|
primaryDb
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { userOrgs, userResources, users, userSites } from "@server/db";
|
import { userOrgs, userResources, users, userSites } from "@server/db";
|
||||||
import { and, count, eq, exists, inArray } from "drizzle-orm";
|
import { and, count, eq, exists, inArray } from "drizzle-orm";
|
||||||
@@ -91,25 +92,12 @@ export async function removeUserOrg(
|
|||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
await removeUserFromOrg(org, userId, trx);
|
await removeUserFromOrg(org, userId, trx);
|
||||||
|
});
|
||||||
|
|
||||||
// if (build === "saas") {
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
// const [rootUser] = await trx
|
logger.error(
|
||||||
// .select()
|
`Failed to calculate user clients after removing user ${userId} from org ${orgId}: ${e}`
|
||||||
// .from(users)
|
);
|
||||||
// .where(eq(users.userId, userId));
|
|
||||||
//
|
|
||||||
// const [leftInOrgs] = await trx
|
|
||||||
// .select({ count: count() })
|
|
||||||
// .from(userOrgs)
|
|
||||||
// .where(eq(userOrgs.userId, userId));
|
|
||||||
//
|
|
||||||
// // if the user is not an internal user and does not belong to any org, delete the entire user
|
|
||||||
// if (rootUser?.type !== UserType.Internal && !leftInOrgs.count) {
|
|
||||||
// await trx.delete(users).where(eq(users.userId, userId));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
await calculateUserClientsForOrgs(userId, trx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import m15 from "./scriptsPg/1.16.0";
|
|||||||
import m16 from "./scriptsPg/1.17.0";
|
import m16 from "./scriptsPg/1.17.0";
|
||||||
import m17 from "./scriptsPg/1.18.0";
|
import m17 from "./scriptsPg/1.18.0";
|
||||||
import m18 from "./scriptsPg/1.18.3";
|
import m18 from "./scriptsPg/1.18.3";
|
||||||
|
import m19 from "./scriptsPg/1.18.4";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -47,7 +48,8 @@ const migrations = [
|
|||||||
{ version: "1.16.0", run: m15 },
|
{ version: "1.16.0", run: m15 },
|
||||||
{ version: "1.17.0", run: m16 },
|
{ version: "1.17.0", run: m16 },
|
||||||
{ version: "1.18.0", run: m17 },
|
{ version: "1.18.0", run: m17 },
|
||||||
{ version: "1.18.3", run: m18 }
|
{ version: "1.18.3", run: m18 },
|
||||||
|
{ version: "1.18.4", run: m19 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import m36 from "./scriptsSqlite/1.16.0";
|
|||||||
import m37 from "./scriptsSqlite/1.17.0";
|
import m37 from "./scriptsSqlite/1.17.0";
|
||||||
import m38 from "./scriptsSqlite/1.18.0";
|
import m38 from "./scriptsSqlite/1.18.0";
|
||||||
import m39 from "./scriptsSqlite/1.18.3";
|
import m39 from "./scriptsSqlite/1.18.3";
|
||||||
|
import m40 from "./scriptsSqlite/1.18.4";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -81,7 +82,8 @@ const migrations = [
|
|||||||
{ version: "1.16.0", run: m36 },
|
{ version: "1.16.0", run: m36 },
|
||||||
{ version: "1.17.0", run: m37 },
|
{ version: "1.17.0", run: m37 },
|
||||||
{ version: "1.18.0", run: m38 },
|
{ version: "1.18.0", run: m38 },
|
||||||
{ version: "1.18.3", run: m39 }
|
{ version: "1.18.3", run: m39 },
|
||||||
|
{ version: "1.18.4", run: m40 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
34
server/setup/scriptsPg/1.18.4.ts
Normal file
34
server/setup/scriptsPg/1.18.4.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
const version = "1.18.4";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "connectionAuditLog" ADD COLUMN "clientEndpoint" text;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "eventStreamingDestinations" ADD COLUMN "lastError" text;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "eventStreamingDestinations" ADD COLUMN "lastErrorAt" bigint;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Unable to migrate database");
|
||||||
|
console.log(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
43
server/setup/scriptsSqlite/1.18.4.ts
Normal file
43
server/setup/scriptsSqlite/1.18.4.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const version = "1.18.4";
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.pragma("foreign_keys = OFF");
|
||||||
|
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'connectionAuditLog' ADD 'clientEndpoint' text;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'eventStreamingDestinations' ADD 'lastError' text;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
db.prepare(
|
||||||
|
`
|
||||||
|
ALTER TABLE 'eventStreamingDestinations' ADD 'lastErrorAt' integer;
|
||||||
|
`
|
||||||
|
).run();
|
||||||
|
})();
|
||||||
|
|
||||||
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
23
src/app/[orgId]/settings/(private)/policies/layout.tsx
Normal file
23
src/app/[orgId]/settings/(private)/policies/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||||
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
|
import type { GetOrgResponse } from "@server/routers/org";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export interface PolicyLayoutPageProps {
|
||||||
|
params: Promise<{ orgId: string }>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PolicyLayoutPage(props: PolicyLayoutPageProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
|
||||||
|
let org: GetOrgResponse | null = null;
|
||||||
|
try {
|
||||||
|
const res = await getCachedOrg(params.orgId);
|
||||||
|
org = res.data.data;
|
||||||
|
} catch {
|
||||||
|
redirect(`/${params.orgId}/settings`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <OrgProvider org={org}>{props.children}</OrgProvider>;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user