-
Notifications
You must be signed in to change notification settings - Fork 2
/
acl.go
185 lines (149 loc) · 5.23 KB
/
acl.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
package acl
import (
"fmt"
"net/http"
"context"
"github.com/Microkubes/microservice-security/auth"
"github.com/Microkubes/microservice-security/chain"
"github.com/Microkubes/microservice-tools/config"
"github.com/keitaroinc/goa"
"github.com/ory/ladon"
)
// Configuration is the configuration for the ACL middleware.
type Configuration struct {
// DBConfig is the configuration for the ACL database.
config.DBConfig
}
// AccessContext is a map string => interface used for additional ACL context data for the ACL check.
type AccessContext map[string]interface{}
type contextKey string
var ladonWardenKey contextKey = "LadonWarden"
var forbidden = goa.NewErrorClass("Unauthorized", 403)
// RequestContext holds the values for the request to an API action.
// Holds the relevant values for the authentication and authorization.
type RequestContext struct {
// Auth is the auth.Auth object generated by the security middlewares
Auth *auth.Auth
// Action is the action to be performed on the resource (api:read or api:write)
Action string
// Subject is a reference to who is accessing the resoruce (usually user id)
Subject string
// Scopes holds the values for the requested and approved scope of access of the client (api:read, api:write)
Scopes []string
// Resource is the accessed resource.
Resource string
// AccessContext contains additional data needed for ACL decision - for example it may hold the owner of the resource.
AccessContext
}
// NewACLMiddleware instantiates new SecurityChainMiddleware for ACL.
func NewACLMiddleware(manager ladon.Manager) (chain.SecurityChainMiddleware, error) {
warden := ladon.Ladon{
Manager: manager,
}
return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) (context.Context, http.ResponseWriter, error) {
authObj := auth.GetAuth(ctx)
if authObj == nil {
return ctx, rw, fmt.Errorf("No auth")
}
aclContext := ladon.Context{
"roles": authObj.Roles,
"organizations": authObj.Organizations,
"userId": authObj.UserID,
"username": authObj.Username,
}
aclRequest := ladon.Request{
Action: getAction(req),
Resource: req.URL.Path,
Subject: authObj.Username,
Context: aclContext,
}
var authErr error
if err := warden.IsAllowed(&aclRequest); err != nil {
authErr = forbidden(err)
}
return context.WithValue(ctx, ladonWardenKey, warden), rw, authErr
}, nil
}
// APIReadAction is "api:read". All HTTP methods except for POST, PUT, PATCH and DELETE are considered to be "api:read" action.
const APIReadAction = "api:read"
// APIWriteAction is "api:write". HTTP methods POST, PUT and DELETE are considered "api:write" action.
const APIWriteAction = "api:write"
func getAction(req *http.Request) string {
switch req.Method {
case "POST":
return APIWriteAction
case "PUT":
return APIWriteAction
case "PATCH":
return APIWriteAction
case "DELETE":
return APIWriteAction
default:
return APIReadAction
}
}
// IsAllowed is a helper function that can be used inside a controller action to perform additional
// checks for ACL when the default check is not enough. An example is prtotecting a resoruce to be accessed
// only by its owner. The resource owner is not known until the resource is fetched from the database,
// and the resource is not fetched util the actual action executes. In this scenario we can use
// IsAllowed to check once we have the resource fetched from database.
func IsAllowed(ctx context.Context, req *http.Request, subject string, aclContext AccessContext) error {
warden := ctx.Value(ladonWardenKey)
if warden == nil {
return fmt.Errorf("not ACL protected")
}
ladonWarden, ok := warden.(ladon.Warden)
if !ok {
return fmt.Errorf("warden is not ladon.Warden")
}
return ladonWarden.IsAllowed(toLadonRequest(req, subject, aclContext))
}
// CheckRequest checks the request represented as RequestContext against the ACL policies.
// This is a helper function to do a custom ACL check with additional context data.
func CheckRequest(ctx context.Context, req *RequestContext) error {
ladonReq := &ladon.Request{
Action: req.Action,
Context: ladon.Context{},
Resource: req.Resource,
Subject: req.Subject,
}
cx := ladonReq.Context
cx["userId"] = req.Auth.UserID
cx["username"] = req.Auth.Username
cx["roles"] = req.Auth.Roles
cx["organizations"] = req.Auth.Organizations
cx["scopes"] = req.Scopes
warden, err := getLadonWarden(ctx)
if err != nil {
return err
}
if warden == nil {
return fmt.Errorf("no ACL warden")
}
return warden.IsAllowed(ladonReq)
}
// getLadonWarden extracts the ladon.Warden from the given Context
func getLadonWarden(ctx context.Context) (ladon.Warden, error) {
warden := ctx.Value(ladonWardenKey)
if warden == nil {
return nil, nil
}
ladonWarden, ok := warden.(ladon.Warden)
if !ok {
return nil, fmt.Errorf("the warden is not ladon.Warden")
}
return ladonWarden, nil
}
// toLadonRequest generates a ladon.Request from given HTTP Request and additional AccessContext.
func toLadonRequest(req *http.Request, subject string, aclCtx AccessContext) *ladon.Request {
ladonCtx := ladon.Context{}
for key, val := range aclCtx {
ladonCtx[key] = val
}
return &ladon.Request{
Action: getAction(req),
Resource: req.URL.Path,
Subject: subject,
Context: ladonCtx,
}
}