feat: Implement basic auth for applications

This commit is contained in:
Pascal Klesse
2023-05-15 09:27:49 +02:00
parent f3beb5d8db
commit d14ca724e9
9 changed files with 1861 additions and 1720 deletions

11
.vscode/settings.json vendored
View File

@@ -18,5 +18,14 @@
"ts",
"json"
],
"i18n-ally.extract.autoDetect": true
"i18n-ally.extract.autoDetect": true,
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true
},
"hide-files.files": []
}

View File

@@ -0,0 +1,29 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "basicAuthPw" TEXT;
ALTER TABLE "Application" ADD COLUMN "basicAuthUser" TEXT;
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"isHttp2" BOOLEAN NOT NULL DEFAULT false,
"basicAuth" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -135,6 +135,8 @@ model Application {
dockerRegistryId String?
dockerRegistryImageName String?
simpleDockerfile String?
basicAuthUser String?
basicAuthPw String?
persistentStorage ApplicationPersistentStorage[]
secrets Secret[]
@@ -187,6 +189,7 @@ model ApplicationSettings {
isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false)
basicAuth Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])

View File

@@ -504,12 +504,17 @@ export async function saveApplicationSettings(
isBot,
isDBBranching,
isCustomSSL,
isHttp2
isHttp2,
basicAuth,
basicAuthUser,
basicAuthPw
} = request.body;
await prisma.application.update({
where: { id },
data: {
fqdn: isBot ? null : undefined,
basicAuthUser,
basicAuthPw,
settings: {
update: {
debug,
@@ -519,7 +524,8 @@ export async function saveApplicationSettings(
isBot,
isDBBranching,
isCustomSSL,
isHttp2
isHttp2,
basicAuth,
}
}
},

View File

@@ -43,6 +43,9 @@ export interface SaveApplicationSettings extends OnlyId {
isDBBranching: boolean;
isCustomSSL: boolean;
isHttp2: boolean;
basicAuth: boolean;
basicAuthUser: string;
basicAuthPw: string;
};
}
export interface DeleteApplication extends OnlyId {

View File

@@ -1,5 +1,5 @@
import { FastifyRequest } from 'fastify';
import { errorHandler, getDomain, isDev, prisma, executeCommand } from '../../../lib/common';
import { errorHandler, executeCommand, getDomain, isDev, prisma } from '../../../lib/common';
import { getTemplates } from '../../../lib/services';
import { OnlyId } from '../../../types';
import { parseAndFindServiceTemplates } from '../../api/v1/services/handlers';
@@ -48,20 +48,30 @@ function generateRouters(
isWWW,
isDualCerts,
isCustomSSL,
isHttp2 = false
isHttp2 = false,
basicAuth = false,
basicAuthUser = '',
basicAuthPw = ''
) {
let rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
let ruleWWW = `Host(\`www.${nakedDomain}\`)${
const rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
const ruleWWW = `Host(\`www.${nakedDomain}\`)${
pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''
}`;
let http: any = {
const httpBasicAuth: any = {
basicauth: {
users: [Buffer.from(basicAuthUser + ':' + basicAuthPw).toString('base64')]
}
};
const http: any = {
entrypoints: ['web'],
rule,
service: `${serviceId}`,
priority: 2,
middlewares: []
};
let https: any = {
const https: any = {
entrypoints: ['websecure'],
rule,
service: `${serviceId}`,
@@ -71,14 +81,14 @@ function generateRouters(
},
middlewares: []
};
let httpWWW: any = {
const httpWWW: any = {
entrypoints: ['web'],
rule: ruleWWW,
service: `${serviceId}`,
priority: 2,
middlewares: []
};
let httpsWWW: any = {
const httpsWWW: any = {
entrypoints: ['websecure'],
rule: ruleWWW,
service: `${serviceId}`,
@@ -97,6 +107,10 @@ function generateRouters(
httpsWWW.middlewares.push('redirect-to-non-www');
delete https.tls;
delete httpsWWW.tls;
if (basicAuth) {
http.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
}
// 3. http + www only
@@ -108,6 +122,10 @@ function generateRouters(
https.middlewares.push('redirect-to-www');
delete https.tls;
delete httpsWWW.tls;
if (basicAuth) {
httpWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
}
// 5. https + non-www only
if (isHttps && !isWWW) {
@@ -136,6 +154,10 @@ function generateRouters(
};
}
}
if (basicAuth) {
https.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
}
// 6. https + www only
if (isHttps && isWWW) {
@@ -145,6 +167,11 @@ function generateRouters(
http.middlewares.push('redirect-to-www');
https.middlewares.push('redirect-to-www');
}
if (basicAuth) {
httpsWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
if (isCustomSSL) {
if (isDualCerts) {
https.tls = true;
@@ -166,23 +193,23 @@ function generateRouters(
}
}
if (isHttp2) {
let http2 = {
const http2 = {
...http,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let http2WWW = {
const http2WWW = {
...httpWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let https2 = {
const https2 = {
...https,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let https2WWW = {
const https2WWW = {
...httpsWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
@@ -198,14 +225,21 @@ function generateRouters(
[`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW }
};
}
return {
const result = {
[`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }
};
if (basicAuth) {
result[`${serviceId}-${pathPrefix}-basic-auth`] = { ...httpBasicAuth };
}
return result;
}
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote: boolean = false) {
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote = false) {
const traefik = {
tls: {
certificates: []
@@ -298,7 +332,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
});
}
let parsedCertificates = [];
const parsedCertificates = [];
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
@@ -369,7 +403,9 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
dockerComposeConfiguration,
destinationDocker,
destinationDockerId,
settings
settings,
basicAuthUser,
basicAuthPw
} = application;
if (!destinationDockerId) {
continue;
@@ -424,7 +460,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
}
continue;
}
const { previews, dualCerts, isCustomSSL, isHttp2 } = settings;
const { previews, dualCerts, isCustomSSL, isHttp2, basicAuth } = settings;
const { network, id: dockerId } = destinationDocker;
if (!fqdn) {
continue;
@@ -446,7 +482,10 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
isWWW,
dualCerts,
isCustomSSL,
isHttp2
isHttp2,
basicAuth,
basicAuthUser,
basicAuthPw
)
};
traefik.http.services = {
@@ -482,7 +521,11 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
isHttps,
isWWW,
dualCerts,
isCustomSSL
isCustomSSL,
false,
basicAuth,
basicAuthUser,
basicAuthPw
)
};
traefik.http.services = {
@@ -542,7 +585,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
if (isDomainAndProxyConfiguration.length > 0) {
const template: any = await parseAndFindServiceTemplates(service, null, true);
const { proxy } = template.services[oneService] || found.services[oneService];
for (let configuration of proxy) {
for (const configuration of proxy) {
if (configuration.hostPort) {
continue;
}

View File

@@ -196,6 +196,9 @@
"domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white '>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?",
"basic_auth": "Basic Auth",
"basic_auth_user": "User",
"basic_auth_pw": "Password",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command",
"build_command": "Build Command",

View File

@@ -29,27 +29,27 @@
export let application: any;
export let settings: any;
import yaml from 'js-yaml';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { get, getAPIUrl, post } from '$lib/api';
import cuid from 'cuid';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Beta from '$lib/components/Beta.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import {
addToast,
appSession,
checkIfDeploymentEnabledApplications,
setLocation,
status,
features,
isDeploymentEnabled,
features
setLocation,
status
} from '$lib/store';
import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation';
import Beta from '$lib/components/Beta.svelte';
import cuid from 'cuid';
import yaml from 'js-yaml';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { saveForm } from './utils';
const { id } = $page.params;
@@ -77,6 +77,7 @@
let isCustomSSL = application.settings?.isCustomSSL;
let autodeploy = application.settings?.autodeploy;
let isBot = application.settings?.isBot;
let basicAuth = application.settings?.basicAuth;
let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings.isHttp2;
@@ -186,6 +187,10 @@
if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL;
}
if (name === 'basicAuth') {
basicAuth = !basicAuth;
// TODO: Set user and password
}
if (name === 'isBot') {
if ($status.application.overallStatus !== 'stopped') return;
isBot = !isBot;
@@ -210,7 +215,10 @@
isCustomSSL,
isHttp2,
branch: application.branch,
projectId: application.projectId
projectId: application.projectId,
basicAuth,
basicAuthUser: application.basicAuthUser,
basicAuthPw: application.basicAuthPw
});
return addToast({
message: $t('application.settings_saved'),
@@ -232,6 +240,9 @@
if (name === 'isBot') {
isBot = !isBot;
}
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
@@ -498,7 +509,7 @@
<div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin}
<button
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary"
type="submit"
class:loading={loading.save}
class:bg-orange-600={forceSave}
@@ -751,7 +762,56 @@
on:click={() => !isDisabled && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="basicAuth"
dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={isDisabled}
isCenter={false}
bind:setting={basicAuth}
title={$t('application.basic_auth')}
description="Activate basic authentication for your application. <br>Useful if you want to protect your application with a password. <br><br>Use the <span class='font-bold text-settings'>username</span> and <span class='font-bold text-settings'>password</span> fields to set the credentials."
on:click={() => !isDisabled && changeSettings('basicAuth')}
/>
</div>
{#if basicAuth}
<div class="grid grid-cols-2 items-center">
<label for="basicAuthUser">{$t('application.basic_auth_user')}</label>
<input
bind:this={fqdnEl}
class="w-full"
required={!application.settings?.basicAuth}
readonly={isDisabled}
disabled={isDisabled}
name="basicAuthUser"
id="basicAuthUser"
class:border={!application.settings?.basicAuth && !application.basicAuthUser}
class:border-red-500={!application.settings?.basicAuth &&
!application.basicAuthUser}
bind:value={application.basicAuthUser}
placeholder="eg: admin"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="basicAuthPw">{$t('application.basic_auth_pw')}</label>
<input
bind:this={fqdnEl}
class="w-full"
required={!application.settings?.basicAuth}
readonly={isDisabled}
disabled={isDisabled}
name="basicAuthPw"
id="basicAuthPw"
class:border={!application.settings?.basicAuth && !application.basicAuthPw}
class:border-red-500={!application.settings?.basicAuth && !application.basicAuthPw}
bind:value={application.basicAuthPw}
placeholder="**********"
/>
</div>
{/if}
{#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
@@ -782,7 +842,7 @@
</div>
<div class="grid grid-flow-row gap-2 px-4 pr-5">
<div class="grid grid-cols-2 items-center pt-4">
<div class="grid grid-cols-2 items-center pt-4">
<label for="simpleDockerfile">Dockerfile</label>
<div class="flex gap-2">
<textarea

3347
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff