forked from open-policy-agent/opa-docker-authz
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
223 lines (179 loc) · 5.91 KB
/
main.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Copyright 2016 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package main
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"github.com/docker/go-plugins-helpers/authorization"
"github.com/fsnotify/fsnotify"
)
// DockerAuthZPlugin implements the authorization.Plugin interface.
// Every request received by the Docker daemon will be forwarded to the
// AuthZReq function. The AuthZReq function returns a response that indicates
// whether the request should be allowed or denied.
type DockerAuthZPlugin struct {
opaURL string
}
// AuthZReq is called when the Docker daemon receives an API request.
// AuthZReq returns an authorization.Response that indicates whether the request should be
// allowed or denied.
func (p DockerAuthZPlugin) AuthZReq(r authorization.Request) authorization.Response {
fmt.Println("Received request from Docker:", r)
b, err := IsAllowed(p.opaURL, r)
if b {
return authorization.Response{Allow: true}
} else if err != nil {
return authorization.Response{Err: err.Error()}
}
return authorization.Response{Msg: "request rejected by administrative policy"}
}
// AuthZRes is called before the Docker daemon returns an API response. All responses
// are allowed.
func (p DockerAuthZPlugin) AuthZRes(r authorization.Request) authorization.Response {
return authorization.Response{Allow: true}
}
// IsAllowed queries the policy that was loaded into OPA and returns (true, nil) if the
// request should be allowed. If the request is not allowed, b will be false and e will
// be set to indicate if an error occurred. This function "fails closed" meaning if an error
// occurs, the request will be rejected.
func IsAllowed(opaURL string, r authorization.Request) (b bool, e error) {
// Query OPA to see if the request should be allowed.
resp, err := QueryDataAPI(opaURL, "/opa/example/allow_request", r)
if err != nil {
return false, err
}
// If the request succeeded, the request should be allowed.
if resp.StatusCode == 200 {
return true, nil
}
// If the response is undefined, the request should be rejected.
if IsUndefined(resp) {
return false, nil
}
// Othewrise, an error occured so reject the request and include an error message.
if resp.StatusCode == 404 {
return false, fmt.Errorf("policy does not exist")
}
return false, fmt.Errorf("unexpected error: %v", resp)
}
// IsUndefined returns true if the http.Response resp from OPA indicates
// an undefined query result
func IsUndefined(resp *http.Response) bool {
return resp.StatusCode == 404
}
// LoadPolicy reads the policy definition from the path f and upserts it into OPA.
func LoadPolicy(opaURL, f string) error {
r, err := os.Open(f)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", opaURL+"/policies/example_policy", r)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
d := json.NewDecoder(resp.Body)
var e map[string]interface{}
if err := d.Decode(&e); err != nil {
return err
}
return fmt.Errorf("upsert failed (code: %v): %v", e["Code"], e["Message"])
}
return nil
}
// WatchPolicy creates a filesystem watch on the path f and waits for changes. When the
// file changes, LoadPolicy is called with the path f.
func WatchPolicy(opaURL, f string) error {
w, err := fsnotify.NewWatcher()
if err != nil {
return err
}
go func() {
for {
select {
case evt := <-w.Events:
if evt.Op&fsnotify.Write != 0 {
if err := LoadPolicy(opaURL, f); err != nil {
fmt.Println("Error reloading policy definition:", err)
} else {
fmt.Println("Reloaded policy definition.")
}
}
}
}
}()
if err := w.Add(f); err != nil {
return err
}
return nil
}
// QueryDataAPI executes a GET request against OPA's Data API. If successful, an http.Response is
// returned. The doc parameter identifies the document defined by the authorization policy. The
// query includes the authorization.Request r as input.
func QueryDataAPI(opaURL string, doc string, r authorization.Request) (*http.Response, error) {
m := map[string]interface{}{
"Headers": r.RequestHeaders,
"Path": r.RequestURI,
"Method": r.RequestMethod,
"Body": r.RequestBody,
"User": r.User,
"AuthMethod": r.UserAuthNMethod,
}
if r.RequestHeaders["Content-Type"] == "application/json" {
var body interface{}
if err := json.Unmarshal(r.RequestBody, &body); err != nil {
return nil, err
}
m["Body"] = body
}
bs, err := json.Marshal(m)
if err != nil {
return nil, err
}
global := url.QueryEscape(string(bs))
// The policy declares an input named "request" that is intended to contain
// the Docker API request.
url := fmt.Sprintf("%s/data%s?global=request:%s", opaURL, doc, global)
return http.Get(url)
}
const (
version = "0.1.1"
)
func main() {
bindAddr := flag.String("bind-addr", ":8080", "sets the address the plugin will bind to")
pluginName := flag.String("plugin-name", "docker-authz-plugin", "sets the plugin name that will be registered with Docker")
opaURL := flag.String("opa-url", "http://localhost:8181/v1", "sets the base URL of OPA's HTTP API")
policyFile := flag.String("policy-file", "", "sets the path of the policy file to load")
vers := flag.Bool("version", false, "print the version of the plugin")
flag.Parse()
if *vers {
fmt.Println(version)
os.Exit(0)
}
p := DockerAuthZPlugin{*opaURL}
h := authorization.NewHandler(p)
if *policyFile != "" {
if err := LoadPolicy(*opaURL, *policyFile); err != nil {
fmt.Println("Error while loading policy:", err)
os.Exit(1)
}
if err := WatchPolicy(*opaURL, *policyFile); err != nil {
fmt.Println("Error while starting watch:", err)
os.Exit(1)
}
}
fmt.Println("Starting server.")
if err := h.ServeTCP(*pluginName, *bindAddr); err != nil {
fmt.Println("Error while serving HTTP:", err)
os.Exit(1)
}
}