diff --git a/README.md b/README.md index 8ac84f4..36cfec8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCeSb3yfsPNNVr13YsYNvCAw?label=achetronic&link=http%3A%2F%2Fyoutube.com%2Fachetronic) ![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/achetronic?style=flat&logo=twitter&link=https%3A%2F%2Ftwitter.com%2Fachetronic) -A tiny HTTP server to be used as external authentication service for Envoy +A tiny HTTP server to be used as external authentication service for Envoy ## Motivation @@ -16,10 +16,12 @@ Life is hard, but beautiful As almost every configuration parameter can be defined in environment vars, there are only few flags that can be defined. They are described in the following table: -| Name | Description | Default | Example | -|:------------------|:-------------------------------|:-------------:|:-------------------------| -| `--log-level` | Verbosity level for logs | `info` | `--log-level info` | -| `--disable-trace` | Disable showing traces in logs | `info` | `--log-level info` | +| Name | Description | Default | Example | +|:------------------|:-----------------------------------------------------|:-----------------:|:-------------------------------| +| `--log-level` | Verbosity level for logs | `info` | `--log-level info` | +| `--disable-trace` | Disable showing traces in logs | `info` | `--log-level info` | +| `--config` | Path to the configuration file
[Config Example] | `doorkeeper.yaml` | `--doorkeeper doorkeeper.yaml` | + > Output is thrown always in JSON as it is more suitable for automations @@ -27,23 +29,13 @@ They are described in the following table: doorkeeper run \ --log-level=info ``` +## Configuration -## Environment vars - -| Name | Values | Description | -|:---------------------------------------|:----------------------------|:------------| -| `DOORKEEPER_AUTHORIZATION_PARAM_TYPE` | `header\|query` | | -| `DOORKEEPER_AUTHORIZATION_PARAM_NAME` | `*` | | -| `DOORKEEPER_AUTHORIZATION_TYPE` | `hmac\|{}` | | -| `DOORKEEPER_HMAC_TYPE` | `url\|{}` | | -| `DOORKEEPER_HMAC_ENCRYPTION_KEY` | `*` | | -| `DOORKEEPER_HMAC_ENCRYPTION_ALGORITHM` | `md5\|sha1\|sha256\|sha512` | | - - +A complete example of the config params can be found in [docs/samples/doorkeeper.yaml](./docs/samples/doorkeeper.yaml) ## How to deploy -This project can be deployed in Kubernetes, but also provides binary files +This project can be deployed in Kubernetes, but also provides binary files and Docker images to make it easy to be deployed however wanted @@ -69,10 +61,10 @@ helm upgrade --install --wait doorkeeper \ ### Docker -Docker images can be found in GitHub's [packages](https://github.com/freepik-company/doorkeeper/pkgs/container/doorkeeper) +Docker images can be found in GitHub's [packages](https://github.com/freepik-company/doorkeeper/pkgs/container/doorkeeper) related to this repository -> Do you need it in a different container registry? I think this is not needed, but if I'm wrong, please, let's discuss +> Do you need it in a different container registry? I think this is not needed, but if I'm wrong, please, let's discuss > it in the best place for that: an issue ## How to contribute @@ -105,3 +97,10 @@ 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. + + + + +[//]: # + +[Config Example]: <./README.md#configuration> diff --git a/api/config_types.go b/api/config_types.go new file mode 100644 index 0000000..4daee63 --- /dev/null +++ b/api/config_types.go @@ -0,0 +1,54 @@ +/* +Copyright 2024. + +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 api + +import "regexp" + +type DoorkeeperConfigT struct { + Auth AuthorizationConfigT `yaml:"authorization"` + Hmac HmacConfigT `yaml:"hmac"` + Modifiers []ModifierConfigT `yaml:"modifiers"` +} + +type AuthorizationConfigT struct { + Type string `yaml:"type"` + Param AuthParamConfigT `yaml:"param"` +} + +type AuthParamConfigT struct { + Type string `yaml:"type"` + Name string `yaml:"name"` +} + +type HmacConfigT struct { + Type string `yaml:"type"` + EncryptionKey string `yaml:"encryptionKey"` + EncryptionAlgorithm string `yaml:"encryptionAlgorithm"` +} + +type ModifierConfigT struct { + Type string `yaml:"type"` + Path ModifierPathConfigT `yaml:"path"` +} + +type ModifierPathConfigT struct { + Pattern string `yaml:"pattern"` + Replace string `yaml:"replace"` + + // Carry stuff + CompiledRegex *regexp.Regexp +} diff --git a/cmd/main.go b/cmd/main.go index 60206fc..f6fc70b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,19 +17,21 @@ package main import ( "flag" "fmt" + "log" "os" "os/signal" "syscall" - "log" - "doorkeeper/internal/httpserver" + "doorkeeper/internal/config" "doorkeeper/internal/globals" + "doorkeeper/internal/httpserver" ) var ( - httpPortFlag = flag.String("port", "8000", "HTTP server port") - logLevelFlag = flag.String("log-level", "info", "Verbosity level for logs") + httpPortFlag = flag.String("port", "8000", "HTTP server port") + logLevelFlag = flag.String("log-level", "info", "Verbosity level for logs") disableTraceFlag = flag.Bool("disable-trace", true, "Disable showing traces in logs") + configFlag = flag.String("config", "doorkeeper.yaml", "Path to the config file") ) func main() { @@ -43,6 +45,14 @@ func main() { log.Fatal(err) } + // Parse and store the config + configContent, err := config.ReadFile(*configFlag) + if err != nil { + globals.Application.Logger.Fatalf(fmt.Sprintf("failed parsing configuration: %s", err.Error())) + } + + globals.Application.Config = configContent + ///////////////////////////// // EXECUTION FLOW RELATED ///////////////////////////// @@ -56,4 +66,3 @@ func main() { signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs } - diff --git a/docs/samples/doorkeeper.yaml b/docs/samples/doorkeeper.yaml new file mode 100644 index 0000000..dfef4e1 --- /dev/null +++ b/docs/samples/doorkeeper.yaml @@ -0,0 +1,20 @@ +authorization: + type: hmac # hmac + param: + type: query # header|query + name: token + +hmac: + type: url + encryptionKey: "${SECRETITO}" + encryptionAlgorithm: "sha256" + +modifiers: + #- type: header # headers|host|path + # header: + # # TODO + + - type: path + path: + pattern: ^(/[a-zA-Z0-9\-_]/) + replace: "" diff --git a/go.mod b/go.mod index ca47b71..856b0ac 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,5 @@ toolchain go1.22.4 require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e1a34f3..7361eb7 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,6 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..96a9220 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,35 @@ +package config + +import ( + "doorkeeper/api" + "os" + + "gopkg.in/yaml.v3" +) + +// Marshal TODO +func Marshal(config api.DoorkeeperConfigT) (bytes []byte, err error) { + bytes, err = yaml.Marshal(config) + return bytes, err +} + +// Unmarshal TODO +func Unmarshal(bytes []byte) (config api.DoorkeeperConfigT, err error) { + err = yaml.Unmarshal(bytes, &config) + return config, err +} + +// ReadFile TODO +func ReadFile(filepath string) (config api.DoorkeeperConfigT, err error) { + var fileBytes []byte + fileBytes, err = os.ReadFile(filepath) + if err != nil { + return config, err + } + + fileBytes = []byte(os.ExpandEnv(string(fileBytes))) + + config, err = Unmarshal(fileBytes) + + return config, err +} diff --git a/internal/globals/globals.go b/internal/globals/globals.go index f9e68b2..50aadf0 100644 --- a/internal/globals/globals.go +++ b/internal/globals/globals.go @@ -1,6 +1,7 @@ package globals import ( + "doorkeeper/api" "time" "go.uber.org/zap" @@ -15,6 +16,8 @@ var ( type ApplicationT struct { Logger zap.SugaredLogger LogLevel string + + Config api.DoorkeeperConfigT } // SetLogger TODO diff --git a/internal/httpserver/httpserver.go b/internal/httpserver/httpserver.go index a195ec6..6db00b4 100644 --- a/internal/httpserver/httpserver.go +++ b/internal/httpserver/httpserver.go @@ -4,71 +4,68 @@ import ( "fmt" "io" "net/http" - "os" + "regexp" "strings" // - "doorkeeper/internal/hmac" "doorkeeper/internal/globals" + "doorkeeper/internal/hmac" ) const ( resultHeader = "x-ext-authz-check-result" receivedHeader = "x-ext-authz-check-received" - resultAllowed = "allowed" - resultDenied = "denied" + resultAllowed = "allowed" + resultDenied = "denied" resultDeniedBody = "Unauthorized" ) -var ( - - // - authorizationParamType = os.Getenv("DOORKEEPER_AUTHORIZATION_PARAM_TYPE") - authorizationParamName = os.Getenv("DOORKEEPER_AUTHORIZATION_PARAM_NAME") - authorizationType = os.Getenv("DOORKEEPER_AUTHORIZATION_TYPE") - - // - hmacEncryptionKey = os.Getenv("DOORKEEPER_HMAC_ENCRYPTION_KEY") - hmacEncryptionArgotithm = os.Getenv("DOORKEEPER_HMAC_ENCRYPTION_ALGORITHM") - hmacType = os.Getenv("DOORKEEPER_HMAC_TYPE") -) - type HttpServer struct { *http.Server } -func NewHttpServer() *HttpServer { - if authorizationParamType == "" || authorizationParamName == "" || authorizationType == "" { +func NewHttpServer() (server *HttpServer) { + if globals.Application.Config.Auth.Param.Type == "" || globals.Application.Config.Auth.Param.Name == "" || + globals.Application.Config.Auth.Type == "" { + globals.Application.Logger.Fatal("environment variables fot authorization must be setted") } - if authorizationType == "hmac" && - (hmacEncryptionKey == "" || hmacEncryptionArgotithm == "" || hmacType == "") { + if globals.Application.Config.Auth.Type == "hmac" && + (globals.Application.Config.Hmac.EncryptionKey == "" || + globals.Application.Config.Hmac.EncryptionAlgorithm == "" || + globals.Application.Config.Hmac.Type == "") { globals.Application.Logger.Fatal("environment variables for 'hmac' authorization type must be setted") } - - return &HttpServer{} + + for index, mod := range globals.Application.Config.Modifiers { + if mod.Type == "path" { + globals.Application.Config.Modifiers[index].Path.CompiledRegex = regexp.MustCompile(mod.Path.Pattern) + } + } + + return server } func (s *HttpServer) handleRequest(response http.ResponseWriter, request *http.Request) { globals.Application.Logger.Infof( - "handle request {authorizationType '%s', host: '%s', path: '%s', query: %s, headers '%v'}", - authorizationType, + "handle request {authorizationType '%s', host: '%s', path: '%s', query: %s, headers '%v'}", + globals.Application.Config.Auth.Type, request.Host, - request.URL.Path, + request.URL.Path, request.URL.RawQuery, request.Header, ) - + var err error - defer func(){ + defer func() { if err != nil { globals.Application.Logger.Errorf( - "denied request {authorizationType '%s', host: '%s', path: '%s', query: %s, headers '%v'}: %s", - authorizationType, + "denied request {authorizationType '%s', host: '%s', path: '%s', query: %s, headers '%v'}: %s", + globals.Application.Config.Auth.Type, request.Host, - request.URL.Path, + request.URL.Path, request.URL.RawQuery, request.Header, err.Error(), @@ -79,45 +76,56 @@ func (s *HttpServer) handleRequest(response http.ResponseWriter, request *http.R } }() + for _, modifier := range globals.Application.Config.Modifiers { + switch modifier.Type { + case "path": + request.URL.Path = modifier.Path.CompiledRegex.ReplaceAllString(request.URL.Path, modifier.Path.Replace) + + case "header": + // TODO + } + } + // body, err := io.ReadAll(request.Body) if err != nil { globals.Application.Logger.Errorf("unable to read request body: %s", err.Error()) return } - + // receivedContent := fmt.Sprintf("%s %s%s, headers: %v, body: [%s]\n", request.Method, request.Host, request.URL, request.Header, returnIfNotTooLong(string(body))) response.Header().Set(receivedHeader, receivedContent) - token := request.URL.Query().Get(authorizationParamName) - if authorizationParamType == "header" { - token = request.Header.Get(authorizationParamName) + token := request.URL.Query().Get(globals.Application.Config.Auth.Param.Name) + if globals.Application.Config.Auth.Param.Type == "header" { + token = request.Header.Get(globals.Application.Config.Auth.Param.Name) } var valid bool - if authorizationType == "hmac" { - pathParts := strings.Split(request.URL.Path, "?") + if globals.Application.Config.Auth.Type == "hmac" { + path := strings.Split(request.URL.Path, "?")[0] - if hmacType == "url" { - valid, err = hmac.ValidateTokenUrl(token, hmacEncryptionKey, hmacEncryptionArgotithm, pathParts[0]) + if globals.Application.Config.Hmac.Type == "url" { + valid, err = hmac.ValidateTokenUrl(token, globals.Application.Config.Hmac.EncryptionKey, + globals.Application.Config.Hmac.EncryptionAlgorithm, path) if err != nil { err = fmt.Errorf("unable to validate token in request: %s", err.Error()) return } } } - + if !valid { - err = fmt.Errorf("invalid token in request") - return + err = fmt.Errorf("invalid token in request") + return } - + globals.Application.Logger.Infof( - "allowed request {authorizationType '%s', host: '%s', path: '%s', query: %s, headers '%v'}", - authorizationType, - request.Host, - request.URL.Path, + "allowed request {authorizationType '%s', host: '%s', path: '%s', query: %s, headers '%v'}", + globals.Application.Config.Auth.Type, + request.Host, + request.URL.Path, request.URL.RawQuery, request.Header, )