/
opa.go
139 lines (118 loc) · 3.24 KB
/
opa.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Package opahttp implements the authz.Authorizer using an Open Policy Agent (OPA).
package opahttp // import "entrogo.com/entroq/pkg/authz/opahttp"
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"entrogo.com/entroq/pkg/authz"
)
const (
DefaultHostURL = "http://localhost:8181"
DefaultAPIPath = "/v1/data/entroq/authz"
)
// OPA is a client-like object for interacting with OPA authorization policies.
// It adheres to the authz.Authorizer interface.
type OPA struct {
hostURL string
apiPath string
allowTestUser bool
}
// Option defines a setting for creating an OPA authorizer.
type Option func(*OPA)
// WithInsecureTestUser must be set when doing testing and the use of the
// Authz.TestUser (instead of a signed token, for example) is desired. Without
// this option, the presence of the TestUser field causes an error.
func WithInsecureTestUser() Option {
return func(a *OPA) {
a.allowTestUser = true
}
}
// WithHostURL sets the host OPA URL for a query authorization request, such as
// its default value given in DefaultURL.
func WithHostURL(u string) Option {
return func(a *OPA) {
if u != "" {
a.hostURL = u
}
}
}
// WithAPIPath sets the API path to request for authorization.
func WithAPIPath(p string) Option {
return func(a *OPA) {
if p != "" {
a.apiPath = p
}
}
}
// New creates a new OPA client with the given options.
func New(opts ...Option) *OPA {
a := &OPA{
hostURL: DefaultHostURL,
apiPath: DefaultAPIPath,
}
for _, opt := range opts {
opt(a)
}
return a
}
func (a *OPA) fullURL() string {
h, p := a.hostURL, a.apiPath
if h == "" {
h = DefaultHostURL
}
if p == "" {
p = DefaultAPIPath
}
return strings.TrimRight(h, "/") + "/" + strings.TrimLeft(p, "/")
}
// Authorize checks for unmatched queues and actions. A nil error means authorized.
// If the error satisfies errors.Is on a *authz.AuthzError, it can be unpacked to find
// which queues and actions were not satisfied.
func (a *OPA) Authorize(ctx context.Context, req *authz.Request) error {
if !a.allowTestUser && req.Authz.TestUser != "" {
return fmt.Errorf("insecure test user present, but not allowed")
}
body := map[string]*authz.Request{
"input": req,
}
b, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("authorize: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, a.fullURL(), bytes.NewBuffer(b))
if err != nil {
return fmt.Errorf("authorize: %w", err)
}
httpReq.Header.Add("Content-Type", "application/json")
httpReq.Header.Add("Authorization", req.Authz.String())
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return fmt.Errorf("authorize: %w", err)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("no read: %w", err)
}
type authzResp struct {
Result *authz.AuthzError `json:"result"`
}
result := new(authzResp)
if err := json.NewDecoder(bytes.NewBuffer(respBytes)).Decode(result); err != nil {
return fmt.Errorf("authorize: %w", err)
}
// Check result value.
if e := result.Result; !e.Allow {
// We got an error with information about missing queue/actions.
return e
}
return nil
}
// Close cleans up any resources used.
func (a *OPA) Close() error {
return nil
}