Skip to content

Commit

Permalink
Added more tests (#29)
Browse files Browse the repository at this point in the history
- Key changes:
  - Minor improvements in docs;
  - Minor improvements in logging;
  - Added more tests;
  - `VictoriaMetrics/metricsql` bumped from `0.42.0` to `0.43.0`.
  • Loading branch information
weisdd authored May 1, 2022
1 parent 328e705 commit b6d18eb
Show file tree
Hide file tree
Showing 11 changed files with 439 additions and 47 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# CHANGELOG

## 0.12.2

- Key changes:
- Minor improvements in docs;
- Minor improvements in logging;
- Added more tests;
- `VictoriaMetrics/metricsql` bumped from `0.42.0` to `0.43.0`.

## 0.12.1

- Key changes:
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ Docker images are published on [ghcr.io/weisdd/lfgw](https://github.com/weisdd/l

## Configuration

Example of `keycloak + grafana + lfgw` setup is described [here](./docs/oidc.md).
Example of `keycloak + grafana + lfgw` setup is described [here](docs/oidc.md).

### Requirements for jwt-tokens

* OIDC-roles must be present in `roles` claim;
* Client ID specified via `OIDC_CLIENT_ID` must be present in `aud` claim (more details in [environment variables section](#Environment variables)), otherwise token verification will fail.
* Client ID specified via `OIDC_CLIENT_ID` must be present in `aud` claim (more details in [environment variables section](#environment-variables)), otherwise token verification will fail.

### Environment variables

Expand Down Expand Up @@ -105,11 +105,10 @@ Note: a user is free to have multiple roles matching the contents of `acl.yaml`.

## Licensing

lfgw code is licensed under MIT, though its dependencies might have other licenses. Please, inspect the modules listed in [go.mod](./go.mod) if needed.
lfgw code is licensed under MIT, though its dependencies might have other licenses. Please, inspect the modules listed in [go.mod](go.mod) if needed.

## TODO

* tests for handlers;
* improve naming;
* log slow requests;
* metrics;
Expand Down
6 changes: 6 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ tasks:
tidy:
cmds:
- go mod tidy

cov:
cmds:
- mkdir -p temp
- go test -coverprofile=temp/coverage.out ./...
- go tool cover -html=temp/coverage.out
12 changes: 11 additions & 1 deletion internal/lfgw/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ func (app *application) getRawAccessToken(r *http.Request) (string, error) {
}
}

return "", fmt.Errorf("no bearer token found")
var err error

isGrafanaRequest := strings.Contains(strings.ToLower(r.UserAgent()), "grafana")

if isGrafanaRequest {
err = fmt.Errorf("no bearer token found, possible causes: grafana data source is not configured with Forward Oauth Identity option; grafana user sessions are not tuned to live shorter than IDP sessions; malicious requests")
} else {
err = fmt.Errorf("no bearer token found")
}

return "", err
}

// isNotAPIRequest returns true if the requested path does not target API or federate endpoints.
Expand Down
1 change: 1 addition & 0 deletions internal/lfgw/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func (s stdErrorLogWrapper) Write(p []byte) (n int, err error) {
return len(p), nil
}

// configureLogging configures zerolog and sets the respective fields in the application struct
func (app *application) configureLogging() {
zlog.Logger = zlog.Output(os.Stdout)
app.logger = &zlog.Logger
Expand Down
87 changes: 54 additions & 33 deletions internal/lfgw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ type application struct {
logger *zerolog.Logger
}

// Run is used as an entrypoint for cli
func Run(c *cli.Context) error {
app, err := newApplication(c)
if err != nil {
return err
}

app.Run()

return nil
}

// newApplication returns application struct built from *cli.Context
func newApplication(c *cli.Context) (application, error) {
upstreamURL, err := url.Parse(c.String("upstream-url"))
Expand Down Expand Up @@ -75,22 +87,13 @@ func newApplication(c *cli.Context) (application, error) {
return app, nil
}

// Run is used as an entrypoint for cli
func Run(c *cli.Context) error {
app, err := newApplication(c)
if err != nil {
return err
}

app.Run()

return nil
}

// Run starts lfgw (main-like function)
func (app *application) Run() {
app.configureLogging()
app.configureACLs()
app.configureOIDCVerifier()

// TODO: expose undo and move to another function?
if app.SetGomaxProcs {
undo, err := maxprocs.Set()
defer undo()
Expand All @@ -102,6 +105,20 @@ func (app *application) Run() {
app.logger.Info().Caller().
Msgf("Runtime settings: GOMAXPROCS = %d", runtime.GOMAXPROCS(0))

err := app.serve()
if err != nil {
app.logger.Fatal().Caller().
Err(err).Msg("")
}
}

// configureACLs logs assumed roles mode, verifies current ACLs settings (assumed roles, aclpath), loads the ACLs from a file and logs roles if needed
func (app *application) configureACLs() {
// Just to make sure our logging calls are always safe
if app.logger == nil {
app.configureLogging()
}

if app.AssumedRolesEnabled {
app.logger.Info().Caller().
Msg("Assumed roles mode is on")
Expand All @@ -110,28 +127,38 @@ func (app *application) Run() {
Msg("Assumed roles mode is off")
}

var err error

if app.ACLPath != "" {
app.ACLs, err = querymodifier.NewACLsFromFile(app.ACLPath)
if err != nil {
app.logger.Fatal().Caller().
Err(err).Msgf("Failed to load ACL")
}

for role, acl := range app.ACLs {
app.logger.Info().Caller().
Msgf("Loaded role definition for %s: %q (converted to %s)", role, acl.RawACL, acl.LabelFilter.AppendString(nil))
}
} else {
if app.ACLPath == "" {
// NOTE: the condition should never happen as it's filtered out by "Before" functionality of cli, though left just in case
if !app.AssumedRolesEnabled {
app.logger.Fatal().Caller().
Msgf("The app cannot run without at least one source of configuration (Non-empty ACL_PATH and/or ASSUMED_ROLES set to true)")
}

app.logger.Info().Caller().
Msgf("ACL_PATH is empty, thus predefined roles are not loaded")
Msgf("ACL_PATH is empty, thus predefined roles will not be loaded")

return
}

var err error

app.ACLs, err = querymodifier.NewACLsFromFile(app.ACLPath)
if err != nil {
app.logger.Fatal().Caller().
Err(err).Msgf("Failed to load ACL")
}

for role, acl := range app.ACLs {
app.logger.Info().Caller().
Msgf("Loaded role definition for %s: %q (converted to %s)", role, acl.RawACL, acl.LabelFilter.AppendString(nil))
}
}

// configureOIDCVerifier sets up OIDC token verifier by using app.OIDCRealmURL and app.OIDCClientID
func (app *application) configureOIDCVerifier() {
// Just to make sure our logging calls are always safe
if app.logger == nil {
app.configureLogging()
}

app.logger.Info().Caller().
Expand All @@ -149,10 +176,4 @@ func (app *application) Run() {
ClientID: app.OIDCClientID,
}
app.verifier = provider.Verifier(oidcConfig)

err = app.serve()
if err != nil {
app.logger.Fatal().Caller().
Err(err).Msg("")
}
}
32 changes: 25 additions & 7 deletions internal/lfgw/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func (app *application) logMiddleware(next http.Handler) http.Handler {
// If any of those are empty, they won't get logged
app.enrichDebugLogContext(r, "get_params", app.unescapedURLQuery(r.URL.Query().Encode()))
app.enrichDebugLogContext(r, "post_params", app.unescapedURLQuery(postForm))

// Workaround to make further r.ParseForm() calls update r.Form and r.PostForm again, might be useful in case there's another middleware before rewriteRequestMiddleware
r.Form = nil
r.PostForm = nil
}

if app.LogRequests || app.Debug {
Expand Down Expand Up @@ -106,6 +110,10 @@ func (app *application) oidcModeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rawAccessToken, err := app.getRawAccessToken(r)
if err != nil {
// Might produce plenty of error messages, though it will make it much easier to understand why requests are failing
hlog.FromRequest(r).Error().Caller().
Err(err).Msg("")

app.clientErrorMessage(w, http.StatusUnauthorized, err)
return
}
Expand Down Expand Up @@ -158,23 +166,29 @@ func (app *application) oidcModeMiddleware(next http.Handler) http.Handler {
// rewriteRequestMiddleware rewrites a request before forwarding it to the upstream.
func (app *application) rewriteRequestMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Rewrite request destination
r.Host = app.UpstreamURL.Host

if app.isNotAPIRequest(r.URL.Path) {
hlog.FromRequest(r).Debug().Caller().
Msg("Not an API request, request is not modified")
next.ServeHTTP(w, r)
// TODO: rewrite?
if app.UpstreamURL == nil {
app.serverError(w, r, fmt.Errorf("UpstreamURL is not initialized"))
return
}

// Rewrite request destination
r.Host = app.UpstreamURL.Host

acl, ok := r.Context().Value(contextKeyACL).(querymodifier.ACL)
if !ok {
// Should never happen. It means OIDC middleware hasn't done it's job
app.serverError(w, r, fmt.Errorf("ACL is not set in the context"))
return
}

if app.isNotAPIRequest(r.URL.Path) {
hlog.FromRequest(r).Debug().Caller().
Msg("Not an API request, request is not modified")
next.ServeHTTP(w, r)
return
}

if acl.Fullaccess {
hlog.FromRequest(r).Debug().Caller().
Msg("User has full access, request is not modified")
Expand Down Expand Up @@ -219,6 +233,10 @@ func (app *application) rewriteRequestMiddleware(next http.Handler) http.Handler
// TODO: the field name is slightly misleading, should, probably, be renamed
app.enrichDebugLogContext(r, "new_post_params", app.unescapedURLQuery(newPostParams))

// Workaround to make further r.ParseForm() calls update r.Form and r.PostForm again, might be useful in case there's another middleware before rewriteRequestMiddleware
r.Form = nil
r.PostForm = nil

next.ServeHTTP(w, r)
})
}
Loading

0 comments on commit b6d18eb

Please sign in to comment.