mirror of
https://github.com/greenpau/caddy-security.git
synced 2025-04-18 08:04:02 +03:00
add code
This commit is contained in:
parent
bed2af02ab
commit
fd5c591066
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [greenpau]
|
12
.github/ISSUE_TEMPLATE/ask-question.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/ask-question.md
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Ask a question!
|
||||
about: There are no stupid questions! It is this project's documentation that needs improvement. Show ❤️️, give 🌟
|
||||
title: 'question: CHANGE_ME'
|
||||
labels: 'need triage'
|
||||
assignees: 'greenpau'
|
||||
|
||||
---
|
||||
|
||||
> A clear and concise description of what you want to accomplish.
|
||||
|
||||
CHANGE_ME
|
36
.github/ISSUE_TEMPLATE/break-fix.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/break-fix.md
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
name: Things are not working!
|
||||
about: You think you are doing the right thing, but it is not working as expected.
|
||||
title: 'breakfix: CHANGE_ME'
|
||||
labels: 'need triage'
|
||||
assignees: 'greenpau'
|
||||
|
||||
---
|
||||
|
||||
**Describe the issue**
|
||||
|
||||
A clear and concise description of what the issue is.
|
||||
|
||||
**Configuration**
|
||||
|
||||
Paste full `Caddyfile` below:
|
||||
|
||||
```
|
||||
Paste configuration here ...
|
||||
```
|
||||
|
||||
**Version Information**
|
||||
|
||||
Provide output of `caddy list-modules -versions | grep git` below:
|
||||
|
||||
```
|
||||
Paste output here ...
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
Describe expected behavior.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
24
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: You understand that some functionality/feature is not available and you want it added.
|
||||
title: 'feature: CHANGE_ME'
|
||||
labels: 'need triage'
|
||||
assignees: 'greenpau'
|
||||
|
||||
---
|
||||
|
||||
> A clear and concise description of what you want the system to do.
|
||||
|
||||
CHANGE_ME
|
||||
|
||||
> What are the Caddyfile directives that need to be added.
|
||||
|
||||
Add Caddyfile directive:
|
||||
|
||||
```
|
||||
git {
|
||||
repo foo {
|
||||
<directive> <args>
|
||||
}
|
||||
}
|
||||
```
|
71
.github/workflows/main.yml
vendored
Normal file
71
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
---
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
core:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
platform: [ubuntu-latest]
|
||||
name: Build
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
GOBIN: /home/runner/.local/bin
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
- name: Amend Environment Path
|
||||
run: |
|
||||
mkdir -p /home/runner/.local/bin
|
||||
echo "/home/runner/.local/bin" >> $GITHUB_PATH
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
mkdir -p .coverage
|
||||
echo "*** Current Directory ***"
|
||||
pwd
|
||||
echo "*** Environment Variables ***"
|
||||
env | sort
|
||||
echo "*** Executable Path ***"
|
||||
echo "$PATH" | tr ':' '\n'
|
||||
echo "*** Workspace Files ***"
|
||||
find .
|
||||
which make
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
sudo apt-get --assume-yes install make
|
||||
sudo apt-get --assume-yes install libnss3-tools
|
||||
sudo apt-get update
|
||||
- name: Install Go modules
|
||||
run: |
|
||||
make dep
|
||||
go mod tidy
|
||||
go mod verify
|
||||
go mod download
|
||||
- name: Validate prerequisites
|
||||
run: |
|
||||
echo "*** Local binaries ***"
|
||||
find /home/runner/.local/bin
|
||||
- name: Run tests
|
||||
run: |
|
||||
make test || true
|
||||
make test
|
||||
- name: Generate coverage report
|
||||
run: make coverage
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: Test Coverage Report
|
||||
path: .coverage/coverage.html
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
assets/config/users.json
|
||||
vendor/**
|
||||
bin/**
|
||||
tmp/**
|
||||
.coverage/**
|
||||
.doc/**
|
||||
*TODO*
|
||||
123*
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
111
Makefile
Normal file
111
Makefile
Normal file
@ -0,0 +1,111 @@
|
||||
.PHONY: test ctest covdir bindir coverage docs linter qtest clean dep release logo license
|
||||
PLUGIN_NAME="caddy-security"
|
||||
PLUGIN_VERSION:=$(shell cat VERSION | head -1)
|
||||
GIT_COMMIT:=$(shell git describe --dirty --always)
|
||||
GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD -- | head -1)
|
||||
LATEST_GIT_COMMIT:=$(shell git log --format="%H" -n 1 | head -1)
|
||||
BUILD_USER:=$(shell whoami)
|
||||
BUILD_DATE:=$(shell date +"%Y-%m-%d")
|
||||
BUILD_DIR:=$(shell pwd)
|
||||
CADDY_VERSION="v2.4.6"
|
||||
|
||||
all: info
|
||||
@mkdir -p bin/
|
||||
@rm -rf ./bin/caddy
|
||||
@rm -rf ../xcaddy-$(PLUGIN_NAME)/*
|
||||
@mkdir -p ../xcaddy-$(PLUGIN_NAME) && cd ../xcaddy-$(PLUGIN_NAME) && \
|
||||
xcaddy build $(CADDY_VERSION) --output ../$(PLUGIN_NAME)/bin/caddy \
|
||||
--with github.com/greenpau/caddy-security@$(LATEST_GIT_COMMIT)=$(BUILD_DIR)
|
||||
@#--with github.com/greenpau/aaasf@v1.0.0=/home/greenpau/dev/go/src/github.com/greenpau/aaasf
|
||||
@#bin/caddy run -config assets/config/Caddyfile
|
||||
@for f in `find ./assets -type f -name 'Caddyfile'`; do bin/caddy fmt -overwrite $$f; done
|
||||
|
||||
info:
|
||||
@echo "DEBUG: Version: $(PLUGIN_VERSION), Branch: $(GIT_BRANCH), Revision: $(GIT_COMMIT)"
|
||||
@echo "DEBUG: Build on $(BUILD_DATE) by $(BUILD_USER)"
|
||||
|
||||
linter:
|
||||
@echo "DEBUG: running lint checks"
|
||||
@#golint -set_exit_status ./...
|
||||
@echo "DEBUG: completed $@"
|
||||
|
||||
test: covdir linter
|
||||
@echo "DEBUG: running tests"
|
||||
@go test -v -coverprofile=.coverage/coverage.out ./...
|
||||
@echo "DEBUG: completed $@"
|
||||
|
||||
ctest: covdir linter
|
||||
@echo "DEBUG: running tests"
|
||||
@#time richgo test -v -coverprofile=.coverage/coverage.out ./...
|
||||
@time richgo test -v -coverprofile=.coverage/coverage.out ./*.go
|
||||
@echo "DEBUG: completed $@"
|
||||
|
||||
covdir:
|
||||
@echo "DEBUG: creating .coverage/ directory"
|
||||
@mkdir -p .coverage
|
||||
@echo "DEBUG: completed $@"
|
||||
|
||||
bindir:
|
||||
@echo "DEBUG: creating bin/ directory"
|
||||
@mkdir -p bin/
|
||||
@echo "DEBUG: completed $@"
|
||||
|
||||
coverage: covdir
|
||||
@echo "DEBUG: running coverage"
|
||||
@go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html
|
||||
@go test -covermode=count -coverprofile=.coverage/coverage.out ./...
|
||||
@go tool cover -func=.coverage/coverage.out | grep -v "100.0"
|
||||
@echo "DEBUG: completed $@"
|
||||
|
||||
clean:
|
||||
@rm -rf .coverage/
|
||||
@rm -rf bin/
|
||||
@echo "DEBUG: completed $@"
|
||||
|
||||
qtest: covdir
|
||||
@echo "DEBUG: perform quick tests ..."
|
||||
@time richgo test -v -coverprofile=.coverage/coverage.out -run TestApp ./*.go
|
||||
@time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileAppConfig ./*.go
|
||||
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileCredentials ./*.go
|
||||
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileAuthentication ./*.go
|
||||
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileAuthorization ./*.go
|
||||
@#go test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfile ./*.go
|
||||
@#go test -v -coverprofile=.coverage/coverage.out -run Test* ./pkg/services/...
|
||||
@go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html
|
||||
@go tool cover -func=.coverage/coverage.out | grep -v "100.0"
|
||||
@echo "DEBUG: completed $@"
|
||||
|
||||
dep:
|
||||
@echo "Making dependencies check ..."
|
||||
@go get -u golang.org/x/lint/golint
|
||||
@go get -u github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||
@go get -u github.com/greenpau/versioned/cmd/versioned@latest
|
||||
@go get -u github.com/kyoh86/richgo
|
||||
|
||||
release:
|
||||
@echo "Making release"
|
||||
@go mod tidy
|
||||
@go mod verify
|
||||
@if [ $(GIT_BRANCH) != "main" ]; then echo "cannot release to non-main branch $(GIT_BRANCH)" && false; fi
|
||||
@git diff-index --quiet HEAD -- || ( echo "git directory is dirty, commit changes first" && false )
|
||||
@versioned -patch
|
||||
@echo "Patched version"
|
||||
@git add VERSION
|
||||
@git commit -m "released v`cat VERSION | head -1`"
|
||||
@git tag -a v`cat VERSION | head -1` -m "v`cat VERSION | head -1`"
|
||||
@git push
|
||||
@git push --tags
|
||||
@@echo "If necessary, run the following commands:"
|
||||
@echo " git push --delete origin v$(PLUGIN_VERSION)"
|
||||
@echo " git tag --delete v$(PLUGIN_VERSION)"
|
||||
|
||||
logo:
|
||||
@mkdir -p assets/docs/images
|
||||
@gm convert -background black -font Bookman-Demi \
|
||||
-size 640x320 "xc:black" \
|
||||
-pointsize 72 \
|
||||
-draw "fill white gravity center text 0,0 'caddy\nsecurity'" \
|
||||
assets/docs/images/logo.png
|
||||
|
||||
license:
|
||||
@for f in `find ./ -type f -name '*.go'`; do versioned -addlicense -copyright="Paul Greenberg greenpau@outlook.com" -year=2022 -filepath=$$f; done
|
12
OWNERS
Normal file
12
OWNERS
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
reviewers:
|
||||
- greenpau
|
||||
|
||||
approvers:
|
||||
- greenpau
|
||||
|
||||
features:
|
||||
- comments
|
||||
- reviewers
|
||||
- aliases
|
||||
- branches
|
15
README.md
15
README.md
@ -3,13 +3,14 @@
|
||||
<a href="https://github.com/greenpau/caddy-security/actions/" target="_blank"><img src="https://github.com/greenpau/caddy-security/workflows/build/badge.svg?branch=main"></a>
|
||||
<a href="https://pkg.go.dev/github.com/greenpau/caddy-security" target="_blank"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
|
||||
<a href="https://caddy.community" target="_blank"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg"></a>
|
||||
<a href="https://caddyserver.com/docs/modules/git" target="_blank"><img src="https://img.shields.io/badge/caddydocs-git-green.svg"></a>
|
||||
<a href="https://caddyserver.com/docs/modules/security" target="_blank"><img src="https://img.shields.io/badge/caddydocs-security-green.svg"></a>
|
||||
|
||||
Security App and Plugin for [Caddy v2](https://github.com/caddyserver/caddy).
|
||||
|
||||
Please see other plugins:
|
||||
* [caddy-trace](https://github.com/greenpau/caddy-trace)
|
||||
* [caddy-systemd](https://github.com/greenpau/caddy-systemd)
|
||||
* [caddy-git](https://github.com/greenpau/caddy-git)
|
||||
|
||||
<!-- begin-markdown-toc -->
|
||||
## Table of Contents
|
||||
@ -26,12 +27,15 @@ authorization security policy and credentials. The **plugin**
|
||||
enforces the security policy on endpoints with `authorize` keyword
|
||||
and serves authentication portal with `authenticate` keyword.
|
||||
|
||||
The app and plugin use Authentication, Authorization, and
|
||||
Accounting (AAA) Security Functions (SF) from
|
||||
[github.com/greenpau/aaasf](https://github.com/greenpau/aaasf).
|
||||
|
||||
## Getting Started
|
||||
|
||||
The configuration happens in Caddy's
|
||||
[**global options block**](https://caddyserver.com/docs/caddyfile/options).
|
||||
|
||||
|
||||
### Credentials
|
||||
|
||||
The following configuration adds SMTP credentials to security app.
|
||||
@ -97,10 +101,9 @@ The following configuration adds authorization functionality and handlers.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
www.myfiosgateway.com {
|
||||
authorize with mypolicy
|
||||
root * {env.HOME}/public_html
|
||||
file_server
|
||||
authorize with mypolicy
|
||||
root * {env.HOME}/public_html
|
||||
file_server
|
||||
}
|
||||
```
|
||||
|
160
app.go
Normal file
160
app.go
Normal file
@ -0,0 +1,160 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/greenpau/aaasf"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
"github.com/greenpau/caddy-security/pkg/authentication"
|
||||
"github.com/greenpau/caddy-security/pkg/authorization"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
appName = "security"
|
||||
|
||||
// Interface guards
|
||||
_ caddy.Provisioner = (*App)(nil)
|
||||
_ caddy.Module = (*App)(nil)
|
||||
_ caddy.App = (*App)(nil)
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(App{})
|
||||
caddy.RegisterModule(authentication.Middleware{})
|
||||
caddy.RegisterModule(authorization.Middleware{})
|
||||
}
|
||||
|
||||
// App implements security manager.
|
||||
type App struct {
|
||||
Name string `json:"-"`
|
||||
Config *aaasf.Config `json:"config,omitempty"`
|
||||
// server *aaasf.Server
|
||||
logger *zap.Logger
|
||||
portals []*authn.Portal
|
||||
gatekeepers []*authz.Gatekeeper
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (App) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: caddy.ModuleID(appName),
|
||||
New: func() caddy.Module { return new(App) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the repo manager.
|
||||
func (app *App) Provision(ctx caddy.Context) error {
|
||||
app.Name = appName
|
||||
app.logger = ctx.Logger(app)
|
||||
|
||||
app.logger.Info(
|
||||
"provisioning app instance",
|
||||
zap.String("app", app.Name),
|
||||
)
|
||||
|
||||
for _, cfg := range app.Config.Portals {
|
||||
portal, err := authn.NewPortal(cfg, app.logger)
|
||||
if err != nil {
|
||||
app.logger.Error(
|
||||
"failed provisioning app instance",
|
||||
zap.String("app", app.Name),
|
||||
zap.String("portal_name", cfg.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
app.portals = append(app.portals, portal)
|
||||
}
|
||||
|
||||
for _, cfg := range app.Config.Policies {
|
||||
gatekeeper, err := authz.NewGatekeeper(cfg, app.logger)
|
||||
if err != nil {
|
||||
app.logger.Error(
|
||||
"failed provisioning app instance",
|
||||
zap.String("app", app.Name),
|
||||
zap.String("gatekeeper_name", cfg.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
app.gatekeepers = append(app.gatekeepers, gatekeeper)
|
||||
}
|
||||
|
||||
app.logger.Info(
|
||||
"provisioned app instance",
|
||||
zap.String("app", app.Name),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the App.
|
||||
func (app App) Start() error {
|
||||
app.logger.Debug(
|
||||
"starting app instance",
|
||||
zap.String("app", app.Name),
|
||||
)
|
||||
|
||||
/*
|
||||
if msgs := app.manager.Start(); msgs != nil {
|
||||
for _, msg := range msgs {
|
||||
app.logger.Error(
|
||||
"failed managing git repo",
|
||||
zap.String("app", app.Name),
|
||||
zap.String("repo", msg.Repository),
|
||||
zap.Error(msg.Error),
|
||||
)
|
||||
}
|
||||
return fmt.Errorf("git repo manager failed to start")
|
||||
}
|
||||
*/
|
||||
|
||||
app.logger.Debug(
|
||||
"started app instance",
|
||||
zap.String("app", app.Name),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the App.
|
||||
func (app App) Stop() error {
|
||||
app.logger.Debug(
|
||||
"stopping app instance",
|
||||
zap.String("app", app.Name),
|
||||
)
|
||||
|
||||
/*
|
||||
if msgs := app.manager.Stop(); msgs != nil {
|
||||
for _, msg := range msgs {
|
||||
app.logger.Error(
|
||||
"failed stoppint git repo manager",
|
||||
zap.String("app", app.Name),
|
||||
zap.String("repo", msg.Repository),
|
||||
zap.Error(msg.Error),
|
||||
)
|
||||
}
|
||||
return fmt.Errorf("git repo manager failed to stop properly")
|
||||
}
|
||||
*/
|
||||
|
||||
app.logger.Debug(
|
||||
"stopped app instance",
|
||||
zap.String("app", app.Name),
|
||||
)
|
||||
return nil
|
||||
}
|
62
app_test.go
Normal file
62
app_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme string = "https"
|
||||
host string = "127.0.0.1"
|
||||
securePort string = "8443"
|
||||
)
|
||||
|
||||
func initCaddyTester(t *testing.T, configFile string) (*caddytest.Tester, map[string]string, error) {
|
||||
hostPort := fmt.Sprintf("%s:%s", host, securePort)
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, hostPort)
|
||||
configContent, err := ioutil.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
// Do not follow redirects.
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
tester.InitServer(string(configContent), "caddyfile")
|
||||
|
||||
params := make(map[string]string)
|
||||
params["base_url"] = baseURL
|
||||
params["version_path"] = fmt.Sprintf("%s/version", baseURL)
|
||||
return tester, params, nil
|
||||
}
|
||||
|
||||
func TestApp(t *testing.T) {
|
||||
tester, config, err := initCaddyTester(t, "assets/config/Caddyfile")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to init caddy tester instance: %v", err)
|
||||
}
|
||||
resp, respBody := tester.AssertGetResponse(config["version_path"], 200, "1.0.0")
|
||||
t.Logf("%v", resp)
|
||||
t.Logf("%v", respBody)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
59
assets/config/Caddyfile
Normal file
59
assets/config/Caddyfile
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
debug
|
||||
local_certs
|
||||
http_port 8080
|
||||
https_port 8443
|
||||
|
||||
security {
|
||||
credentials email smtp.contoso.com {
|
||||
address smtp.contoso.com:993
|
||||
protocol smtp
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
|
||||
authentication portal myportal {
|
||||
crypto default token lifetime 3600
|
||||
crypto key sign-verify 01ee2688-36e4-47f9-8c06-d18483702520
|
||||
backend local assets/config/users.json local
|
||||
ui {
|
||||
links {
|
||||
"My Website" "/app" icon "las la-star"
|
||||
"My Identity" "/auth/whoami" icon "las la-user"
|
||||
}
|
||||
}
|
||||
transform user {
|
||||
match origin local
|
||||
action add role authp/user
|
||||
ui link "Portal Settings" /auth/settings icon "las la-cog"
|
||||
}
|
||||
}
|
||||
|
||||
authorization policy mypolicy {
|
||||
set auth url /auth/
|
||||
crypto key verify 01ee2688-36e4-47f9-8c06-d18483702520
|
||||
allow roles authp/admin authp/user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
127.0.0.1, localhost {
|
||||
route /version* {
|
||||
respond * "1.0.0" 200
|
||||
}
|
||||
|
||||
route /auth* {
|
||||
authenticate * with myportal
|
||||
}
|
||||
|
||||
route /app {
|
||||
authorize with mypolicy
|
||||
file_server {
|
||||
root ./assets/config
|
||||
}
|
||||
}
|
||||
|
||||
route {
|
||||
redir https://{hostport}/auth 302
|
||||
}
|
||||
}
|
BIN
assets/docs/images/logo.png
Normal file
BIN
assets/docs/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
80
caddyfile.go
Normal file
80
caddyfile.go
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
// "fmt"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
// "github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/greenpau/aaasf"
|
||||
// "strconv"
|
||||
// "strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
httpcaddyfile.RegisterGlobalOption("security", parseCaddyfile)
|
||||
}
|
||||
|
||||
// parseCaddyfile parses security app configuration.
|
||||
//
|
||||
// Syntax:
|
||||
//
|
||||
// security {
|
||||
// credentials ...
|
||||
// authentication ...
|
||||
// authorization ...
|
||||
// }
|
||||
//
|
||||
func parseCaddyfile(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||
repl := caddy.NewReplacer()
|
||||
app := new(App)
|
||||
app.Config = aaasf.NewConfig()
|
||||
|
||||
if !d.Next() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
|
||||
for d.NextBlock(0) {
|
||||
tld := d.Val()
|
||||
switch tld {
|
||||
case "credentials":
|
||||
if err := parseCaddyfileCredentials(d, repl, app.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "authentication":
|
||||
if err := parseCaddyfileAuthentication(d, repl, app.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "authorization":
|
||||
if err := parseCaddyfileAuthorization(d, repl, app.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
if err := app.Config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return httpcaddyfile.App{
|
||||
Name: appName,
|
||||
Value: caddyconfig.JSON(app, nil),
|
||||
}, nil
|
||||
}
|
179
caddyfile_authn.go
Normal file
179
caddyfile_authn.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/aaasf/pkg/authn/cookie"
|
||||
"github.com/greenpau/aaasf/pkg/authn/registration"
|
||||
"github.com/greenpau/aaasf/pkg/authn/ui"
|
||||
"github.com/greenpau/aaasf/pkg/authz/options"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
authnPrefix = "security.authentication"
|
||||
)
|
||||
|
||||
// parseCaddyfileAuthentication parses authentication configuration.
|
||||
//
|
||||
// Syntax:
|
||||
//
|
||||
// authentication portal <name> {
|
||||
//
|
||||
// backend local <file/path/to/user/db> <realm/name>
|
||||
// backend local {
|
||||
// method <local>
|
||||
// file <file_path>
|
||||
// realm <name>
|
||||
// }
|
||||
//
|
||||
// backend oauth2_generic {
|
||||
// method oauth2
|
||||
// realm generic
|
||||
// provider generic
|
||||
// base_auth_url <base_url>
|
||||
// metadata_url <metadata_url>
|
||||
// client_id <client_id>
|
||||
// client_secret <client_secret>
|
||||
// scopes openid email profile
|
||||
// disable metadata_discovery
|
||||
// authorization_url <authorization_url>
|
||||
// disable key_verification
|
||||
// }
|
||||
//
|
||||
// backend gitlab {
|
||||
// method oauth2
|
||||
// realm gitlab
|
||||
// provider gitlab
|
||||
// domain_name <domain>
|
||||
// client_id <client_id>
|
||||
// client_secret <client_secret>
|
||||
// user_group_filters <regex_pattern>
|
||||
// }
|
||||
//
|
||||
// backend google <client_id> <client_secret>
|
||||
// backend github <client_id> <client_secret>
|
||||
// backend facebook <client_id> <client_secret>
|
||||
//
|
||||
// crypto key sign-verify <shared_secret>
|
||||
//
|
||||
// ui {
|
||||
// template <login|portal> <file_path>
|
||||
// logo_url <file_path|url_path>
|
||||
// logo_description <value>
|
||||
// custom css path <path>
|
||||
// custom js path <path>
|
||||
// custom html header path <path>
|
||||
// static_asset <uri> <content_type> <path>
|
||||
// allow settings for role <role>
|
||||
// }
|
||||
//
|
||||
// cookie domain <name>
|
||||
// cookie path <name>
|
||||
// cookie lifetime <seconds>
|
||||
// cookie samesite <lax|strict|none>
|
||||
// cookie insecure <on|off>
|
||||
//
|
||||
// registration {
|
||||
// disabled <on|off>
|
||||
// title "User Registration"
|
||||
// code "NY2020"
|
||||
// dropbox <file/path/to/registration/dir/>
|
||||
// require accept terms
|
||||
// require domain mx
|
||||
// }
|
||||
//
|
||||
// validate source address
|
||||
// }
|
||||
//
|
||||
func parseCaddyfileAuthentication(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *aaasf.Config) error {
|
||||
// rootDirective is config key prefix.
|
||||
var rootDirective string
|
||||
args := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
switch args[0] {
|
||||
case "portal":
|
||||
p := &authn.PortalConfig{
|
||||
Name: args[1],
|
||||
UI: &ui.Parameters{
|
||||
Templates: make(map[string]string),
|
||||
},
|
||||
UserRegistrationConfig: ®istration.Config{},
|
||||
CookieConfig: &cookie.Config{},
|
||||
TokenValidatorOptions: &options.TokenValidatorOptions{},
|
||||
TokenGrantorOptions: &options.TokenGrantorOptions{},
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
k := d.Val()
|
||||
v := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
rootDirective = mkcp(authnPrefix, args[0], k)
|
||||
switch k {
|
||||
case "crypto":
|
||||
if err := parseCaddyfileAuthPortalCrypto(d, repl, p, rootDirective, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "cookie":
|
||||
if err := parseCaddyfileAuthPortalCookie(d, repl, p, rootDirective, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "backend":
|
||||
if err := parseCaddyfileAuthPortalBackendShortcuts(d, repl, p, rootDirective, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "backends":
|
||||
if err := parseCaddyfileAuthPortalBackends(d, repl, p, rootDirective); err != nil {
|
||||
return err
|
||||
}
|
||||
case "ui":
|
||||
if err := parseCaddyfileAuthPortalUI(d, repl, p, rootDirective); err != nil {
|
||||
return err
|
||||
}
|
||||
case "transform":
|
||||
if err := parseCaddyfileAuthPortalTransform(d, repl, p, rootDirective, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "registration":
|
||||
if err := parseCaddyfileAuthPortalRegistration(d, repl, p, rootDirective); err != nil {
|
||||
return err
|
||||
}
|
||||
case "enable", "validate":
|
||||
if err := parseCaddyfileAuthPortalMisc(d, repl, p, rootDirective, k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs(rootDirective, v)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cfg.AddAuthenticationPortal(p); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs(authnPrefix, args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mkcp(parts ...string) string {
|
||||
return strings.Join(parts, ".")
|
||||
}
|
87
caddyfile_authn_backend_shortcut.go
Normal file
87
caddyfile_authn_backend_shortcut.go
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/aaasf/pkg/authn/backends"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalBackendShortcuts(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, ckp string, v []string) error {
|
||||
if len(v) == 0 {
|
||||
return errors.ErrConfigDirectiveShort.WithArgs(ckp, v)
|
||||
}
|
||||
if v[len(v)-1] == "disabled" {
|
||||
return nil
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
switch v[0] {
|
||||
case "local":
|
||||
if len(v) != 3 {
|
||||
return errors.ErrMalformedDirective.WithArgs(ckp, v)
|
||||
}
|
||||
m["name"] = fmt.Sprintf("local_backend_%d", len(portal.BackendConfigs))
|
||||
m["method"] = "local"
|
||||
m["path"] = v[1]
|
||||
m["realm"] = v[2]
|
||||
case "google":
|
||||
if len(v) != 3 {
|
||||
return errors.ErrMalformedDirective.WithArgs(ckp, v)
|
||||
}
|
||||
m["name"] = fmt.Sprintf("google_backend_%d", len(portal.BackendConfigs))
|
||||
m["method"] = "oauth2"
|
||||
m["realm"] = "google"
|
||||
m["provider"] = "google"
|
||||
m["client_id"] = v[1]
|
||||
m["client_secret"] = v[2]
|
||||
m["scopes"] = []string{"openid", "email", "profile"}
|
||||
case "github":
|
||||
if len(v) != 3 {
|
||||
return errors.ErrMalformedDirective.WithArgs(ckp, v)
|
||||
}
|
||||
m["name"] = fmt.Sprintf("github_backend_%d", len(portal.BackendConfigs))
|
||||
m["method"] = "oauth2"
|
||||
m["realm"] = "github"
|
||||
m["provider"] = "github"
|
||||
m["client_id"] = v[1]
|
||||
m["client_secret"] = v[2]
|
||||
m["scopes"] = []string{"user"}
|
||||
|
||||
case "facebook":
|
||||
if len(v) != 3 {
|
||||
return errors.ErrMalformedDirective.WithArgs(ckp, v)
|
||||
}
|
||||
m["name"] = fmt.Sprintf("facebook_backend_%d", len(portal.BackendConfigs))
|
||||
m["method"] = "oauth2"
|
||||
m["realm"] = "facebook"
|
||||
m["provider"] = "facebook"
|
||||
m["client_id"] = v[1]
|
||||
m["client_secret"] = v[2]
|
||||
m["scopes"] = []string{"email"}
|
||||
default:
|
||||
return errors.ErrConfigDirectiveValueUnsupported.WithArgs(ckp, v)
|
||||
}
|
||||
|
||||
backendConfig, err := backends.NewConfig(m)
|
||||
if err != nil {
|
||||
return errors.ErrConfigDirectiveFail.WithArgs(ckp, v, err)
|
||||
}
|
||||
portal.BackendConfigs = append(portal.BackendConfigs, *backendConfig)
|
||||
return nil
|
||||
}
|
196
caddyfile_authn_backends.go
Normal file
196
caddyfile_authn_backends.go
Normal file
@ -0,0 +1,196 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/aaasf/pkg/authn/backends"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalBackends(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, rootDirective string) error {
|
||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||
backendName := h.Val()
|
||||
cfg := make(map[string]interface{})
|
||||
cfg["name"] = backendName
|
||||
backendDisabled := false
|
||||
var backendAuthMethod string
|
||||
for subNesting := h.Nesting(); h.NextBlock(subNesting); {
|
||||
backendArg := h.Val()
|
||||
switch backendArg {
|
||||
case "method", "type":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
backendAuthMethod = h.Val()
|
||||
cfg["method"] = backendAuthMethod
|
||||
case "trusted_authority":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
var trustedAuthorities []string
|
||||
if v, exists := cfg["trusted_authorities"]; exists {
|
||||
trustedAuthorities = v.([]string)
|
||||
}
|
||||
trustedAuthorities = append(trustedAuthorities, h.Val())
|
||||
cfg["trusted_authorities"] = trustedAuthorities
|
||||
case "disabled":
|
||||
backendDisabled = true
|
||||
break
|
||||
case "username", "password", "search_base_dn", "search_group_filter", "path", "realm":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
cfg[backendArg] = util.FindReplace(repl, h.Val())
|
||||
case "search_filter", "search_user_filter":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
cfg["search_user_filter"] = util.FindReplace(repl, h.Val())
|
||||
case "attributes":
|
||||
attrMap := make(map[string]interface{})
|
||||
for attrNesting := h.Nesting(); h.NextBlock(attrNesting); {
|
||||
attrName := h.Val()
|
||||
if !h.NextArg() {
|
||||
return backendPropErr(h, backendName, backendArg, attrName, "has no value")
|
||||
}
|
||||
attrMap[attrName] = h.Val()
|
||||
}
|
||||
cfg[backendArg] = attrMap
|
||||
case "servers":
|
||||
serverMaps := []map[string]interface{}{}
|
||||
for serverNesting := h.Nesting(); h.NextBlock(serverNesting); {
|
||||
serverMap := make(map[string]interface{})
|
||||
serverMap["address"] = h.Val()
|
||||
serverProps := h.RemainingArgs()
|
||||
if len(serverProps) > 0 {
|
||||
for _, serverProp := range serverProps {
|
||||
switch serverProp {
|
||||
case "ignore_cert_errors", "posix_groups":
|
||||
serverMap[serverProp] = true
|
||||
default:
|
||||
return backendPropErr(h, backendName, backendArg, serverProp, "is unsupported")
|
||||
}
|
||||
}
|
||||
}
|
||||
serverMaps = append(serverMaps, serverMap)
|
||||
}
|
||||
cfg[backendArg] = serverMaps
|
||||
case "groups":
|
||||
groupMaps := []map[string]interface{}{}
|
||||
for groupNesting := h.Nesting(); h.NextBlock(groupNesting); {
|
||||
groupMap := make(map[string]interface{})
|
||||
groupDN := h.Val()
|
||||
groupMap["dn"] = groupDN
|
||||
groupRoles := h.RemainingArgs()
|
||||
if len(groupRoles) == 0 {
|
||||
return backendPropErr(h, backendName, backendArg, groupDN, "has no roles")
|
||||
}
|
||||
groupMap["roles"] = groupRoles
|
||||
groupMaps = append(groupMaps, groupMap)
|
||||
}
|
||||
cfg[backendArg] = groupMaps
|
||||
case "provider":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
cfg[backendArg] = h.Val()
|
||||
case "idp_metadata_location", "idp_sign_cert_location", "tenant_id",
|
||||
"application_id", "application_name", "entity_id", "domain_name",
|
||||
"client_id", "client_secret", "server_id", "base_auth_url", "metadata_url",
|
||||
"identity_token_name", "authorization_url", "token_url":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
cfg[backendArg] = util.FindReplace(repl, h.Val())
|
||||
case "acs_url":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
var acsURLs []string
|
||||
if v, exists := cfg["acs_urls"]; exists {
|
||||
acsURLs = v.([]string)
|
||||
}
|
||||
acsURLs = append(acsURLs, h.Val())
|
||||
cfg["acs_urls"] = acsURLs
|
||||
case "scopes", "user_group_filters", "user_org_filters":
|
||||
if _, exists := cfg[backendArg]; exists {
|
||||
values := cfg[backendArg].([]string)
|
||||
values = append(values, h.RemainingArgs()...)
|
||||
cfg[backendArg] = values
|
||||
} else {
|
||||
cfg[backendArg] = h.RemainingArgs()
|
||||
}
|
||||
case "delay_start", "retry_attempts", "retry_interval":
|
||||
backendVal := strings.Join(h.RemainingArgs(), "|")
|
||||
i, err := strconv.Atoi(backendVal)
|
||||
if err != nil {
|
||||
return backendValueConversionErr(h, backendName, backendArg, backendVal, err)
|
||||
}
|
||||
cfg[backendArg] = i
|
||||
case "disable":
|
||||
backendVal := strings.Join(h.RemainingArgs(), "_")
|
||||
switch backendVal {
|
||||
case "metadata_discovery":
|
||||
case "key_verification":
|
||||
case "pass_grant_type":
|
||||
case "response_type":
|
||||
case "nonce":
|
||||
default:
|
||||
return backendPropErr(h, backendName, backendArg, backendVal, "is unsupported")
|
||||
}
|
||||
cfg[backendVal+"_disabled"] = true
|
||||
case "enable":
|
||||
backendVal := strings.Join(h.RemainingArgs(), "_")
|
||||
switch backendVal {
|
||||
case "accept_header":
|
||||
default:
|
||||
return backendPropErr(h, backendName, backendArg, backendVal, "is unsupported")
|
||||
}
|
||||
cfg[backendVal+"_enabled"] = true
|
||||
default:
|
||||
return backendUnsupportedValueErr(h, backendName, backendArg)
|
||||
}
|
||||
}
|
||||
if !backendDisabled {
|
||||
backendConfig, err := backends.NewConfig(cfg)
|
||||
if err != nil {
|
||||
return h.Errf("auth backend %s directive failed: %v", rootDirective, err.Error())
|
||||
}
|
||||
portal.BackendConfigs = append(portal.BackendConfigs, *backendConfig)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func backendValueErr(h *caddyfile.Dispenser, backendName, backendArg string) error {
|
||||
return h.Errf("auth backend %s subdirective %s has no value", backendName, backendArg)
|
||||
}
|
||||
|
||||
func backendUnsupportedValueErr(h *caddyfile.Dispenser, backendName, backendArg string) error {
|
||||
return h.Errf("auth backend %s subdirective %s is unsupported", backendName, backendArg)
|
||||
}
|
||||
|
||||
func backendPropErr(h *caddyfile.Dispenser, backendName, backendArg, attrName, attrErr string) error {
|
||||
return h.Errf("auth backend %q subdirective %q key %q %s", backendName, backendArg, attrName, attrErr)
|
||||
}
|
||||
|
||||
func backendValueConversionErr(h *caddyfile.Dispenser, backendName, k, v string, err error) error {
|
||||
return h.Errf("auth backend %s subdirective %s value %q error: %v", backendName, k, v, err)
|
||||
}
|
56
caddyfile_authn_cookie.go
Normal file
56
caddyfile_authn_cookie.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalCookie(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, rootDirective string, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return h.Errf("%s %s directive is invalid", rootDirective, strings.Join(args, " "))
|
||||
}
|
||||
switch args[0] {
|
||||
case "domain":
|
||||
portal.CookieConfig.Domain = args[1]
|
||||
case "path":
|
||||
portal.CookieConfig.Path = args[1]
|
||||
case "lifetime":
|
||||
lifetime, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return h.Errf("%s %s value %q conversion failed: %v", rootDirective, args[0], args[1], err)
|
||||
}
|
||||
if lifetime < 1 {
|
||||
return h.Errf("%s %s value must be greater than zero", rootDirective, args[0])
|
||||
}
|
||||
portal.CookieConfig.Lifetime = lifetime
|
||||
case "samesite":
|
||||
portal.CookieConfig.SameSite = args[1]
|
||||
case "insecure":
|
||||
enabled, err := cfgutil.ParseBoolArg(args[1])
|
||||
if err != nil {
|
||||
return h.Errf("%s %s directive value of %q is invalid: %v", rootDirective, args[0], args[1], err)
|
||||
}
|
||||
portal.CookieConfig.Insecure = enabled
|
||||
default:
|
||||
return h.Errf("%s %s directive is unsupported", rootDirective, strings.Join(args, " "))
|
||||
}
|
||||
return nil
|
||||
}
|
38
caddyfile_authn_crypto.go
Normal file
38
caddyfile_authn_crypto.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalCrypto(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, rootDirective string, args []string) error {
|
||||
if len(args) < 3 {
|
||||
return errors.ErrConfigDirectiveShort.WithArgs(rootDirective, args)
|
||||
}
|
||||
encodedArgs := cfgutil.EncodeArgs(args)
|
||||
switch args[0] {
|
||||
case "key":
|
||||
case "default":
|
||||
default:
|
||||
return errors.ErrConfigDirectiveValueUnsupported.WithArgs(rootDirective, args)
|
||||
}
|
||||
portal.AddRawCryptoConfigs(encodedArgs)
|
||||
return nil
|
||||
}
|
46
caddyfile_authn_misc.go
Normal file
46
caddyfile_authn_misc.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalMisc(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, rootDirective, k string, args []string) error {
|
||||
v := strings.Join(args, " ")
|
||||
v = strings.TrimSpace(v)
|
||||
switch k {
|
||||
case "enable":
|
||||
switch v {
|
||||
case "source ip tracking":
|
||||
portal.TokenGrantorOptions.EnableSourceAddress = true
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
case "validate":
|
||||
switch v {
|
||||
case "source address":
|
||||
portal.TokenValidatorOptions.ValidateSourceAddress = true
|
||||
case "":
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
default:
|
||||
return h.Errf("%s directive %q is unsupported", rootDirective, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
69
caddyfile_authn_registration.go
Normal file
69
caddyfile_authn_registration.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalRegistration(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, rootDirective string) error {
|
||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||
subDirective := h.Val()
|
||||
switch subDirective {
|
||||
case "title":
|
||||
if !h.NextArg() {
|
||||
return h.Errf("%s %s subdirective has no value", rootDirective, subDirective)
|
||||
}
|
||||
portal.UserRegistrationConfig.Title = h.Val()
|
||||
case "disabled":
|
||||
if !h.NextArg() {
|
||||
return h.Errf("%s %s subdirective has no value", rootDirective, subDirective)
|
||||
}
|
||||
if h.Val() == "yes" || h.Val() == "on" {
|
||||
portal.UserRegistrationConfig.Disabled = true
|
||||
}
|
||||
case "code":
|
||||
if !h.NextArg() {
|
||||
return h.Errf("%s %s subdirective has no value", rootDirective, subDirective)
|
||||
}
|
||||
portal.UserRegistrationConfig.Code = h.Val()
|
||||
case "dropbox":
|
||||
if !h.NextArg() {
|
||||
return h.Errf("%s %s subdirective has no value", rootDirective, subDirective)
|
||||
}
|
||||
portal.UserRegistrationConfig.Dropbox = h.Val()
|
||||
case "require":
|
||||
args := strings.Join(h.RemainingArgs(), " ")
|
||||
args = strings.TrimSpace(args)
|
||||
switch args {
|
||||
case "accept terms":
|
||||
portal.UserRegistrationConfig.RequireAcceptTerms = true
|
||||
case "domain mx":
|
||||
portal.UserRegistrationConfig.RequireDomainMailRecord = true
|
||||
case "":
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
default:
|
||||
return h.Errf("%s directive %q is unsupported", rootDirective, args)
|
||||
}
|
||||
default:
|
||||
return h.Errf("unsupported subdirective for %s: %s", rootDirective, subDirective)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
390
caddyfile_authn_test.go
Normal file
390
caddyfile_authn_test.go
Normal file
@ -0,0 +1,390 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
)
|
||||
|
||||
func TestParseCaddyfileAuthentication(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
d *caddyfile.Dispenser
|
||||
want string
|
||||
shouldErr bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "test valid authentication portal config",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authentication portal myportal {
|
||||
crypto default token lifetime 3600
|
||||
crypto key sign-verify 01ee2688-36e4-47f9-8c06-d18483702520
|
||||
backend local assets/config/users.json local
|
||||
cookie domain contoso.com
|
||||
ui {
|
||||
links {
|
||||
"My Website" "/app" icon "las la-star"
|
||||
"My Identity" "/auth/whoami" icon "las la-user"
|
||||
}
|
||||
}
|
||||
transform user {
|
||||
match origin local
|
||||
action add role authp/user
|
||||
ui link "Portal Settings" /auth/settings icon "las la-cog"
|
||||
}
|
||||
registration {
|
||||
title "User Registration"
|
||||
code "NY2020"
|
||||
dropbox assets/config/registrations.json
|
||||
require accept terms
|
||||
require domain mx
|
||||
}
|
||||
enable source ip tracking
|
||||
validate source address
|
||||
|
||||
backends {
|
||||
ldap_backend {
|
||||
method ldap
|
||||
realm contoso.com
|
||||
servers {
|
||||
ldaps://ldaps.contoso.com ignore_cert_errors
|
||||
}
|
||||
attributes {
|
||||
name givenName
|
||||
surname sn
|
||||
username sAMAccountName
|
||||
member_of memberOf
|
||||
email mail
|
||||
}
|
||||
username "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM"
|
||||
password "P@ssW0rd123"
|
||||
search_base_dn "DC=CONTOSO,DC=COM"
|
||||
search_filter "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))"
|
||||
groups {
|
||||
"CN=Admins,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" admin
|
||||
"CN=Editors,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" editor
|
||||
"CN=Viewers,OU=Security,OU=Groups,DC=CONTOSO,DC=COM" viewer
|
||||
}
|
||||
}
|
||||
ldap_backend2 {
|
||||
method ldap
|
||||
realm example.com
|
||||
servers {
|
||||
ldap://ldap.forumsys.com posix_groups
|
||||
}
|
||||
attributes {
|
||||
name cn
|
||||
surname foo
|
||||
username uid
|
||||
member_of uniqueMember
|
||||
email mail
|
||||
}
|
||||
username "cn=read-only-admin,dc=example,dc=com"
|
||||
password "password"
|
||||
search_base_dn "DC=EXAMPLE,DC=COM"
|
||||
search_filter "(&(|(uid=%s)(mail=%s))(objectClass=inetOrgPerson))"
|
||||
groups {
|
||||
"ou=mathematicians,dc=example,dc=com" authp/admin
|
||||
"ou=scientists,dc=example,dc=com" authp/user
|
||||
}
|
||||
}
|
||||
azure_saml_backend {
|
||||
method saml
|
||||
provider azure
|
||||
realm azure
|
||||
idp_metadata_location assets/conf/saml/azure/idp/azure_ad_app_metadata.xml
|
||||
idp_sign_cert_location assets/conf/saml/azure/idp/azure_ad_app_signing_cert.pem
|
||||
tenant_id "1b9e886b-8ff2-4378-b6c8-6771259a5f51"
|
||||
application_id "623cae7c-e6b2-43c5-853c-2059c9b2cb58"
|
||||
application_name "My Gatekeeper"
|
||||
entity_id "urn:caddy:mygatekeeper"
|
||||
acs_url https://mygatekeeper/saml
|
||||
acs_url https://mygatekeeper.local/saml
|
||||
acs_url https://192.168.10.10:3443/saml
|
||||
acs_url https://localhost:3443/saml
|
||||
}
|
||||
okta_oauth2_backend {
|
||||
method oauth2
|
||||
realm okta
|
||||
provider okta
|
||||
domain_name dev-680653.okta.com
|
||||
client_id 0oa121qw81PJW0Tj34x7
|
||||
client_secret b3aJC5E59hU18YKC7Yca3994F4qFhWiAo_ZojanF
|
||||
server_id default
|
||||
scopes openid email profile groups
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"auth_portal_configs": [
|
||||
{
|
||||
"name": "myportal",
|
||||
"ui": {
|
||||
"private_links": [
|
||||
{"link": "/app", "title": "My Website", "icon_name": "las la-star", "icon_enabled": true},
|
||||
{"link": "/auth/whoami", "title": "My Identity", "icon_name": "las la-user", "icon_enabled": true}
|
||||
]
|
||||
},
|
||||
"user_registration_config": {
|
||||
"code": "NY2020",
|
||||
"dropbox": "assets/config/registrations.json",
|
||||
"require_accept_terms": true,
|
||||
"require_domain_mx": true,
|
||||
"title": "User Registration"
|
||||
},
|
||||
"user_transformer_configs": [
|
||||
{
|
||||
"matchers": ["exact match origin local"],
|
||||
"actions": [
|
||||
"action add role authp/user",
|
||||
"ui link \"Portal Settings\" /auth/settings icon \"las la-cog\""
|
||||
]
|
||||
}
|
||||
],
|
||||
"cookie_config": {
|
||||
"domain": "contoso.com"
|
||||
},
|
||||
"backend_configs": [
|
||||
{
|
||||
"local": {
|
||||
"name": "local_backend_0",
|
||||
"method": "local",
|
||||
"realm": "local",
|
||||
"path": "assets/config/users.json"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ldap": {
|
||||
"name": "ldap_backend",
|
||||
"method": "ldap",
|
||||
"realm": "contoso.com",
|
||||
"search_base_dn": "DC=CONTOSO,DC=COM",
|
||||
"search_user_filter": "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))",
|
||||
"servers": [
|
||||
{
|
||||
"address": "ldaps://ldaps.contoso.com",
|
||||
"ignore_cert_errors": true
|
||||
}
|
||||
],
|
||||
"attributes": {
|
||||
"email": "mail",
|
||||
"member_of": "memberOf",
|
||||
"name": "givenName",
|
||||
"username": "sAMAccountName",
|
||||
"surname": "sn"
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"dn": "CN=Admins,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
|
||||
"roles": ["admin"]
|
||||
},
|
||||
{
|
||||
"dn": "CN=Editors,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
|
||||
"roles": ["editor"]
|
||||
},
|
||||
{
|
||||
"dn": "CN=Viewers,OU=Security,OU=Groups,DC=CONTOSO,DC=COM",
|
||||
"roles": ["viewer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"ldap": {
|
||||
"name": "ldap_backend2",
|
||||
"method": "ldap",
|
||||
"realm": "example.com",
|
||||
"servers": [
|
||||
{
|
||||
"address": "ldap://ldap.forumsys.com",
|
||||
"posix_groups": true
|
||||
}
|
||||
],
|
||||
"attributes": {
|
||||
"name": "cn",
|
||||
"surname": "foo",
|
||||
"username": "uid",
|
||||
"member_of": "uniqueMember",
|
||||
"email": "mail"
|
||||
},
|
||||
"search_base_dn": "DC=EXAMPLE,DC=COM",
|
||||
"search_user_filter": "(&(|(uid=%s)(mail=%s))(objectClass=inetOrgPerson))",
|
||||
"groups": [
|
||||
{
|
||||
"dn": "ou=mathematicians,dc=example,dc=com",
|
||||
"roles": ["authp/admin"]
|
||||
},
|
||||
{
|
||||
"dn": "ou=scientists,dc=example,dc=com",
|
||||
"roles": ["authp/user"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"saml": {
|
||||
"name": "azure_saml_backend",
|
||||
"method": "saml",
|
||||
"realm": "azure",
|
||||
"provider": "azure",
|
||||
"idp_metadata_location": "assets/conf/saml/azure/idp/azure_ad_app_metadata.xml",
|
||||
"idp_sign_cert_location": "assets/conf/saml/azure/idp/azure_ad_app_signing_cert.pem",
|
||||
"tenant_id": "1b9e886b-8ff2-4378-b6c8-6771259a5f51",
|
||||
"application_id": "623cae7c-e6b2-43c5-853c-2059c9b2cb58",
|
||||
"application_name": "My Gatekeeper",
|
||||
"entity_id": "urn:caddy:mygatekeeper",
|
||||
"acs_urls": [
|
||||
"https://mygatekeeper/saml",
|
||||
"https://mygatekeeper.local/saml",
|
||||
"https://192.168.10.10:3443/saml",
|
||||
"https://localhost:3443/saml"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"oauth2": {
|
||||
"name": "okta_oauth2_backend",
|
||||
"method": "oauth2",
|
||||
"realm": "okta",
|
||||
"provider": "okta",
|
||||
"domain_name": "dev-680653.okta.com",
|
||||
"client_id": "0oa121qw81PJW0Tj34x7",
|
||||
"client_secret": "b3aJC5E59hU18YKC7Yca3994F4qFhWiAo_ZojanF",
|
||||
"server_id": "default",
|
||||
"scopes": ["openid", "email", "profile", "groups"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"token_validator_options": {
|
||||
"validate_source_address": true
|
||||
},
|
||||
"crypto_key_configs": [
|
||||
{
|
||||
"id": "0",
|
||||
"usage": "sign-verify",
|
||||
"token_name": "access_token",
|
||||
"source": "config",
|
||||
"algorithm": "hmac",
|
||||
"token_lifetime": 3600,
|
||||
"token_secret": "01ee2688-36e4-47f9-8c06-d18483702520"
|
||||
}
|
||||
],
|
||||
"crypto_key_store_config": {
|
||||
"token_lifetime": 3600
|
||||
},
|
||||
"token_grantor_options": {
|
||||
"enable_source_address": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test malformed authentication portal definition",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authentication portal myportal foo {
|
||||
backend local assets/config/users.json local
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: Wrong argument count or unexpected line ending after '%s'", tf, 3, "foo"),
|
||||
},
|
||||
{
|
||||
name: "test unsupported authentication portal keyword",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authentication portal myportal {
|
||||
foo bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: errors.ErrMalformedDirective.WithArgs(
|
||||
mkcp(authnPrefix, "portal", "foo"),
|
||||
[]string{"bar"},
|
||||
),
|
||||
},
|
||||
/*
|
||||
{
|
||||
name: "test smtp credentials without address",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
credentials email smtp.contoso.com {
|
||||
protocol smtp
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: errors.ErrMalformedDirective.WithArgs(
|
||||
[]string{credPrefix, "email", "smtp.contoso.com"},
|
||||
errors.ErrCredKeyValueEmpty.WithArgs("address"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test unsupported credentials type",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
credentials foo bar {
|
||||
protocol smtp
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: errors.ErrMalformedDirective.WithArgs(
|
||||
credPrefix,
|
||||
[]string{"foo", "bar"},
|
||||
),
|
||||
},
|
||||
*/
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app, err := parseCaddyfile(tc.d, nil)
|
||||
if err != nil {
|
||||
if !tc.shouldErr {
|
||||
t.Fatalf("expected success, got: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
|
||||
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tc.shouldErr {
|
||||
t.Fatalf("unexpected success, want: %v", tc.err)
|
||||
}
|
||||
// t.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
|
||||
|
||||
got := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
want := unpack(t, tc.want)
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("parseCaddyfileAuthentication() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
59
caddyfile_authn_transform.go
Normal file
59
caddyfile_authn_transform.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/aaasf/pkg/authn/transformer"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalTransform(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, rootDirective string, rootArgs []string) error {
|
||||
args := strings.Join(rootArgs, " ")
|
||||
switch args {
|
||||
case "user", "users":
|
||||
tc := &transformer.Config{}
|
||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||
trKey := h.Val()
|
||||
trArgs := h.RemainingArgs()
|
||||
trArgs = append([]string{trKey}, trArgs...)
|
||||
encodedArgs := cfgutil.EncodeArgs(trArgs)
|
||||
var matchArgs bool
|
||||
for _, arg := range trArgs {
|
||||
if arg == "match" {
|
||||
matchArgs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchArgs {
|
||||
if trArgs[0] == "match" {
|
||||
trArgs = append([]string{"exact"}, trArgs...)
|
||||
encodedArgs = cfgutil.EncodeArgs(trArgs)
|
||||
}
|
||||
tc.Matchers = append(tc.Matchers, encodedArgs)
|
||||
} else {
|
||||
tc.Actions = append(tc.Actions, encodedArgs)
|
||||
}
|
||||
}
|
||||
portal.UserTransformerConfigs = append(portal.UserTransformerConfigs, tc)
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, args)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
162
caddyfile_authn_ui.go
Normal file
162
caddyfile_authn_ui.go
Normal file
@ -0,0 +1,162 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/aaasf/pkg/authn/ui"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalUI(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, rootDirective string) error {
|
||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||
subDirective := h.Val()
|
||||
switch subDirective {
|
||||
case "template":
|
||||
hargs := h.RemainingArgs()
|
||||
switch {
|
||||
case len(hargs) == 2:
|
||||
portal.UI.Templates[hargs[0]] = hargs[1]
|
||||
default:
|
||||
args := strings.Join(h.RemainingArgs(), " ")
|
||||
return h.Errf("%s directive %q is invalid", rootDirective, args)
|
||||
}
|
||||
case "theme":
|
||||
if !h.NextArg() {
|
||||
return h.Errf("%s %s subdirective has no value", rootDirective, subDirective)
|
||||
}
|
||||
portal.UI.Theme = h.Val()
|
||||
case "logo":
|
||||
args := strings.Join(h.RemainingArgs(), " ")
|
||||
args = strings.TrimSpace(args)
|
||||
switch {
|
||||
case strings.HasPrefix(args, "url"):
|
||||
portal.UI.LogoURL = strings.ReplaceAll(args, "url ", "")
|
||||
case strings.HasPrefix(args, "description"):
|
||||
portal.UI.LogoDescription = strings.ReplaceAll(args, "description ", "")
|
||||
case args == "":
|
||||
return h.Errf("%s %s directive has no value", rootDirective, subDirective)
|
||||
default:
|
||||
return h.Errf("%s directive %q is unsupported", rootDirective, args)
|
||||
}
|
||||
case "auto_redirect_url":
|
||||
if !h.NextArg() {
|
||||
return h.Errf("%s %s subdirective has no value", rootDirective, subDirective)
|
||||
}
|
||||
portal.UI.AutoRedirectURL = h.Val()
|
||||
case "password_recovery_enabled":
|
||||
if !h.NextArg() {
|
||||
return h.Errf("%s %s subdirective has no value", rootDirective, subDirective)
|
||||
}
|
||||
if h.Val() == "yes" || h.Val() == "true" {
|
||||
portal.UI.PasswordRecoveryEnabled = true
|
||||
}
|
||||
case "links":
|
||||
for subNesting := h.Nesting(); h.NextBlock(subNesting); {
|
||||
title := h.Val()
|
||||
args := h.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return h.Errf("auth backend %s subdirective %s has no value", subDirective, title)
|
||||
}
|
||||
privateLink := ui.Link{
|
||||
Title: title,
|
||||
Link: args[0],
|
||||
}
|
||||
if len(args) == 1 {
|
||||
portal.UI.PrivateLinks = append(portal.UI.PrivateLinks, privateLink)
|
||||
continue
|
||||
}
|
||||
argp := 1
|
||||
disabledLink := false
|
||||
for argp < len(args) {
|
||||
switch args[argp] {
|
||||
case "target_blank":
|
||||
privateLink.Target = "_blank"
|
||||
privateLink.TargetEnabled = true
|
||||
case "icon":
|
||||
argp++
|
||||
if argp < len(args) {
|
||||
privateLink.IconName = args[argp]
|
||||
privateLink.IconEnabled = true
|
||||
}
|
||||
case "disabled":
|
||||
disabledLink = true
|
||||
default:
|
||||
return h.Errf("auth backend %s subdirective %s has unsupported key %s", subDirective, title, args[argp])
|
||||
}
|
||||
argp++
|
||||
}
|
||||
if disabledLink {
|
||||
continue
|
||||
}
|
||||
portal.UI.PrivateLinks = append(portal.UI.PrivateLinks, privateLink)
|
||||
}
|
||||
case "custom":
|
||||
args := strings.Join(h.RemainingArgs(), " ")
|
||||
args = strings.TrimSpace(args)
|
||||
switch {
|
||||
case strings.HasPrefix(args, "css path"):
|
||||
portal.UI.CustomCSSPath = strings.ReplaceAll(args, "css path ", "")
|
||||
case strings.HasPrefix(args, "css"):
|
||||
portal.UI.CustomCSSPath = strings.ReplaceAll(args, "css ", "")
|
||||
case strings.HasPrefix(args, "js path"):
|
||||
portal.UI.CustomJsPath = strings.ReplaceAll(args, "js path ", "")
|
||||
case strings.HasPrefix(args, "js"):
|
||||
portal.UI.CustomJsPath = strings.ReplaceAll(args, "js ", "")
|
||||
case strings.HasPrefix(args, "html header path"):
|
||||
args = strings.ReplaceAll(args, "html header path ", "")
|
||||
b, err := ioutil.ReadFile(args)
|
||||
if err != nil {
|
||||
return h.Errf("%s %s subdirective: %s %v", rootDirective, subDirective, args, err)
|
||||
}
|
||||
for k, v := range ui.PageTemplates {
|
||||
headIndex := strings.Index(v, "<meta name=\"description\"")
|
||||
if headIndex < 1 {
|
||||
continue
|
||||
}
|
||||
v = v[:headIndex] + string(b) + v[headIndex:]
|
||||
ui.PageTemplates[k] = v
|
||||
}
|
||||
case args == "":
|
||||
return h.Errf("%s %s directive has no value", rootDirective, subDirective)
|
||||
default:
|
||||
return h.Errf("%s directive %q is unsupported", rootDirective, args)
|
||||
}
|
||||
case "static_asset":
|
||||
args := h.RemainingArgs()
|
||||
if len(args) != 3 {
|
||||
return h.Errf("auth backend %s subdirective %s is malformed", rootDirective, subDirective)
|
||||
}
|
||||
prefix := "assets/"
|
||||
assetURI := args[0]
|
||||
assetContentType := args[1]
|
||||
assetPath := args[2]
|
||||
if !strings.HasPrefix(assetURI, prefix) {
|
||||
return h.Errf("auth backend %s subdirective %s URI must be prefixed with %s, got %s",
|
||||
rootDirective, subDirective, prefix, assetURI)
|
||||
}
|
||||
if err := ui.StaticAssets.AddAsset(assetURI, assetContentType, assetPath); err != nil {
|
||||
return h.Errf("auth backend %s subdirective %s failed: %s", rootDirective, subDirective, err)
|
||||
}
|
||||
default:
|
||||
return h.Errf("unsupported subdirective for %s: %s", rootDirective, subDirective)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
86
caddyfile_authz.go
Normal file
86
caddyfile_authz.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
authzPrefix = "security.authorization"
|
||||
)
|
||||
|
||||
// parseCaddyfileAuthorization parses authorization configuration.
|
||||
//
|
||||
// Syntax:
|
||||
//
|
||||
// authorization portal <name> {
|
||||
// }
|
||||
//
|
||||
func parseCaddyfileAuthorization(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *aaasf.Config) error {
|
||||
var rootDirective string
|
||||
args := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
switch args[0] {
|
||||
case "policy":
|
||||
p := &authz.PolicyConfig{Name: args[1]}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
k := d.Val()
|
||||
v := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
rootDirective = mkcp(authzPrefix, args[0], k)
|
||||
switch k {
|
||||
case "crypto":
|
||||
if err := parseCaddyfileAuthorizationCrypto(d, repl, p, rootDirective, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "acl":
|
||||
if err := parseCaddyfileAuthorizationACL(d, repl, p, rootDirective, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "allow", "deny":
|
||||
if err := parseCaddyfileAuthorizationACLShortcuts(d, repl, p, rootDirective, k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "bypass":
|
||||
if err := parseCaddyfileAuthorizationBypass(d, repl, p, rootDirective, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "enable", "disable", "validate", "set", "with":
|
||||
if err := parseCaddyfileAuthorizationMisc(d, repl, p, rootDirective, k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case "inject":
|
||||
if err := parseCaddyfileAuthorizationHeaderInjection(d, repl, p, rootDirective, v); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs(rootDirective, v)
|
||||
}
|
||||
}
|
||||
if err := cfg.AddAuthorizationPolicy(p); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs(authzPrefix, args)
|
||||
}
|
||||
return nil
|
||||
}
|
71
caddyfile_authz_acl.go
Normal file
71
caddyfile_authz_acl.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/acl"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthorizationACL(h *caddyfile.Dispenser, repl *caddy.Replacer, p *authz.PolicyConfig, rootDirective string, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
}
|
||||
switch args[0] {
|
||||
case "rule":
|
||||
if len(args) > 1 {
|
||||
return h.Errf("%s directive %q is too long", rootDirective, strings.Join(args, " "))
|
||||
}
|
||||
rule := &acl.RuleConfiguration{}
|
||||
for subNesting := h.Nesting(); h.NextBlock(subNesting); {
|
||||
k := h.Val()
|
||||
rargs := h.RemainingArgs()
|
||||
if len(rargs) == 0 {
|
||||
return h.Errf("%s %s directive %v has no values", rootDirective, args[0], k)
|
||||
}
|
||||
rargs = append([]string{k}, rargs...)
|
||||
switch k {
|
||||
case "comment":
|
||||
rule.Comment = cfgutil.EncodeArgs(rargs)
|
||||
case "allow", "deny":
|
||||
rule.Action = cfgutil.EncodeArgs(rargs)
|
||||
default:
|
||||
rule.Conditions = append(rule.Conditions, cfgutil.EncodeArgs(rargs))
|
||||
}
|
||||
}
|
||||
p.AccessListRules = append(p.AccessListRules, rule)
|
||||
case "default":
|
||||
if len(args) != 2 {
|
||||
return h.Errf("%s directive %q is too long", rootDirective, strings.Join(args, " "))
|
||||
}
|
||||
rule := &acl.RuleConfiguration{
|
||||
Conditions: []string{"always match iss any"},
|
||||
}
|
||||
switch args[1] {
|
||||
case "allow", "deny":
|
||||
rule.Action = args[1]
|
||||
default:
|
||||
return h.Errf("%s directive %q must have either allow or deny", rootDirective, strings.Join(args, " "))
|
||||
}
|
||||
p.AccessListRules = append(p.AccessListRules, rule)
|
||||
default:
|
||||
return h.Errf("%s directive value of %q is unsupported", rootDirective, strings.Join(args, " "))
|
||||
}
|
||||
return nil
|
||||
}
|
84
caddyfile_authz_acl_shortcuts.go
Normal file
84
caddyfile_authz_acl_shortcuts.go
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/acl"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthorizationACLShortcuts(h *caddyfile.Dispenser, repl *caddy.Replacer, p *authz.PolicyConfig, rootDirective, k string, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
}
|
||||
if len(args) < 2 {
|
||||
return h.Errf("%s directive %q is too short", rootDirective, strings.Join(args, " "))
|
||||
}
|
||||
rule := &acl.RuleConfiguration{}
|
||||
mode := "field"
|
||||
var cond []string
|
||||
var matchMethod, matchPath string
|
||||
var matchAlways bool
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case "with":
|
||||
mode = "method"
|
||||
continue
|
||||
case "to":
|
||||
mode = "path"
|
||||
continue
|
||||
}
|
||||
switch mode {
|
||||
case "field":
|
||||
if arg == "*" || arg == "any" {
|
||||
matchAlways = true
|
||||
}
|
||||
cond = append(cond, arg)
|
||||
case "method":
|
||||
matchMethod = strings.ToUpper(arg)
|
||||
mode = "path"
|
||||
case "path":
|
||||
matchPath = arg
|
||||
mode = "complete"
|
||||
default:
|
||||
return h.Errf("%s directive value of %q is unsupported", rootDirective, strings.Join(args, " "))
|
||||
}
|
||||
}
|
||||
if matchAlways {
|
||||
rule.Conditions = append(rule.Conditions, cfgutil.EncodeArgs(append([]string{"always", "match"}, cond...)))
|
||||
} else {
|
||||
rule.Conditions = append(rule.Conditions, cfgutil.EncodeArgs(append([]string{"match"}, cond...)))
|
||||
}
|
||||
if matchMethod != "" {
|
||||
rule.Conditions = append(rule.Conditions, cfgutil.EncodeArgs([]string{"match", "method", matchMethod}))
|
||||
p.ValidateMethodPath = true
|
||||
}
|
||||
if matchPath != "" {
|
||||
rule.Conditions = append(rule.Conditions, cfgutil.EncodeArgs([]string{"partial", "match", "path", matchPath}))
|
||||
p.ValidateMethodPath = true
|
||||
}
|
||||
switch k {
|
||||
case "allow":
|
||||
rule.Action = cfgutil.EncodeArgs([]string{k, "log", "debug"})
|
||||
case "deny":
|
||||
rule.Action = cfgutil.EncodeArgs([]string{k, "stop", "log", "warn"})
|
||||
}
|
||||
p.AccessListRules = append(p.AccessListRules, rule)
|
||||
return nil
|
||||
}
|
44
caddyfile_authz_bypass.go
Normal file
44
caddyfile_authz_bypass.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
"github.com/greenpau/aaasf/pkg/authz/bypass"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthorizationBypass(h *caddyfile.Dispenser, repl *caddy.Replacer, p *authz.PolicyConfig, rootDirective string, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
}
|
||||
if len(args) != 3 {
|
||||
return h.Errf("%s %s is invalid", rootDirective, cfgutil.EncodeArgs(args))
|
||||
}
|
||||
if args[0] != "uri" {
|
||||
return h.Errf("%s %s is invalid", rootDirective, cfgutil.EncodeArgs(args))
|
||||
}
|
||||
bc := &bypass.Config{
|
||||
MatchType: args[1],
|
||||
URI: args[2],
|
||||
}
|
||||
if err := bc.Validate(); err != nil {
|
||||
return h.Errf("%s %s erred: %v", rootDirective, cfgutil.EncodeArgs(args), err)
|
||||
}
|
||||
p.BypassConfigs = append(p.BypassConfigs, bc)
|
||||
return nil
|
||||
}
|
38
caddyfile_authz_crypto.go
Normal file
38
caddyfile_authz_crypto.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthorizationCrypto(h *caddyfile.Dispenser, repl *caddy.Replacer, policy *authz.PolicyConfig, rootDirective string, args []string) error {
|
||||
if len(args) < 3 {
|
||||
return h.Errf("%v", errors.ErrConfigDirectiveShort.WithArgs(rootDirective, args))
|
||||
}
|
||||
encodedArgs := cfgutil.EncodeArgs(args)
|
||||
switch args[0] {
|
||||
case "key":
|
||||
case "default":
|
||||
default:
|
||||
return h.Errf("%v", errors.ErrConfigDirectiveValueUnsupported.WithArgs(rootDirective, args))
|
||||
}
|
||||
policy.AddRawCryptoConfigs(encodedArgs)
|
||||
return nil
|
||||
}
|
51
caddyfile_authz_inject.go
Normal file
51
caddyfile_authz_inject.go
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
"github.com/greenpau/aaasf/pkg/authz/injector"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthorizationHeaderInjection(h *caddyfile.Dispenser, repl *caddy.Replacer, p *authz.PolicyConfig, rootDirective string, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
}
|
||||
switch {
|
||||
case cfgutil.EncodeArgs(args) == "headers with claims":
|
||||
p.PassClaimsWithHeaders = true
|
||||
case args[0] == "header":
|
||||
if len(args) != 4 {
|
||||
return h.Errf("%s directive %q is invalid", rootDirective, cfgutil.EncodeArgs(args))
|
||||
}
|
||||
if args[2] != "from" {
|
||||
return h.Errf("%s directive %q has invalid syntax", rootDirective, cfgutil.EncodeArgs(args))
|
||||
}
|
||||
cfg := &injector.Config{
|
||||
Header: args[1],
|
||||
Field: args[3],
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return h.Errf("%s %s erred: %v", rootDirective, cfgutil.EncodeArgs(args), err)
|
||||
}
|
||||
p.HeaderInjectionConfigs = append(p.HeaderInjectionConfigs, cfg)
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, cfgutil.EncodeArgs(args))
|
||||
}
|
||||
return nil
|
||||
}
|
106
caddyfile_authz_misc.go
Normal file
106
caddyfile_authz_misc.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
cfgutil "github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthorizationMisc(h *caddyfile.Dispenser, repl *caddy.Replacer, p *authz.PolicyConfig, rootDirective, k string, args []string) error {
|
||||
v := strings.Join(args, " ")
|
||||
v = strings.TrimSpace(v)
|
||||
switch k {
|
||||
case "enable":
|
||||
switch {
|
||||
case v == "js redirect":
|
||||
p.RedirectWithJavascript = true
|
||||
case v == "strip token":
|
||||
p.StripTokenEnabled = true
|
||||
case v == "":
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
case "validate":
|
||||
switch {
|
||||
case v == "path acl":
|
||||
p.ValidateAccessListPathClaim = true
|
||||
p.ValidateMethodPath = true
|
||||
case v == "source address":
|
||||
p.ValidateSourceAddress = true
|
||||
case v == "bearer header":
|
||||
p.ValidateBearerHeader = true
|
||||
|
||||
case v == "":
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
case "disable":
|
||||
switch {
|
||||
case v == "auth redirect query":
|
||||
p.AuthRedirectQueryDisabled = true
|
||||
case v == "auth redirect":
|
||||
p.AuthRedirectDisabled = true
|
||||
case v == "":
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
case "set":
|
||||
switch {
|
||||
case strings.HasPrefix(v, "token sources "):
|
||||
p.AllowedTokenSources = strings.Split(strings.TrimPrefix(v, "token sources "), " ")
|
||||
case strings.HasPrefix(v, "auth url "):
|
||||
p.AuthURLPath = strings.TrimPrefix(v, "auth url ")
|
||||
case strings.HasPrefix(v, "forbidden url "):
|
||||
p.ForbiddenURL = strings.TrimPrefix(v, "forbidden url ")
|
||||
case strings.HasPrefix(v, "redirect query parameter "):
|
||||
p.AuthRedirectQueryParameter = strings.TrimPrefix(v, "redirect query parameter ")
|
||||
case strings.HasPrefix(v, "redirect status "):
|
||||
n, err := strconv.Atoi(strings.TrimPrefix(v, "redirect status "))
|
||||
if err != nil {
|
||||
return h.Errf("%s %s directive failed: %v", rootDirective, v, err)
|
||||
}
|
||||
if n < 300 || n > 308 {
|
||||
return h.Errf("%s %s directive contains invalid value", rootDirective, v)
|
||||
}
|
||||
p.AuthRedirectStatusCode = n
|
||||
case strings.HasPrefix(v, "user identity "):
|
||||
p.UserIdentityField = strings.TrimPrefix(v, "user identity ")
|
||||
case v == "":
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
case "with":
|
||||
switch {
|
||||
case strings.HasPrefix(v, "basic auth"):
|
||||
p.AddRawIdpConfig(cfgutil.EncodeArgs(args))
|
||||
case strings.HasPrefix(v, "api key auth"):
|
||||
p.AddRawIdpConfig(cfgutil.EncodeArgs(args))
|
||||
case v == "":
|
||||
return h.Errf("%s directive has no value", rootDirective)
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
738
caddyfile_authz_test.go
Normal file
738
caddyfile_authz_test.go
Normal file
@ -0,0 +1,738 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
)
|
||||
|
||||
func TestParseCaddyfileAuthorization(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
d *caddyfile.Dispenser
|
||||
want string
|
||||
shouldErr bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "test valid authorization policy config",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
crypto key verify 0e2fdcf8-6868-41a7-884b-7308795fc286
|
||||
set auth url /auth
|
||||
allow roles authp/admin authp/user
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"auth_url_path": "/auth",
|
||||
"access_list_rules": [
|
||||
{
|
||||
"conditions": [
|
||||
"match roles authp/admin authp/user"
|
||||
],
|
||||
"action": "allow log debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test valid authorization policy config with misc settings",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
crypto key verify 0e2fdcf8-6868-41a7-884b-7308795fc286
|
||||
set auth url /auth
|
||||
set token sources query
|
||||
set forbidden url /forbidden
|
||||
set redirect status 302
|
||||
set redirect query parameter return_path_url
|
||||
disable auth redirect query
|
||||
disable auth redirect
|
||||
validate path acl
|
||||
validate source address
|
||||
validate bearer header
|
||||
with basic auth
|
||||
with api key auth
|
||||
allow roles authp/admin authp/user
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"auth_url_path": "/auth",
|
||||
"access_list_rules": [
|
||||
{
|
||||
"conditions": [
|
||||
"match roles authp/admin authp/user"
|
||||
],
|
||||
"action": "allow log debug"
|
||||
}
|
||||
],
|
||||
"disable_auth_redirect": true,
|
||||
"disable_auth_redirect_query": true,
|
||||
"auth_redirect_status_code": 302,
|
||||
"allowed_token_sources": ["query"],
|
||||
"forbidden_url": "/forbidden",
|
||||
"validate_bearer_header": true,
|
||||
"validate_method_path": true,
|
||||
"validate_access_list_path_claim": true,
|
||||
"validate_source_address": true,
|
||||
"auth_redirect_query_param": "return_path_url"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test valid authorization policy config with custom acl",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy2 {
|
||||
crypto key verify 0e2fdcf8-6868-41a7-884b-7308795fc286
|
||||
bypass uri exact /foo
|
||||
set user identity id
|
||||
inject headers with claims
|
||||
inject header "X-Picture" from picture
|
||||
enable js redirect
|
||||
set auth url /auth
|
||||
enable strip token
|
||||
acl rule {
|
||||
comment allow users
|
||||
match role authp/user
|
||||
allow stop log info
|
||||
}
|
||||
acl rule {
|
||||
comment default deny
|
||||
always match role any
|
||||
deny log warn
|
||||
}
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
{
|
||||
"name": "mypolicy2",
|
||||
"auth_url_path": "/auth",
|
||||
"access_list_rules": [
|
||||
{
|
||||
"comment": "comment allow users",
|
||||
"conditions": [
|
||||
"match role authp/user"
|
||||
],
|
||||
"action": "allow stop log info"
|
||||
},
|
||||
{
|
||||
"comment": "comment default deny",
|
||||
"conditions": [
|
||||
"always match role any"
|
||||
],
|
||||
"action": "deny log warn"
|
||||
}
|
||||
],
|
||||
"strip_token_enabled": true,
|
||||
"user_identity_field": "id",
|
||||
"pass_claims_with_headers": true,
|
||||
"redirect_with_javascript": true,
|
||||
"header_injection_configs": [
|
||||
{
|
||||
"header": "X-Picture",
|
||||
"field": "picture"
|
||||
}
|
||||
],
|
||||
"bypass_configs": [
|
||||
{
|
||||
"match_type": "exact",
|
||||
"uri": "/foo"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test valid authorization policy with custom acl shortcuts",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
allow roles authp/admin authp/user
|
||||
allow roles authp/guest with get to /foo
|
||||
allow origin any
|
||||
deny iss foo
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"access_list_rules": [
|
||||
{
|
||||
"conditions": ["match roles authp/admin authp/user"],
|
||||
"action": "allow log debug"
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
"match roles authp/guest",
|
||||
"match method GET",
|
||||
"partial match path /foo"
|
||||
],
|
||||
"action": "allow log debug"
|
||||
},
|
||||
{
|
||||
"conditions": ["always match origin any"],
|
||||
"action": "allow log debug"
|
||||
},
|
||||
{
|
||||
"conditions": ["match iss foo"],
|
||||
"action": "deny stop log warn"
|
||||
}
|
||||
],
|
||||
"validate_method_path": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test valid authorization policy with custom acl",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
acl rule {
|
||||
match roles authp/admin authp/user
|
||||
allow stop log info
|
||||
}
|
||||
acl default deny
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"access_list_rules": [
|
||||
{
|
||||
"conditions": ["match roles authp/admin authp/user"],
|
||||
"action": "allow stop log info"
|
||||
},
|
||||
{
|
||||
"conditions": ["always match iss any"],
|
||||
"action": "deny"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test malformed authorization policy definition",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy foo {
|
||||
bypass uri /foo/bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: Wrong argument count or unexpected line ending after '%s'", tf, 3, "foo"),
|
||||
},
|
||||
{
|
||||
name: "test unsupported authorization policy keyword",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
foo bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: errors.ErrMalformedDirective.WithArgs(
|
||||
mkcp(authzPrefix, "policy", "foo"),
|
||||
[]string{"bar"},
|
||||
),
|
||||
},
|
||||
// Authorization header injection.
|
||||
{
|
||||
name: "test authorization policy injection with unsupported directive",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
inject foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: unsupported directive for security.authorization.policy.inject: %v", tf, 4, "foo"),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy header injection with too many args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
inject header bar baz foo bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.inject directive %q is invalid", tf, 4, "header bar baz foo bar"),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy header injection with bad syntax",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
inject header "X-Picture" foo picture
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.inject directive %q has invalid syntax", tf, 4, "header X-Picture foo picture"),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy injection without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
inject
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.inject directive has no value", tf, 4),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy injection without empty args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
inject header "X-Picture" from " "
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.inject %s erred: undefined field name", tf, 4, "header X-Picture from \" \""),
|
||||
},
|
||||
// Enable features.
|
||||
{
|
||||
name: "test authorization policy enable without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
enable
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.enable directive has no value", tf, 4),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy injection with unsupported directive",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
enable foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: unsupported directive for security.authorization.policy.enable: %v", tf, 4, "foo"),
|
||||
},
|
||||
// Validate features.
|
||||
{
|
||||
name: "test authorization policy validate without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
validate
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.validate directive has no value", tf, 4),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy validate with unsupported directive",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
validate foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: unsupported directive for security.authorization.policy.validate: %v", tf, 4, "foo"),
|
||||
},
|
||||
// Disabled features.
|
||||
{
|
||||
name: "test authorization policy disable without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
disable
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.disable directive has no value", tf, 4),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy disable with unsupported directive",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
disable foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: unsupported directive for security.authorization.policy.disable: %v", tf, 4, "foo"),
|
||||
},
|
||||
// Configure features.
|
||||
{
|
||||
name: "test authorization policy set without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
set
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.set directive has no value", tf, 4),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy set with unsupported directive",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
set foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: unsupported directive for security.authorization.policy.set: %v", tf, 4, "foo"),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy set redirect status success",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
set redirect status 200
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.set %v directive contains invalid value", tf, 4, "redirect status 200"),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy set redirect status alphanumeric",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
set redirect status foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.set %v directive failed: %v",
|
||||
tf, 4, "redirect status foo", "strconv.Atoi: parsing \"foo\": invalid syntax"),
|
||||
},
|
||||
// With features.
|
||||
{
|
||||
name: "test authorization policy with without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
with
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: security.authorization.policy.with directive has no value", tf, 4),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy with with unsupported directive",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
with foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: unsupported directive for security.authorization.policy.with: %v", tf, 4, "foo"),
|
||||
},
|
||||
// Crypto errors.
|
||||
{
|
||||
name: "test authorization policy crypto with too little args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
crypto foo bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: %v", tf, 4,
|
||||
errors.ErrConfigDirectiveShort.WithArgs(
|
||||
"security.authorization.policy.crypto",
|
||||
[]string{"foo", "bar"},
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy crypto with unsupported args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
crypto foo bar baz
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: %v", tf, 4,
|
||||
errors.ErrConfigDirectiveValueUnsupported.WithArgs(
|
||||
"security.authorization.policy.crypto",
|
||||
[]string{"foo", "bar", "baz"},
|
||||
),
|
||||
),
|
||||
},
|
||||
// Bypass errors.
|
||||
{
|
||||
name: "test authorization policy bypass without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
bypass
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.bypass directive has no value",
|
||||
tf, 4,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy bypass with wrong args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
bypass foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.bypass %s is invalid",
|
||||
tf, 4, "foo",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy bypass with invalid keyword",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
bypass foo bar baz
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.bypass %s is invalid",
|
||||
tf, 4, "foo bar baz",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy bypass with invalid syntax",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
bypass uri bar baz
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.bypass %s erred: %v",
|
||||
tf, 4, "uri bar baz", "invalid \"bar\" bypass match type",
|
||||
),
|
||||
},
|
||||
// ACL errors.
|
||||
{
|
||||
name: "test authorization policy acl without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
acl
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.acl directive has no value",
|
||||
tf, 4,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy acl rule with args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
acl rule foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.acl directive %q is too long",
|
||||
tf, 4, "rule foo",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy acl default with args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
acl default allow bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.acl directive %q is too long",
|
||||
tf, 4, "default allow bar",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy acl default with args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
acl default foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.acl directive %q must have either allow or deny",
|
||||
tf, 4, "default foo",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy acl invalid",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
acl foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.acl directive value of %q is unsupported",
|
||||
tf, 4, "foo",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy acl rule without comment value",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
acl rule {
|
||||
comment
|
||||
}
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.acl rule directive %v has no values",
|
||||
tf, 5, "comment",
|
||||
),
|
||||
},
|
||||
// ACL shortcuts errors.
|
||||
{
|
||||
name: "test authorization policy acl shortcut without args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
allow
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.allow directive has no value",
|
||||
tf, 4,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy acl shortcut without too few args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
allow foo
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.allow directive %q is too short",
|
||||
tf, 4, "foo",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test authorization policy acl shortcut with unsupported args",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
allow roles foo method get to /foo bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf(
|
||||
"%s:%d - Error during parsing: security.authorization.policy.allow directive value of %q is unsupported",
|
||||
tf, 4, "roles foo method get to /foo bar",
|
||||
),
|
||||
},
|
||||
// Post config processing errors.
|
||||
{
|
||||
name: "test authorization invalid keyword",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
authorization foo bar {
|
||||
baz zag
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: errors.ErrMalformedDirective.WithArgs(authzPrefix, []string{"foo", "bar"}),
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app, err := parseCaddyfile(tc.d, nil)
|
||||
if err != nil {
|
||||
if !tc.shouldErr {
|
||||
t.Fatalf("expected success, got: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
|
||||
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tc.shouldErr {
|
||||
t.Fatalf("unexpected success, want: %v", tc.err)
|
||||
}
|
||||
t.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
|
||||
|
||||
got := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
want := unpack(t, tc.want)
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("parseCaddyfileAuthorization() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
72
caddyfile_credentials.go
Normal file
72
caddyfile_credentials.go
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/aaasf"
|
||||
"github.com/greenpau/aaasf/pkg/credentials"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
credPrefix = "security.credentials"
|
||||
)
|
||||
|
||||
// parseCaddyfileCredentials parses credentials configuration.
|
||||
//
|
||||
// Syntax:
|
||||
//
|
||||
// credentials email <label> {
|
||||
// address <uri>
|
||||
// protocol <smtp|pop3|imap>
|
||||
// username <username>
|
||||
// password <password>
|
||||
// }
|
||||
//
|
||||
func parseCaddyfileCredentials(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *aaasf.Config) error {
|
||||
args := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
switch args[0] {
|
||||
case "email":
|
||||
c := &credentials.SMTP{Name: args[1]}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
k := d.Val()
|
||||
v := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
switch k {
|
||||
case "address":
|
||||
c.Address = v[0]
|
||||
case "protocol":
|
||||
c.Protocol = v[0]
|
||||
case "username":
|
||||
c.Username = v[0]
|
||||
case "password":
|
||||
c.Password = v[0]
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs([]string{credPrefix, args[0], k}, v)
|
||||
}
|
||||
}
|
||||
if err := cfg.AddCredential(c); err != nil {
|
||||
return errors.ErrMalformedDirective.WithArgs([]string{credPrefix, args[0], args[1]}, err)
|
||||
}
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs(credPrefix, args)
|
||||
}
|
||||
return nil
|
||||
}
|
144
caddyfile_credentials_test.go
Normal file
144
caddyfile_credentials_test.go
Normal file
@ -0,0 +1,144 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
)
|
||||
|
||||
func TestParseCaddyfileCredentials(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
d *caddyfile.Dispenser
|
||||
want string
|
||||
shouldErr bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "test valid smtp credentials",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
credentials email smtp.contoso.com {
|
||||
address smtp.contoso.com:993
|
||||
protocol smtp
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"credentials": {
|
||||
"email": [
|
||||
{
|
||||
"address": "smtp.contoso.com:993",
|
||||
"name": "smtp.contoso.com",
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"protocol": "smtp"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test malformed credentials definition",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
credentials email smtp.contoso.com foo {
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: fmt.Errorf("%s:%d - Error during parsing: Wrong argument count or unexpected line ending after '%s'", tf, 3, "foo"),
|
||||
},
|
||||
{
|
||||
name: "test unsupported credentials keyword",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
credentials email smtp.contoso.com {
|
||||
foo bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: errors.ErrMalformedDirective.WithArgs(
|
||||
[]string{credPrefix, "email", "foo"},
|
||||
[]string{"bar"},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test smtp credentials without address",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
credentials email smtp.contoso.com {
|
||||
protocol smtp
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: errors.ErrMalformedDirective.WithArgs(
|
||||
[]string{credPrefix, "email", "smtp.contoso.com"},
|
||||
errors.ErrCredKeyValueEmpty.WithArgs("address"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "test unsupported credentials type",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
credentials foo bar {
|
||||
protocol smtp
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
}`),
|
||||
shouldErr: true,
|
||||
err: errors.ErrMalformedDirective.WithArgs(
|
||||
credPrefix,
|
||||
[]string{"foo", "bar"},
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app, err := parseCaddyfile(tc.d, nil)
|
||||
if err != nil {
|
||||
if !tc.shouldErr {
|
||||
t.Fatalf("expected success, got: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
|
||||
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tc.shouldErr {
|
||||
t.Fatalf("unexpected success, want: %v", tc.err)
|
||||
}
|
||||
got := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
want := unpack(t, tc.want)
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("parseCaddyfileCredentials() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
106
caddyfile_test.go
Normal file
106
caddyfile_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
const tf string = "Testfile"
|
||||
|
||||
func TestParseCaddyfileAppConfig(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
d *caddyfile.Dispenser
|
||||
want string
|
||||
shouldErr bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "test email credentials",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
credentials email smtp.contoso.com {
|
||||
address smtp.contoso.com:993
|
||||
protocol smtp
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"credentials": {
|
||||
"email": [
|
||||
{
|
||||
"address": "smtp.contoso.com:993",
|
||||
"name": "smtp.contoso.com",
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"protocol": "smtp"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app, err := parseCaddyfile(tc.d, nil)
|
||||
if err != nil {
|
||||
if !tc.shouldErr {
|
||||
t.Fatalf("expected success, got: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
|
||||
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tc.shouldErr {
|
||||
t.Fatalf("unexpected success, want: %v", tc.err)
|
||||
}
|
||||
|
||||
got := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
want := unpack(t, tc.want)
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("parseCaddyfileAppConfig() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func unpack(t *testing.T, i interface{}) (m map[string]interface{}) {
|
||||
switch v := i.(type) {
|
||||
case string:
|
||||
if err := json.Unmarshal([]byte(v), &m); err != nil {
|
||||
t.Fatalf("failed to parse %q: %v", v, err)
|
||||
}
|
||||
default:
|
||||
b, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal %T: %v", i, err)
|
||||
}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
t.Fatalf("failed to parse %q: %v", b, err)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
13
go.mod
Normal file
13
go.mod
Normal file
@ -0,0 +1,13 @@
|
||||
module github.com/greenpau/caddy-security
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/caddyserver/caddy/v2 v2.4.6
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/greenpau/aaasf v1.0.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
go.uber.org/zap v1.20.0
|
||||
)
|
||||
|
||||
replace github.com/greenpau/aaasf v1.0.1 => /home/greenpau/dev/go/src/github.com/greenpau/aaasf
|
86
pkg/authentication/caddyfile.go
Normal file
86
pkg/authentication/caddyfile.go
Normal file
@ -0,0 +1,86 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
httpcaddyfile.RegisterDirective("authenticate", getRouteFromParseCaddyfile)
|
||||
}
|
||||
|
||||
func getRouteFromParseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
|
||||
a, err := parseCaddyfile(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pathMatcher := caddy.ModuleMap{
|
||||
"path": h.JSON(caddyhttp.MatchPath{a.Path}),
|
||||
}
|
||||
|
||||
route := caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(
|
||||
&Middleware{
|
||||
Authenticator: a,
|
||||
},
|
||||
"handler",
|
||||
"authenticator",
|
||||
nil,
|
||||
),
|
||||
},
|
||||
}
|
||||
subroute := new(caddyhttp.Subroute)
|
||||
subroute.Routes = append([]caddyhttp.Route{route}, subroute.Routes...)
|
||||
return h.NewRoute(pathMatcher, subroute), nil
|
||||
}
|
||||
|
||||
// parseCaddyfile parses authentication plugin configuration.
|
||||
//
|
||||
// Syntax:
|
||||
//
|
||||
// authenticate [<matcher>] with <portal_name>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authenticate with myportal
|
||||
// authenticate * with myportal
|
||||
// authenticate /* with myportal
|
||||
// authenticate /auth* with myportal
|
||||
//
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (*authn.Authenticator, error) {
|
||||
var i int
|
||||
repl := caddy.NewReplacer()
|
||||
args := util.FindReplaceAll(repl, h.RemainingArgs())
|
||||
a := &authn.Authenticator{}
|
||||
if args[0] != "authenticate" {
|
||||
return nil, h.Errf("directive should start with authenticate: %s", args)
|
||||
}
|
||||
|
||||
switch len(args) {
|
||||
case 3:
|
||||
i = 1
|
||||
a.Path = "*"
|
||||
a.PortalName = args[2]
|
||||
case 4:
|
||||
i = 2
|
||||
a.Path = args[1]
|
||||
a.PortalName = args[3]
|
||||
default:
|
||||
return nil, h.Errf("malformed directive: %s", args)
|
||||
}
|
||||
|
||||
if args[0] != "authenticate" {
|
||||
return nil, h.Errf("directive should start with authenticate: %s", args)
|
||||
}
|
||||
if args[i] != "with" {
|
||||
return nil, h.Errf("directive must contain %q keyword: %s", "with", args)
|
||||
}
|
||||
return a, nil
|
||||
}
|
84
pkg/authentication/plugin.go
Normal file
84
pkg/authentication/plugin.go
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2020 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/greenpau/aaasf/pkg/authn"
|
||||
"github.com/greenpau/aaasf/pkg/requests"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginName = "authenticator"
|
||||
)
|
||||
|
||||
//func init() {
|
||||
// caddy.RegisterModule(Middleware{})
|
||||
//}
|
||||
|
||||
// Middleware implements Form-Based, Basic, Local, LDAP,
|
||||
// OpenID Connect, OAuth 2.0, SAML Authentication.
|
||||
type Middleware struct {
|
||||
Authenticator *authn.Authenticator `json:"authenticator,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (Middleware) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.handlers." + pluginName,
|
||||
New: func() caddy.Module { return new(Middleware) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision provisions Authenticator.
|
||||
func (m *Middleware) Provision(ctx caddy.Context) error {
|
||||
return m.Authenticator.Provision(ctx.Logger(m))
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile unmarshals a caddyfile.
|
||||
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
|
||||
a, err := parseCaddyfile(httpcaddyfile.Helper{Dispenser: d})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Authenticator = a
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate implements caddy.Validator.
|
||||
func (m *Middleware) Validate() error {
|
||||
return m.Authenticator.Validate()
|
||||
}
|
||||
|
||||
// ServeHTTP serves authentication portal.
|
||||
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
|
||||
rr := requests.NewRequest()
|
||||
rr.ID = util.GetRequestID(r)
|
||||
return m.Authenticator.ServeHTTP(r.Context(), w, r, rr)
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*Middleware)(nil)
|
||||
_ caddy.Validator = (*Middleware)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
|
||||
_ caddyfile.Unmarshaler = (*Middleware)(nil)
|
||||
)
|
72
pkg/authorization/caddyfile.go
Normal file
72
pkg/authorization/caddyfile.go
Normal file
@ -0,0 +1,72 @@
|
||||
package authorization
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
httpcaddyfile.RegisterHandlerDirective("authorize", getMiddlewareFromParseCaddyfile)
|
||||
}
|
||||
|
||||
func getMiddlewareFromParseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
a, err := parseCaddyfile(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return caddyauth.Authentication{
|
||||
ProvidersRaw: caddy.ModuleMap{
|
||||
"authorizer": caddyconfig.JSON(Middleware{Authorizer: a}, nil),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseCaddyfile parses authorization plugin configuration.
|
||||
//
|
||||
// Syntax:
|
||||
//
|
||||
// authorize [<matcher>] with <policy_name>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorize with mypolicy
|
||||
// authorize * with mypolicy
|
||||
// authorize /* with mypolicy
|
||||
// authorize /app* with mypolicy
|
||||
//
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (*authz.Authorizer, error) {
|
||||
var i int
|
||||
repl := caddy.NewReplacer()
|
||||
args := util.FindReplaceAll(repl, h.RemainingArgs())
|
||||
a := &authz.Authorizer{}
|
||||
if args[0] != "authorize" {
|
||||
return nil, h.Errf("directive should start with authorize: %s", args)
|
||||
}
|
||||
|
||||
switch len(args) {
|
||||
case 3:
|
||||
i = 1
|
||||
a.Path = "*"
|
||||
a.GatekeeperName = args[2]
|
||||
case 4:
|
||||
i = 2
|
||||
a.Path = args[1]
|
||||
a.GatekeeperName = args[3]
|
||||
default:
|
||||
return nil, h.Errf("malformed directive: %s", args)
|
||||
}
|
||||
|
||||
if args[0] != "authorize" {
|
||||
return nil, h.Errf("directive should start with authorize: %s", args)
|
||||
}
|
||||
if args[i] != "with" {
|
||||
return nil, h.Errf("directive must contain %q keyword: %s", "with", args)
|
||||
}
|
||||
return a, nil
|
||||
}
|
107
pkg/authorization/plugin.go
Normal file
107
pkg/authorization/plugin.go
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright 2020 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package authorization
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth"
|
||||
"github.com/greenpau/aaasf/pkg/authz"
|
||||
"github.com/greenpau/aaasf/pkg/errors"
|
||||
"github.com/greenpau/aaasf/pkg/requests"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginName = "authorizer"
|
||||
)
|
||||
|
||||
// func init() {
|
||||
// caddy.RegisterModule(Middleware{})
|
||||
// }
|
||||
|
||||
// Middleware authorizes access to endpoints based on
|
||||
// the presense and content of JWT token.
|
||||
type Middleware struct {
|
||||
Authorizer *authz.Authorizer `json:"authorizer,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (Middleware) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.authentication.providers." + pluginName,
|
||||
New: func() caddy.Module { return new(Middleware) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision provisions Authorizer.
|
||||
func (m *Middleware) Provision(ctx caddy.Context) error {
|
||||
return m.Authorizer.Provision(ctx.Logger(m))
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile unmarshals caddyfile.
|
||||
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
|
||||
a, err := parseCaddyfile(httpcaddyfile.Helper{Dispenser: d})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Authorizer = a
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate implements caddy.Validator.
|
||||
func (m *Middleware) Validate() error {
|
||||
return m.Authorizer.Validate()
|
||||
}
|
||||
|
||||
// Authenticate authorizes access based on the presense and content of
|
||||
// authorization token.
|
||||
func (m Middleware) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.User, bool, error) {
|
||||
rr := requests.NewAuthorizationRequest()
|
||||
rr.ID = util.GetRequestID(r)
|
||||
if err := m.Authorizer.Authenticate(w, r, rr); err != nil {
|
||||
return caddyauth.User{}, false, errors.ErrAuthorizationFailed
|
||||
}
|
||||
|
||||
if rr.Response.User == nil {
|
||||
return caddyauth.User{}, false, errors.ErrAuthorizationFailed
|
||||
}
|
||||
|
||||
u := caddyauth.User{
|
||||
Metadata: map[string]string{
|
||||
"roles": rr.Response.User["roles"].(string),
|
||||
},
|
||||
}
|
||||
if v, exists := rr.Response.User["id"]; exists {
|
||||
u.ID = v.(string)
|
||||
}
|
||||
for _, k := range []string{"claim_id", "sub", "email", "name"} {
|
||||
if v, exists := rr.Response.User[k]; exists {
|
||||
u.Metadata[k] = v.(string)
|
||||
}
|
||||
}
|
||||
return u, rr.Response.Authorized, nil
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*Middleware)(nil)
|
||||
_ caddy.Validator = (*Middleware)(nil)
|
||||
_ caddyauth.Authenticator = (*Middleware)(nil)
|
||||
_ caddyfile.Unmarshaler = (*Middleware)(nil)
|
||||
)
|
50
pkg/util/util.go
Normal file
50
pkg/util/util.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2022 Paul Greenberg greenpau@outlook.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/greenpau/aaasf/pkg/util/cfg"
|
||||
"github.com/satori/go.uuid"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FindReplaceAll uses caddy.Replacer to replace strings in a given slice.
|
||||
func FindReplaceAll(repl *caddy.Replacer, arr []string) (output []string) {
|
||||
for _, item := range arr {
|
||||
output = append(output, repl.ReplaceAll(item, cfg.ReplErrStr))
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// FindReplace uses caddy.Replacer to replace strings in a given string.
|
||||
func FindReplace(repl *caddy.Replacer, s string) string {
|
||||
return repl.ReplaceAll(s, cfg.ReplErrStr)
|
||||
}
|
||||
|
||||
// GetRequestID returns HTTP request id.
|
||||
func GetRequestID(r *http.Request) string {
|
||||
rawRequestID := caddyhttp.GetVar(r.Context(), "request_id")
|
||||
if rawRequestID == nil {
|
||||
requestID := r.Header.Get("X-Request-Id")
|
||||
if requestID == "" {
|
||||
requestID = uuid.NewV4().String()
|
||||
}
|
||||
caddyhttp.SetVar(r.Context(), "request_id", requestID)
|
||||
return requestID
|
||||
}
|
||||
return rawRequestID.(string)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user