diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index e3d0a44..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: [fosrl] diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml index 41dbe7b..c945608 100644 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -14,12 +14,13 @@ body: label: Environment description: Please fill out the relevant details below for your environment. value: | - - OS Type & Version: (e.g., Ubuntu 22.04) + - OS Type & Version: - Pangolin Version: + - Edition (Community or Enterprise): - Gerbil Version: - Traefik Version: - Newt Version: - - Olm Version: (if applicable) + - Client Version: validations: required: true diff --git a/README.md b/README.md index f82bb55..9fbb858 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Take a look at the [quick install guide](https://docs.pangolin.net/manage/remote ## Licensing -Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net). +Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net). ## Contributions -Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. \ No newline at end of file +Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices. diff --git a/server/lib/types.ts b/server/lib/types.ts index 9799786..9c38d96 100644 --- a/server/lib/types.ts +++ b/server/lib/types.ts @@ -47,7 +47,7 @@ export type ResourceHeaderAuthExtendedCompatibility = { resourceId: number; headerAuthExtendedCompatibilityId: number; extendedCompatibilityIsActivated: boolean; -} +}; export type Org = { name: string; @@ -61,7 +61,7 @@ export type Org = { settingsLogRetentionDaysRequest: number; settingsLogRetentionDaysAccess: number; settingsLogRetentionDaysAction: number; -} +}; export type LoginPage = { loginPageId: number; @@ -103,6 +103,9 @@ export type ResourceSession = { accessTokenId: string | null; isRequestToken: boolean; userSessionId: string | null; + policyPasswordId: number | null; + policyPincodeId: number | null; + policyWhitelistId: number | null; issuedAt: number | null; }; diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 8b57458..4a7ae67 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -29,7 +29,8 @@ import { ResourceHeaderAuthExtendedCompatibility, ResourcePassword, ResourcePincode, - ResourceRule + ResourceRule, + ResourceSession } from "@server/lib/types"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { logRequestAudit } from "./logRequestAudit"; @@ -64,6 +65,7 @@ export type VerifyResourceSessionSchema = z.infer< >; type BasicUserData = { + userId: string; username: string; email: string | null; name: string | null; @@ -138,6 +140,9 @@ export async function verifyResourceSession( password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; + applyRules: boolean | null; + sso: boolean | null; + emailWhitelistEnabled: boolean | null; org: Org; } | undefined = localCache.get(resourceCacheKey); @@ -169,9 +174,12 @@ export async function verifyResourceSession( const { resource, + applyRules, + sso, pincode, password, headerAuth, + emailWhitelistEnabled, headerAuthExtendedCompatibility } = resourceData; @@ -193,7 +201,7 @@ export async function verifyResourceSession( return notAllowed(res); } - const { sso, blockAccess } = resource; + const { blockAccess } = resource; if (blockAccess) { logger.debug("Resource blocked", host); @@ -213,7 +221,7 @@ export async function verifyResourceSession( } // check the rules - if (resource.applyRules) { + if (applyRules) { const action = await checkRules( resource.resourceId, clientIp, @@ -268,7 +276,7 @@ export async function verifyResourceSession( !sso && !pincode && !password && - !resource.emailWhitelistEnabled && + !emailWhitelistEnabled && !headerAuth ) { logger.debug("Resource allowed because no auth"); @@ -451,7 +459,7 @@ export async function verifyResourceSession( !sso && !pincode && !password && - !resource.emailWhitelistEnabled && + !emailWhitelistEnabled && !headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated ) { logRequestAudit( @@ -473,7 +481,7 @@ export async function verifyResourceSession( !sso && !pincode && !password && - !resource.emailWhitelistEnabled && + !emailWhitelistEnabled && !headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated ) { logRequestAudit( @@ -521,7 +529,8 @@ export async function verifyResourceSession( if (resourceSessionToken) { const sessionCacheKey = `session:${resourceSessionToken}`; - let resourceSession: any = localCache.get(sessionCacheKey); + let resourceSession: ResourceSession | null | undefined = + localCache.get(sessionCacheKey); if (!resourceSession) { const result = await validateResourceSessionToken( @@ -574,7 +583,11 @@ export async function verifyResourceSession( return notAllowed(res, redirectPath, resource.orgId); } - if (pincode && resourceSession.pincodeId) { + if ( + pincode && + (resourceSession.pincodeId || + resourceSession.policyPincodeId) + ) { logger.debug( "Resource allowed because pincode session is valid" ); @@ -593,7 +606,11 @@ export async function verifyResourceSession( return allowed(res); } - if (password && resourceSession.passwordId) { + if ( + password && + (resourceSession.passwordId || + resourceSession.policyPasswordId) + ) { logger.debug( "Resource allowed because password session is valid" ); @@ -613,8 +630,9 @@ export async function verifyResourceSession( } if ( - resource.emailWhitelistEnabled && - resourceSession.whitelistId + emailWhitelistEnabled && + (resourceSession.whitelistId || + resourceSession.policyWhitelistId) ) { logger.debug( "Resource allowed because whitelist session is valid" @@ -647,7 +665,7 @@ export async function verifyResourceSession( orgId: resource.orgId, location: ipCC, apiKey: { - name: resourceSession.accessTokenTitle, + name: null, apiKeyId: resourceSession.accessTokenId } }, @@ -692,7 +710,7 @@ export async function verifyResourceSession( location: ipCC, user: { username: allowedUserData.username, - userId: resourceSession.userId + userId: allowedUserData.userId } }, parsedBody.data @@ -957,6 +975,7 @@ async function isUserAllowedToAccessResource( ); if (roleResourceAccess && roleResourceAccess.length > 0) { return { + userId: user.userId, username: user.username, email: user.email, name: user.name, @@ -977,6 +996,7 @@ async function isUserAllowedToAccessResource( if (userResourceAccess) { return { + userId: user.userId, username: user.username, email: user.email, name: user.name, @@ -1035,18 +1055,31 @@ async function checkRules( isPathAllowed(rule.value, path) ) { return rule.action as any; - } else if ( - clientIp && - rule.match == "COUNTRY" && - (await isIpInGeoIP(ipCC, rule.value)) - ) { - return rule.action as any; - } else if ( - clientIp && - rule.match == "ASN" && - (await isIpInAsn(ipAsn, rule.value)) - ) { - return rule.action as any; + } else if (clientIp && rule.match == "COUNTRY") { + // COUNTRY=ALL should not affect local/private/CGNAT addresses. + if ( + rule.value.toUpperCase() === "ALL" && + isLocalOrCarrierGradeNatIp(clientIp) + ) { + continue; + } + + if (await isIpInGeoIP(ipCC, rule.value)) { + return rule.action as any; + } + } else if (clientIp && rule.match == "ASN") { + // ASN=ALL/AS0 should not affect local/private/CGNAT addresses. + if ( + (rule.value.toUpperCase() === "ALL" || + rule.value.toUpperCase() === "AS0") && + isLocalOrCarrierGradeNatIp(clientIp) + ) { + continue; + } + + if (await isIpInAsn(ipAsn, rule.value)) { + return rule.action as any; + } } else if ( clientIp && rule.match == "REGION" && @@ -1208,6 +1241,26 @@ async function isIpInGeoIP( return ipCountryCode?.toUpperCase() === checkCountryCode.toUpperCase(); } +function isLocalOrCarrierGradeNatIp(ip: string): boolean { + const localAndCgnatCidrs = [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "::1/128", + "fc00::/7", + "fe80::/10" + ]; + + try { + return localAndCgnatCidrs.some((cidr) => isIpInCidr(ip, cidr)); + } catch { + return false; + } +} + async function isIpInAsn( ipAsn: number | undefined, checkAsn: string diff --git a/server/routers/ws/verifySessionQueries.ts b/server/routers/ws/verifySessionQueries.ts index 2d526ad..eaa7022 100644 --- a/server/routers/ws/verifySessionQueries.ts +++ b/server/routers/ws/verifySessionQueries.ts @@ -19,6 +19,9 @@ export type ResourceWithAuth = { password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; + applyRules: boolean; + sso: boolean; + emailWhitelistEnabled: boolean; org: Org; };