diff --git a/.all-contributorsrc b/.all-contributorsrc index 72f97d226..7dcf00357 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -795,6 +795,34 @@ "contributions": [ "doc" ] + }, + { + "login": "LongerHV", + "name": "Michał Mieszczak", + "avatar_url": "https://avatars.githubusercontent.com/u/46924944?v=4", + "profile": "https://github.com/LongerHV", + "contributions": [ + "ideas", + "code" + ] + }, + { + "login": "paul-ohl", + "name": "Paul Ohl", + "avatar_url": "https://avatars.githubusercontent.com/u/37795294?v=4", + "profile": "https://github.com/paul-ohl", + "contributions": [ + "doc" + ] + }, + { + "login": "smkent", + "name": "Stephen Kent", + "avatar_url": "https://avatars.githubusercontent.com/u/2831985?v=4", + "profile": "https://github.com/smkent", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7 diff --git a/.codecov.yml b/.codecov.yml index 4287a1ca8..6fec44956 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -16,10 +16,12 @@ coverage: default: false backend: base: auto + threshold: 0.15% flags: - backend frontend: base: auto + threshold: 0.15% flags: - frontend diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..13ee8d8b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,144 @@ +--- +name: Bug Report +description: Report a bug +labels: + - type/bug/unconfirmed + - status/needs-triage + - priority/4/normal +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report. If you are unsure if this is actually a bug we generally recommend creating a [Question and Answer Discussion](https://github.com/authelia/authelia/discussions/new?category=q-a) first. + + Please review the following requirements before submitting this issue type: + + 1. Please ensure you do not report security vulnerabilities via this method. See our [Security Policy](https://www.authelia.com/security-policy). + 2. Please try to give as much information as possible for us to be able to reproduce the issue and provide a quick fix. + 3. Please ensure an issue does not already exist for this potential bug. + 4. Please only provide specific versions. Latest is not a version. + 5. Please read the [Troubleshooting Sanitization](https://www.authelia.com/r/sanitize) reference guide if you plan on removing or adjusting any values for the logs or configuration files. + 6. Please consider including a [HTTP Archive File](https://www.authelia.com/r/har) if you're having redirection issues. + - type: dropdown + id: version + attributes: + label: Version + description: What version(s) of Authelia can you reproduce this bug on? + multiple: true + options: + - v4.37.2 + - v4.37.1 + - v4.37.0 + - v4.36.9 + - v4.36.8 + - v4.36.7 + - v4.36.6 + - v4.36.5 + - v4.36.4 + - v4.36.3 + - v4.36.2 + - v4.36.1 + - v4.36.0 + - v4.35.6 + - v4.35.5 + - v4.35.4 + - v4.35.3 + - v4.35.2 + - v4.35.1 + - v4.35.0 + - v4.34.6 + - v4.34.5 + - v4.34.4 + - v4.34.3 + - v4.34.2 + - v4.34.1 + - v4.34.0 + - v4.33.2 + - v4.33.1 + - v4.33.0 + - v4.32.2 + - v4.32.1 + - v4.32.0 + validations: + required: true + - type: dropdown + id: deployment + attributes: + label: Deployment Method + description: How are you deploying Authelia? + options: + - Docker + - Kubernetes + - Bare-metal + - Other + validations: + required: true + - type: dropdown + id: proxy + attributes: + label: Reverse Proxy + description: What reverse proxy are you using? + options: + - Caddy + - Traefik + - Envoy + - Istio + - NGINX + - SWAG + - NGINX Proxy Manager + - HAProxy + validations: + required: true + - type: input + id: proxy-version + attributes: + label: Reverse Proxy Version + description: What is the version of your reverse proxy? + placeholder: x.x.x + validations: + required: false + - type: textarea + id: description + attributes: + label: Description + description: Describe the bug + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Describe how we can reproduce this issue + validations: + required: true + - type: textarea + id: expectations + attributes: + label: Expectations + description: Describe the desired or expected results + validations: + required: false + - type: textarea + id: logs + attributes: + label: Logs + description: Provide the logs (the template will automatically put this content in a code block) + render: shell + validations: + required: false + - type: textarea + id: configuration + attributes: + label: Configuration + description: Provide the Authelia configuration file (the template will automatically put this content in a code block) + render: yaml + validations: + required: false + - type: textarea + id: documentation + attributes: + label: Documentation + description: Provide any relevant specification or other documentation if applicable + validations: + required: false +... diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 76a60c8f8..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: Bug Report -about: Use this template to report bugs other than security vulnerabilities -labels: Possible Bug ---- -## Bug Report - - -### Description - - - -### Expected Behaviour - -_N/A_ - - -### Reproduction Steps - -_N/A_ - - -### Additional Information - -_N/A_ - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 899f94db9..2a89a14ac 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,15 @@ --- blank_issues_enabled: false contact_links: + - name: Idea + url: https://github.com/authelia/authelia/discussions/new?category=ideas + about: Submit an Idea for Voting + - name: Question + url: https://github.com/authelia/authelia/discussions/new?category=q-a + about: Ask a Question + - name: Discussion + url: https://github.com/authelia/authelia/discussions/new + about: Start a Discussion related to Ideas, Polls, Show and Tell, or General Topics - name: Documentation url: https://www.authelia.com/ about: Read the Documentation diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..44832359d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,47 @@ +--- +name: Feature Request +description: Submit a Feature Request +labels: + - type/feature + - status/needs-design + - priority/4/normal +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request. A feature request is created as issue for the purpose of tracking the design and implementation of a feature. + + Please review the following requirements before submitting this issue type: + + 1. Ensure there are no other similar feature requests. + 2. Make sure you've checked the [Documentation](https://www.authelia.com) doesn't clearly document the features existence already. + 3. Consider creating an [Idea Discussion](https://github.com/authelia/authelia/discussions/new?category=ideas) which can be voted on instead if one doesn't exist. + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature + validations: + required: true + - type: textarea + id: use-case + attributes: + label: Use Case + description: Provide a use case + validations: + required: true + - type: textarea + id: details + attributes: + label: Details + description: Describe the feature in detail + validations: + required: false + - type: textarea + id: documentation + attributes: + label: Documentation + description: Provide any relevant specification or other documentation if applicable + validations: + required: false +... diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 4354ccba9..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Feature Request -about: Use this template to request features -labels: Feature Request ---- -## Feature Request - - -### Description - - - -### Use Case - -_N/A_ - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/misc.md b/.github/ISSUE_TEMPLATE/misc.md deleted file mode 100644 index 23a941098..000000000 --- a/.github/ISSUE_TEMPLATE/misc.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: Miscellaneous -about: Use this template for everything other than feature requests, security vulnerabilities, or bug reports such as questions ---- - \ No newline at end of file diff --git a/.github/pre-commit b/.github/pre-commit index d38e39af9..8ed8ea753 100755 --- a/.github/pre-commit +++ b/.github/pre-commit @@ -1,7 +1,6 @@ #!/bin/sh -if [[ ! -z "$NO_HOOK" ]] -then +if [ -n "$NO_HOOK" ]; then exit 0 fi @@ -9,4 +8,4 @@ fi . "$(dirname "$0")/required-apps" golangci-lint run -v --fix && \ -cd web && ${PMGR} lint +cd web && "${PMGR}" lint diff --git a/.github/required-apps b/.github/required-apps index 2de152a46..f4d8368db 100644 --- a/.github/required-apps +++ b/.github/required-apps @@ -1,19 +1,19 @@ #!/bin/sh -PMGR=pnpm +export PMGR=pnpm -if [[ ! -x "$(command -v golangci-lint)" ]]; then +if [ ! -x "$(command -v golangci-lint)" ]; then echo "You must install golangci-lint." exit 1 fi -if [[ ! -x "$(command -v pnpm)" ]]; then - PMGR=yarn - if [[ ! -x "$(command -v yarn)" ]]; then - PMGR=npm - if [[ ! -x "$(command -v npm)" ]]; then +if [ ! -x "$(command -v pnpm)" ]; then + export PMGR=yarn + if [ ! -x "$(command -v yarn)" ]; then + export PMGR=npm + if [ ! -x "$(command -v npm)" ]; then echo "You must install a node package manager." exit 1 fi fi -fi \ No newline at end of file +fi diff --git a/.renovaterc b/.renovaterc index 09fe00a98..9ddef69e8 100644 --- a/.renovaterc +++ b/.renovaterc @@ -2,16 +2,16 @@ "extends": [ "config:base", ":semanticCommitTypeAll(build)", - ":separatePatchReleases" + ":separatePatchReleases", + "workarounds:doNotUpgradeFromAlpineStableToEdge" ], "ignorePaths": [ "docs/**" ], "ignorePresets": [ - ":combinePatchMinorReleases", - "helpers:disableTypesNodeMajor", ":prHourlyLimit2", - ":semanticPrefixFixDepsChoreOthers" + ":semanticPrefixFixDepsChoreOthers", + "workarounds:all" ], "enabledManagers": [ "bundler", @@ -24,6 +24,13 @@ "dependencies" ], "packageRules": [ + { + "matchUpdateTypes": ["digest", "minor", "patch"], + "matchCurrentVersion": "!/^0/", + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true + }, { "datasources": [ "docker" @@ -47,14 +54,6 @@ "addLabels": [ "javascript" ] - }, - { - "datasources": [ - "rubygems" - ], - "addLabels": [ - "ruby" - ] } ], "postUpdateOptions": [ diff --git a/.yamllint.yml b/.yamllint.yml index 3d31542b4..d7fc7209f 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -6,7 +6,8 @@ ignore: | internal/configuration/test_resources/config_bad_quoting.yml web/pnpm-lock.yaml web/node_modules/ - + .github/ISSUE_TEMPLATE/feature-request.yml + .github/ISSUE_TEMPLATE/bug-report.yml rules: document-end: level: warning diff --git a/Dockerfile.coverage b/Dockerfile.coverage index 110b772bd..6db2f7178 100644 --- a/Dockerfile.coverage +++ b/Dockerfile.coverage @@ -1,7 +1,7 @@ # ======================================== # ===== Build image for the frontend ===== # ======================================== -FROM node:18-alpine AS builder-frontend +FROM node:19-alpine AS builder-frontend WORKDIR /node/src/app @@ -15,7 +15,7 @@ RUN yarn global add pnpm && \ # ======================================= # ===== Build image for the backend ===== # ======================================= -FROM golang:1.19.1-alpine AS builder-backend +FROM golang:1.19.3-alpine AS builder-backend WORKDIR /go/src/app diff --git a/Dockerfile.dev b/Dockerfile.dev index 26592e422..11f74ea67 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,7 +1,7 @@ # ======================================== # ===== Build image for the frontend ===== # ======================================== -FROM node:18-alpine AS builder-frontend +FROM node:19-alpine AS builder-frontend WORKDIR /node/src/app @@ -13,7 +13,7 @@ RUN yarn install --frozen-lockfile && yarn build # ======================================= # ===== Build image for the backend ===== # ======================================= -FROM golang:1.19.1-alpine AS builder-backend +FROM golang:1.19.3-alpine AS builder-backend WORKDIR /go/src/app diff --git a/README.md b/README.md index 589de509e..9faedeae5 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,8 @@ [![Matrix](https://img.shields.io/matrix/authelia-support:matrix.org?label=matrix&logo=matrix&style=flat-square&color=blue)](https://matrix.to/#/#support:authelia.com) **Authelia** is an open-source authentication and authorization server providing two-factor authentication and single -sign-on (SSO) for your applications via a web portal. It acts as a companion for reverse proxies like [nginx], -[Traefik], [caddy] or [HAProxy] to let them know whether requests should either be allowed or redirected to Authelia's -portal for authentication. +sign-on (SSO) for your applications via a web portal. It acts as a companion for [reverse proxies](#proxy-support) by +allowing, denying, or redirecting requests. Documentation is available at [https://www.authelia.com/](https://www.authelia.com/). @@ -31,9 +30,12 @@ The following is a simple diagram of the architecture: **Authelia** can be installed as a standalone service from the [AUR](https://aur.archlinux.org/packages/authelia/), [APT](https://apt.authelia.com/stable/debian/packages/authelia/), [FreeBSD Ports](https://svnweb.freebsd.org/ports/head/www/authelia/), or using a -[Static binary](https://github.com/authelia/authelia/releases/latest), -[.deb package]((https://github.com/authelia/authelia/releases/latest)), [Docker] or [Kubernetes] either manually or via -the Helm [Chart](https://charts.authelia.com) (beta) leveraging ingress controllers and ingress configurations. +[static binary](https://github.com/authelia/authelia/releases/latest), +[.deb package]((https://github.com/authelia/authelia/releases/latest)), as a container on [Docker] or [Kubernetes]. + + +Deployment can be orchestrated via the Helm [Chart](https://charts.authelia.com) (beta) leveraging ingress controllers +and ingress configurations.

@@ -70,13 +72,14 @@ This is a list of the key features of Authelia: * Curated configuration from [LinuxServer](https://www.linuxserver.io/) via their [Swag](https://docs.linuxserver.io/general/swag) container as well as a [guide](https://blog.linuxserver.io/2020/08/26/setting-up-authelia/). -* Compatible with [caddy] using the [forward_auth](https://caddyserver.com/docs/caddyfile/directives/forward_auth) +* Compatible with [Caddy] using the [forward_auth](https://caddyserver.com/docs/caddyfile/directives/forward_auth) directive. * Kubernetes Support: - * Compatible with the [ingress-nginx](https://github.com/kubernetes/ingress-nginx), the - [Traefik Kubernetes CRD](https://doc.traefik.io/traefik/providers/kubernetes-crd/), and the - [Traefik Kubernetes Ingress](https://doc.traefik.io/traefik/providers/kubernetes-crd/) Kubernetes ingress - controllers out of the box. + * Compatible with several Kubernetes ingress controllers: + * [ingress-nginx](https://www.authelia.com/integration/kubernetes/nginx-ingress/) + * [Traefik Kubernetes CRD](https://www.authelia.com/integration/kubernetes/traefik-ingress/#ingressroute) + * [Traefik Kubernetes Ingress](https://www.authelia.com/integration/kubernetes/traefik-ingress/#ingress) + * [Istio](https://www.authelia.com/integration/kubernetes/istio/) * Beta support for installing via Helm using our [Charts](https://charts.authelia.com). * Beta support for [OpenID Connect](https://www.authelia.com/roadmap/active/openid-connect/). @@ -86,22 +89,14 @@ If you want to know more about the roadmap, follow [Roadmap](https://www.autheli ## Proxy support -Authelia works in combination with [nginx], [Traefik], [caddy] or [HAProxy]. It can be deployed on bare metal with -Docker or on top of [Kubernetes]. +Authelia works in combination with [nginx], [Traefik], [Caddy], [Skipper], [Envoy], or [HAProxy].

- - -

- -***Help Wanted:*** Assistance would be appreciated in getting Authelia working with -[Envoy](https://www.envoyproxy.io/). - -

+

## Getting Started @@ -200,113 +195,120 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Clément Michaud

💻 📖 🤔 🚧 💬 👀 ⚠️

Amir Zarrinkafsh

💻 📖 🤔 🚧 💬 👀 ⚠️

James Elliott

💻 📖 🤔 🚧 💬 👀 ⚠️

Antoine Favre

🐛 🤔

BankaiNoJutsu

💻 🎨

Philipp Rintz

📖

Callan Bryant

💻 📖

Ian

💻

FrozenDragoon

💻

vdot0x23

💻

alexw1982

📖

Sohalt

💻 📖

Stoica Tedy

💻

Dylan Smith

💻

Lukas Klass

📖

Philipp Staiger

💻 📖 ⚠️

James Hodgkinson

📖

Chris Smith

📖

Mihály

📖

Silver Bullet

📖

Paul Williams

💻 ⚠️

Timo

📖

Andrew Kliskey

📖

Kristof Mattei

📖

ZMiguel Valdiviesso

📖

akusei

💻 📖

Daniel Miller

📖

Dustin Sweigart

💻 📖 ⚠️

Shawn Haggard

💻 ⚠️

Kevyn Bruyere

📖

Daniel Sutton

💻

Valentin Höbel

💻

thehedgefrog

📖

Victor

📖

Chris Whisker

📖

nasatome

📖

Begley Brothers (Development)

📖

Mike Kusold

💻

Dimitris Zervas

📖

TheCatLady

🤔

Lauri Võsandi

🤔

Kennard Vermeiren

🤔

ThinkChaos

💻 📖 ⚠️

Hasan

🛡️

David Chidell

📖

Marcel Marquardt

🐛

Ian Gallagher

📖

Wu Han

📖

lavih

📖

Jon B.

🛡️

Alex Gustafsson

💻 📖

Arsenović Arsen

💻 ⚠️ 🛡️

dakriy

💻

Dave

📓

Nicolas Reymundo

📖

polandy

📖

yossbg

💻 🎨

Michael Campbell

📖

Justin Sievenpiper

💻

Aram Akhavan

📖

Shadow

📖

Patrick Ruckstuhl

📖

Andrew Moore

💻 📖 ⚠️

Dennis Gaida

📖

Alestrix

📖

bgh-github

📖

Manuel Nuñez

💻 🌍

protvis74

🌍

Jamie (Bear) Murphy

👀

Robin van Boven

🛡️

alphabet5

🤔

Robert Meredith

🤔

Adrian Gąsior

🛡️

James White

💬

Zhao Xiang Lim

📖

Auzborn123

🌍

SvanGlan

🌍

HannesJo0139

📖

andreas-berg

🐛

Clément Radenac

📖

boomam

📖

Northguy

📖

Brennan Kinney

📖
Clément Michaud
Clément Michaud

💻 📖 🤔 🚧 💬 👀 ⚠️
Amir Zarrinkafsh
Amir Zarrinkafsh

💻 📖 🤔 🚧 💬 👀 ⚠️
James Elliott
James Elliott

💻 📖 🤔 🚧 💬 👀 ⚠️
Antoine Favre
Antoine Favre

🐛 🤔
BankaiNoJutsu
BankaiNoJutsu

💻 🎨
Philipp Rintz
Philipp Rintz

📖
Callan Bryant
Callan Bryant

💻 📖
Ian
Ian

💻
FrozenDragoon
FrozenDragoon

💻
vdot0x23
vdot0x23

💻
alexw1982
alexw1982

📖
Sohalt
Sohalt

💻 📖
Stoica Tedy
Stoica Tedy

💻
Dylan Smith
Dylan Smith

💻
Lukas Klass
Lukas Klass

📖
Philipp Staiger
Philipp Staiger

💻 📖 ⚠️
James Hodgkinson
James Hodgkinson

📖
Chris Smith
Chris Smith

📖
Mihály
Mihály

📖
Silver Bullet
Silver Bullet

📖
Paul Williams
Paul Williams

💻 ⚠️
Timo
Timo

📖
Andrew Kliskey
Andrew Kliskey

📖
Kristof Mattei
Kristof Mattei

📖
ZMiguel Valdiviesso
ZMiguel Valdiviesso

📖
akusei
akusei

💻 📖
Daniel Miller
Daniel Miller

📖
Dustin Sweigart
Dustin Sweigart

💻 📖 ⚠️
Shawn Haggard
Shawn Haggard

💻 ⚠️
Kevyn Bruyere
Kevyn Bruyere

📖
Daniel Sutton
Daniel Sutton

💻
Valentin Höbel
Valentin Höbel

💻
thehedgefrog
thehedgefrog

📖
Victor
Victor

📖
Chris Whisker
Chris Whisker

📖
nasatome
nasatome

📖
Begley Brothers (Development)
Begley Brothers (Development)

📖
Mike Kusold
Mike Kusold

💻
Dimitris Zervas
Dimitris Zervas

📖
TheCatLady
TheCatLady

🤔
Lauri Võsandi
Lauri Võsandi

🤔
Kennard Vermeiren
Kennard Vermeiren

🤔
ThinkChaos
ThinkChaos

💻 📖 ⚠️
Hasan
Hasan

🛡️
David Chidell
David Chidell

📖
Marcel Marquardt
Marcel Marquardt

🐛
Ian Gallagher
Ian Gallagher

📖
Wu Han
Wu Han

📖
lavih
lavih

📖
Jon B.
Jon B.

🛡️
Alex Gustafsson
Alex Gustafsson

💻 📖
Arsenović Arsen
Arsenović Arsen

💻 ⚠️ 🛡️
dakriy
dakriy

💻
Dave
Dave

📓
Nicolas Reymundo
Nicolas Reymundo

📖
polandy
polandy

📖
yossbg
yossbg

💻 🎨
Michael Campbell
Michael Campbell

📖
Justin Sievenpiper
Justin Sievenpiper

💻
Aram Akhavan
Aram Akhavan

📖
Shadow
Shadow

📖
Patrick Ruckstuhl
Patrick Ruckstuhl

📖
Andrew Moore
Andrew Moore

💻 📖 ⚠️
Dennis Gaida
Dennis Gaida

📖
Alestrix
Alestrix

📖
bgh-github
bgh-github

📖
Manuel Nuñez
Manuel Nuñez

💻 🌍
protvis74
protvis74

🌍
Jamie (Bear) Murphy
Jamie (Bear) Murphy

👀
Robin van Boven
Robin van Boven

🛡️
alphabet5
alphabet5

🤔
Robert Meredith
Robert Meredith

🤔
Adrian Gąsior
Adrian Gąsior

🛡️
James White
James White

💬
Zhao Xiang Lim
Zhao Xiang Lim

📖
Auzborn123
Auzborn123

🌍
SvanGlan
SvanGlan

🌍
HannesJo0139
HannesJo0139

📖
andreas-berg
andreas-berg

🐛
Clément Radenac
Clément Radenac

📖
boomam
boomam

📖
Northguy
Northguy

📖
Brennan Kinney
Brennan Kinney

📖
Michał Mieszczak
Michał Mieszczak

🤔 💻
Paul Ohl
Paul Ohl

📖
Stephen Kent
Stephen Kent

🤔
@@ -389,10 +391,12 @@ Companies contributing to Authelia via Open Collective will have a special menti [Webauthn]: https://www.yubico.com/authentication-standards/webauthn/ [auth_request]: https://nginx.org/en/docs/http/ngx_http_auth_request_module.html [config.template.yml]: ./config.template.yml -[nginx]: https://www.nginx.com/ -[Traefik]: https://traefik.io/ -[caddy]: https://caddyserver.com/ -[HAProxy]: https://www.haproxy.org/ +[nginx]: https://www.authelia.com/integration/proxies/nginx/ +[Traefik]: https://www.authelia.com/integration/proxies/traefik/ +[Caddy]: https://www.authelia.com/integration/proxies/caddy/ +[Skipper]: https://www.authelia.com/integration/proxies/skipper/ +[Envoy]: https://www.authelia.com/integration/proxies/envoy/ +[HAProxy]: https://www.authelia.com/integration/proxies/haproxy/ [Docker]: https://docker.com/ [Kubernetes]: https://kubernetes.io/ [security]: https://github.com/authelia/authelia/security/policy diff --git a/api/openapi.yml b/api/openapi.yml index 10ec36b9d..15443a24e 100644 --- a/api/openapi.yml +++ b/api/openapi.yml @@ -176,7 +176,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/handlers.firstFactorRequestBody' + $ref: '#/components/schemas/handlers.bodyFirstFactorRequest' responses: "200": description: Successful Operation @@ -446,7 +446,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/handlers.signTOTPRequestBody' + $ref: '#/components/schemas/handlers.bodySignTOTPRequest' responses: "200": description: Successful Operation @@ -579,7 +579,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/handlers.signDuoRequestBody' + $ref: '#/components/schemas/handlers.bodySignDuoRequest' responses: "200": description: Successful Operation @@ -785,7 +785,7 @@ components: items: type: string example: push - handlers.firstFactorRequestBody: + handlers.bodyFirstFactorRequest: required: - username - password @@ -800,6 +800,12 @@ components: targetURL: type: string example: https://home.example.com + workflow: + type: string + example: openid_connect + workflowID: + type: string + example: 3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c requestMethod: type: string example: GET @@ -852,13 +858,21 @@ components: password: type: string example: password - handlers.signDuoRequestBody: + handlers.bodySignDuoRequest: type: object properties: targetURL: type: string example: https://secure.example.com - handlers.signTOTPRequestBody: + passcode: + type: string + workflow: + type: string + example: openid_connect + workflowID: + type: string + example: 3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c + handlers.bodySignTOTPRequest: type: object properties: token: @@ -867,6 +881,12 @@ components: targetURL: type: string example: https://secure.example.com + workflow: + type: string + example: openid_connect + workflowID: + type: string + example: 3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c handlers.StateResponse: type: object properties: @@ -1047,6 +1067,12 @@ components: userHandle: type: string format: byte + workflow: + type: string + example: openid_connect + workflowID: + type: string + example: 3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c webauthn.PublicKeyCredentialCreationOptions: type: object properties: diff --git a/cmd/authelia-gen/cmd_all.go b/cmd/authelia-gen/cmd_all.go deleted file mode 100644 index aba00a5a3..000000000 --- a/cmd/authelia-gen/cmd_all.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" -) - -func newAllCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "all", - Short: "Run all generators with default options", - RunE: allRunE, - - DisableAutoGenTag: true, - } - - return cmd -} - -func allRunE(cmd *cobra.Command, args []string) (err error) { - for _, subCmd := range cmd.Parent().Commands() { - if subCmd == cmd || subCmd.Use == "completion" || subCmd.Use == "help [command]" { - continue - } - - switch { - case subCmd.RunE != nil: - if err = subCmd.RunE(subCmd, args); err != nil { - return err - } - case subCmd.Run != nil: - subCmd.Run(subCmd, args) - } - } - - return nil -} diff --git a/cmd/authelia-gen/cmd_code.go b/cmd/authelia-gen/cmd_code.go index f0eea6b02..1359298c5 100644 --- a/cmd/authelia-gen/cmd_code.go +++ b/cmd/authelia-gen/cmd_code.go @@ -1,34 +1,330 @@ package main import ( + "crypto/ecdsa" + "crypto/rsa" + "encoding/json" + "fmt" + "io" + "net/http" + "net/mail" + "net/url" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "time" + "github.com/spf13/cobra" + + "github.com/authelia/authelia/v4/internal/configuration/schema" ) func newCodeCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "code", + Use: cmdUseCode, Short: "Generate code", - RunE: codeRunE, + RunE: rootSubCommandsRunE, DisableAutoGenTag: true, } - cmd.AddCommand(newCodeKeysCmd()) + cmd.AddCommand(newCodeKeysCmd(), newCodeServerCmd(), newCodeScriptsCmd()) return cmd } -func codeRunE(cmd *cobra.Command, args []string) (err error) { - for _, subCmd := range cmd.Commands() { - switch { - case subCmd.RunE != nil: - if err = subCmd.RunE(subCmd, args); err != nil { - return err - } - case subCmd.Run != nil: - subCmd.Run(subCmd, args) - } +func newCodeServerCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseServer, + Short: "Generate the Authelia server files", + RunE: codeServerRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func newCodeScriptsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseCodeScripts, + Short: "Generate the generated portion of the authelia-scripts command", + RunE: codeScriptsRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func newCodeKeysCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseKeys, + Short: "Generate the list of valid configuration keys", + RunE: codeKeysRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func codeServerRunE(cmd *cobra.Command, args []string) (err error) { + data := TemplateCSP{ + PlaceholderNONCE: codeCSPNonce, + TemplateDefault: buildCSP(codeCSPProductionDefaultSrc, codeCSPValuesCommon, codeCSPValuesProduction), + TemplateDevelopment: buildCSP(codeCSPDevelopmentDefaultSrc, codeCSPValuesCommon, codeCSPValuesDevelopment), + } + + var outputPath string + + if outputPath, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagFileServerGenerated); err != nil { + return err + } + + var f *os.File + + if f, err = os.Create(outputPath); err != nil { + return fmt.Errorf("failed to create file '%s': %w", outputPath, err) + } + + if err = tmplServer.Execute(f, data); err != nil { + _ = f.Close() + + return fmt.Errorf("failed to write output file '%s': %w", outputPath, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", outputPath, err) } return nil } + +func codeScriptsRunE(cmd *cobra.Command, args []string) (err error) { + var ( + root, pathScriptsGen string + resp *http.Response + ) + + data := &tmplScriptsGEnData{} + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathScriptsGen, err = cmd.Flags().GetString(cmdFlagFileScriptsGen); err != nil { + return err + } + + if data.Package, err = cmd.Flags().GetString(cmdFlagPackageScriptsGen); err != nil { + return err + } + + if resp, err = http.Get("https://api.github.com/repos/swagger-api/swagger-ui/tags"); err != nil { + return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) + } + + defer resp.Body.Close() + + var ( + respJSON []GitHubTagsJSON + respRaw []byte + ) + + if respRaw, err = io.ReadAll(resp.Body); err != nil { + return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) + } + + if err = json.Unmarshal(respRaw, &respJSON); err != nil { + return fmt.Errorf("failed to get latest version of the Swagger UI: %w", err) + } + + if len(respJSON) < 1 { + return fmt.Errorf("failed to get latest version of the Swagger UI: the api returned zero results") + } + + if strings.HasPrefix(respJSON[0].Name, "v") { + data.VersionSwaggerUI = respJSON[0].Name[1:] + } else { + data.VersionSwaggerUI = respJSON[0].Name + } + + fullPathScriptsGen := filepath.Join(root, pathScriptsGen) + + var f *os.File + + if f, err = os.Create(fullPathScriptsGen); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPathScriptsGen, err) + } + + if err = tmplScriptsGen.Execute(f, data); err != nil { + _ = f.Close() + + return fmt.Errorf("failed to write output file '%s': %w", fullPathScriptsGen, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", fullPathScriptsGen, err) + } + + return nil +} + +func codeKeysRunE(cmd *cobra.Command, args []string) (err error) { + var ( + pathCodeConfigKeys, root string + + f *os.File + ) + + data := tmplConfigurationKeysData{ + Timestamp: time.Now(), + Keys: readTags("", reflect.TypeOf(schema.Configuration{})), + } + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathCodeConfigKeys, err = cmd.Flags().GetString(cmdFlagFileConfigKeys); err != nil { + return err + } + + if data.Package, err = cmd.Flags().GetString(cmdFlagPackageConfigKeys); err != nil { + return err + } + + fullPathCodeConfigKeys := filepath.Join(root, pathCodeConfigKeys) + + if f, err = os.Create(fullPathCodeConfigKeys); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPathCodeConfigKeys, err) + } + + if err = tmplCodeConfigurationSchemaKeys.Execute(f, data); err != nil { + _ = f.Close() + + return fmt.Errorf("failed to write output file '%s': %w", fullPathCodeConfigKeys, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", fullPathCodeConfigKeys, err) + } + + return nil +} + +var decodedTypes = []reflect.Type{ + reflect.TypeOf(mail.Address{}), + reflect.TypeOf(regexp.Regexp{}), + reflect.TypeOf(url.URL{}), + reflect.TypeOf(time.Duration(0)), + reflect.TypeOf(schema.Address{}), + reflect.TypeOf(rsa.PrivateKey{}), + reflect.TypeOf(ecdsa.PrivateKey{}), +} + +func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool) { + for _, t := range haystack { + if needle.Kind() == reflect.Ptr { + if needle.Elem() == t { + return true + } + } else if needle == t { + return true + } + } + + return false +} + +//nolint:gocyclo +func readTags(prefix string, t reflect.Type) (tags []string) { + tags = make([]string, 0) + + if t.Kind() != reflect.Struct { + if t.Kind() == reflect.Slice { + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, "", true), t.Elem())...) + } + + return + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + tag := field.Tag.Get("koanf") + + if tag == "" { + tags = append(tags, prefix) + + continue + } + + switch field.Type.Kind() { + case reflect.Struct: + if !containsType(field.Type, decodedTypes) { + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type)...) + + continue + } + case reflect.Slice: + switch field.Type.Elem().Kind() { + case reflect.Struct: + if !containsType(field.Type.Elem(), decodedTypes) { + tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) + + continue + } + case reflect.Slice: + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) + } + case reflect.Ptr: + switch field.Type.Elem().Kind() { + case reflect.Struct: + if !containsType(field.Type.Elem(), decodedTypes) { + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type.Elem())...) + + continue + } + case reflect.Slice: + if field.Type.Elem().Elem().Kind() == reflect.Struct { + if !containsType(field.Type.Elem(), decodedTypes) { + tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) + + continue + } + } + } + } + + tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) + } + + return tags +} + +func getKeyNameFromTagAndPrefix(prefix, name string, slice bool) string { + nameParts := strings.SplitN(name, ",", 2) + + if prefix == "" { + return nameParts[0] + } + + if len(nameParts) == 2 && nameParts[1] == "squash" { + return prefix + } + + if slice { + if name == "" { + return fmt.Sprintf("%s[]", prefix) + } + + return fmt.Sprintf("%s.%s[]", prefix, nameParts[0]) + } + + return fmt.Sprintf("%s.%s", prefix, nameParts[0]) +} diff --git a/cmd/authelia-gen/cmd_code_keys.go b/cmd/authelia-gen/cmd_code_keys.go deleted file mode 100644 index 4599ae148..000000000 --- a/cmd/authelia-gen/cmd_code_keys.go +++ /dev/null @@ -1,173 +0,0 @@ -package main - -import ( - "fmt" - "net/mail" - "net/url" - "os" - "reflect" - "regexp" - "strings" - "text/template" - "time" - - "github.com/spf13/cobra" - - "github.com/authelia/authelia/v4/internal/configuration/schema" -) - -func newCodeKeysCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "keys", - Short: "Generate the list of valid configuration keys", - RunE: codeKeysRunE, - - DisableAutoGenTag: true, - } - - cmd.Flags().StringP("file", "f", "./internal/configuration/schema/keys.go", "Sets the path of the keys file") - cmd.Flags().String("package", "schema", "Sets the package name of the keys file") - - return cmd -} - -func codeKeysRunE(cmd *cobra.Command, args []string) (err error) { - var ( - file string - - f *os.File - ) - - data := keysTemplateStruct{ - Timestamp: time.Now(), - Keys: readTags("", reflect.TypeOf(schema.Configuration{})), - } - - if file, err = cmd.Flags().GetString("file"); err != nil { - return err - } - - if data.Package, err = cmd.Flags().GetString("package"); err != nil { - return err - } - - if f, err = os.Create(file); err != nil { - return fmt.Errorf("failed to create file '%s': %w", file, err) - } - - var ( - content []byte - tmpl *template.Template - ) - - if content, err = templatesFS.ReadFile("templates/config_keys.go.tmpl"); err != nil { - return err - } - - if tmpl, err = template.New("keys").Parse(string(content)); err != nil { - return err - } - - return tmpl.Execute(f, data) -} - -type keysTemplateStruct struct { - Timestamp time.Time - Keys []string - Package string -} - -var decodedTypes = []reflect.Type{ - reflect.TypeOf(mail.Address{}), - reflect.TypeOf(regexp.Regexp{}), - reflect.TypeOf(url.URL{}), - reflect.TypeOf(time.Duration(0)), - reflect.TypeOf(schema.Address{}), -} - -func containsType(needle reflect.Type, haystack []reflect.Type) (contains bool) { - for _, t := range haystack { - if needle.Kind() == reflect.Ptr { - if needle.Elem() == t { - return true - } - } else if needle == t { - return true - } - } - - return false -} - -func readTags(prefix string, t reflect.Type) (tags []string) { - tags = make([]string, 0) - - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - - tag := field.Tag.Get("koanf") - - if tag == "" { - tags = append(tags, prefix) - - continue - } - - switch field.Type.Kind() { - case reflect.Struct: - if !containsType(field.Type, decodedTypes) { - tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type)...) - - continue - } - case reflect.Slice: - if field.Type.Elem().Kind() == reflect.Struct { - if !containsType(field.Type.Elem(), decodedTypes) { - tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) - tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) - - continue - } - } - case reflect.Ptr: - switch field.Type.Elem().Kind() { - case reflect.Struct: - if !containsType(field.Type.Elem(), decodedTypes) { - tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, false), field.Type.Elem())...) - - continue - } - case reflect.Slice: - if field.Type.Elem().Elem().Kind() == reflect.Struct { - if !containsType(field.Type.Elem(), decodedTypes) { - tags = append(tags, readTags(getKeyNameFromTagAndPrefix(prefix, tag, true), field.Type.Elem())...) - - continue - } - } - } - } - - tags = append(tags, getKeyNameFromTagAndPrefix(prefix, tag, false)) - } - - return tags -} - -func getKeyNameFromTagAndPrefix(prefix, name string, slice bool) string { - nameParts := strings.SplitN(name, ",", 2) - - if prefix == "" { - return nameParts[0] - } - - if len(nameParts) == 2 && nameParts[1] == "squash" { - return prefix - } - - if slice { - return fmt.Sprintf("%s.%s[]", prefix, nameParts[0]) - } - - return fmt.Sprintf("%s.%s", prefix, nameParts[0]) -} diff --git a/cmd/authelia-gen/cmd_commit_msg.go b/cmd/authelia-gen/cmd_commit_msg.go new file mode 100644 index 000000000..cadf70d68 --- /dev/null +++ b/cmd/authelia-gen/cmd_commit_msg.go @@ -0,0 +1,220 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" +) + +// CommitMessageTmpl is a template data structure which is used to generate files with commit message information. +type CommitMessageTmpl struct { + Scopes ScopesTmpl + Types TypesTmpl +} + +// TypesTmpl is a template data structure which is used to generate files with commit message types. +type TypesTmpl struct { + List []string + Details []NameDescriptionTmpl +} + +// ScopesTmpl is a template data structure which is used to generate files with commit message scopes. +type ScopesTmpl struct { + All []string + Packages []string + Extra []NameDescriptionTmpl +} + +// NameDescriptionTmpl is a template item which includes a name, description and list of scopes. +type NameDescriptionTmpl struct { + Name string + Description string + Scopes []string +} + +func newCommitLintCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseCommitLint, + Short: "Generate commit lint files", + RunE: commitLintRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +var commitScopesExtra = []NameDescriptionTmpl{ + {"api", "used for changes that change the openapi specification", nil}, + {"cmd", "used for changes to the `%s` top level binaries", nil}, + {"web", "used for changes to the React based frontend", nil}, +} + +var commitTypes = []NameDescriptionTmpl{ + {"build", "Changes that affect the build system or external dependencies", []string{"bundler", "deps", "docker", "go", "npm"}}, + {"ci", "Changes to our CI configuration files and scripts", []string{"autheliabot", "buildkite", "codecov", "golangci-lint", "renovate", "reviewdog"}}, + {"docs", "Documentation only changes", nil}, + {"feat", "A new feature", nil}, + {"fix", "A bug fix", nil}, + {"i18n", "Updating translations or internationalization settings", nil}, + {"perf", "A code change that improves performance", nil}, + {"refactor", "A code change that neither fixes a bug nor adds a feature", nil}, + {"release", "Releasing a new version of Authelia", nil}, + {"test", "Adding missing tests or correcting existing tests", nil}, +} + +var commitTypesExtra = []string{"revert"} + +func getGoPackages(dir string) (pkgs []string, err error) { + var ( + entries []os.DirEntry + entriesSub []os.DirEntry + ) + + if entries, err = os.ReadDir(dir); err != nil { + return nil, fmt.Errorf("failed to detect go packages in directory '%s': %w", dir, err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + if entriesSub, err = os.ReadDir(filepath.Join(dir, entry.Name())); err != nil { + continue + } + + for _, entrySub := range entriesSub { + if entrySub.IsDir() { + continue + } + + if strings.HasSuffix(entrySub.Name(), ".go") { + pkgs = append(pkgs, entry.Name()) + break + } + } + } + + return pkgs, nil +} + +func commitLintRunE(cmd *cobra.Command, args []string) (err error) { + var root, pathCommitLintConfig, pathDocsCommitMessageGuidelines string + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathCommitLintConfig, err = cmd.Flags().GetString(cmdFlagFileConfigCommitLint); err != nil { + return err + } + + if pathDocsCommitMessageGuidelines, err = cmd.Flags().GetString(cmdFlagFileDocsCommitMsgGuidelines); err != nil { + return err + } + + data := &CommitMessageTmpl{ + Scopes: ScopesTmpl{ + All: []string{}, + Packages: []string{}, + Extra: []NameDescriptionTmpl{}, + }, + Types: TypesTmpl{ + List: []string{}, + Details: []NameDescriptionTmpl{}, + }, + } + + var ( + cmds []string + pkgs []string + ) + + if cmds, err = getGoPackages(filepath.Join(root, subPathCmd)); err != nil { + return err + } + + if pkgs, err = getGoPackages(filepath.Join(root, subPathInternal)); err != nil { + return err + } + + data.Scopes.All = append(data.Scopes.All, pkgs...) + data.Scopes.Packages = append(data.Scopes.Packages, pkgs...) + + for _, scope := range commitScopesExtra { + switch scope.Name { + case subPathCmd: + data.Scopes.Extra = append(data.Scopes.Extra, NameDescriptionTmpl{Name: scope.Name, Description: fmt.Sprintf(scope.Description, strings.Join(cmds, "|"))}) + default: + data.Scopes.Extra = append(data.Scopes.Extra, scope) + } + + data.Scopes.All = append(data.Scopes.All, scope.Name) + } + + for _, cType := range commitTypes { + data.Types.List = append(data.Types.List, cType.Name) + data.Types.Details = append(data.Types.Details, cType) + + data.Scopes.All = append(data.Scopes.All, cType.Scopes...) + } + + data.Types.List = append(data.Types.List, commitTypesExtra...) + + sort.Slice(data.Scopes.All, func(i, j int) bool { + return data.Scopes.All[i] < data.Scopes.All[j] + }) + + sort.Slice(data.Scopes.Packages, func(i, j int) bool { + return data.Scopes.Packages[i] < data.Scopes.Packages[j] + }) + + sort.Slice(data.Scopes.Extra, func(i, j int) bool { + return data.Scopes.Extra[i].Name < data.Scopes.Extra[j].Name + }) + + sort.Slice(data.Types.List, func(i, j int) bool { + return data.Types.List[i] < data.Types.List[j] + }) + + sort.Slice(data.Types.Details, func(i, j int) bool { + return data.Types.Details[i].Name < data.Types.Details[j].Name + }) + + var f *os.File + + fullPathCommitLintConfig := filepath.Join(root, pathCommitLintConfig) + + if f, err = os.Create(fullPathCommitLintConfig); err != nil { + return fmt.Errorf("failed to create output file '%s': %w", fullPathCommitLintConfig, err) + } + + if err = tmplDotCommitLintRC.Execute(f, data); err != nil { + return fmt.Errorf("failed to write output file '%s': %w", fullPathCommitLintConfig, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", fullPathCommitLintConfig, err) + } + + fullPathDocsCommitMessageGuidelines := filepath.Join(root, pathDocsCommitMessageGuidelines) + + if f, err = os.Create(fullPathDocsCommitMessageGuidelines); err != nil { + return fmt.Errorf("failed to create output file '%s': %w", fullPathDocsCommitMessageGuidelines, err) + } + + if err = tmplDocsCommitMessageGuidelines.Execute(f, data); err != nil { + return fmt.Errorf("failed to write output file '%s': %w", fullPathDocsCommitMessageGuidelines, err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close output file '%s': %w", fullPathDocsCommitMessageGuidelines, err) + } + + return nil +} diff --git a/cmd/authelia-gen/cmd_docs.go b/cmd/authelia-gen/cmd_docs.go index 8fa4b26b6..54d1135f1 100644 --- a/cmd/authelia-gen/cmd_docs.go +++ b/cmd/authelia-gen/cmd_docs.go @@ -6,30 +6,14 @@ import ( func newDocsCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "docs", + Use: cmdUseDocs, Short: "Generate docs", - RunE: docsRunE, + RunE: rootSubCommandsRunE, DisableAutoGenTag: true, } - cmd.PersistentFlags().StringP("cwd", "C", "", "Sets the CWD for git commands") - cmd.AddCommand(newDocsCLICmd(), newDocsDateCmd()) + cmd.AddCommand(newDocsCLICmd(), newDocsDataCmd(), newDocsDateCmd()) return cmd } - -func docsRunE(cmd *cobra.Command, args []string) (err error) { - for _, subCmd := range cmd.Commands() { - switch { - case subCmd.RunE != nil: - if err = subCmd.RunE(subCmd, args); err != nil { - return err - } - case subCmd.Run != nil: - subCmd.Run(subCmd, args) - } - } - - return nil -} diff --git a/cmd/authelia-gen/cmd_docs_cli.go b/cmd/authelia-gen/cmd_docs_cli.go index 5e0c01bb7..178583749 100644 --- a/cmd/authelia-gen/cmd_docs_cli.go +++ b/cmd/authelia-gen/cmd_docs_cli.go @@ -16,52 +16,50 @@ import ( func newDocsCLICmd() *cobra.Command { cmd := &cobra.Command{ - Use: "cli", + Use: cmdUseDocsCLI, Short: "Generate CLI docs", RunE: docsCLIRunE, DisableAutoGenTag: true, } - cmd.Flags().StringP("directory", "d", "./docs/content/en/reference/cli", "The directory to store the markdown in") - return cmd } func docsCLIRunE(cmd *cobra.Command, args []string) (err error) { - var root string + var outputPath string - if root, err = cmd.Flags().GetString("directory"); err != nil { + if outputPath, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDocs, cmdFlagDocsContent, cmdFlagDocsCLIReference); err != nil { return err } - if err = os.MkdirAll(root, 0775); err != nil { + if err = os.MkdirAll(outputPath, 0775); err != nil { if !os.IsExist(err) { return err } } - if err = genCLIDoc(commands.NewRootCmd(), filepath.Join(root, "authelia")); err != nil { + if err = genCLIDoc(commands.NewRootCmd(), filepath.Join(outputPath, "authelia")); err != nil { return err } - if err = genCLIDocWriteIndex(root, "authelia"); err != nil { + if err = genCLIDocWriteIndex(outputPath, "authelia"); err != nil { return err } - if err = genCLIDoc(cmdscripts.NewRootCmd(), filepath.Join(root, "authelia-scripts")); err != nil { + if err = genCLIDoc(cmdscripts.NewRootCmd(), filepath.Join(outputPath, "authelia-scripts")); err != nil { return err } - if err = genCLIDocWriteIndex(root, "authelia-scripts"); err != nil { + if err = genCLIDocWriteIndex(outputPath, "authelia-scripts"); err != nil { return err } - if err = genCLIDoc(newRootCmd(), filepath.Join(root, "authelia-gen")); err != nil { + if err = genCLIDoc(newRootCmd(), filepath.Join(outputPath, cmdUseRoot)); err != nil { return err } - if err = genCLIDocWriteIndex(root, "authelia-gen"); err != nil { + if err = genCLIDocWriteIndex(outputPath, cmdUseRoot); err != nil { return err } @@ -69,6 +67,16 @@ func docsCLIRunE(cmd *cobra.Command, args []string) (err error) { } func genCLIDoc(cmd *cobra.Command, path string) (err error) { + if _, err = os.Stat(path); err != nil && !os.IsNotExist(err) { + return err + } + + if err == nil || !os.IsNotExist(err) { + if err = os.RemoveAll(path); err != nil { + return fmt.Errorf("failed to remove docs: %w", err) + } + } + if err = os.Mkdir(path, 0755); err != nil { if !os.IsExist(err) { return err diff --git a/cmd/authelia-gen/cmd_docs_data.go b/cmd/authelia-gen/cmd_docs_data.go new file mode 100644 index 000000000..b0eddf21b --- /dev/null +++ b/cmd/authelia-gen/cmd_docs_data.go @@ -0,0 +1,132 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + + "github.com/spf13/cobra" + + "github.com/authelia/authelia/v4/internal/configuration" + "github.com/authelia/authelia/v4/internal/configuration/schema" +) + +func newDocsDataCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseDocsData, + Short: "Generate docs data files", + RunE: rootSubCommandsRunE, + + DisableAutoGenTag: true, + } + + cmd.AddCommand(newDocsDataMiscCmd(), newDocsDataKeysCmd()) + + return cmd +} + +func newDocsDataMiscCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseDocsDataMisc, + Short: "Generate docs data file misc.json", + RunE: docsDataMiscRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func docsDataMiscRunE(cmd *cobra.Command, args []string) (err error) { + data := DocsDataMisc{ + CSP: TemplateCSP{ + PlaceholderNONCE: codeCSPNonce, + TemplateDefault: buildCSP(codeCSPProductionDefaultSrc, codeCSPValuesCommon, codeCSPValuesProduction), + TemplateDevelopment: buildCSP(codeCSPDevelopmentDefaultSrc, codeCSPValuesCommon, codeCSPValuesDevelopment), + }, + } + + data.CSP.TemplateDefault = strings.ReplaceAll(data.CSP.TemplateDefault, "%s", codeCSPNonce) + data.CSP.TemplateDevelopment = strings.ReplaceAll(data.CSP.TemplateDevelopment, "%s", codeCSPNonce) + + var ( + outputPath string + dataJSON []byte + ) + + if outputPath, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDocs, cmdFlagDocsData, cmdFlagDocsDataMisc); err != nil { + return err + } + + if dataJSON, err = json.Marshal(data); err != nil { + return err + } + + if err = os.WriteFile(outputPath, dataJSON, 0600); err != nil { + return fmt.Errorf("failed to write file '%s': %w", outputPath, err) + } + + return nil +} + +func newDocsDataKeysCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseKeys, + Short: "Generate the docs data file for configuration keys", + RunE: docsKeysRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func docsKeysRunE(cmd *cobra.Command, args []string) (err error) { + //nolint:prealloc + var ( + data []ConfigurationKey + ) + + keys := readTags("", reflect.TypeOf(schema.Configuration{})) + + for _, key := range keys { + if strings.Contains(key, "[]") { + continue + } + + ck := ConfigurationKey{ + Path: key, + Secret: configuration.IsSecretKey(key), + } + + switch { + case ck.Secret: + ck.Env = configuration.ToEnvironmentSecretKey(key, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter) + default: + ck.Env = configuration.ToEnvironmentKey(key, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter) + } + + data = append(data, ck) + } + + var ( + dataJSON []byte + outputPath string + ) + + if outputPath, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDocs, cmdFlagDocsData, cmdFlagDocsDataKeys); err != nil { + return err + } + + if dataJSON, err = json.Marshal(data); err != nil { + return err + } + + if err = os.WriteFile(outputPath, dataJSON, 0600); err != nil { + return fmt.Errorf("failed to write file '%s': %w", outputPath, err) + } + + return nil +} diff --git a/cmd/authelia-gen/cmd_docs_date.go b/cmd/authelia-gen/cmd_docs_date.go index 7d9f83f85..9eea8ccad 100644 --- a/cmd/authelia-gen/cmd_docs_date.go +++ b/cmd/authelia-gen/cmd_docs_date.go @@ -17,14 +17,13 @@ import ( func newDocsDateCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "date", + Use: cmdUseDocsDate, Short: "Generate doc dates", RunE: docsDateRunE, DisableAutoGenTag: true, } - cmd.Flags().StringP("directory", "d", "./docs/content", "The directory to modify") cmd.Flags().String("commit-until", "HEAD", "The commit to check the logs until") cmd.Flags().String("commit-since", "", "The commit to check the logs since") @@ -33,14 +32,14 @@ func newDocsDateCmd() *cobra.Command { func docsDateRunE(cmd *cobra.Command, args []string) (err error) { var ( - dir, cwd, commitUtil, commitSince, commitFilter string + pathDocsContent, cwd, commitUtil, commitSince, commitFilter string ) - if dir, err = cmd.Flags().GetString("directory"); err != nil { + if pathDocsContent, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDocs, cmdFlagDocsContent); err != nil { return err } - if cwd, err = cmd.Flags().GetString("cwd"); err != nil { + if cwd, err = cmd.Flags().GetString(cmdFlagCwd); err != nil { return err } @@ -56,7 +55,7 @@ func docsDateRunE(cmd *cobra.Command, args []string) (err error) { commitFilter = fmt.Sprintf("%s...%s", commitUtil, commitSince) } - return filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { + return filepath.Walk(pathDocsContent, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } @@ -76,7 +75,7 @@ func docsDateRunE(cmd *cobra.Command, args []string) (err error) { return nil } - frontmatter := map[string]interface{}{} + frontmatter := map[string]any{} if err = yaml.Unmarshal(frontmatterBytes, frontmatter); err != nil { return err @@ -163,7 +162,7 @@ func replaceDates(path string, date time.Time, dateGit *time.Time) { for scanner.Scan() { if found < 2 && frontmatter < 2 { switch { - case scanner.Text() == frontmatterDelimiterLine: + case scanner.Text() == delimiterLineFrontMatter: buf.Write(scanner.Bytes()) frontmatter++ case frontmatter != 0 && strings.HasPrefix(scanner.Text(), "date: "): @@ -207,13 +206,13 @@ func getFrontmatter(path string) []byte { for scanner.Scan() { if start { - if scanner.Text() == frontmatterDelimiterLine { + if scanner.Text() == delimiterLineFrontMatter { break } buf.Write(scanner.Bytes()) buf.Write(newline) - } else if scanner.Text() == frontmatterDelimiterLine { + } else if scanner.Text() == delimiterLineFrontMatter { start = true } } diff --git a/cmd/authelia-gen/cmd_github.go b/cmd/authelia-gen/cmd_github.go new file mode 100644 index 000000000..99d80378a --- /dev/null +++ b/cmd/authelia-gen/cmd_github.go @@ -0,0 +1,236 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/spf13/cobra" +) + +func newGitHubCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseGitHub, + Short: "Generate GitHub files", + RunE: rootSubCommandsRunE, + + DisableAutoGenTag: true, + } + + cmd.AddCommand(newGitHubIssueTemplatesCmd()) + + return cmd +} + +func newGitHubIssueTemplatesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseGitHubIssueTemplates, + Short: "Generate GitHub issue templates", + RunE: rootSubCommandsRunE, + + DisableAutoGenTag: true, + } + + cmd.AddCommand(newGitHubIssueTemplatesBugReportCmd(), newGitHubIssueTemplatesFeatureCmd()) + + return cmd +} + +func newGitHubIssueTemplatesFeatureCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseGitHubIssueTemplatesFR, + Short: "Generate GitHub feature request issue template", + RunE: cmdGitHubIssueTemplatesFeatureRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func newGitHubIssueTemplatesBugReportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseGitHubIssueTemplatesBR, + Short: "Generate GitHub bug report issue template", + RunE: cmdGitHubIssueTemplatesBugReportRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func cmdGitHubIssueTemplatesFeatureRunE(cmd *cobra.Command, args []string) (err error) { + var ( + cwd, file, root string + tags, tagsFuture []string + latestMajor, latestMinor, latestPatch, versions int + ) + + if cwd, err = cmd.Flags().GetString(cmdFlagCwd); err != nil { + return err + } + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if file, err = cmd.Flags().GetString(cmdFlagFeatureRequest); err != nil { + return err + } + + if versions, err = cmd.Flags().GetInt(cmdFlagVersions); err != nil { + return err + } + + if tags, err = getGitTags(cwd); err != nil { + return err + } + + latest := tags[0] + + if _, err = fmt.Sscanf(latest, "v%d.%d.%d", &latestMajor, &latestMinor, &latestPatch); err != nil { + return fmt.Errorf("error occurred parsing version as semver: %w", err) + } + + var ( + minor int + ) + + for minor = latestMinor + 1; minor < latestMinor+versions; minor++ { + tagsFuture = append(tagsFuture, fmt.Sprintf("v%d.%d.0", latestMajor, minor)) + } + + tagsFuture = append(tagsFuture, fmt.Sprintf("v%d.0.0", latestMajor+1)) + + var ( + f *os.File + ) + + fullPath := filepath.Join(root, file) + + if f, err = os.Create(fullPath); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPath, err) + } + + data := &tmplIssueTemplateData{ + Labels: []string{labelTypeFeature.String(), labelStatusNeedsDesign.String(), labelPriorityNormal.String()}, + Versions: tagsFuture, + } + + if err = tmplIssueTemplateFeature.Execute(f, data); err != nil { + return err + } + + return nil +} + +func cmdGitHubIssueTemplatesBugReportRunE(cmd *cobra.Command, args []string) (err error) { + var ( + cwd, file, dirRoot string + latestMinor, versions int + + tags []string + ) + + if cwd, err = cmd.Flags().GetString(cmdFlagCwd); err != nil { + return err + } + + if dirRoot, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if file, err = cmd.Flags().GetString(cmdFlagBugReport); err != nil { + return err + } + + if versions, err = cmd.Flags().GetInt(cmdFlagVersions); err != nil { + return err + } + + if tags, err = getGitTags(cwd); err != nil { + return err + } + + latest := tags[0] + + latestParts := strings.Split(latest, ".") + + if len(latestParts) < 2 { + return fmt.Errorf("error extracting latest minor version from tag: %s does not appear to be a semver", latest) + } + + if latestMinor, err = strconv.Atoi(latestParts[1]); err != nil { + return fmt.Errorf("error extracting latest minor version from tag: %w", err) + } + + //nolint:prealloc + var ( + tagsRecent []string + parts []string + minor int + ) + + for _, tag := range tags { + if parts = strings.Split(tag, "."); len(parts) < 2 { + return fmt.Errorf("error extracting minor version from tag: %s does not appear to be a semver", tag) + } + + if minor, err = strconv.Atoi(parts[1]); err != nil { + return fmt.Errorf("error extracting minor version from tag: %w", err) + } + + if minor < latestMinor-versions { + break + } + + tagsRecent = append(tagsRecent, tag) + } + + var ( + f *os.File + ) + + fullPath := filepath.Join(dirRoot, file) + + if f, err = os.Create(fullPath); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPath, err) + } + + data := &tmplIssueTemplateData{ + Labels: []string{labelTypeBugUnconfirmed.String(), labelStatusNeedsTriage.String(), labelPriorityNormal.String()}, + Versions: tagsRecent, + Proxies: []string{"Caddy", "Traefik", "Envoy", "Istio", "NGINX", "SWAG", "NGINX Proxy Manager", "HAProxy"}, + } + + if err = tmplGitHubIssueTemplateBug.Execute(f, data); err != nil { + return err + } + + return nil +} + +func getGitTags(cwd string) (tags []string, err error) { + var ( + args []string + tagsOutput []byte + ) + + if len(cwd) != 0 { + args = append(args, "-C", cwd) + } + + args = append(args, "tag", "--sort=-creatordate") + + cmd := exec.Command("git", args...) + + if tagsOutput, err = cmd.Output(); err != nil { + return nil, err + } + + return strings.Split(string(tagsOutput), "\n"), nil +} diff --git a/cmd/authelia-gen/cmd_locales.go b/cmd/authelia-gen/cmd_locales.go new file mode 100644 index 000000000..0fcc390c3 --- /dev/null +++ b/cmd/authelia-gen/cmd_locales.go @@ -0,0 +1,215 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" + "golang.org/x/text/language" + "golang.org/x/text/language/display" + + "github.com/authelia/authelia/v4/internal/utils" +) + +func newLocalesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseLocales, + Short: "Generate locales files", + RunE: localesRunE, + + DisableAutoGenTag: true, + } + + return cmd +} + +func localesRunE(cmd *cobra.Command, args []string) (err error) { + var ( + root, pathLocales string + pathWebI18NIndex, pathDocsDataLanguages string + ) + + if root, err = cmd.Flags().GetString(cmdFlagRoot); err != nil { + return err + } + + if pathLocales, err = cmd.Flags().GetString(cmdFlagDirLocales); err != nil { + return err + } + + if pathWebI18NIndex, err = cmd.Flags().GetString(cmdFlagFileWebI18N); err != nil { + return err + } + + if pathDocsDataLanguages, err = getPFlagPath(cmd.Flags(), cmdFlagRoot, cmdFlagDocs, cmdFlagDocsData, cmdFlagDocsDataLanguages); err != nil { + return err + } + + data, err := getLanguages(filepath.Join(root, pathLocales)) + if err != nil { + return err + } + + fullPathWebI18NIndex := filepath.Join(root, pathWebI18NIndex) + + var ( + f *os.File + dataJSON []byte + ) + + if f, err = os.Create(fullPathWebI18NIndex); err != nil { + return fmt.Errorf("failed to create file '%s': %w", fullPathWebI18NIndex, err) + } + + if err = tmplWebI18NIndex.Execute(f, data); err != nil { + return err + } + + if dataJSON, err = json.Marshal(data); err != nil { + return err + } + + fullPathDocsDataLanguages := filepath.Join(root, pathDocsDataLanguages) + + if err = os.WriteFile(fullPathDocsDataLanguages, dataJSON, 0600); err != nil { + return fmt.Errorf("failed to write file '%s': %w", fullPathDocsDataLanguages, err) + } + + return nil +} + +//nolint:gocyclo +func getLanguages(dir string) (languages *Languages, err error) { + //nolint:prealloc + var locales []string + + languages = &Languages{ + Defaults: DefaultsLanguages{ + Namespace: localeNamespaceDefault, + }, + } + + var defaultTag language.Tag + + if defaultTag, err = language.Parse(localeDefault); err != nil { + return nil, fmt.Errorf("failed to parse default language: %w", err) + } + + languages.Defaults.Language = Language{ + Display: display.English.Tags().Name(defaultTag), + Locale: localeDefault, + } + + if err = filepath.Walk(dir, func(path string, info fs.FileInfo, errWalk error) (err error) { + if errWalk != nil { + return errWalk + } + + nameLower := strings.ToLower(info.Name()) + ext := filepath.Ext(nameLower) + ns := strings.Replace(nameLower, ext, "", 1) + + if ext != ".json" { + return nil + } + + if !utils.IsStringInSlice(ns, languages.Namespaces) { + languages.Namespaces = append(languages.Namespaces, ns) + } + + fdir, _ := filepath.Split(path) + + locale := filepath.Base(fdir) + + if utils.IsStringInSlice(locale, locales) { + for i, l := range languages.Languages { + if l.Locale == locale { + if utils.IsStringInSlice(ns, languages.Languages[i].Namespaces) { + break + } + + languages.Languages[i].Namespaces = append(languages.Languages[i].Namespaces, ns) + break + } + } + + return nil + } + + var localeReal string + + parts := strings.SplitN(locale, "-", 2) + if len(parts) == 2 && strings.EqualFold(parts[0], parts[1]) { + localeReal = parts[0] + } else { + localeReal = locale + } + + var tag language.Tag + + if tag, err = language.Parse(localeReal); err != nil { + return fmt.Errorf("failed to parse language '%s': %w", localeReal, err) + } + + l := Language{ + Display: display.English.Tags().Name(tag), + Locale: localeReal, + Namespaces: []string{ns}, + Fallbacks: []string{languages.Defaults.Language.Locale}, + Tag: tag, + } + + languages.Languages = append(languages.Languages, l) + + locales = append(locales, l.Locale) + + return nil + }); err != nil { + return nil, err + } + + var langs []Language //nolint:prealloc + + for i, lang := range languages.Languages { + p := lang.Tag.Parent() + + if p.String() == "und" || strings.Contains(p.String(), "-") { + continue + } + + if utils.IsStringInSlice(p.String(), locales) { + continue + } + + if p.String() != lang.Locale { + lang.Fallbacks = append([]string{p.String()}, lang.Fallbacks...) + } + + languages.Languages[i] = lang + + l := Language{ + Display: display.English.Tags().Name(p), + Locale: p.String(), + Namespaces: lang.Namespaces, + Fallbacks: []string{languages.Defaults.Language.Locale}, + Tag: p, + } + + langs = append(langs, l) + + locales = append(locales, l.Locale) + } + + languages.Languages = append(languages.Languages, langs...) + + sort.Slice(languages.Languages, func(i, j int) bool { + return languages.Languages[i].Locale == localeDefault || languages.Languages[i].Locale < languages.Languages[j].Locale + }) + + return languages, nil +} diff --git a/cmd/authelia-gen/cmd_root.go b/cmd/authelia-gen/cmd_root.go new file mode 100644 index 000000000..d0809957c --- /dev/null +++ b/cmd/authelia-gen/cmd_root.go @@ -0,0 +1,128 @@ +package main + +import ( + "sort" + "strings" + + "github.com/spf13/cobra" + + "github.com/authelia/authelia/v4/internal/utils" +) + +var rootCmd *cobra.Command + +func init() { + rootCmd = newRootCmd() +} + +func newRootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: cmdUseRoot, + Short: "Authelia's generator tooling", + RunE: rootSubCommandsRunE, + + DisableAutoGenTag: true, + } + + cmd.PersistentFlags().StringP(cmdFlagCwd, "C", "", "Sets the CWD for git commands") + cmd.PersistentFlags().StringP(cmdFlagRoot, "d", dirCurrent, "The repository root") + cmd.PersistentFlags().StringSliceP(cmdFlagExclude, "X", nil, "Sets the names of excluded generators") + cmd.PersistentFlags().String(cmdFlagFeatureRequest, fileGitHubIssueTemplateFR, "Sets the path of the feature request issue template file") + cmd.PersistentFlags().String(cmdFlagBugReport, fileGitHubIssueTemplateBR, "Sets the path of the bug report issue template file") + cmd.PersistentFlags().Int(cmdFlagVersions, 5, "the maximum number of minor versions to list in output templates") + cmd.PersistentFlags().String(cmdFlagDirLocales, dirLocales, "The locales directory in relation to the root") + cmd.PersistentFlags().String(cmdFlagFileWebI18N, fileWebI18NIndex, "The i18n typescript configuration file in relation to the root") + cmd.PersistentFlags().String(cmdFlagDocsDataLanguages, fileDocsDataLanguages, "The languages docs data file in relation to the docs data folder") + cmd.PersistentFlags().String(cmdFlagDocsDataMisc, fileDocsDataMisc, "The misc docs data file in relation to the docs data folder") + cmd.PersistentFlags().String(cmdFlagDocsCLIReference, dirDocsCLIReference, "The directory to store the markdown in") + cmd.PersistentFlags().String(cmdFlagDocs, dirDocs, "The directory with the docs") + cmd.PersistentFlags().String(cmdFlagDocsContent, dirDocsContent, "The directory with the docs content") + cmd.PersistentFlags().String(cmdFlagDocsData, dirDocsData, "The directory with the docs data") + cmd.PersistentFlags().String(cmdFlagFileConfigKeys, fileCodeConfigKeys, "Sets the path of the keys file") + cmd.PersistentFlags().String(cmdFlagDocsDataKeys, fileDocsDataConfigKeys, "Sets the path of the docs keys file") + cmd.PersistentFlags().String(cmdFlagPackageConfigKeys, pkgConfigSchema, "Sets the package name of the keys file") + cmd.PersistentFlags().String(cmdFlagFileScriptsGen, fileScriptsGen, "Sets the path of the authelia-scripts gen file") + cmd.PersistentFlags().String(cmdFlagFileServerGenerated, fileServerGenerated, "Sets the path of the server generated file") + cmd.PersistentFlags().String(cmdFlagPackageScriptsGen, pkgScriptsGen, "Sets the package name of the authelia-scripts gen file") + cmd.PersistentFlags().String(cmdFlagFileConfigCommitLint, fileCICommitLintConfig, "The commit lint javascript configuration file in relation to the root") + cmd.PersistentFlags().String(cmdFlagFileDocsCommitMsgGuidelines, fileDocsCommitMessageGuidelines, "The commit message guidelines documentation file in relation to the root") + + cmd.AddCommand(newCodeCmd(), newDocsCmd(), newGitHubCmd(), newLocalesCmd(), newCommitLintCmd()) + + return cmd +} + +func rootSubCommandsRunE(cmd *cobra.Command, args []string) (err error) { + var exclude []string + + if exclude, err = cmd.Flags().GetStringSlice(cmdFlagExclude); err != nil { + return err + } + + subCmds := cmd.Commands() + + switch cmd.Use { + case cmdUseRoot: + sort.Slice(subCmds, func(i, j int) bool { + switch subCmds[j].Use { + case cmdUseDocs: + // Ensure `docs` subCmd is last. + return true + default: + return subCmds[i].Use < subCmds[j].Use + } + }) + case cmdUseDocs: + sort.Slice(subCmds, func(i, j int) bool { + switch subCmds[j].Use { + case cmdUseDocsDate: + // Ensure `date` subCmd is last. + return true + default: + return subCmds[i].Use < subCmds[j].Use + } + }) + default: + sort.Slice(subCmds, func(i, j int) bool { + return subCmds[i].Use < subCmds[j].Use + }) + } + + for _, subCmd := range subCmds { + if subCmd.Use == cmdUseCompletion || strings.HasPrefix(subCmd.Use, "help ") || utils.IsStringSliceContainsAny([]string{resolveCmdName(subCmd), subCmd.Use}, exclude) { + continue + } + + rootCmd.SetArgs(rootCmdGetArgs(subCmd, args)) + + if err = rootCmd.Execute(); err != nil { + return err + } + } + + return nil +} + +func resolveCmdName(cmd *cobra.Command) string { + parent := cmd.Parent() + + if parent != nil && parent.Use != cmd.Use && parent.Use != cmdUseRoot { + return resolveCmdName(parent) + "." + cmd.Use + } + + return cmd.Use +} + +func rootCmdGetArgs(cmd *cobra.Command, args []string) []string { + for { + if cmd == rootCmd { + break + } + + args = append([]string{cmd.Use}, args...) + + cmd = cmd.Parent() + } + + return args +} diff --git a/cmd/authelia-gen/const.go b/cmd/authelia-gen/const.go index 71f3ada18..f68364e9e 100644 --- a/cmd/authelia-gen/const.go +++ b/cmd/authelia-gen/const.go @@ -1,7 +1,113 @@ package main const ( - dateFmtRFC2822 = "Mon, _2 Jan 2006 15:04:05 -0700" - dateFmtYAML = "2006-01-02T15:04:05-07:00" - frontmatterDelimiterLine = "---" + dirCurrent = "./" + dirLocales = "internal/server/locales" + + subPathCmd = "cmd" + subPathInternal = "internal" + + fileCICommitLintConfig = "web/.commitlintrc.js" + fileWebI18NIndex = "web/src/i18n/index.ts" + + fileDocsCommitMessageGuidelines = "docs/content/en/contributing/guidelines/commit-message.md" + + fileCodeConfigKeys = "internal/configuration/schema/keys.go" + fileServerGenerated = "internal/server/gen.go" + fileScriptsGen = "cmd/authelia-scripts/cmd/gen.go" + + dirDocs = "docs" + dirDocsContent = "content" + dirDocsData = "data" + dirDocsCLIReference = "en/reference/cli" + + fileDocsDataLanguages = "languages.json" + fileDocsDataMisc = "misc.json" + fileDocsDataConfigKeys = "configkeys.json" + + fileGitHubIssueTemplateFR = ".github/ISSUE_TEMPLATE/feature-request.yml" + fileGitHubIssueTemplateBR = ".github/ISSUE_TEMPLATE/bug-report.yml" +) + +const ( + dateFmtRFC2822 = "Mon, _2 Jan 2006 15:04:05 -0700" + dateFmtYAML = "2006-01-02T15:04:05-07:00" +) + +const ( + delimiterLineFrontMatter = "---" + + localeDefault = "en" + localeNamespaceDefault = "portal" +) + +const ( + pkgConfigSchema = "schema" + pkgScriptsGen = "cmd" +) + +const ( + cmdUseRoot = "authelia-gen" + cmdUseCompletion = "completion" + cmdUseDocs = "docs" + cmdUseDocsDate = "date" + cmdUseDocsCLI = "cli" + cmdUseDocsData = "data" + cmdUseDocsDataMisc = "misc" + cmdUseGitHub = "github" + cmdUseGitHubIssueTemplates = "issue-templates" + cmdUseGitHubIssueTemplatesFR = "feature-request" + cmdUseGitHubIssueTemplatesBR = "bug-report" + cmdUseLocales = "locales" + cmdUseCommitLint = "commit-lint" + cmdUseCode = "code" + cmdUseCodeScripts = "scripts" + cmdUseKeys = "keys" + cmdUseServer = "server" +) + +const ( + cmdFlagRoot = "dir.root" + cmdFlagExclude = "exclude" + cmdFlagVersions = "versions" + cmdFlagDirLocales = "dir.locales" + cmdFlagDocsCLIReference = "dir.docs.cli-reference" + cmdFlagDocsContent = "dir.docs.content" + cmdFlagDocsData = "dir.docs.data" + cmdFlagDocs = "dir.docs" + cmdFlagDocsDataLanguages = "file.docs.data.languages" + cmdFlagDocsDataMisc = "file.docs.data.misc" + cmdFlagDocsDataKeys = "file.docs.data.keys" + cmdFlagCwd = "cwd" + cmdFlagFileConfigKeys = "file.configuration-keys" + cmdFlagFileScriptsGen = "file.scripts.gen" + cmdFlagFileServerGenerated = "file.server.generated" + cmdFlagFileConfigCommitLint = "file.commit-lint-config" + cmdFlagFileDocsCommitMsgGuidelines = "file.docs-commit-msg-guidelines" + cmdFlagFileWebI18N = "file.web-i18n" + cmdFlagFeatureRequest = "file.feature-request" + cmdFlagBugReport = "file.bug-report" + cmdFlagPackageConfigKeys = "package.configuration.keys" + cmdFlagPackageScriptsGen = "package.scripts.gen" +) + +const ( + codeCSPProductionDefaultSrc = "'self'" + codeCSPDevelopmentDefaultSrc = "'self' 'unsafe-eval'" + codeCSPNonce = "${NONCE}" +) + +var ( + codeCSPValuesCommon = []CSPValue{ + {Name: "default-src", Value: ""}, + {Name: "frame-src", Value: "'none'"}, + {Name: "object-src", Value: "'none'"}, + {Name: "style-src", Value: "'self' 'nonce-%s'"}, + {Name: "frame-ancestors", Value: "'none'"}, + {Name: "base-uri", Value: "'self'"}, + } + + codeCSPValuesProduction = []CSPValue{} + + codeCSPValuesDevelopment = []CSPValue{} ) diff --git a/cmd/authelia-gen/helpers.go b/cmd/authelia-gen/helpers.go new file mode 100644 index 000000000..09ac84e8f --- /dev/null +++ b/cmd/authelia-gen/helpers.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/spf13/pflag" +) + +func getPFlagPath(flags *pflag.FlagSet, flagNames ...string) (fullPath string, err error) { + if len(flagNames) == 0 { + return "", fmt.Errorf("no flag names") + } + + var p string + + for i, flagName := range flagNames { + if p, err = flags.GetString(flagName); err != nil { + return "", fmt.Errorf("failed to lookup flag '%s': %w", flagName, err) + } + + if i == 0 { + fullPath = p + } else { + fullPath = filepath.Join(fullPath, p) + } + } + + return fullPath, nil +} + +func buildCSP(defaultSrc string, ruleSets ...[]CSPValue) string { + var rules []string + + for _, ruleSet := range ruleSets { + for _, rule := range ruleSet { + switch rule.Name { + case "default-src": + rules = append(rules, fmt.Sprintf("%s %s", rule.Name, defaultSrc)) + default: + rules = append(rules, fmt.Sprintf("%s %s", rule.Name, rule.Value)) + } + } + } + + return strings.Join(rules, "; ") +} diff --git a/cmd/authelia-gen/main.go b/cmd/authelia-gen/main.go index 15a8d4586..ffbdca170 100644 --- a/cmd/authelia-gen/main.go +++ b/cmd/authelia-gen/main.go @@ -1,29 +1,7 @@ package main -import ( - "embed" - - "github.com/spf13/cobra" -) - -//go:embed templates/* -var templatesFS embed.FS - func main() { - if err := newRootCmd().Execute(); err != nil { + if err := rootCmd.Execute(); err != nil { panic(err) } } - -func newRootCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "authelia-gen", - Short: "Authelia's generator tooling", - - DisableAutoGenTag: true, - } - - cmd.AddCommand(newAllCmd(), newCodeCmd(), newDocsCmd()) - - return cmd -} diff --git a/cmd/authelia-gen/templates.go b/cmd/authelia-gen/templates.go new file mode 100644 index 000000000..794add4b3 --- /dev/null +++ b/cmd/authelia-gen/templates.go @@ -0,0 +1,70 @@ +package main + +import ( + "embed" + "fmt" + "strings" + "text/template" +) + +//go:embed templates/* +var templatesFS embed.FS + +var ( + funcMap = template.FuncMap{ + "stringsContains": strings.Contains, + "join": strings.Join, + "joinX": fmJoinX, + } + + tmplCodeConfigurationSchemaKeys = template.Must(newTMPL("internal_configuration_schema_keys.go")) + tmplGitHubIssueTemplateBug = template.Must(newTMPL("github_issue_template_bug_report.yml")) + tmplIssueTemplateFeature = template.Must(newTMPL("github_issue_template_feature.yml")) + tmplWebI18NIndex = template.Must(newTMPL("web_i18n_index.ts")) + tmplDotCommitLintRC = template.Must(newTMPL("dot_commitlintrc.js")) + tmplDocsCommitMessageGuidelines = template.Must(newTMPL("docs-contributing-development-commitmsg.md")) + tmplScriptsGen = template.Must(newTMPL("cmd-authelia-scripts-gen.go")) + tmplServer = template.Must(newTMPL("server_gen.go")) +) + +func fmJoinX(elems []string, sep string, n int, p string) string { + buf := strings.Builder{} + + c := 0 + e := len(elems) - 1 + + for i := 0; i <= e; i++ { + if c+len(elems[i])+1 > n { + c = 0 + + buf.WriteString(p) + } + + c += len(elems[i]) + 1 + + buf.WriteString(elems[i]) + + if i < e { + buf.WriteString(sep) + } + } + + return buf.String() +} + +func newTMPL(name string) (tmpl *template.Template, err error) { + return template.New(name).Funcs(funcMap).Parse(mustLoadTmplFS(name)) +} + +func mustLoadTmplFS(tmpl string) string { + var ( + content []byte + err error + ) + + if content, err = templatesFS.ReadFile(fmt.Sprintf("templates/%s.tmpl", tmpl)); err != nil { + panic(err) + } + + return string(content) +} diff --git a/cmd/authelia-gen/templates/cmd-authelia-scripts-gen.go.tmpl b/cmd/authelia-gen/templates/cmd-authelia-scripts-gen.go.tmpl new file mode 100644 index 000000000..5955d5f7d --- /dev/null +++ b/cmd/authelia-gen/templates/cmd-authelia-scripts-gen.go.tmpl @@ -0,0 +1,11 @@ +// Code generated by go generate. DO NOT EDIT. +// +// Run the following command to generate this file: +// go run ./cmd/authelia-gen code scripts +// + +package {{ .Package }} + +const ( + versionSwaggerUI = "{{ .VersionSwaggerUI }}" +) diff --git a/cmd/authelia-gen/templates/docs-contributing-development-commitmsg.md.tmpl b/cmd/authelia-gen/templates/docs-contributing-development-commitmsg.md.tmpl new file mode 100644 index 000000000..28f51882c --- /dev/null +++ b/cmd/authelia-gen/templates/docs-contributing-development-commitmsg.md.tmpl @@ -0,0 +1,139 @@ +--- +title: "Commit Message" +description: "Authelia Development Commit Message Guidelines" +lead: "This section covers the git commit message guidelines we use for development." +date: 2021-01-30T19:29:07+11:00 +draft: false +images: [] +menu: + contributing: + parent: "guidelines" +weight: 320 +toc: true +aliases: + - /docs/contributing/commitmsg-guidelines.html + - /contributing/development/guidelines-commit-message/ +--- + +The reasons for these conventions are as follows: + +* simple navigation though git history +* easier to read git history + +## Commit Message Format + +Each commit message consists of a __header__, a __body__, and a __footer__. + +```bash +
+ + + +