mirror of
https://github.com/greenpau/caddy-security.git
synced 2025-04-18 08:04:02 +03:00
upgrade to github.com/greenpau/go-authcrunc v1.0.20
More info: * split backends to identity stores and providers * oauth: use first available key to validate token when kid not found (greenpau/caddy-security#77)
This commit is contained in:
parent
e3ef04c4b4
commit
44a1711ec0
@ -81,10 +81,10 @@ module github.com/greenpau/caddy-security
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/greenpau/go-authcrunch v1.0.19
|
||||
github.com/greenpau/go-authcrunch v1.0.20
|
||||
)
|
||||
|
||||
replace github.com/greenpau/go-authcrunch v1.0.19 => /home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
|
||||
replace github.com/greenpau/go-authcrunch v1.0.20 => /home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
|
||||
```
|
||||
|
||||
Then, modify `Makefile` such that that replacement passes to `xcaddy` builder:
|
||||
@ -93,7 +93,7 @@ Then, modify `Makefile` such that that replacement passes to `xcaddy` builder:
|
||||
@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/go-authcrunch@v1.0.19=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
|
||||
--with github.com/greenpau/go-authcrunch@v1.0.20=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
|
||||
```
|
||||
|
||||
Once all the necessary packages are installed, you should be ready to compile
|
||||
|
6
Makefile
6
Makefile
@ -17,7 +17,7 @@ all: info
|
||||
xcaddy build $(CADDY_VERSION) --output ../$(PLUGIN_NAME)/bin/caddy \
|
||||
--with github.com/greenpau/caddy-security@$(LATEST_GIT_COMMIT)=$(BUILD_DIR) \
|
||||
--with github.com/greenpau/caddy-trace@v1.1.8
|
||||
@#--with github.com/greenpau/go-authcrunch@v1.0.19=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
|
||||
@#--with github.com/greenpau/go-authcrunch@v1.0.20=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
|
||||
@#bin/caddy run -config assets/config/Caddyfile
|
||||
@for f in `find ./assets -type f -name 'Caddyfile'`; do bin/caddy fmt -overwrite $$f; done
|
||||
|
||||
@ -68,8 +68,10 @@ 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 TestParseCaddyfileIdentity ./*.go
|
||||
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileCredentials ./*.go
|
||||
@time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileMessaging ./*.go
|
||||
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileMessaging ./*.go
|
||||
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileIdentit* ./*.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
|
||||
|
118
app.go
118
app.go
@ -37,17 +37,15 @@ func init() {
|
||||
|
||||
// App implements security manager.
|
||||
type App struct {
|
||||
Name string `json:"-"`
|
||||
Config *authcrunch.Config `json:"config,omitempty"`
|
||||
logger *zap.Logger
|
||||
portals []*authn.Portal
|
||||
gatekeepers []*authz.Gatekeeper
|
||||
Name string `json:"-"`
|
||||
Config *authcrunch.Config `json:"config,omitempty"`
|
||||
server *authcrunch.Server
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (App) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
// ID: caddy.ModuleID("security"),
|
||||
ID: "security",
|
||||
New: func() caddy.Module { return new(App) },
|
||||
}
|
||||
@ -63,60 +61,15 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
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 initializing auth portal",
|
||||
zap.String("app", app.Name),
|
||||
zap.String("portal_name", cfg.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
if app.Config.Credentials != nil {
|
||||
portal.SetCredentials(app.Config.Credentials)
|
||||
}
|
||||
|
||||
if app.Config.Messaging != nil {
|
||||
portal.SetMessaging(app.Config.Messaging)
|
||||
}
|
||||
|
||||
if err := portal.Register(); err != nil {
|
||||
app.logger.Error(
|
||||
"failed registering auth portal",
|
||||
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 initializing gatekeeper",
|
||||
zap.String("app", app.Name),
|
||||
zap.String("gatekeeper_name", cfg.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
if err := gatekeeper.Register(); err != nil {
|
||||
app.logger.Error(
|
||||
"failed registering gatekeeper",
|
||||
zap.String("app", app.Name),
|
||||
zap.String("gatekeeper_name", cfg.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
app.gatekeepers = append(app.gatekeepers, gatekeeper)
|
||||
server, err := authcrunch.NewServer(app.Config, app.logger)
|
||||
if err != nil {
|
||||
app.logger.Error(
|
||||
"failed provisioning app server instance",
|
||||
zap.String("app", app.Name),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
app.server = server
|
||||
|
||||
app.logger.Info(
|
||||
"provisioned app instance",
|
||||
@ -127,57 +80,26 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (app *App) getPortal(s string) (*authn.Portal, error) {
|
||||
return app.server.GetPortalByName(s)
|
||||
}
|
||||
|
||||
func (app *App) getGatekeeper(s string) (*authz.Gatekeeper, error) {
|
||||
return app.server.GetGatekeeperByName(s)
|
||||
}
|
||||
|
@ -18,10 +18,15 @@
|
||||
bcc greenpau@localhost
|
||||
}
|
||||
|
||||
local identity store localdb {
|
||||
realm local
|
||||
path assets/config/users.json
|
||||
}
|
||||
|
||||
authentication portal myportal {
|
||||
crypto default token lifetime 3600
|
||||
crypto key sign-verify 01ee2688-36e4-47f9-8c06-d18483702520
|
||||
backend local assets/config/users.json local
|
||||
enable identity store localdb
|
||||
ui {
|
||||
links {
|
||||
"My Website" "/app" icon "las la-star"
|
||||
|
@ -36,6 +36,8 @@ func init() {
|
||||
//
|
||||
// security {
|
||||
// credentials ...
|
||||
// identity store <name>
|
||||
// [saml|oauth] identity provider <name>
|
||||
// authentication ...
|
||||
// authorization ...
|
||||
// }
|
||||
@ -60,6 +62,10 @@ func parseCaddyfile(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
|
||||
if err := parseCaddyfileMessaging(d, repl, app.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "local", "ldap", "oauth", "saml":
|
||||
if err := parseCaddyfileIdentity(d, repl, app.Config, tld); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "authentication":
|
||||
if err := parseCaddyfileAuthentication(d, repl, app.Config); err != nil {
|
||||
return nil, err
|
||||
|
@ -15,6 +15,7 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
@ -38,41 +39,6 @@ const (
|
||||
//
|
||||
// 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 {
|
||||
@ -106,11 +72,14 @@ const (
|
||||
//
|
||||
// enable source ip tracking
|
||||
// enable admin api
|
||||
// enable identity store <name>
|
||||
// enable identity provider <name>
|
||||
// }
|
||||
//
|
||||
func parseCaddyfileAuthentication(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config) error {
|
||||
// rootDirective is config key prefix.
|
||||
var rootDirective string
|
||||
backendHelpURL := "https://github.com/greenpau/caddy-security/issues/83"
|
||||
args := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
if len(args) != 2 {
|
||||
return d.ArgErr()
|
||||
@ -140,14 +109,8 @@ func parseCaddyfileAuthentication(d *caddyfile.Dispenser, repl *caddy.Replacer,
|
||||
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 "backend", "backends":
|
||||
return fmt.Errorf("The backend directive is no longer supported. Please see %s for details", backendHelpURL)
|
||||
case "ui":
|
||||
if err := parseCaddyfileAuthPortalUI(d, repl, p, rootDirective); err != nil {
|
||||
return err
|
||||
|
@ -1,89 +0,0 @@
|
||||
// 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/caddy-security/pkg/util"
|
||||
"github.com/greenpau/go-authcrunch/pkg/authn"
|
||||
"github.com/greenpau/go-authcrunch/pkg/authn/backends"
|
||||
"github.com/greenpau/go-authcrunch/pkg/errors"
|
||||
)
|
||||
|
||||
func parseCaddyfileAuthPortalBackendShortcuts(h *caddyfile.Dispenser, repl *caddy.Replacer, portal *authn.PortalConfig, ckp string, args []string) error {
|
||||
v := util.FindReplaceAll(repl, args)
|
||||
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{"read: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
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
// 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/caddy-security/pkg/util"
|
||||
"github.com/greenpau/go-authcrunch/pkg/authn"
|
||||
"github.com/greenpau/go-authcrunch/pkg/authn/backends"
|
||||
"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":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
cfg["bind_username"] = util.FindReplace(repl, h.Val())
|
||||
case "password":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
cfg["bind_password"] = util.FindReplace(repl, h.Val())
|
||||
case "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 "required_token_fields":
|
||||
if !h.NextArg() {
|
||||
return backendValueErr(h, backendName, backendArg)
|
||||
}
|
||||
cfg[backendArg] = util.FindReplaceAll(repl, h.RemainingArgs())
|
||||
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", "idp_login_url",
|
||||
"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", "response_type":
|
||||
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 "scope":
|
||||
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":
|
||||
case "js_callback":
|
||||
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)
|
||||
}
|
@ -26,14 +26,30 @@ func parseCaddyfileAuthPortalMisc(h *caddyfile.Dispenser, repl *caddy.Replacer,
|
||||
v = strings.TrimSpace(v)
|
||||
switch k {
|
||||
case "enable":
|
||||
switch v {
|
||||
case "source ip tracking":
|
||||
switch {
|
||||
case v == "source ip tracking":
|
||||
portal.TokenGrantorOptions.EnableSourceAddress = true
|
||||
case "admin api":
|
||||
case v == "admin api":
|
||||
if portal.API == nil {
|
||||
portal.API = &authn.APIConfig{}
|
||||
portal.API.Enabled = true
|
||||
}
|
||||
case strings.HasPrefix(v, "identity provider"):
|
||||
if len(args) < 3 {
|
||||
return h.Errf("malformed directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
for _, providerName := range args[2:] {
|
||||
portal.IdentityProviders = append(portal.IdentityProviders, providerName)
|
||||
}
|
||||
return nil
|
||||
case strings.HasPrefix(v, "identity store"):
|
||||
if len(args) < 3 {
|
||||
return h.Errf("malformed directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
for _, storeName := range args[2:] {
|
||||
portal.IdentityStores = append(portal.IdentityStores, storeName)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return h.Errf("unsupported directive for %s: %s", rootDirective, v)
|
||||
}
|
||||
|
@ -36,10 +36,10 @@ func TestParseCaddyfileAuthentication(t *testing.T) {
|
||||
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 {
|
||||
@ -61,255 +61,205 @@ func TestParseCaddyfileAuthentication(t *testing.T) {
|
||||
}
|
||||
enable source ip tracking
|
||||
validate source address
|
||||
enable identity provider contoso.com example.com
|
||||
enable identity provider azure okta
|
||||
}
|
||||
|
||||
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 identity store contoso.com {
|
||||
realm contoso.com
|
||||
servers {
|
||||
ldaps://ldaps.contoso.com ignore_cert_errors
|
||||
}
|
||||
attributes {
|
||||
name givenName
|
||||
surname sn
|
||||
username sAMAccountName
|
||||
member_of memberOf
|
||||
email mail
|
||||
}
|
||||
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
|
||||
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 identity store example.com {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
saml identity provider azure {
|
||||
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
|
||||
}
|
||||
|
||||
oauth identity provider okta {
|
||||
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"
|
||||
"config": {
|
||||
"authentication_portals": [
|
||||
{
|
||||
"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_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": {
|
||||
"user_registration_config": {
|
||||
"title": "User Registration",
|
||||
"code": "NY2020",
|
||||
"dropbox": "assets/config/registrations.json",
|
||||
"require_accept_terms": true,
|
||||
"require_domain_mx": true
|
||||
},
|
||||
"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": {
|
||||
"domains": {
|
||||
"contoso.com": {
|
||||
"seq": 1,
|
||||
"contoso.com": {
|
||||
"seq": 1,
|
||||
"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",
|
||||
"bind_password": "P@ssW0rd123",
|
||||
"bind_username": "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=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"
|
||||
},
|
||||
"bind_password": "password",
|
||||
"bind_username": "cn=read-only-admin,dc=example,dc=com",
|
||||
"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": {
|
||||
},
|
||||
"identity_providers": [
|
||||
"contoso.com",
|
||||
"example.com",
|
||||
"azure",
|
||||
"okta"
|
||||
],
|
||||
"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": {
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
}
|
||||
],
|
||||
"identity_stores": [
|
||||
{
|
||||
"name": "contoso.com",
|
||||
"kind": "ldap",
|
||||
"params": {
|
||||
"attributes": {
|
||||
"email": "mail",
|
||||
"member_of": "memberOf",
|
||||
"name": "givenName",
|
||||
"surname": "sn",
|
||||
"username": "sAMAccountName"
|
||||
},
|
||||
"bind_password": "P@ssW0rd123",
|
||||
"bind_username": "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test malformed authentication portal definition",
|
||||
@ -386,12 +336,12 @@ func TestParseCaddyfileAuthentication(t *testing.T) {
|
||||
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.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
|
||||
t.Errorf("parseCaddyfileAuthentication() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
@ -44,7 +44,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
"authorization_policies": [
|
||||
{
|
||||
"auth_url_path": "/auth",
|
||||
"auth_redirect_query_param": "redirect_url",
|
||||
@ -98,7 +98,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
"authorization_policies": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"auth_url_path": "/auth",
|
||||
@ -182,7 +182,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
"authorization_policies": [
|
||||
{
|
||||
"name": "mypolicy2",
|
||||
"auth_url_path": "/auth",
|
||||
@ -250,7 +250,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
"authorization_policies": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"auth_url_path": "/auth",
|
||||
@ -298,7 +298,7 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
"authorization_policies": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"auth_url_path": "/auth",
|
||||
@ -325,17 +325,26 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
enable login hint
|
||||
allow roles authp/admin authp/user
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
"authorization_policies": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"auth_url_path": "/auth",
|
||||
"auth_redirect_query_param": "redirect_url",
|
||||
"auth_redirect_status_code": 302,
|
||||
"login_hint_validators": ["email", "phone", "alphanumeric"]
|
||||
"login_hint_validators": ["email", "phone", "alphanumeric"],
|
||||
"access_list_rules": [
|
||||
{
|
||||
"conditions": [
|
||||
"match roles authp/admin authp/user"
|
||||
],
|
||||
"action": "allow log debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -347,17 +356,26 @@ func TestParseCaddyfileAuthorization(t *testing.T) {
|
||||
security {
|
||||
authorization policy mypolicy {
|
||||
enable login hint with email phone
|
||||
allow roles authp/admin authp/user
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authz_policy_configs": [
|
||||
"authorization_policies": [
|
||||
{
|
||||
"name": "mypolicy",
|
||||
"auth_url_path": "/auth",
|
||||
"auth_redirect_query_param": "redirect_url",
|
||||
"auth_redirect_status_code": 302,
|
||||
"login_hint_validators": ["email", "phone"]
|
||||
"login_hint_validators": ["email", "phone"],
|
||||
"access_list_rules": [
|
||||
{
|
||||
"conditions": [
|
||||
"match roles authp/admin authp/user"
|
||||
],
|
||||
"action": "allow log debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -40,19 +40,24 @@ func TestParseCaddyfileCredentials(t *testing.T) {
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
|
||||
local identity store localdb {
|
||||
realm local
|
||||
path /tmp/localdb
|
||||
}
|
||||
|
||||
authentication portal myportal {
|
||||
enable identity store localdb
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"credentials": {
|
||||
"generic": [
|
||||
{
|
||||
"name": "smtp.contoso.com",
|
||||
"username": "foo",
|
||||
"password": "bar"
|
||||
}
|
||||
]
|
||||
"generic": [
|
||||
{
|
||||
"name": "smtp.contoso.com",
|
||||
"username": "foo",
|
||||
"password": "bar"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
@ -64,20 +69,25 @@ func TestParseCaddyfileCredentials(t *testing.T) {
|
||||
password bar
|
||||
domain contoso.com
|
||||
}
|
||||
|
||||
local identity store localdb {
|
||||
realm local
|
||||
path /tmp/localdb
|
||||
}
|
||||
|
||||
authentication portal myportal {
|
||||
enable identity store localdb
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"credentials": {
|
||||
"generic": [
|
||||
{
|
||||
"name": "smtp.contoso.com",
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"domain": "contoso.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"generic": [
|
||||
{
|
||||
"name": "smtp.contoso.com",
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"domain": "contoso.com"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
@ -150,7 +160,9 @@ func TestParseCaddyfileCredentials(t *testing.T) {
|
||||
if tc.shouldErr {
|
||||
t.Fatalf("unexpected success, want: %v", tc.err)
|
||||
}
|
||||
got := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
fullCfg := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
cfg := fullCfg["config"].(map[string]interface{})
|
||||
got := cfg["credentials"].(map[string]interface{})
|
||||
want := unpack(t, tc.want)
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
|
53
caddyfile_identity.go
Normal file
53
caddyfile_identity.go
Normal file
@ -0,0 +1,53 @@
|
||||
// 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/caddy-security/pkg/util"
|
||||
"github.com/greenpau/go-authcrunch"
|
||||
"github.com/greenpau/go-authcrunch/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
identPrefix = "security.identity"
|
||||
)
|
||||
|
||||
func parseCaddyfileIdentity(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config, kind string) error {
|
||||
args := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
if len(args) < 3 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
switch {
|
||||
case ((kind == "local" || kind == "ldap") && (args[0] == "identity")):
|
||||
if args[1] != "store" {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if err := parseCaddyfileIdentityStore(d, repl, cfg, kind, args[2], args[3:]); err != nil {
|
||||
return err
|
||||
}
|
||||
case ((kind == "oauth" || kind == "saml") && (args[0] == "identity")):
|
||||
if args[1] != "provider" {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if err := parseCaddyfileIdentityProvider(d, repl, cfg, kind, args[2], args[3:]); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs(identPrefix, args)
|
||||
}
|
||||
return nil
|
||||
}
|
182
caddyfile_identity_provider.go
Normal file
182
caddyfile_identity_provider.go
Normal file
@ -0,0 +1,182 @@
|
||||
// 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/caddy-security/pkg/util"
|
||||
"github.com/greenpau/go-authcrunch"
|
||||
"github.com/greenpau/go-authcrunch/pkg/errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseCaddyfileIdentityProvider parses identity provider configuration.
|
||||
//
|
||||
// Syntax:
|
||||
//
|
||||
// oauth identity provider <name> {
|
||||
// realm <name>
|
||||
// driver <name>
|
||||
// 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
|
||||
// }
|
||||
//
|
||||
// oauth identity provider <name> {
|
||||
// realm gitlab
|
||||
// driver gitlab
|
||||
// domain_name <domain>
|
||||
// client_id <client_id>
|
||||
// client_secret <client_secret>
|
||||
// user_group_filters <regex_pattern>
|
||||
// }
|
||||
//
|
||||
// saml identity provider <name> {
|
||||
// realm <name>
|
||||
// driver <name>
|
||||
// }
|
||||
//
|
||||
func parseCaddyfileIdentityProvider(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config, kind, name string, shortcuts []string) error {
|
||||
m := make(map[string]interface{})
|
||||
if len(shortcuts) > 0 {
|
||||
switch kind {
|
||||
case "oauth":
|
||||
switch name {
|
||||
case "github", "google", "facebook":
|
||||
if len(shortcuts) != 2 {
|
||||
return d.Errf("invalid %q shortcut: %v", name, shortcuts)
|
||||
}
|
||||
m["realm"] = name
|
||||
m["driver"] = name
|
||||
m["client_id"] = shortcuts[0]
|
||||
m["client_secret"] = shortcuts[1]
|
||||
default:
|
||||
return d.Errf("unsupported %q shortcut: %v", name, shortcuts)
|
||||
}
|
||||
default:
|
||||
return d.Errf("unsupported %q shortcut for %q provider type: %v", name, kind, shortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
k := d.Val()
|
||||
args := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
rd := mkcp("security."+kind+".identity.provider["+name+"]", k)
|
||||
switch k {
|
||||
case "disabled":
|
||||
return nil
|
||||
case "realm", "driver", "tenant_id",
|
||||
// OAuth
|
||||
"domain_name", "client_id", "client_secret", "server_id", "base_auth_url",
|
||||
"metadata_url", "identity_token_name", "authorization_url", "token_url",
|
||||
// SAML
|
||||
"idp_metadata_location", "idp_sign_cert_location", "idp_login_url",
|
||||
"application_id", "application_name", "entity_id":
|
||||
if len(args) != 1 {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single value")
|
||||
}
|
||||
m[k] = args[0]
|
||||
case "acs_url":
|
||||
// SAML only.
|
||||
if len(args) != 1 {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single value")
|
||||
}
|
||||
var acsURLs []string
|
||||
if v, exists := m["acs_urls"]; exists {
|
||||
acsURLs = v.([]string)
|
||||
}
|
||||
acsURLs = append(acsURLs, args[0])
|
||||
m["acs_urls"] = acsURLs
|
||||
case "scopes", "user_group_filters", "user_org_filters", "response_type":
|
||||
// OAuth only.
|
||||
if v, exists := m[k]; exists {
|
||||
values := v.([]string)
|
||||
values = append(values, args...)
|
||||
m[k] = values
|
||||
} else {
|
||||
m[k] = args
|
||||
}
|
||||
case "delay_start", "retry_attempts", "retry_interval":
|
||||
// OAuth only.
|
||||
if len(args) != 1 {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single value")
|
||||
}
|
||||
i, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, err)
|
||||
}
|
||||
m[k] = i
|
||||
case "disable":
|
||||
// OAuth only.
|
||||
v := strings.Join(args, "_")
|
||||
switch v {
|
||||
case "metadata_discovery", "key_verification", "pass_grant_type",
|
||||
"response_type", "scope", "nonce":
|
||||
m[v+"_disabled"] = true
|
||||
case "tls_verification":
|
||||
m["tls_insecure_skip_verify"] = true
|
||||
default:
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "unsupported value")
|
||||
}
|
||||
case "enable":
|
||||
// OAuth only.
|
||||
v := strings.Join(args, "_")
|
||||
switch v {
|
||||
case "accept_header":
|
||||
case "js_callback":
|
||||
default:
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "unsupported value")
|
||||
}
|
||||
m[v+"_enabled"] = true
|
||||
case "required_token_fields":
|
||||
// OAuth only.
|
||||
if len(args) < 1 {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain one or more values")
|
||||
}
|
||||
m[k] = args
|
||||
case "jwks":
|
||||
if len(args) != 3 {
|
||||
return errors.ErrMalformedDirective.WithArgs(rd, args)
|
||||
}
|
||||
if args[0] != "key" {
|
||||
return errors.ErrMalformedDirective.WithArgs(rd, args)
|
||||
}
|
||||
if v, exists := m["jwks_keys"]; exists {
|
||||
data := v.(map[string]interface{})
|
||||
data[args[1]] = args[2]
|
||||
m["jwks_keys"] = data
|
||||
} else {
|
||||
m["jwks_keys"] = map[string]interface{}{
|
||||
args[1]: args[2],
|
||||
}
|
||||
}
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs(rd, args)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cfg.AddIdentityProvider(name, kind, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
123
caddyfile_identity_provider_test.go
Normal file
123
caddyfile_identity_provider_test.go
Normal file
@ -0,0 +1,123 @@
|
||||
// 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/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCaddyfileIdentityProvider(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
d *caddyfile.Dispenser
|
||||
want string
|
||||
shouldErr bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "test generic oauth identity provider",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
oauth identity provider authp {
|
||||
realm authp
|
||||
driver generic
|
||||
client_id foo
|
||||
client_secret bar
|
||||
base_auth_url https://localhost/oauth
|
||||
response_type code
|
||||
required_token_fields access_token
|
||||
authorization_url https://localhost/oauth/authorize
|
||||
token_url https://localhost/oauth/access_token
|
||||
jwks key 87329db33bf testdata/oauth/87329db33bf_pub.pem
|
||||
disable key verification
|
||||
disable tls verification
|
||||
}
|
||||
authentication portal myportal {
|
||||
enable identity provider authp
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authentication_portals": [
|
||||
{
|
||||
"name": "myportal",
|
||||
"ui": {},
|
||||
"user_registration_config": {},
|
||||
"cookie_config": {},
|
||||
"identity_providers": [
|
||||
"authp"
|
||||
],
|
||||
"token_validator_options": {},
|
||||
"token_grantor_options": {}
|
||||
}
|
||||
],
|
||||
"identity_providers": [
|
||||
{
|
||||
"name": "authp",
|
||||
"kind": "oauth",
|
||||
"params": {
|
||||
"authorization_url": "https://localhost/oauth/authorize",
|
||||
"base_auth_url": "https://localhost/oauth",
|
||||
"client_id": "foo",
|
||||
"client_secret": "bar",
|
||||
"driver": "generic",
|
||||
"jwks_keys": {
|
||||
"87329db33bf": "testdata/oauth/87329db33bf_pub.pem"
|
||||
},
|
||||
"key_verification_disabled": true,
|
||||
"realm": "authp",
|
||||
"required_token_fields": [
|
||||
"access_token"
|
||||
],
|
||||
"response_type": [
|
||||
"code"
|
||||
],
|
||||
"tls_insecure_skip_verify": true,
|
||||
"token_url": "https://localhost/oauth/access_token"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
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.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
|
||||
t.Errorf("parseCaddyfileIdentityProvider() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
147
caddyfile_identity_store.go
Normal file
147
caddyfile_identity_store.go
Normal file
@ -0,0 +1,147 @@
|
||||
// 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/caddy-security/pkg/util"
|
||||
"github.com/greenpau/go-authcrunch"
|
||||
"github.com/greenpau/go-authcrunch/pkg/errors"
|
||||
// "github.com/greenpau/go-authcrunch/pkg/authn"
|
||||
// "github.com/greenpau/go-authcrunch/pkg/authn/backends"
|
||||
// "strconv"
|
||||
// "strings"
|
||||
)
|
||||
|
||||
// parseCaddyfileIdentityStore parses identity store configuration.
|
||||
//
|
||||
// Syntax:
|
||||
//
|
||||
// identity store <name> {
|
||||
// type <local>
|
||||
// file <file_path>
|
||||
// realm <name>
|
||||
// }
|
||||
//
|
||||
func parseCaddyfileIdentityStore(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config, kind, name string, shortcuts []string) error {
|
||||
m := make(map[string]interface{})
|
||||
|
||||
if len(shortcuts) > 0 {
|
||||
switch kind {
|
||||
case "local":
|
||||
if len(shortcuts) != 1 {
|
||||
return d.Errf("invalid %q shortcut: %v", name, shortcuts)
|
||||
}
|
||||
m["realm"] = "local"
|
||||
m["path"] = shortcuts[0]
|
||||
default:
|
||||
return d.Errf("unsupported %q shortcut for %q store type: %v", name, kind, shortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
k := d.Val()
|
||||
args := util.FindReplaceAll(repl, d.RemainingArgs())
|
||||
rd := mkcp("security.identity.store["+name+"]", k)
|
||||
switch k {
|
||||
case "disabled":
|
||||
return nil
|
||||
case "realm",
|
||||
// Local.
|
||||
"path",
|
||||
// LDAP
|
||||
"search_base_dn", "search_group_filter", "search_user_filter",
|
||||
"search_filter", "username", "password":
|
||||
if len(args) != 1 {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single value")
|
||||
}
|
||||
switch k {
|
||||
case "search_filter":
|
||||
m["search_user_filter"] = args[0]
|
||||
case "username":
|
||||
m["bind_username"] = args[0]
|
||||
case "password":
|
||||
m["bind_password"] = args[0]
|
||||
default:
|
||||
m[k] = args[0]
|
||||
}
|
||||
case "trusted_authority":
|
||||
// LDAP only.
|
||||
if len(args) != 1 {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "must contain single path")
|
||||
}
|
||||
var values []string
|
||||
if v, exists := m["trusted_authorities"]; exists {
|
||||
values = v.([]string)
|
||||
}
|
||||
values = append(values, args[0])
|
||||
m["trusted_authorities"] = values
|
||||
case "attributes":
|
||||
// LDAP only.
|
||||
attrMap := make(map[string]interface{})
|
||||
for attrNesting := d.Nesting(); d.NextBlock(attrNesting); {
|
||||
attrName := d.Val()
|
||||
if !d.NextArg() {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "no attribute values found")
|
||||
}
|
||||
attrMap[attrName] = d.Val()
|
||||
}
|
||||
m["attributes"] = attrMap
|
||||
case "servers":
|
||||
// LDAP only.
|
||||
serverMaps := []map[string]interface{}{}
|
||||
for serverNesting := d.Nesting(); d.NextBlock(serverNesting); {
|
||||
serverMap := make(map[string]interface{})
|
||||
serverMap["address"] = d.Val()
|
||||
serverProps := d.RemainingArgs()
|
||||
if len(serverProps) > 0 {
|
||||
for _, serverProp := range serverProps {
|
||||
switch serverProp {
|
||||
case "ignore_cert_errors", "posix_groups":
|
||||
serverMap[serverProp] = true
|
||||
default:
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "unsupported prop "+serverProp)
|
||||
}
|
||||
}
|
||||
}
|
||||
serverMaps = append(serverMaps, serverMap)
|
||||
}
|
||||
m[k] = serverMaps
|
||||
case "groups":
|
||||
// LDAP only.
|
||||
groupMaps := []map[string]interface{}{}
|
||||
for groupNesting := d.Nesting(); d.NextBlock(groupNesting); {
|
||||
groupMap := make(map[string]interface{})
|
||||
groupDN := d.Val()
|
||||
groupMap["dn"] = groupDN
|
||||
groupRoles := d.RemainingArgs()
|
||||
if len(groupRoles) == 0 {
|
||||
return errors.ErrMalformedDirectiveValue.WithArgs(rd, args, "no roles found")
|
||||
}
|
||||
groupMap["roles"] = groupRoles
|
||||
groupMaps = append(groupMaps, groupMap)
|
||||
}
|
||||
m[k] = groupMaps
|
||||
default:
|
||||
return errors.ErrMalformedDirective.WithArgs(rd, args)
|
||||
}
|
||||
}
|
||||
if err := cfg.AddIdentityStore(name, kind, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
191
caddyfile_identity_store_test.go
Normal file
191
caddyfile_identity_store_test.go
Normal file
@ -0,0 +1,191 @@
|
||||
// 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/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCaddyfileIdentityStore(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
d *caddyfile.Dispenser
|
||||
want string
|
||||
shouldErr bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "test authdb identity store",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
local identity store localdb {
|
||||
realm local
|
||||
path /tmp/localdb
|
||||
}
|
||||
authentication portal myportal {
|
||||
enable identity store localdb
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authentication_portals": [
|
||||
{
|
||||
"name": "myportal",
|
||||
"ui": {},
|
||||
"user_registration_config": {},
|
||||
"cookie_config": {},
|
||||
"identity_stores": [
|
||||
"localdb"
|
||||
],
|
||||
"token_validator_options": {},
|
||||
"token_grantor_options": {}
|
||||
}
|
||||
],
|
||||
"identity_stores": [
|
||||
{
|
||||
"name": "localdb",
|
||||
"kind": "local",
|
||||
"params": {
|
||||
"path": "/tmp/localdb",
|
||||
"realm": "local"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "test msad ldap identity store",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
ldap identity store contoso.com {
|
||||
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
|
||||
}
|
||||
}
|
||||
authentication portal myportal {
|
||||
enable identity store contoso.com
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authentication_portals": [
|
||||
{
|
||||
"name": "myportal",
|
||||
"ui": {},
|
||||
"user_registration_config": {},
|
||||
"cookie_config": {},
|
||||
"identity_stores": [
|
||||
"contoso.com"
|
||||
],
|
||||
"token_validator_options": {},
|
||||
"token_grantor_options": {}
|
||||
}
|
||||
],
|
||||
"identity_stores": [
|
||||
{
|
||||
"name": "contoso.com",
|
||||
"kind": "ldap",
|
||||
"params": {
|
||||
"attributes": {
|
||||
"email": "mail",
|
||||
"member_of": "memberOf",
|
||||
"name": "givenName",
|
||||
"surname": "sn",
|
||||
"username": "sAMAccountName"
|
||||
},
|
||||
"bind_password": "P@ssW0rd123",
|
||||
"bind_username": "CN=authzsvc,OU=Service Accounts,OU=Administrative Accounts,DC=CONTOSO,DC=COM",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
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.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
|
||||
t.Errorf("parseCaddyfileIdentityStore() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
158
caddyfile_identity_test.go
Normal file
158
caddyfile_identity_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
// 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/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCaddyfileIdentity(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
d *caddyfile.Dispenser
|
||||
want string
|
||||
shouldErr bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "test authdb identity store and google oauth identity provider",
|
||||
d: caddyfile.NewTestDispenser(`
|
||||
security {
|
||||
#local identity store localdb {
|
||||
# realm local
|
||||
# path /tmp/localdb
|
||||
#}
|
||||
|
||||
local identity store localdb /tmp/localdb
|
||||
|
||||
oauth identity provider github foo bar
|
||||
|
||||
oauth identity provider authp {
|
||||
realm authp
|
||||
driver generic
|
||||
client_id foo
|
||||
client_secret bar
|
||||
base_auth_url https://localhost/oauth
|
||||
response_type code
|
||||
required_token_fields access_token
|
||||
authorization_url https://localhost/oauth/authorize
|
||||
token_url https://localhost/oauth/access_token
|
||||
jwks key 87329db33bf testdata/oauth/87329db33bf_pub.pem
|
||||
disable key verification
|
||||
disable tls verification
|
||||
}
|
||||
|
||||
authentication portal myportal {
|
||||
enable identity store localdb
|
||||
enable identity provider authp github
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"authentication_portals": [
|
||||
{
|
||||
"name": "myportal",
|
||||
"ui": {},
|
||||
"user_registration_config": {},
|
||||
"cookie_config": {},
|
||||
"identity_stores": [
|
||||
"localdb"
|
||||
],
|
||||
"identity_providers": [
|
||||
"authp",
|
||||
"github"
|
||||
],
|
||||
"token_validator_options": {},
|
||||
"token_grantor_options": {}
|
||||
}
|
||||
],
|
||||
"identity_stores": [
|
||||
{
|
||||
"name": "localdb",
|
||||
"kind": "local",
|
||||
"params": {
|
||||
"path": "/tmp/localdb",
|
||||
"realm": "local"
|
||||
}
|
||||
}
|
||||
],
|
||||
"identity_providers": [
|
||||
{
|
||||
"name": "github",
|
||||
"kind": "oauth",
|
||||
"params": {
|
||||
"client_id": "foo",
|
||||
"client_secret": "bar",
|
||||
"driver": "github",
|
||||
"realm": "github"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authp",
|
||||
"kind": "oauth",
|
||||
"params": {
|
||||
"authorization_url": "https://localhost/oauth/authorize",
|
||||
"base_auth_url": "https://localhost/oauth",
|
||||
"client_id": "foo",
|
||||
"client_secret": "bar",
|
||||
"driver": "generic",
|
||||
"jwks_keys": {
|
||||
"87329db33bf": "testdata/oauth/87329db33bf_pub.pem"
|
||||
},
|
||||
"key_verification_disabled": true,
|
||||
"realm": "authp",
|
||||
"required_token_fields": [
|
||||
"access_token"
|
||||
],
|
||||
"response_type": [
|
||||
"code"
|
||||
],
|
||||
"tls_insecure_skip_verify": true,
|
||||
"token_url": "https://localhost/oauth/access_token"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
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.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
|
||||
t.Errorf("parseCaddyfileIdentity() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -51,9 +51,18 @@ func TestParseCaddyfileMessaging(t *testing.T) {
|
||||
template registration_ready path/to/registration_ready.tmpl
|
||||
template registration_verdict path/to/registration_verdict.tmpl
|
||||
template mfa_otp path/to/mfa_otp.tmpl
|
||||
}
|
||||
|
||||
local identity store localdb {
|
||||
realm local
|
||||
path /tmp/localdb
|
||||
}
|
||||
|
||||
authentication portal myportal {
|
||||
enable identity store localdb
|
||||
}
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"credentials": {
|
||||
"generic": [
|
||||
{
|
||||
@ -82,7 +91,6 @@ func TestParseCaddyfileMessaging(t *testing.T) {
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
@ -123,7 +131,15 @@ func TestParseCaddyfileMessaging(t *testing.T) {
|
||||
if tc.shouldErr {
|
||||
t.Fatalf("unexpected success, want: %v", tc.err)
|
||||
}
|
||||
got := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
|
||||
fullCfg := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
cfg := fullCfg["config"].(map[string]interface{})
|
||||
|
||||
got := make(map[string]interface{})
|
||||
for _, k := range []string{"credentials", "messaging"} {
|
||||
got[k] = cfg[k].(map[string]interface{})
|
||||
}
|
||||
|
||||
want := unpack(t, tc.want)
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/greenpau/go-authcrunch/pkg/authn"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
@ -29,20 +28,21 @@ func init() {
|
||||
}
|
||||
|
||||
func getRouteFromParseAuthnPluginCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
|
||||
a, err := parseAuthnPluginCaddyfile(h)
|
||||
m, err := parseAuthnPluginCaddyfile(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pathMatcher := caddy.ModuleMap{
|
||||
"path": h.JSON(caddyhttp.MatchPath{a.Path}),
|
||||
"path": h.JSON(caddyhttp.MatchPath{m["path"]}),
|
||||
}
|
||||
|
||||
route := caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{
|
||||
caddyconfig.JSONModuleObject(
|
||||
&AuthnMiddleware{
|
||||
Authenticator: a,
|
||||
RouteMatcher: m["path"],
|
||||
PortalName: m["portal_name"],
|
||||
},
|
||||
"handler",
|
||||
authnPluginName,
|
||||
@ -68,11 +68,12 @@ func getRouteFromParseAuthnPluginCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyf
|
||||
// authenticate /* with myportal
|
||||
// authenticate /auth* with myportal
|
||||
//
|
||||
func parseAuthnPluginCaddyfile(h httpcaddyfile.Helper) (*authn.Authenticator, error) {
|
||||
func parseAuthnPluginCaddyfile(h httpcaddyfile.Helper) (map[string]string, error) {
|
||||
var i int
|
||||
repl := caddy.NewReplacer()
|
||||
args := util.FindReplaceAll(repl, h.RemainingArgs())
|
||||
a := &authn.Authenticator{}
|
||||
m := make(map[string]string)
|
||||
|
||||
if args[0] != "authenticate" {
|
||||
return nil, h.Errf("directive should start with authenticate: %s", args)
|
||||
}
|
||||
@ -80,12 +81,12 @@ func parseAuthnPluginCaddyfile(h httpcaddyfile.Helper) (*authn.Authenticator, er
|
||||
switch len(args) {
|
||||
case 3:
|
||||
i = 1
|
||||
a.Path = "*"
|
||||
a.PortalName = args[2]
|
||||
m["path"] = "*"
|
||||
m["portal_name"] = args[2]
|
||||
case 4:
|
||||
i = 2
|
||||
a.Path = args[1]
|
||||
a.PortalName = args[3]
|
||||
m["path"] = args[1]
|
||||
m["portal_name"] = args[3]
|
||||
default:
|
||||
return nil, h.Errf("malformed directive: %s", args)
|
||||
}
|
||||
@ -96,5 +97,5 @@ func parseAuthnPluginCaddyfile(h httpcaddyfile.Helper) (*authn.Authenticator, er
|
||||
if args[i] != "with" {
|
||||
return nil, h.Errf("directive must contain %q keyword: %s", "with", args)
|
||||
}
|
||||
return a, nil
|
||||
return m, nil
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"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/go-authcrunch/pkg/authz"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
@ -29,14 +28,20 @@ func init() {
|
||||
}
|
||||
|
||||
func getMiddlewareFromParseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
a, err := parseAuthzPluginCaddyfile(h)
|
||||
m, err := parseAuthzPluginCaddyfile(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return caddyauth.Authentication{
|
||||
ProvidersRaw: caddy.ModuleMap{
|
||||
authzPluginName: caddyconfig.JSON(AuthzMiddleware{Authorizer: a}, nil),
|
||||
authzPluginName: caddyconfig.JSON(
|
||||
AuthzMiddleware{
|
||||
RouteMatcher: m["path"],
|
||||
GatekeeperName: m["gatekeeper_name"],
|
||||
},
|
||||
nil,
|
||||
),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@ -54,24 +59,25 @@ func getMiddlewareFromParseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (caddyht
|
||||
// authorize /* with mypolicy
|
||||
// authorize /app* with mypolicy
|
||||
//
|
||||
func parseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (*authz.Authorizer, error) {
|
||||
func parseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (map[string]string, 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)
|
||||
}
|
||||
|
||||
m := make(map[string]string)
|
||||
|
||||
switch len(args) {
|
||||
case 3:
|
||||
i = 1
|
||||
a.Path = "*"
|
||||
a.GatekeeperName = args[2]
|
||||
m["path"] = "*"
|
||||
m["gatekeeper_name"] = args[2]
|
||||
case 4:
|
||||
i = 2
|
||||
a.Path = args[1]
|
||||
a.GatekeeperName = args[3]
|
||||
m["path"] = args[1]
|
||||
m["gatekeeper_name"] = args[3]
|
||||
default:
|
||||
return nil, h.Errf("malformed directive: %s", args)
|
||||
}
|
||||
@ -82,5 +88,5 @@ func parseAuthzPluginCaddyfile(h httpcaddyfile.Helper) (*authz.Authorizer, error
|
||||
if args[i] != "with" {
|
||||
return nil, h.Errf("directive must contain %q keyword: %s", "with", args)
|
||||
}
|
||||
return a, nil
|
||||
return m, nil
|
||||
}
|
||||
|
@ -41,9 +41,18 @@ func TestParseCaddyfileAppConfig(t *testing.T) {
|
||||
username foo
|
||||
password bar
|
||||
}
|
||||
|
||||
local identity store localdb {
|
||||
realm local
|
||||
path /tmp/localdb
|
||||
}
|
||||
|
||||
authentication portal myportal {
|
||||
enable identity store localdb
|
||||
}
|
||||
|
||||
}`),
|
||||
want: `{
|
||||
"config": {
|
||||
"credentials": {
|
||||
"generic": [
|
||||
{
|
||||
@ -53,7 +62,6 @@ func TestParseCaddyfileAppConfig(t *testing.T) {
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
@ -73,7 +81,14 @@ func TestParseCaddyfileAppConfig(t *testing.T) {
|
||||
t.Fatalf("unexpected success, want: %v", tc.err)
|
||||
}
|
||||
|
||||
got := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
fullCfg := unpack(t, string(app.(httpcaddyfile.App).Value))
|
||||
cfg := fullCfg["config"].(map[string]interface{})
|
||||
|
||||
got := make(map[string]interface{})
|
||||
for _, k := range []string{"credentials"} {
|
||||
got[k] = cfg[k].(map[string]interface{})
|
||||
}
|
||||
|
||||
want := unpack(t, tc.want)
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
|
2
go.mod
2
go.mod
@ -5,7 +5,7 @@ go 1.16
|
||||
require (
|
||||
github.com/caddyserver/caddy/v2 v2.4.6
|
||||
github.com/google/go-cmp v0.5.7
|
||||
github.com/greenpau/go-authcrunch v1.0.19
|
||||
github.com/greenpau/go-authcrunch v1.0.20
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
go.uber.org/zap v1.20.0
|
||||
)
|
||||
|
4
go.sum
4
go.sum
@ -476,8 +476,8 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/greenpau/go-authcrunch v1.0.19 h1:O5qmYJpmQrEmGzOkgU3voZoSrd4OVr7AszpWGX0M6VA=
|
||||
github.com/greenpau/go-authcrunch v1.0.19/go.mod h1:wiUiQW5IPGxX8jAZMnqs/nhMF0DFHQAqEYsb8nU3KEE=
|
||||
github.com/greenpau/go-authcrunch v1.0.20 h1:78rPPqo/56CgBmtsdykn3VhVH2ujEfD1dYHFYY37Pd0=
|
||||
github.com/greenpau/go-authcrunch v1.0.20/go.mod h1:d54vnpcLS68I0YzGX+d2Svv4tKWmAYs5F9vl5ali6uA=
|
||||
github.com/greenpau/versioned v1.0.27 h1:aFJ16tzsUkbc6WT7DRia60S0VrgWzBNuul3h0RXFKxM=
|
||||
github.com/greenpau/versioned v1.0.27/go.mod h1:rtFCvaWWNbMH4CJnje/xicgmrM63j++rUh5juSu0k/A=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
|
@ -22,9 +22,9 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
"github.com/greenpau/go-authcrunch/pkg/authn"
|
||||
"github.com/greenpau/go-authcrunch/pkg/requests"
|
||||
"github.com/greenpau/caddy-security/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -38,7 +38,9 @@ func init() {
|
||||
// AuthnMiddleware implements Form-Based, Basic, Local, LDAP,
|
||||
// OpenID Connect, OAuth 2.0, SAML Authentication.
|
||||
type AuthnMiddleware struct {
|
||||
Authenticator *authn.Authenticator `json:"authenticator,omitempty"`
|
||||
RouteMatcher string `json:"route_matcher,omitempty" xml:"route_matcher,omitempty" yaml:"route_matcher,omitempty"`
|
||||
PortalName string `json:"portal_name,omitempty" xml:"portal_name,omitempty" yaml:"portal_name,omitempty"`
|
||||
portal *authn.Portal
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
@ -56,48 +58,54 @@ func (m *AuthnMiddleware) Provision(ctx caddy.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
secApp := appModule.(*App)
|
||||
if secApp == nil {
|
||||
app := appModule.(*App)
|
||||
if app == nil {
|
||||
return fmt.Errorf("security app is nil")
|
||||
}
|
||||
if secApp.Config == nil {
|
||||
if app.Config == nil {
|
||||
return fmt.Errorf("security app config is nil")
|
||||
}
|
||||
|
||||
var foundRef bool
|
||||
for _, cfg := range secApp.Config.Portals {
|
||||
if cfg.Name == m.Authenticator.PortalName {
|
||||
foundRef = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundRef {
|
||||
return fmt.Errorf("security app has no %q authentication portal", m.Authenticator.PortalName)
|
||||
portal, err := app.getPortal(m.PortalName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("security app erred with %q authentication portal: %v", m.PortalName, err)
|
||||
}
|
||||
m.portal = portal
|
||||
|
||||
return m.Authenticator.Provision(ctx.Logger(m))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile unmarshals a caddyfile.
|
||||
func (m *AuthnMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
|
||||
a, err := parseAuthnPluginCaddyfile(httpcaddyfile.Helper{Dispenser: d})
|
||||
cfg, err := parseAuthnPluginCaddyfile(httpcaddyfile.Helper{Dispenser: d})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Authenticator = a
|
||||
m.RouteMatcher = cfg["path"]
|
||||
m.PortalName = cfg["portal_name"]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate implements caddy.Validator.
|
||||
func (m *AuthnMiddleware) Validate() error {
|
||||
return m.Authenticator.Validate()
|
||||
if m.RouteMatcher == "" {
|
||||
return fmt.Errorf("empty route matcher")
|
||||
}
|
||||
if m.PortalName == "" {
|
||||
return fmt.Errorf("empty portal name")
|
||||
}
|
||||
if m.portal == nil {
|
||||
return fmt.Errorf("portal is nil")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP serves authentication portal.
|
||||
func (m *AuthnMiddleware) 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)
|
||||
return m.portal.ServeHTTP(r.Context(), w, r, rr)
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
|
@ -41,7 +41,9 @@ func init() {
|
||||
// AuthzMiddleware authorizes access to endpoints based on
|
||||
// the presense and content of JWT token.
|
||||
type AuthzMiddleware struct {
|
||||
Authorizer *authz.Authorizer `json:"authorizer,omitempty"`
|
||||
RouteMatcher string `json:"route_matcher,omitempty" xml:"route_matcher,omitempty" yaml:"route_matcher,omitempty"`
|
||||
GatekeeperName string `json:"gatekeeper_name,omitempty" xml:"gatekeeper_name,omitempty" yaml:"gatekeeper_name,omitempty"`
|
||||
gatekeeper *authz.Gatekeeper
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
@ -59,41 +61,46 @@ func (m *AuthzMiddleware) Provision(ctx caddy.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
secApp := appModule.(*App)
|
||||
if secApp == nil {
|
||||
app := appModule.(*App)
|
||||
if app == nil {
|
||||
return fmt.Errorf("security app is nil")
|
||||
}
|
||||
if secApp.Config == nil {
|
||||
if app.Config == nil {
|
||||
return fmt.Errorf("security app config is nil")
|
||||
}
|
||||
|
||||
var foundRef bool
|
||||
for _, cfg := range secApp.Config.Policies {
|
||||
if cfg.Name == m.Authorizer.GatekeeperName {
|
||||
foundRef = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundRef {
|
||||
return fmt.Errorf("security app has no %q authorization policy", m.Authorizer.GatekeeperName)
|
||||
gatekeeper, err := app.getGatekeeper(m.GatekeeperName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("security app erred with %q authorization policy: %v", m.GatekeeperName, err)
|
||||
}
|
||||
m.gatekeeper = gatekeeper
|
||||
|
||||
return m.Authorizer.Provision(ctx.Logger(m))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile unmarshals caddyfile.
|
||||
func (m *AuthzMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) (err error) {
|
||||
a, err := parseAuthzPluginCaddyfile(httpcaddyfile.Helper{Dispenser: d})
|
||||
cfg, err := parseAuthzPluginCaddyfile(httpcaddyfile.Helper{Dispenser: d})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Authorizer = a
|
||||
m.RouteMatcher = cfg["path"]
|
||||
m.GatekeeperName = cfg["gatekeeper_name"]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate implements caddy.Validator.
|
||||
func (m *AuthzMiddleware) Validate() error {
|
||||
return m.Authorizer.Validate()
|
||||
if m.RouteMatcher == "" {
|
||||
return fmt.Errorf("empty route matcher")
|
||||
}
|
||||
if m.GatekeeperName == "" {
|
||||
return fmt.Errorf("empty gatekeeper name")
|
||||
}
|
||||
if m.gatekeeper == nil {
|
||||
return fmt.Errorf("gatekeeper is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate authorizes access based on the presense and content of
|
||||
@ -101,7 +108,7 @@ func (m *AuthzMiddleware) Validate() error {
|
||||
func (m AuthzMiddleware) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.User, bool, error) {
|
||||
ar := requests.NewAuthorizationRequest()
|
||||
ar.ID = util.GetRequestID(r)
|
||||
if err := m.Authorizer.Authenticate(w, r, ar); err != nil {
|
||||
if err := m.gatekeeper.Authenticate(w, r, ar); err != nil {
|
||||
return caddyauth.User{}, false, errors.ErrAuthorizationFailed.WithArgs(
|
||||
getAuthorizationDetails(r, ar), err,
|
||||
)
|
||||
|
51
testdata/oauth/87329db33bf_pri.pem
vendored
Normal file
51
testdata/oauth/87329db33bf_pri.pem
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKQIBAAKCAgEAt04Rq3MJETW1WwsaNlr1mmth9f03a/dyHlopCMS0rf96ISAi
|
||||
QT1Sq8JUJQHZRWpl67ox9wWa+jxiwoe8G2rCZtlXBzot/HRc4spbjL2NWBX5RtD+
|
||||
PGDUekzgGPME4lusG9PhPpclgKoucEy2U7wPldLIf+fAy3Jis72J9BEHRcTyhnbV
|
||||
+qQUN3WpnZYrtIXYbu0hyBB0JnMP3k8Zs1jWLsCtATsGEimSp1xAkAH5ek7l65Y1
|
||||
vzn/bSCa4gVxpdnTqeJaWpTg1lgtxG69RD7W8ZA2K4U94Aisjuen2+liaQ+pGVL0
|
||||
dMAoODedq32nV4v4uUfiM7XLkb1ceBK1FkOm/d5346y9Dk0zX8Xcf385qRX83r0B
|
||||
zHMUsgMmNDOuHXAMGhR6rrD7M0kTCw+5TWbmGicMT896614du5P2HNpkeY8NwgxQ
|
||||
wWG7G0KYA8jkCb4InqBKxbKXYqv8sWwvtlDa67pUzlvAUhOL3gICxEjEJsdbaA1c
|
||||
51dsXaK+OuSr8irQtq12RCJ2/GXkcGWtt5FxO9BFXfxnYcYRofdvbYzA2lgz5sgP
|
||||
DB98ZAro5D9y4A/vtcDcJir9ud1lLSnxMZZ+8ZMYCVHGHiFuqum6uUNJDTpuv18B
|
||||
p6WCd/v6E9v5is6wNTe/C7/syyFiDj+vW1fFHmKw4GKAL03ogHD3r0Cm88ECAwEA
|
||||
AQKCAgAbeMnRJjZvq9r4QBu3lb5FXF6cHU0XE0i5H84SkFh701QcbtJzcJtZPpCZ
|
||||
00Ma0i1gPNGMMfzeD/rFKM6aVU+5Vk1iued2dIIPQ0ChQS9TykdHw762kdSGV8sm
|
||||
MhXDTvXmXcTeOozWUasQDY8t0XuMesCt2q/cTdJZGcj5GSw72p52ZI0BOWIGRc4b
|
||||
3gvCzGR5i3SKpVyi/o+cQ8KYTHIdI6dKDeyFAFIIXx0V+BXJrNVsj2CSvkB3vHE5
|
||||
+pLjxlegmFnuRW3E8gY58FbRSZGxNmnH7/or/2DNMg7IOJwVHtG7B+G3Na1Rb5Rj
|
||||
xTs1NbcbHZjbIx3VQ8n9/N/C7HS0SobUrR5wvkybCJc11KUvvmYOh23gqcQnnvRP
|
||||
ehF9eY3ac1fKORYzsxAwrne4brXKxSSfip/zT34ksrWbxax70NAZJlBv2dCnjGtv
|
||||
qTYmWL6k3m+gkx/++Y3xM/1/7g0kp99MAWGisehV7Ixe3mXJqwcKDf/6LqlYm3bf
|
||||
Ja3b53TPH1nUZydPFdfTxsaefz43deRPbnRxNPtxs7E6a1/nzTpKBHXNtI4k4trS
|
||||
VDquFHyCUvjn9oNgUo1kydb+pyVdHHMQB49y6+MvvGRsI8v3a5Y+MNfGiweMCXAF
|
||||
D3t0bDd4IwV1dlBtp6nl4lP+01QNnFfUk/m2SKi1dz+RFS4aeQKCAQEA43fm6183
|
||||
bF+SylT8dDBbeilJDj5HgicwGRFNgGuLaYoc4uYI3+/qcazfR48Tn21LL2V7HSHK
|
||||
xBctYodBDcUXeEbk/i7K8dzopovmxtTolWpk0ZtqElbLLlgtsEGRzO9hGu3ntNlA
|
||||
6nGAKSpIkcpwYZzcYvJOGcts2mPT2tmGDw1dafTjfeZrc3bC20J2DfdZHA0MphSU
|
||||
81cvbvGfVPLhSqhgvnciuADoX4OwOLJQELmPEtrvoySJZwCSALPB/qL15KA9Xgzk
|
||||
qBduSyfcwrXYTW0EPkWg0Ts/ilAwLxg5HfqKtWfxnLMnz66LWbc4hzqEXUwABNRV
|
||||
ymMEHqbj+4yg/wKCAQEAzkwQC0E5iXpd3eoTINn1/he+aG7ByJyRoMqo6zhdgWQD
|
||||
8GofaHboeeJzVhlCu+THYmzmIIHx9jOxdiEs7dJF9VAd/uwO4N422yYEzcefIOCY
|
||||
PCP+Ztg6qaqys/0NpJOH/WPGyL4yYL+rITEC93DFVj5wbYe7u5T4X0bkBtMPBqH4
|
||||
xVTDMOaE0sNVeyKbzXqmLkHf8Gqu+AjvJCxTpk6lW66tbTblvvT0od0RBWw2JnLO
|
||||
i4QiRJ7jeLGrFH+tdx9gtdA+xgVe60ocQk2fJFuoqU0qcdipL8Khqh3cY/CYVQT+
|
||||
1tmeIZyuhjSk0+JRBH1w4cDOVbArZaBUBx7LdX+rPwKCAQBXxws7RPEURwVUQttw
|
||||
0sBaMdhZQLtDhG/RHJY1ukqAHaXsASznjaOA3l8DNDk0Sm67CYQqx8GBThhbbyox
|
||||
cB8QcPspA1GZZ8/3hQE3NS3Tis0A/eI+1XNunOR0objrxmxIggnqBfRBBC+asxBy
|
||||
Aha/9FIvdKWi2pdU5zT3vP04jcXAf5nSGbrZQbkL92erGAoxAvAgnsyj4r8RJvh/
|
||||
RYKe1r8OgNbK/r6tLRoxps2yxohplEbpQ84qC1RMJRH1e1k8MoG762nJW9FZ+zX7
|
||||
hUTFDA6ZITFfzGdGro8JfWV9JhOk0UmxWIlCYW6w3j1YIcK2Zf+T3YSFpxQN6AUO
|
||||
K0RtAoIBAQCqXw7w/JD0BOb4mpPpkZginKFXxgCsGQH79OLEP+yZK2xFZJ9DutAL
|
||||
uQTfmkUOv8Yady0ms6qMVey9TnC4h+vWyK+9FF7FPz+2hRN6jt3QXSvcny0+6lyo
|
||||
Op0TIG3f+SdaEMjeiJU6aZB+/OciSzPuIerfyjU0mbb1mKpBKJVEOQgmj/YTsI0J
|
||||
MuCprM9XR29uzGCRQMn3dglpqmH6+wB9UylPBQOATPSrqNKh09h0sGP7vMhAO6hI
|
||||
yRIs/7TWqEdKYA03pL/bOX1VFJ3VfQ0xpNTk6LXxB6BTyg11TAHCVTnRXi/GOou3
|
||||
skpd4o5eUuqixoShJ7jvWRWMO9Zz11gpAoIBAQCllUw5UoSw+233QR4FWtyKdktc
|
||||
FUKI4LHUamADhbDfbxS3V1jl5D0WB6bT/xlvdC+4vxsUNHhR2do5DsPqUyP5ivWy
|
||||
MgYX8rXJesnVlXRti5uAiOU2C/k886phCZH9KM7VEz7ZmUIbE753umcuNtDMBa5p
|
||||
KxmAwQjcqsht14agxhdhId3nqTT+02d4rFclqfytH9fditrvSE45LxLUzHJcLBd8
|
||||
eeGo/d6SA9T1lhREhJDnlDrdMzj9qZcOA1W+ZCYj7ENZhLWc0JYDeODV43XtDUAM
|
||||
DlZB5BkyIsIF/Q7xAI+z7WAqsj8yvdb9nco8K4HkeM+GJzguUUAVMy10N1Nc
|
||||
-----END RSA PRIVATE KEY-----
|
14
testdata/oauth/87329db33bf_pub.pem
vendored
Normal file
14
testdata/oauth/87329db33bf_pub.pem
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt04Rq3MJETW1WwsaNlr1
|
||||
mmth9f03a/dyHlopCMS0rf96ISAiQT1Sq8JUJQHZRWpl67ox9wWa+jxiwoe8G2rC
|
||||
ZtlXBzot/HRc4spbjL2NWBX5RtD+PGDUekzgGPME4lusG9PhPpclgKoucEy2U7wP
|
||||
ldLIf+fAy3Jis72J9BEHRcTyhnbV+qQUN3WpnZYrtIXYbu0hyBB0JnMP3k8Zs1jW
|
||||
LsCtATsGEimSp1xAkAH5ek7l65Y1vzn/bSCa4gVxpdnTqeJaWpTg1lgtxG69RD7W
|
||||
8ZA2K4U94Aisjuen2+liaQ+pGVL0dMAoODedq32nV4v4uUfiM7XLkb1ceBK1FkOm
|
||||
/d5346y9Dk0zX8Xcf385qRX83r0BzHMUsgMmNDOuHXAMGhR6rrD7M0kTCw+5TWbm
|
||||
GicMT896614du5P2HNpkeY8NwgxQwWG7G0KYA8jkCb4InqBKxbKXYqv8sWwvtlDa
|
||||
67pUzlvAUhOL3gICxEjEJsdbaA1c51dsXaK+OuSr8irQtq12RCJ2/GXkcGWtt5Fx
|
||||
O9BFXfxnYcYRofdvbYzA2lgz5sgPDB98ZAro5D9y4A/vtcDcJir9ud1lLSnxMZZ+
|
||||
8ZMYCVHGHiFuqum6uUNJDTpuv18Bp6WCd/v6E9v5is6wNTe/C7/syyFiDj+vW1fF
|
||||
HmKw4GKAL03ogHD3r0Cm88ECAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
6
testdata/oauth/README.md
vendored
Normal file
6
testdata/oauth/README.md
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
Generate private-public RSA key pair:
|
||||
|
||||
```
|
||||
openssl genrsa -out testdata/oauth/87329db33bf_pri.pem 4096
|
||||
openssl rsa -in testdata/oauth/87329db33bf_pri.pem -pubout -out testdata/oauth/87329db33bf_pub.pem
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user