-
Notifications
You must be signed in to change notification settings - Fork 9
/
service.go
222 lines (186 loc) · 6.21 KB
/
service.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
package service
import (
"fmt"
"strings"
mdmv1 "github.com/metal-stack/masterdata-api/api/v1"
"github.com/metal-stack/metal-lib/jwt/sec"
"github.com/metal-stack/metal-lib/rest"
"github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore"
"github.com/metal-stack/metal-api/cmd/metal-api/internal/metal"
"github.com/metal-stack/metal-lib/httperrors"
"github.com/emicklei/go-restful/v3"
"github.com/metal-stack/security"
"go.uber.org/zap"
)
const (
viewUserEmail = "metal-view@metal-stack.io"
editUserEmail = "metal-edit@metal-stack.io"
adminUserEmail = "metal-admin@metal-stack.io"
)
// BasePath is the URL base path for the metal-api
var BasePath = "/"
type webResource struct {
log *zap.SugaredLogger
ds *datastore.RethinkStore
}
// logger returns the request logger from the request.
func (w *webResource) logger(rq *restful.Request) *zap.SugaredLogger {
requestLogger := rest.GetLoggerFromContext(rq.Request, w.log)
return requestLogger.WithOptions(zap.AddCallerSkip(1))
}
func (w *webResource) sendError(rq *restful.Request, rsp *restful.Response, httperr *httperrors.HTTPErrorResponse) {
w.logger(rq).Errorw("service error", "status", httperr.StatusCode, "error", httperr.Message)
w.send(rq, rsp, httperr.StatusCode, httperr)
}
func (w *webResource) send(rq *restful.Request, rsp *restful.Response, status int, value any) {
send(w.logger(rq), rsp, status, value)
}
func defaultError(err error) *httperrors.HTTPErrorResponse {
if metal.IsNotFound(err) {
return httperrors.NotFound(err)
}
if metal.IsConflict(err) {
return httperrors.Conflict(err)
}
if metal.IsInternal(err) {
return httperrors.InternalServerError(err)
}
if mdmv1.IsNotFound(err) {
return httperrors.NotFound(err)
}
if mdmv1.IsConflict(err) {
return httperrors.Conflict(err)
}
if mdmv1.IsInternal(err) {
return httperrors.InternalServerError(err)
}
return httperrors.UnprocessableEntity(err)
}
func send(log *zap.SugaredLogger, rsp *restful.Response, status int, value any) {
err := rsp.WriteHeaderAndEntity(status, value)
if err != nil {
log.Errorw("failed to send response", "error", err)
}
}
// UserDirectory is the directory of users
type UserDirectory struct {
viewer security.User
edit security.User
admin security.User
metalUsers map[string]security.User
}
// NewUserDirectory creates a new user directory with default users
func NewUserDirectory(providerTenant string) *UserDirectory {
ud := &UserDirectory{}
// User.Name is used as AuthType for HMAC
ud.viewer = security.User{
EMail: viewUserEmail,
Name: "Metal-View",
Groups: sec.MergeResourceAccess(metal.ViewGroups),
Tenant: providerTenant,
}
ud.edit = security.User{
EMail: editUserEmail,
Name: "Metal-Edit",
Groups: sec.MergeResourceAccess(metal.EditGroups),
Tenant: providerTenant,
}
ud.admin = security.User{
EMail: adminUserEmail,
Name: "Metal-Admin",
Groups: sec.MergeResourceAccess(metal.AdminGroups),
Tenant: providerTenant,
}
ud.metalUsers = map[string]security.User{
"view": ud.viewer,
"edit": ud.edit,
"admin": ud.admin,
}
return ud
}
// UserNames returns the list of user names in the directory.
func (ud *UserDirectory) UserNames() []string {
keys := make([]string, 0, len(ud.metalUsers))
for k := range ud.metalUsers {
keys = append(keys, k)
}
return keys
}
// Get a user by its user name.
func (ud *UserDirectory) Get(user string) security.User {
return ud.metalUsers[user]
}
func viewer(rf restful.RouteFunction) restful.RouteFunction {
return oneOf(rf, metal.ViewAccess...)
}
func editor(rf restful.RouteFunction) restful.RouteFunction {
return oneOf(rf, metal.EditAccess...)
}
func admin(rf restful.RouteFunction) restful.RouteFunction {
return oneOf(rf, metal.AdminAccess...)
}
func oneOf(rf restful.RouteFunction, acc ...security.ResourceAccess) restful.RouteFunction {
return func(request *restful.Request, response *restful.Response) {
usr := security.GetUser(request.Request)
if !usr.HasGroup(acc...) {
log := rest.GetLoggerFromContext(request.Request, nil)
if log != nil {
log.Infow("missing group", "user", usr, "required-group", acc)
}
httperr := httperrors.Forbidden(fmt.Errorf("you are not member in one of %+v", acc))
err := response.WriteHeaderAndEntity(httperr.StatusCode, httperr)
if err != nil && log != nil {
log.Errorw("failed to send response", "error", err)
}
return
}
rf(request, response)
}
}
func tenant(request *restful.Request) string {
return security.GetUser(request.Request).Tenant
}
// TenantEnsurer holds allowed tenants and a list of path suffixes that
type TenantEnsurer struct {
logger *zap.SugaredLogger
allowedTenants map[string]bool
excludedPathSuffixes []string
}
// NewTenantEnsurer creates a new ensurer with the given tenants.
func NewTenantEnsurer(log *zap.SugaredLogger, tenants, excludedPathSuffixes []string) TenantEnsurer {
result := TenantEnsurer{
logger: log,
allowedTenants: make(map[string]bool),
excludedPathSuffixes: excludedPathSuffixes,
}
for _, t := range tenants {
result.allowedTenants[strings.ToLower(t)] = true
}
return result
}
// EnsureAllowedTenantFilter checks if the tenant of the user is allowed.
func (e *TenantEnsurer) EnsureAllowedTenantFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
p := req.Request.URL.Path
// securing health checks would break monitoring tools
// preventing liveliness would break status of machines
for _, suffix := range e.excludedPathSuffixes {
if strings.HasSuffix(p, suffix) {
chain.ProcessFilter(req, resp)
return
}
}
// enforce tenant check otherwise
tenantID := tenant(req)
if !e.allowed(tenantID) {
httperror := httperrors.Forbidden(fmt.Errorf("tenant %s not allowed", tenantID))
requestLogger := rest.GetLoggerFromContext(req.Request, e.logger).Desugar().WithOptions(zap.AddCallerSkip(1)).Sugar()
requestLogger.Errorw("service error", "status", httperror.StatusCode, "error", httperror.Message)
send(requestLogger, resp, httperror.StatusCode, httperror.Message)
return
}
chain.ProcessFilter(req, resp)
}
// allowed checks if the given tenant is allowed (case insensitive)
func (e *TenantEnsurer) allowed(tenant string) bool {
return e.allowedTenants[strings.ToLower(tenant)]
}