forked from goadesign/goa
-
Notifications
You must be signed in to change notification settings - Fork 0
/
method.go
349 lines (331 loc) · 10.4 KB
/
method.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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package expr
import (
"fmt"
"goa.design/goa/eval"
)
type (
// StreamKind is a type denoting the kind of stream.
StreamKind int
// MethodExpr defines a single method.
MethodExpr struct {
// DSLFunc contains the DSL used to initialize the expression.
eval.DSLFunc
// Name of method.
Name string
// Description of method for consumption by humans.
Description string
// Docs points to the method external documentation if any.
Docs *DocsExpr
// Payload attribute
Payload *AttributeExpr
// Result attribute
Result *AttributeExpr
// Errors lists the error responses.
Errors []*ErrorExpr
// Requirements contains the security requirements for the
// method. One requirement is composed of potentially multiple
// schemes. Incoming requests must validate at least one
// requirement to be authorized.
Requirements []*SecurityExpr
// Service that owns method.
Service *ServiceExpr
// Meta is an arbitrary set of key/value pairs, see dsl.Meta
Meta MetaExpr
// Stream is the kind of stream (none, payload, result, or both)
// the method defines.
Stream StreamKind
// StreamingPayload is the payload sent across the stream.
StreamingPayload *AttributeExpr
}
)
const (
// NoStreamKind represents no payload or result stream in method.
NoStreamKind StreamKind = iota + 1
// ClientStreamKind represents client sends a streaming payload to
// method.
ClientStreamKind
// ServerStreamKind represents server sends a streaming result from
// method.
ServerStreamKind
// BidirectionalStreamKind represents client and server sending payload
// and result respectively via a stream.
BidirectionalStreamKind
)
// Error returns the error with the given name. It looks up recursively in the
// endpoint then the service and finally the root expression.
func (m *MethodExpr) Error(name string) *ErrorExpr {
for _, err := range m.Errors {
if err.Name == name {
return err
}
}
return m.Service.Error(name)
}
// EvalName returns the generic expression name used in error messages.
func (m *MethodExpr) EvalName() string {
var prefix, suffix string
if m.Name != "" {
suffix = fmt.Sprintf("method %#v", m.Name)
} else {
suffix = "unnamed method"
}
if m.Service != nil {
prefix = m.Service.EvalName() + " "
}
return prefix + suffix
}
// Prepare makes sure the payload and result types are initialized (to the Empty
// type if nil).
func (m *MethodExpr) Prepare() {
if m.Payload == nil {
m.Payload = &AttributeExpr{Type: Empty}
}
if m.StreamingPayload == nil {
m.StreamingPayload = &AttributeExpr{Type: Empty}
}
if m.Result == nil {
m.Result = &AttributeExpr{Type: Empty}
}
}
// Validate validates the method payloads, results, and errors (if any).
func (m *MethodExpr) Validate() error {
verr := new(eval.ValidationErrors)
verr.Merge(m.Payload.Validate("payload", m))
// validate security scheme requirements
var requirements []*SecurityExpr
if len(m.Requirements) > 0 {
requirements = m.Requirements
} else if len(m.Service.Requirements) > 0 {
requirements = m.Service.Requirements
}
var (
hasBasicAuth bool
hasAPIKey bool
hasJWT bool
hasOAuth bool
)
for _, r := range requirements {
for _, s := range r.Schemes {
verr.Merge(s.Validate())
switch s.Kind {
case BasicAuthKind:
hasBasicAuth = true
if !hasTag(m.Payload, "security:username") {
verr.Add(m, "payload of method %q of service %q does not define a username attribute, use Username to define one", m.Name, m.Service.Name)
}
if !hasTag(m.Payload, "security:password") {
verr.Add(m, "payload of method %q of service %q does not define a password attribute, use Password to define one", m.Name, m.Service.Name)
}
case APIKeyKind:
hasAPIKey = true
if !hasTag(m.Payload, "security:apikey:"+s.SchemeName) {
verr.Add(m, "payload of method %q of service %q does not define an API key attribute, use APIKey to define one", m.Name, m.Service.Name)
}
case JWTKind:
hasJWT = true
if !hasTag(m.Payload, "security:token") {
verr.Add(m, "payload of method %q of service %q does not define a JWT attribute, use Token to define one", m.Name, m.Service.Name)
}
case OAuth2Kind:
hasOAuth = true
if !hasTag(m.Payload, "security:accesstoken") {
verr.Add(m, "payload of method %q of service %q does not define a OAuth2 access token attribute, use AccessToken to define one", m.Name, m.Service.Name)
}
}
}
for _, scope := range r.Scopes {
found := false
for _, s := range r.Schemes {
if s.Kind == BasicAuthKind || s.Kind == APIKeyKind || s.Kind == OAuth2Kind || s.Kind == JWTKind {
for _, se := range s.Scopes {
if se.Name == scope {
found = true
break
}
}
}
}
if !found {
verr.Add(m, "security scope %q not found in any of the security schemes.", scope)
}
}
}
if !hasBasicAuth {
if hasTag(m.Payload, "security:username") {
verr.Add(m, "payload of method %q of service %q defines a username attribute, but no basic auth security scheme exist", m.Name, m.Service.Name)
}
if hasTag(m.Payload, "security:password") {
verr.Add(m, "payload of method %q of service %q defines a password attribute, but no basic auth security scheme exist", m.Name, m.Service.Name)
}
}
if !hasAPIKey {
if hasTagPrefix(m.Payload, "security:apikey") {
verr.Add(m, "payload of method %q of service %q defines an API key attribute, but no APIKey security scheme exist", m.Name, m.Service.Name)
}
}
if !hasJWT {
if hasTag(m.Payload, "security:token") {
verr.Add(m, "payload of method %q of service %q defines a JWT token attribute, but no JWT auth security scheme exist", m.Name, m.Service.Name)
}
}
if !hasOAuth {
if hasTag(m.Payload, "security:accesstoken") {
verr.Add(m, "payload of method %q of service %q defines a OAuth2 access token attribute, but no OAuth2 security scheme exist", m.Name, m.Service.Name)
}
}
if m.StreamingPayload.Type != Empty {
verr.Merge(m.StreamingPayload.Validate("streaming_payload", m))
}
if m.Result.Type != Empty {
verr.Merge(m.Result.Validate("result", m))
}
for i, e := range m.Errors {
if err := e.Validate(); err != nil {
if verrs, ok := err.(*eval.ValidationErrors); ok {
verr.Merge(verrs)
}
}
for j, e2 := range m.Errors {
// If an object type is used to define more than one errors validate the
// presence of struct:error:name meta in the object type.
if i != j && e.Type == e2.Type && IsObject(e.Type) {
var found bool
walkAttribute(e.AttributeExpr, func(name string, att *AttributeExpr) error {
if _, ok := att.Meta["struct:error:name"]; ok {
found = true
return fmt.Errorf("struct:error:name found: stop iteration")
}
return nil
})
if !found {
verr.Add(e, "type %q is used to define multiple errors and must identify the attribute containing error name. Use Meta with the key 'struct:error:name' on the error name attribute", e.AttributeExpr.Type.Name())
break
}
}
}
}
return verr
}
// hasTag is a helper function that traverses the given attribute and all its
// bases recursively looking for an attribute with the given tag meta. This
// recursion is only needed for attributes that have not been finalized yet.
func hasTag(p *AttributeExpr, tag string) bool {
if p.HasTag(tag) {
return true
}
for _, base := range p.Bases {
ut, ok := base.(UserType)
if !ok {
continue
}
if hasTag(ut.Attribute(), tag) {
return true
}
}
if ut, ok := p.Type.(UserType); ok {
return hasTag(ut.Attribute(), tag)
}
return false
}
// hasTag is a helper function that traverses the given attribute and all its
// bases recursively looking for an attribute with the given tag meta prefix. This
// recursion is only needed for attributes that have not been finalized yet.
func hasTagPrefix(p *AttributeExpr, prefix string) bool {
if p.HasTagPrefix(prefix) {
return true
}
for _, base := range p.Bases {
ut, ok := base.(UserType)
if !ok {
continue
}
if hasTagPrefix(ut.Attribute(), prefix) {
return true
}
}
if ut, ok := p.Type.(UserType); ok {
return hasTagPrefix(ut.Attribute(), prefix)
}
return false
}
// Finalize makes sure the method payload and result types are set. It also
// projects the result if it is a result type and a view is explicitly set in
// the design or a result type having at most one view.
func (m *MethodExpr) Finalize() {
if m.Payload == nil {
m.Payload = &AttributeExpr{Type: Empty}
} else {
m.Payload.Finalize()
}
if m.StreamingPayload == nil {
m.StreamingPayload = &AttributeExpr{Type: Empty}
} else {
m.StreamingPayload.Finalize()
}
if m.Result == nil {
m.Result = &AttributeExpr{Type: Empty}
} else {
m.Result.Finalize()
if rt, ok := m.Result.Type.(*ResultTypeExpr); ok {
rt.Finalize()
}
}
for _, e := range m.Errors {
e.Finalize()
}
// Inherit security requirements
noreq := false
for _, r := range m.Requirements {
// Handle special case of no security
for _, s := range r.Schemes {
if s.Kind == NoKind {
noreq = true
break
}
}
if noreq {
break
}
}
if noreq {
m.Requirements = nil
} else if len(m.Requirements) == 0 && len(m.Service.Requirements) > 0 {
m.Requirements = copyReqs(m.Service.Requirements)
}
}
// IsStreaming determines whether the method streams payload or result.
func (m *MethodExpr) IsStreaming() bool {
return m.IsPayloadStreaming() || m.IsResultStreaming()
}
// IsPayloadStreaming determines whether the method streams payload.
func (m *MethodExpr) IsPayloadStreaming() bool {
return m.Stream == ClientStreamKind || m.Stream == BidirectionalStreamKind
}
// IsResultStreaming determines whether the method streams payload.
func (m *MethodExpr) IsResultStreaming() bool {
return m.Stream == ServerStreamKind || m.Stream == BidirectionalStreamKind
}
// helper function that duplicates just enough of a security expression so that
// its scheme names can be overridden without affecting the original.
func copyReqs(reqs []*SecurityExpr) []*SecurityExpr {
reqs2 := make([]*SecurityExpr, len(reqs))
for i, req := range reqs {
req2 := &SecurityExpr{Scopes: req.Scopes}
schs := make([]*SchemeExpr, len(req.Schemes))
for j, sch := range req.Schemes {
schs[j] = &SchemeExpr{
Kind: sch.Kind,
SchemeName: sch.SchemeName,
Description: sch.Description,
In: sch.In,
Name: sch.Name,
Scopes: sch.Scopes,
Flows: sch.Flows,
Meta: sch.Meta,
}
}
req2.Schemes = schs
reqs2[i] = req2
}
return reqs2
}