1
0
mirror of https://github.com/NginxProxyManager/nginx-proxy-manager.git synced 2025-04-25 05:42:29 +03:00

CI stack for Authentik with ldap

This commit is contained in:
Jamie Curnow 2024-11-03 13:17:24 +10:00
parent a277a5d167
commit 6e820a36ac
No known key found for this signature in database
GPG Key ID: FFBB624C43388E9E
35 changed files with 116 additions and 172 deletions

9
Jenkinsfile vendored
View File

@ -188,6 +188,11 @@ pipeline {
sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1'
junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
@ -248,16 +253,16 @@ pipeline {
printResult()
}
failure {
archiveArtifacts(artifacts: 'debug/**/*', allowEmptyArchive: true)
dir(path: 'test') {
archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml'
}
archiveArtifacts(artifacts: 'debug/*', allowEmptyArchive: true)
}
unstable {
archiveArtifacts(artifacts: 'debug/**/*', allowEmptyArchive: true)
dir(path: 'test') {
archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml'
}
archiveArtifacts(artifacts: 'debug/*', allowEmptyArchive: true)
}
}
}

View File

@ -18,4 +18,4 @@ threshold:
# package: 30
# (optional; default 0)
# The minimum total coverage project should have
total: 34
total: 33

View File

@ -24,7 +24,7 @@
},
"type": {
"type": "string",
"pattern": "^password$"
"pattern": "^(local|ldap|oidc)$"
}
}
}

View File

@ -53,7 +53,7 @@
},
"type": {
"type": "string",
"pattern": "^password$"
"pattern": "^(local|ldap|oidc)$"
}
}
},

View File

@ -125,14 +125,12 @@ INSERT INTO `user` (
`created_at`,
`updated_at`,
`name`,
`nickname`,
`email`,
`is_system`
) VALUES (
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
"System",
"System",
"system@localhost",
TRUE
);

View File

@ -125,14 +125,12 @@ INSERT INTO "user" (
"created_at",
"updated_at",
"name",
"nickname",
"email",
"is_system"
) VALUES (
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
'System',
'System',
'system@localhost',
TRUE
);

View File

@ -124,14 +124,12 @@ INSERT INTO `user` (
created_at,
updated_at,
name,
nickname,
email,
is_system
) VALUES (
unixepoch() * 1000,
unixepoch() * 1000,
"System",
"System",
"system@localhost",
1
);

View File

