Skip to content

Commit

Permalink
Merge pull request #9 from topfreegames/add/http-auth
Browse files Browse the repository at this point in the history
Add support for HTTP-based authorization
  • Loading branch information
lmsilva-wls committed May 26, 2021
2 parents 779bbcd + 1d4f33d commit 6390984
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 22 deletions.
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 httpAuthorize(app, userID, topics)
}

return mongoAuthorize(ctx, userID, topics)
}

func httpAuthorize(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 mongoAuthorize(ctx context.Context, userID string, topics []string) (bool, []string, error) {
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"

0 comments on commit 6390984

Please sign in to comment.