Skip to content
This repository has been archived by the owner on Aug 5, 2022. It is now read-only.

Add OPA in place of the local PDP. #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,13 @@ services:
context: .
image: kenfdev/opa-api-auth-go
ports:
- 1323:1323
- 1323:1323
environment:
PDP_ENDPOINT: http://pdp:8181/v1/data/httpapi/authz/allow
pdp:
image: openpolicyagent/opa:0.12.0
ports:
- 8181:8181
volumes:
- ./opa:/etc/opt/opa
command: ["run", "--server", "/etc/opt/opa/authz.rego"]
123 changes: 72 additions & 51 deletions gateway/policy.go
Original file line number Diff line number Diff line change
@@ -1,82 +1,103 @@
package gateway

import (
"bytes"
"encoding/json"
"github.com/dgrijalva/jwt-go"
"github.com/kenfdev/opa-api-auth-go/entity"
"github.com/labstack/echo"
"github.com/sirupsen/logrus"
"regexp"
"io/ioutil"
"net/http"
"strings"
)

type PolicyGateway interface {
Ask(echo.Context) bool
}

type PolicyLocalGateway struct {
getSalaryPathRegex *regexp.Regexp
type opaRequest struct {
// Input wraps the OPA request (https://www.openpolicyagent.org/docs/latest/rest-api/#get-a-document-with-input)
Input *opaInput `json:"input"`
}

func NewPolicyLocalGateway() *PolicyLocalGateway {
return &PolicyLocalGateway{
getSalaryPathRegex: regexp.MustCompile("GET /finance/salary/.*"),
}
type opaInput struct {
// The token of the requester
Token string `json:"token"`
// The path to which the request was made split to an array
Path []string `json:"path"`
// The HTTP Method
Method string `json:"method"`
}

// Ask checks if an action is permitted as it inspects the request context
func (gw *PolicyLocalGateway) Ask(c echo.Context) bool {
path := c.Request().Method + " " + c.Path()
type opaResponse struct {
Result bool `json:"result"`
}

raw := c.Get("token").(*jwt.Token)
claims := raw.Claims.(*entity.TokenClaims)
// PolicyOpaGateway makes policy decision requests to OPA
type PolicyOpaGateway struct {
endpoint string
}

var allow = false
switch {
case gw.getSalaryPathRegex.MatchString(path):
allow = gw.checkGETSalary(c, claims)
func NewPolicyOpaGateway(endpoint string) *PolicyOpaGateway {
return &PolicyOpaGateway{
endpoint: endpoint,
}

return allow
}

func (gw *PolicyLocalGateway) checkGETSalary(c echo.Context, claims *entity.TokenClaims) bool {
userID := c.Param("id")
logrus.WithFields(logrus.Fields{
"userID": userID,
"claims": claims,
}).Info("Checking GET salary policies")

if yes := gw.checkIfOwner(userID, claims); yes {
logrus.Info("Allowing because requester is the owner")
return true
// Ask requests to OPA with required inputs and returns the decision made by OPA
func (gw *PolicyOpaGateway) Ask(c echo.Context) bool {
token := c.Get("token").(*jwt.Token)
// After splitting, the first element isn't necessary
// "/finance/salary/alice" -> ["", "finance", "salary", "alice"]
paths := strings.Split(c.Request().RequestURI, "/")[1:]
method := c.Request().Method

// create input to send to OPA
input := &opaInput{
Token: token.Raw,
Path: paths,
Method: method,
}

if yes := gw.checkIfSubordinate(userID, claims); yes {
logrus.Info("Allowing because target is a subordinate of requester")
return true
opaRequest := &opaRequest{
Input: input,
}

if yes := gw.checkIfHR(claims); yes {
logrus.Info("Allowing because requester is a member of HR")
return true
logrus.WithFields(logrus.Fields{
"token": input.Token,
"path": input.Path,
"method": input.Method,
}).Info("Requesting PDP for decision")

requestBody, err := json.Marshal(opaRequest)
if err != nil {
logrus.WithFields(logrus.Fields{"error": err}).Error("Marshalling request body failed")
return false
}

logrus.Info("Denying request")
return false
}
// request OPA
resp, err := http.Post(gw.endpoint, "application/json", bytes.NewBuffer(requestBody))
if err != nil {
logrus.WithFields(logrus.Fields{"error": err}).Error("PDP Request failed")
return false
}
defer resp.Body.Close()

func (*PolicyLocalGateway) checkIfOwner(userID string, claims *entity.TokenClaims) bool {
return userID == claims.User
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
logrus.WithFields(logrus.Fields{"error": err}).Error("Reading body of response failed")
return false
}

func (*PolicyLocalGateway) checkIfSubordinate(userID string, claims *entity.TokenClaims) bool {
for _, subordinate := range claims.Subordinates {
if subordinate == userID {
return true
}
var opaResponse opaResponse
err = json.Unmarshal(body, &opaResponse)
if err != nil {
logrus.WithFields(logrus.Fields{"error": err}).Error("Unmarshalling response body failed")
return false
}
return false
}

func (*PolicyLocalGateway) checkIfHR(claims *entity.TokenClaims) bool {
return claims.BelongsToHR
logrus.WithFields(logrus.Fields{
"result": opaResponse.Result,
}).Info("Decision")

return opaResponse.Result
}
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import (
localmiddleware "github.com/kenfdev/opa-api-auth-go/middleware"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"os"
)

func main() {
e := echo.New()

// inits
policyGateway := gateway.NewPolicyLocalGateway()
endpoint := os.Getenv("PDP_ENDPOINT")
policyGateway := gateway.NewPolicyOpaGateway(endpoint)
salaryGateway := gateway.NewSalaryInMemoryGateway()

// middleware
Expand Down
42 changes: 42 additions & 0 deletions opa/authz.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package httpapi.authz

# io.jwt.decode_verify
# https://www.openpolicyagent.org/docs/latest/language-reference/#tokens
token = t {
[valid, _, payload] = io.jwt.decode_verify(input.token, { "secret": "secret" })
t := {
"valid": valid,
"payload": payload
}
}

default allow = false

# Allow users to get their own salaries.
allow {
token.valid

some username
input.method == "GET"
input.path = ["finance", "salary", username]
token.payload.user == username
}

# Allow managers to get their subordinate's salaries.
allow {
token.valid

some username
input.method == "GET"
input.path = ["finance", "salary", username]
token.payload.subordinates[_] == username
}

# Allow HR members to get anyone's salary.
allow {
token.valid

input.method == "GET"
input.path = ["finance", "salary", _]
token.payload.hr == true
}