mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-02 21:23:50 +00:00
Compare commits
5 Commits
dependabot
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
280cbb6e22 | ||
|
|
c20babcb53 | ||
|
|
768eebe2cd | ||
|
|
81c1a1da9c | ||
|
|
52f26396ac |
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
@@ -299,7 +299,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Upload artifacts from /install/bin
|
- name: Upload artifacts from /install/bin
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: install-bin
|
name: install-bin
|
||||||
path: install/bin/
|
path: install/bin/
|
||||||
|
|||||||
@@ -1102,6 +1102,12 @@
|
|||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Get User",
|
||||||
"actionGetOrgUser": "Get Organization User",
|
"actionGetOrgUser": "Get Organization User",
|
||||||
"actionListOrgDomains": "List Organization Domains",
|
"actionListOrgDomains": "List Organization Domains",
|
||||||
|
"actionGetDomain": "Get Domain",
|
||||||
|
"actionCreateOrgDomain": "Create Domain",
|
||||||
|
"actionUpdateOrgDomain": "Update Domain",
|
||||||
|
"actionDeleteOrgDomain": "Delete Domain",
|
||||||
|
"actionGetDNSRecords": "Get DNS Records",
|
||||||
|
"actionRestartOrgDomain": "Restart Domain",
|
||||||
"actionCreateSite": "Create Site",
|
"actionCreateSite": "Create Site",
|
||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
|
|||||||
@@ -477,10 +477,7 @@ export async function getTraefikConfig(
|
|||||||
|
|
||||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||||
const anySitesOnline = targets.some(
|
const anySitesOnline = targets.some(
|
||||||
(target) =>
|
(target) => target.site.online
|
||||||
target.site.online ||
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -493,7 +490,7 @@ export async function getTraefikConfig(
|
|||||||
if (target.health == "unhealthy") {
|
if (target.health == "unhealthy") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If any sites are online, exclude offline sites
|
// If any sites are online, exclude offline sites
|
||||||
if (anySitesOnline && !target.site.online) {
|
if (anySitesOnline && !target.site.online) {
|
||||||
return false;
|
return false;
|
||||||
@@ -608,10 +605,7 @@ export async function getTraefikConfig(
|
|||||||
servers: (() => {
|
servers: (() => {
|
||||||
// Check if any sites are online
|
// Check if any sites are online
|
||||||
const anySitesOnline = targets.some(
|
const anySitesOnline = targets.some(
|
||||||
(target) =>
|
(target) => target.site.online
|
||||||
target.site.online ||
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
@@ -619,7 +613,7 @@ export async function getTraefikConfig(
|
|||||||
if (!target.enabled) {
|
if (!target.enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If any sites are online, exclude offline sites
|
// If any sites are online, exclude offline sites
|
||||||
if (anySitesOnline && !target.site.online) {
|
if (anySitesOnline && !target.site.online) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export * from "./verifyApiKeyApiKeyAccess";
|
|||||||
export * from "./verifyApiKeyClientAccess";
|
export * from "./verifyApiKeyClientAccess";
|
||||||
export * from "./verifyApiKeySiteResourceAccess";
|
export * from "./verifyApiKeySiteResourceAccess";
|
||||||
export * from "./verifyApiKeyIdpAccess";
|
export * from "./verifyApiKeyIdpAccess";
|
||||||
|
export * from "./verifyApiKeyDomainAccess";
|
||||||
|
|||||||
90
server/middlewares/integration/verifyApiKeyDomainAccess.ts
Normal file
90
server/middlewares/integration/verifyApiKeyDomainAccess.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { db, domains, orgDomains, apiKeyOrg } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
export async function verifyApiKeyDomainAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const domainId =
|
||||||
|
req.params.domainId || req.body.domainId || req.query.domainId;
|
||||||
|
const orgId = req.params.orgId;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid domain ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any domain in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify domain exists and belongs to the organization
|
||||||
|
const [domain] = await db
|
||||||
|
.select()
|
||||||
|
.from(domains)
|
||||||
|
.innerJoin(orgDomains, eq(orgDomains.domainId, domains.domainId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(orgDomains.domainId, domainId),
|
||||||
|
eq(orgDomains.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Domain with ID ${domainId} not found in organization ${orgId}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the API key has access to this organization
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[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 domain access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -665,10 +665,7 @@ export async function getTraefikConfig(
|
|||||||
|
|
||||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||||
const anySitesOnline = targets.some(
|
const anySitesOnline = targets.some(
|
||||||
(target) =>
|
(target) => target.site.online
|
||||||
target.site.online ||
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -796,10 +793,7 @@ export async function getTraefikConfig(
|
|||||||
servers: (() => {
|
servers: (() => {
|
||||||
// Check if any sites are online
|
// Check if any sites are online
|
||||||
const anySitesOnline = targets.some(
|
const anySitesOnline = targets.some(
|
||||||
(target) =>
|
(target) => target.site.online
|
||||||
target.site.online ||
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import {
|
|||||||
verifyApiKeyClientAccess,
|
verifyApiKeyClientAccess,
|
||||||
verifyApiKeySiteResourceAccess,
|
verifyApiKeySiteResourceAccess,
|
||||||
verifyApiKeySetResourceClients,
|
verifyApiKeySetResourceClients,
|
||||||
verifyLimits
|
verifyLimits,
|
||||||
|
verifyApiKeyDomainAccess
|
||||||
} 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";
|
||||||
@@ -347,6 +348,56 @@ authenticated.get(
|
|||||||
domain.listDomains
|
domain.listDomains
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/domain/:domainId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getDomain),
|
||||||
|
domain.getDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/domain",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createOrgDomain),
|
||||||
|
logActionAudit(ActionsEnum.createOrgDomain),
|
||||||
|
domain.createOrgDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.patch(
|
||||||
|
"/org/:orgId/domain/:domainId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateOrgDomain),
|
||||||
|
domain.updateOrgDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/domain/:domainId",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.deleteOrgDomain),
|
||||||
|
logActionAudit(ActionsEnum.deleteOrgDomain),
|
||||||
|
domain.deleteAccountDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/domain/:domainId/dns-records",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getDNSRecords),
|
||||||
|
domain.getDNSRecords
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/domain/:domainId/restart",
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyDomainAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.restartOrgDomain),
|
||||||
|
logActionAudit(ActionsEnum.restartOrgDomain),
|
||||||
|
domain.restartOrgDomain
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/invitations",
|
"/org/:orgId/invitations",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ const DockerContainersTable: FC<{
|
|||||||
...Object.values(container.networks)
|
...Object.values(container.networks)
|
||||||
.map((n) => n.ipAddress)
|
.map((n) => n.ipAddress)
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
...getExposedPorts(container).map((p) => p.toString())
|
...getExposedPorts(container).map((p) => p.toString()),
|
||||||
|
...Object.entries(container.labels).flat()
|
||||||
];
|
];
|
||||||
|
|
||||||
return searchableFields.some((field) =>
|
return searchableFields.some((field) =>
|
||||||
|
|||||||
@@ -69,15 +69,16 @@ export function LayoutMobileMenu({
|
|||||||
<SheetDescription className="sr-only">
|
<SheetDescription className="sr-only">
|
||||||
{t("navbarDescription")}
|
{t("navbarDescription")}
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
<div className="flex-1 overflow-y-auto relative">
|
<div className="w-full border-b border-border">
|
||||||
<div className="px-1">
|
<div className="px-1 shrink-0">
|
||||||
<OrgSelector
|
<OrgSelector
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
orgs={orgs}
|
orgs={orgs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full border-b border-border" />
|
</div>
|
||||||
<div className="px-3 pt-3">
|
<div className="flex-1 overflow-y-auto relative">
|
||||||
|
<div className="px-3">
|
||||||
{!isAdminPage &&
|
{!isAdminPage &&
|
||||||
user.serverAdmin && (
|
user.serverAdmin && (
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionListInvitations")]: "listInvitations",
|
[t("actionListInvitations")]: "listInvitations",
|
||||||
[t("actionRemoveUser")]: "removeUser",
|
[t("actionRemoveUser")]: "removeUser",
|
||||||
[t("actionListUsers")]: "listUsers",
|
[t("actionListUsers")]: "listUsers",
|
||||||
[t("actionListOrgDomains")]: "listOrgDomains",
|
|
||||||
[t("updateOrgUser")]: "updateOrgUser",
|
[t("updateOrgUser")]: "updateOrgUser",
|
||||||
[t("createOrgUser")]: "createOrgUser",
|
[t("createOrgUser")]: "createOrgUser",
|
||||||
[t("actionApplyBlueprint")]: "applyBlueprint",
|
[t("actionApplyBlueprint")]: "applyBlueprint",
|
||||||
@@ -39,6 +38,16 @@ function getActionsCategories(root: boolean) {
|
|||||||
[t("actionGetBlueprint")]: "getBlueprint"
|
[t("actionGetBlueprint")]: "getBlueprint"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Domain: {
|
||||||
|
[t("actionListOrgDomains")]: "listOrgDomains",
|
||||||
|
[t("actionGetDomain")]: "getDomain",
|
||||||
|
[t("actionCreateOrgDomain")]: "createOrgDomain",
|
||||||
|
[t("actionUpdateOrgDomain")]: "updateOrgDomain",
|
||||||
|
[t("actionDeleteOrgDomain")]: "deleteOrgDomain",
|
||||||
|
[t("actionGetDNSRecords")]: "getDNSRecords",
|
||||||
|
[t("actionRestartOrgDomain")]: "restartOrgDomain"
|
||||||
|
},
|
||||||
|
|
||||||
Site: {
|
Site: {
|
||||||
[t("actionCreateSite")]: "createSite",
|
[t("actionCreateSite")]: "createSite",
|
||||||
[t("actionDeleteSite")]: "deleteSite",
|
[t("actionDeleteSite")]: "deleteSite",
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ export function NewtSiteInstallCommands({
|
|||||||
`helm install newt fossorial/newt \\
|
`helm install newt fossorial/newt \\
|
||||||
--create-namespace \\
|
--create-namespace \\
|
||||||
--set newtInstances[0].name="main-tunnel" \\
|
--set newtInstances[0].name="main-tunnel" \\
|
||||||
--set newtInstances[0].enabled=true \\
|
|
||||||
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
|
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
|
||||||
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
|
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
|
||||||
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
|
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
|
||||||
@@ -186,72 +185,59 @@ WantedBy=default.target`
|
|||||||
className="mt-4"
|
className="mt-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<p className="font-bold mb-3">{t("siteConfiguration")}</p>
|
<p className="font-bold mb-3">
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
{t("siteConfiguration")}
|
||||||
<CheckboxWithLabel
|
</p>
|
||||||
id="acceptClients"
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
aria-describedby="acceptClients-desc"
|
<CheckboxWithLabel
|
||||||
checked={acceptClients}
|
id="acceptClients"
|
||||||
onCheckedChange={(checked) => {
|
aria-describedby="acceptClients-desc"
|
||||||
const value = checked as boolean;
|
checked={acceptClients}
|
||||||
setAcceptClients(value);
|
onCheckedChange={(checked) => {
|
||||||
}}
|
const value = checked as boolean;
|
||||||
label={t("siteAcceptClientConnections")}
|
setAcceptClients(value);
|
||||||
/>
|
}}
|
||||||
</div>
|
label={t("siteAcceptClientConnections")}
|
||||||
<p
|
/>
|
||||||
id="acceptClients-desc"
|
</div>
|
||||||
className="text-sm text-muted-foreground"
|
<p
|
||||||
>
|
id="acceptClients-desc"
|
||||||
{t("siteAcceptClientConnectionsDescription")}
|
className="text-sm text-muted-foreground"
|
||||||
</p>
|
>
|
||||||
</div>
|
{t("siteAcceptClientConnectionsDescription")}
|
||||||
|
|
||||||
<div className="pt-4">
|
|
||||||
<p className="font-bold mb-3">{t("commands")}</p>
|
|
||||||
{platform === "kubernetes" && (
|
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
|
||||||
For more and up to date Kubernetes installation
|
|
||||||
information, see{" "}
|
|
||||||
<a
|
|
||||||
href="https://docs.pangolin.net/manage/sites/install-kubernetes"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="underline"
|
|
||||||
>
|
|
||||||
docs.pangolin.net/manage/sites/install-kubernetes
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
<div className="mt-2 space-y-3">
|
|
||||||
{commands.map((item, index) => {
|
|
||||||
const commandText =
|
|
||||||
typeof item === "string" ? item : item.command;
|
|
||||||
const title =
|
|
||||||
typeof item === "string"
|
|
||||||
? undefined
|
|
||||||
: item.title;
|
|
||||||
|
|
||||||
const key = `${title ?? ""}::${commandText}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={key}>
|
|
||||||
{title && (
|
|
||||||
<p className="text-sm font-medium mb-1.5">
|
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<CopyTextBox
|
|
||||||
text={commandText}
|
|
||||||
outline={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<p className="font-bold mb-3">{t("commands")}</p>
|
||||||
|
<div className="mt-2 space-y-3">
|
||||||
|
{commands.map((item, index) => {
|
||||||
|
const commandText =
|
||||||
|
typeof item === "string"
|
||||||
|
? item
|
||||||
|
: item.command;
|
||||||
|
const title =
|
||||||
|
typeof item === "string"
|
||||||
|
? undefined
|
||||||
|
: item.title;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
{title && (
|
||||||
|
<p className="text-sm font-medium mb-1.5">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<CopyTextBox
|
||||||
|
text={commandText}
|
||||||
|
outline={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user