@ -97,13 +97,14 @@ func (s *testsuite) TestSave() {
defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener"))
s.mock.ExpectBegin()
s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "auth" ("created_at","updated_at","is_deleted","user_id","type","secret") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id"`)).
s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "auth" ("created_at","updated_at","is_deleted","user_id","type","identity","secret") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`)).
WithArgs(
sqlmock.AnyArg(),
sqlmock.AnyArg(),
0,
100,
TypeLocal,
"",
"abc123",
).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11"))

View File

@ -39,7 +39,6 @@ func (m *Model) LoadByID(id int) error {
// Save will save this model to the DB
func (m *Model) Save() error {
db := database.GetDB()
// todo: touch? not sure that save does this or not?
result := db.Save(m)
return result.Error
}

View File

@ -84,7 +84,6 @@ func (s *testsuite) SetupTest() {
).AddRow(
11,
"Jane Doe",
"Jane",
"jane@example.com",
true,
false,
@ -183,7 +182,6 @@ func (s *testsuite) TestSave() {
sqlmock.AnyArg(),
0,
"John Doe",
"Jonny",
"sarah@example.com",
false,
false,

8
docker/ci.env Normal file
View File

@ -0,0 +1,8 @@
AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0
AUTHENTIK_REDIS__HOST=authentik-redis
AUTHENTIK_POSTGRESQL__HOST=db-postgres
AUTHENTIK_POSTGRESQL__USER=authentik
AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj
AUTHENTIK_BOOTSTRAP_PASSWORD=admin
AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com

Binary file not shown.

View File

@ -12,15 +12,63 @@ services:
NPM_DB_SSLMODE: 'disable'
depends_on:
- db-postgres
- authentik
- authentik-worker
- authentik-ldap
db-postgres:
image: postgres:15
image: postgres:latest
environment:
POSTGRES_USER: 'npm'
POSTGRES_PASSWORD: 'npmpass'
POSTGRES_DB: 'npm'
volumes:
- psql_vol:/var/lib/postgresql/data
- ./ci/postgres:/docker-entrypoint-initdb.d
authentik-redis:
image: 'redis:alpine'
command: --save 60 1 --loglevel warning
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'redis-cli ping | grep PONG']
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redis_vol:/data
authentik:
image: ghcr.io/goauthentik/server:2024.8.3
restart: unless-stopped
command: server
env_file:
- ci.env
depends_on:
- authentik-redis
- db-postgres
authentik-worker:
image: ghcr.io/goauthentik/server:2024.8.3
restart: unless-stopped
command: worker
env_file:
- ci.env
depends_on:
- authentik-redis
- db-postgres
authentik-ldap:
image: ghcr.io/goauthentik/ldap
environment:
AUTHENTIK_HOST: 'http://authentik:9000'
AUTHENTIK_INSECURE: 'true'
AUTHENTIK_TOKEN: '1N7z2r5PZrNBauuyDZSnlhU4gPSih7bkooIgqbvhzBbrA1MGYyDGZmBasJqU'
restart: unless-stopped
depends_on:
- authentik
volumes:
psql_vol:
redis_vol:

View File

@ -25,7 +25,6 @@ curl --request POST \
--header 'Content-Type: application/json' \
--data '{
"name": "Bobby Tables",
"nickname": "Bobby",
"email": "you@example.com",
"roles": ["admin"],
"is_disabled": false,

View File

@ -8,7 +8,6 @@ export interface AuthOptions {
export interface NewUser {
name: string;
nickname: string;
email: string;
isDisabled: boolean;
auth: AuthOptions;

View File

@ -14,7 +14,6 @@ export interface UserAuth {
export interface User {
id: number;
name: string;
nickname: string;
email: string;
createdOn: number;
updatedOn: number;

View File

@ -185,6 +185,9 @@
"error.email-already-exists": {
"defaultMessage": "Es existiert bereits ein Benutzer mit dieser E-Mail-Adresse"
},
"error.invalid-auth-type": {
"defaultMessage": "Invalid authentication type"
},
"error.invalid-login-credentials": {
"defaultMessage": "Ungültige Login-Details"
},
@ -440,9 +443,6 @@
"user.name": {
"defaultMessage": "Name"
},
"user.nickname": {
"defaultMessage": "Benutzername"
},
"user.password": {
"defaultMessage": "Passwort"
},

View File

@ -461,6 +461,9 @@
"error.email-already-exists": {
"defaultMessage": "A user already exists with this email address"
},
"error.invalid-auth-type": {
"defaultMessage": "Invalid authentication type"
},
"error.invalid-login-credentials": {
"defaultMessage": "Invalid login credentials"
},
@ -722,9 +725,6 @@
"user.name": {
"defaultMessage": "Name"
},
"user.nickname": {
"defaultMessage": "Nickname"
},
"user.password": {
"defaultMessage": "Password"
},

View File

@ -185,6 +185,9 @@
"error.email-already-exists": {
"defaultMessage": "کاربری از قبل با این آدرس ایمیل وجود دارد"
},
"error.invalid-auth-type": {
"defaultMessage": "Invalid authentication type"
},
"error.invalid-login-credentials": {
"defaultMessage": "اعتبار ورود نامعتبر است"
},
@ -443,9 +446,6 @@
"user.name": {
"defaultMessage": "نام"
},
"user.nickname": {
"defaultMessage": "کنیه"
},
"user.password": {
"defaultMessage": "کلمه عبور"
},

View File

@ -5,19 +5,19 @@ import {
FormLabel,
Input,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
useToast,
} from "@chakra-ui/react";
import { Formik, Form, Field } from "formik";
import { Field, Form, Formik } from "formik";
import { PrettyButton } from "src/components";
import { useUser, useSetUser } from "src/hooks";
import { useSetUser, useUser } from "src/hooks";
import { intl } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations";
@ -59,7 +59,6 @@ function ProfileModal({ isOpen, onClose }: ProfileModalProps) {
<Formik
initialValues={{
name: user.data?.name,
nickname: user.data?.nickname,
email: user.data?.email,
}}
onSubmit={onSubmit}>
@ -71,7 +70,7 @@ function ProfileModal({ isOpen, onClose }: ProfileModalProps) {
<ModalCloseButton />
<ModalBody>
<Stack spacing={4}>
<Field name="name" validate={validateString(2, 100)}>
<Field name="name" validate={validateString(2, 50)}>
{({ field, form }: any) => (
<FormControl
isRequired
@ -91,30 +90,6 @@ function ProfileModal({ isOpen, onClose }: ProfileModalProps) {
</FormControl>
)}
</Field>
<Field name="nickname" validate={validateString(2, 100)}>
{({ field, form }: any) => (
<FormControl
isRequired
isInvalid={
form.errors.nickname && form.touched.nickname
}>
<FormLabel htmlFor="nickname">
{intl.formatMessage({ id: "user.nickname" })}
</FormLabel>
<Input
{...field}
id="nickname"
defaultValue={values.nickname}
placeholder={intl.formatMessage({
id: "user.nickname",
})}
/>
<FormErrorMessage>
{form.errors.nickname}
</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="email" validate={validateEmail()}>
{({ field, form }: any) => (
<FormControl

View File

@ -35,7 +35,6 @@ import { validateEmail, validateString } from "src/modules/Validations";
interface Payload {
name: string;
nickname: string;
email: string;
password: string;
}
@ -110,7 +109,6 @@ function UserCreateModal({ isOpen, onClose }: UserCreateModalProps) {
initialValues={
{
name: "",
nickname: "",
email: "",
password: "",
} as Payload
@ -131,7 +129,7 @@ function UserCreateModal({ isOpen, onClose }: UserCreateModalProps) {
<TabPanels>
<TabPanel>
<Stack spacing={4}>
<Field name="name" validate={validateString(2, 100)}>
<Field name="name" validate={validateString(2, 50)}>
{({ field, form }: any) => (
<FormControl
isRequired
@ -152,31 +150,6 @@ function UserCreateModal({ isOpen, onClose }: UserCreateModalProps) {
</FormControl>
)}
</Field>
<Field
name="nickname"
validate={validateString(2, 100)}>
{({ field, form }: any) => (
<FormControl
isRequired
isInvalid={
form.errors.nickname && form.touched.nickname
}>
<FormLabel htmlFor="nickname">
{intl.formatMessage({ id: "user.nickname" })}
</FormLabel>
<Input
{...field}
id="nickname"
placeholder={intl.formatMessage({
id: "user.nickname",
})}
/>
<FormErrorMessage>
{form.errors.nickname}
</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="email" validate={validateEmail()}>
{({ field, form }: any) => (
<FormControl

View File

@ -8,28 +8,28 @@ import {
FormLabel,
Input,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
Tabs,
useToast,
} from "@chakra-ui/react";
import { Formik, Form, Field } from "formik";
import { Field, Form, Formik } from "formik";
import {
AdminPermissionSelector,
PermissionSelector,
PrettyButton,
} from "src/components";
import { useUser, useSetUser } from "src/hooks";
import { useSetUser, useUser } from "src/hooks";
import { intl } from "src/locale";
import { validateEmail, validateString } from "src/modules/Validations";
@ -106,7 +106,6 @@ function UserEditModal({ userId, isOpen, onClose }: UserEditModalProps) {
initialValues={
{
name: data?.name,
nickname: data?.nickname,
email: data?.email,
isDisabled: data?.isDisabled,
} as any
@ -129,7 +128,7 @@ function UserEditModal({ userId, isOpen, onClose }: UserEditModalProps) {
<TabPanels>
<TabPanel>
<Stack spacing={4}>
<Field name="name" validate={validateString(2, 100)}>
<Field name="name" validate={validateString(2, 50)}>
{({ field, form }: any) => (
<FormControl
isRequired
@ -152,31 +151,6 @@ function UserEditModal({ userId, isOpen, onClose }: UserEditModalProps) {
</FormControl>
)}
</Field>
<Field
name="nickname"
validate={validateString(2, 100)}>
{({ field, form }: any) => (
<FormControl
isRequired
isInvalid={
form.errors.nickname && form.touched.nickname
}>
<FormLabel htmlFor="nickname">
{intl.formatMessage({ id: "user.nickname" })}
</FormLabel>
<Input
{...field}
id="nickname"
placeholder={intl.formatMessage({
id: "user.nickname",
})}
/>
<FormErrorMessage>
{form.errors.nickname}
</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="email" validate={validateEmail()}>
{({ field, form }: any) => (
<FormControl

View File

@ -1,10 +1,9 @@
import { useEffect, useMemo, useState } from "react";
import { FiDownload, FiEdit, FiRefreshCw, FiTrash2 } from "react-icons/fi";
import { useSortBy, useFilters, useTable, usePagination } from "react-table";
import { useFilters, usePagination, useSortBy, useTable } from "react-table";
import {
tableEvents,
ActionsFormatter,
CertificateStatusFormatter,
CertificateTypeFormatter,
@ -12,6 +11,7 @@ import {
GravatarFormatter,
IDFormatter,
MonospaceFormatter,
tableEvents,
TableFilter,
TableLayout,
TablePagination,
@ -123,7 +123,7 @@ function Table({
},
];
return [columns, data];
}, [data, onRenewal]);
}, [data, onRenewal, onDelete]);
const tableInstance = useTable(
{

View File

@ -25,7 +25,6 @@ import { validateEmail, validateString } from "src/modules/Validations";
interface Payload {
name: string;
nickname: string;
email: string;
password: string;
}
@ -122,7 +121,6 @@ function Setup() {
initialValues={
{
name: "",
nickname: "",
email: "",
password: "",
} as Payload
@ -131,7 +129,7 @@ function Setup() {
{({ isSubmitting }) => (
<Form>
<Stack spacing={4}>
<Field name="name" validate={validateString(2, 100)}>
<Field name="name" validate={validateString(2, 50)}>
{({ field, form }: any) => (
<FormControl
isRequired
@ -153,29 +151,6 @@ function Setup() {
</FormControl>
)}
</Field>
<Field name="nickname" validate={validateString(2, 100)}>
{({ field, form }: any) => (
<FormControl
isRequired
isInvalid={
form.errors.nickname && form.touched.nickname
}>
<FormLabel htmlFor="nickname">
{intl.formatMessage({ id: "user.nickname" })}
</FormLabel>
<Input
{...field}
id="nickname"
placeholder={intl.formatMessage({
id: "user.nickname",
})}
/>
<FormErrorMessage>
{form.errors.nickname}
</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="email" validate={validateEmail()}>
{({ field, form }: any) => (
<FormControl

View File

@ -71,6 +71,7 @@ printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}"
docker-compose up -d --remove-orphans stepca
docker-compose pull db-mysql || true # ok to fail
docker-compose pull db-postgres || true # ok to fail
docker-compose pull authentik authentik-redis authentik-ldap || true # ok to fail
docker-compose up -d --remove-orphans --pull=never fullstack
# wait for main container to be healthy

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
describe('Certificates endpoints', () => {
let token;

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
describe('Full Certificate Provisions', () => {
let token;

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
describe('Basic API checks', () => {
it('Should return a valid health payload', function() {

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
// Settings are stored lowercase in the backend

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
describe('Setup Phase', () => {
@ -8,15 +8,15 @@ describe('Setup Phase', () => {
it('Should NOT be able to get a token', function() {
cy.task('backendApiPost', {
path: '/api/tokens',
path: '/api/auth',
data: {
type: 'password',
type: 'local',
identity: 'cypress@example.com',
secret: 'changeme'
},
returnOnError: true
}).then((data) => {
cy.validateSwaggerSchema('post', 403, '/tokens', data);
cy.validateSwaggerSchema('post', 403, '/auth', data);
expect(data.error).to.have.property('code', 403);
expect(data.error).to.have.property('message', 'Not available during setup phase');
});

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
describe('Swagger Schema Checks', () => {
it('Should be valid with swagger-validator', function() {

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
describe('Upstream endpoints', () => {
let token;

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
describe('Users endpoints', () => {
let token;
@ -82,11 +82,10 @@ describe('Users endpoints', () => {
path: '/api/users',
data: {
name: 'Example user 1',
nickname: 'User1',
email: uniqueEmail,
is_disabled: false,
auth: {
type: 'password',
type: 'local',
secret: 'changeme'
},
capabilities: [
@ -108,11 +107,10 @@ describe('Users endpoints', () => {
returnOnError: true,
data: {
name: 'Example user 2',
nickname: 'User2',
email: uniqueEmail,
is_disabled: false,
auth: {
type: 'password',
type: 'local',
secret: 'changeme'
},
capabilities: [

View File

@ -1,4 +1,4 @@
/// <reference types="Cypress" />
/// <reference types="cypress" />
describe('UI Setup and Login', () => {
@ -10,7 +10,6 @@ describe('UI Setup and Login', () => {
it('Should be able to setup a new user', function() {
cy.visit('/');
cy.get('input[name="name"]').type('Cypress McGee');
cy.get('input[name="nickname"]').type('Cypress');
cy.get('input[name="email"]').type('cypress@example.com');
cy.get('input[name="password"]').type('changeme');
cy.get('form button:last').click();

View File

@ -66,7 +66,7 @@ Cypress.Commands.add('getToken', (defaultUser, defaultAuth) => {
});
} else {
let auth = {
type: 'password',
type: 'local',
identity: 'cypress@example.com',
secret: 'changeme',
};
@ -78,7 +78,7 @@ Cypress.Commands.add('getToken', (defaultUser, defaultAuth) => {
cy.log('Setup = true');
// login with existing user
cy.task('backendApiPost', {
path: '/api/tokens',
path: '/api/auth',
data: auth,
}).then((res) => {
cy.wrap(res.result.token);
@ -90,11 +90,10 @@ Cypress.Commands.add('getToken', (defaultUser, defaultAuth) => {
Cypress.Commands.add('createInitialUser', (defaultUser) => {
let user = {
name: 'Cypress McGee',
nickname: 'Cypress',
email: 'cypress@example.com',
is_disabled: false,
auth: {
type: 'password',
type: 'local',
secret: 'changeme'
},
capabilities: ['full-admin']