Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for HTTP-based authorization #9

Merged
merged 5 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ FROM alpine:3.13
COPY --from=build /src/mqtt-history ./mqtt-history
COPY --from=build /src/config ./config

ENV MQTTHISTORY_REDIS_HOST localhost
ENV MQTTHISTORY_REDIS_PORT 6379
ENV MQTTHISTORY_REDIS_DB 0
ENV MQTTHISTORY_API_TLS false
ENV MQTTHISTORY_API_CERTFILE ./misc/example.crt
ENV MQTTHISTORY_API_KEYFILE ./misc/example.key
Expand Down
36 changes: 29 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,14 @@

An MQTT-based history handler for messages recorded by [mqttbot](https://github.com/topfreegames/mqttbot) in Cassandra


## Features

MqttHistory is an extensible

The bot is capable of:
- Listen to healthcheck requests
- Send history messages requested by users
- Retrieve message history from Cassandra when requested by users
- Authorization handling with support for MongoDB or an HTTP Authorization API

## Setup

Make sure you have go installed on your machine.
Make sure you have Go installed on your machine.

You also need to have access to running instances of Cassandra and Mongo.

Expand All @@ -42,3 +38,29 @@ If you are interested in running the tests yourself you will need docker (versio
and up) and docker-compose.

To run the tests simply run `make test`

## Authorization

The project supports checking whether a user is authorized to retrieve the message history for a given topic.
This can be done via either MongoDB- or HTTP-based authorization, depending on the configuration.

For MongoDB, which is the default method, the required settings are
```
mongo:
host: "mongodb://localhost:27017"
allow_anonymous: false # whether to make authorization checks or not
database: "mqtt"
```

For HTTP auth, the required settings are
```
httpAuth:
enabled: true # whether to use HTTP or MongoDB for authorization
requestURL: "http://localhost:8080/auth" # endpoint to make auth requests
timeout: 10 # request timeout in seconds
iam:
enabled: true # whether to use Basic Auth when accessing the Auth API
credentials: # credentials for Basic Auth
username: user
password: pass
```
74 changes: 68 additions & 6 deletions app/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
package app

import (
"bytes"
"context"
"encoding/json"
"net/http"
"strings"
"time"

"github.com/labstack/echo"
newrelic "github.com/newrelic/go-agent"
Expand All @@ -26,7 +30,12 @@ type ACL struct {
Pubsub []string `bson:"pubsub"`
}

//GetTX returns new relic transaction
type authRequest struct {
Username string `json:"username"`
Topic string `json:"topic"`
}

// GetTX returns new relic transaction
func GetTX(c echo.Context) newrelic.Transaction {
tx := c.Get("txn")
if tx == nil {
Expand All @@ -36,7 +45,7 @@ func GetTX(c echo.Context) newrelic.Transaction {
return tx.(newrelic.Transaction)
}

//WithSegment adds a segment to new relic transaction
// WithSegment adds a segment to new relic transaction
func WithSegment(name string, c echo.Context, f func() error) error {
tx := GetTX(c)
if tx == nil {
Expand All @@ -47,9 +56,9 @@ func WithSegment(name string, c echo.Context, f func() error) error {
return f()
}

// MongoSearch searchs on mongo
// MongoSearch searches on mongo
func MongoSearch(ctx context.Context, q interface{}) ([]ACL, error) {
searchResults := []ACL{}
searchResults := make([]ACL, 0)
query := func(c interfaces.Collection) error {
fn := c.Find(q).All(&searchResults)
return fn
Expand Down Expand Up @@ -77,7 +86,60 @@ func GetTopics(ctx context.Context, username string, _topics []string) ([]string
return topics, err
}

func authenticate(ctx context.Context, app *App, userID string, topics ...string) (bool, []string, error) {
// IsAuthorized returns a boolean indicating whether the user is authorized to read messages
// from at least one of the given topics, and also a slice of all topics on which the user has authorization.
func IsAuthorized(ctx context.Context, app *App, userID string, topics ...string) (bool, []string, error) {
httpAuthEnabled := app.Config.GetBool("httpAuth.enabled")

if httpAuthEnabled {
return httpAuthenticate(app, userID, topics)
}

return mongoAuthenticate(ctx, userID, topics)
}

func httpAuthenticate(app *App, userID string, topics []string) (bool, []string, error) {
timeout := app.Config.GetDuration("httpAuth.timeout") * time.Second
address := app.Config.GetString("httpAuth.requestURL")

client := http.Client{
Timeout: timeout,
}

isAuthorized := false
allowedTopics := make([]string, 0)
for _, topic := range topics {
authRequest := authRequest{
Username: userID,
Topic: topic,
}

jsonPayload, _ := json.Marshal(authRequest)
request, _ := http.NewRequest(http.MethodPost, address, bytes.NewReader(jsonPayload))

credentialsNeeded := app.Config.GetBool("httpAuth.iam.enabled")
if credentialsNeeded {
username := app.Config.GetString("httpAuth.iam.credentials.username")
password := app.Config.GetString("httpAuth.iam.credentials.password")

request.SetBasicAuth(username, password)
}

response, err := client.Do(request)
if err != nil {
return false, nil, err
}

if response.StatusCode == 200 {
isAuthorized = true
allowedTopics = append(allowedTopics, topic)
}
}

return isAuthorized, allowedTopics, nil
}

func mongoAuthenticate(ctx context.Context, userID string, topics []string) (bool, []string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could you call this mongoAuthorize? Authentication != authorization

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, done!

for _, topic := range topics {
pieces := strings.Split(topic, "/")
pieces[len(pieces)-1] = "+"
Expand All @@ -92,7 +154,7 @@ func authenticate(ctx context.Context, app *App, userID string, topics ...string
for _, topic := range allowedTopics {
allowed[topic] = true
}
authorizedTopics := []string{}
authorizedTopics := make([]string, 0)
isAuthorized := false
for _, topic := range topics {
isAuthorized = isAuthorized || allowed[topic]
Expand Down
2 changes: 1 addition & 1 deletion app/histories.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func HistoriesHandler(app *App) func(c echo.Context) error {
}

logger.Logger.Debugf("user %s is asking for histories for topicPrefix %s with args topics=%s from=%d and limit=%d", userID, topicPrefix, topics, from, limit)
authenticated, authorizedTopics, err := authenticate(c.StdContext(), app, userID, topics...)
authenticated, authorizedTopics, err := IsAuthorized(c.StdContext(), app, userID, topics...)
if err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions app/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ func HistoryHandler(app *App) func(c echo.Context) error {
from = time.Now().Unix()
}

authenticated, _, err := authenticate(c.StdContext(), app, userID, topic)
authenticated, _, err := IsAuthorized(c.StdContext(), app, userID, topic)
if err != nil {
return err
}

logger.Logger.Debugf(
"user %s is asking for history for topic %s with args from=%d and limit=%d",
userID, topic, from, limit)
"user %s (authenticated=%v) is asking for history for topic %s with args from=%d and limit=%d",
userID, authenticated, topic, from, limit)

if !authenticated {
return c.String(echo.ErrUnauthorized.Code, echo.ErrUnauthorized.Message)
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ services:
- mosquitto
- cassandra
mongo:
image: mongo:3.0.15-wheezy
image: mongo:3.6.23
ports:
- "27017:27017"
2 changes: 1 addition & 1 deletion test_containers/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ services:
- PORT=8080
mongo:
container_name: mqtthistory_test_mongo
image: mongo:3.0.15-wheezy
image: mongo:3.6.23
ports:
- "27017:27017"