1
0
mirror of https://github.com/greenpau/caddy-security.git synced 2025-04-18 08:04:02 +03:00
This commit is contained in:
Paul Greenberg 2022-01-20 22:05:25 -05:00
parent bed2af02ab
commit fd5c591066
44 changed files with 5719 additions and 6 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [greenpau]

12
.github/ISSUE_TEMPLATE/ask-question.md vendored Normal file
View 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
View 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.

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,12 @@
---
reviewers:
- greenpau
approvers:
- greenpau
features:
- comments
- reviewers
- aliases
- branches

View File

@ -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
}
```

1
VERSION Normal file
View File

@ -0,0 +1 @@
1.0.0

160
app.go Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

80
caddyfile.go Normal file
View 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
View 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: &registration.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, ".")
}

View 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
View 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
View 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
View 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
View 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
}

View 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
View 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)
}
})
}
}

View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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

1620
go.sum Normal file

File diff suppressed because it is too large Load Diff

View 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
}

View 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)
)

View 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
View 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
View 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)
}