Skip to content

Commit feb52b3

Browse files
fix(codegen): dedupe security schemes by type (#3690)
* fix(codegen): dedupe security schemes by type This commit solves a bug where designs that use multiple security schemes _of the same type_ break the generated `Auther` interface for a service as well as endpoint-related init methods due to unnecessary code duplication from ranging over `.Schemes`. * chore: add tests for multiple security services
1 parent baf70d8 commit feb52b3

7 files changed

Lines changed: 148 additions & 3 deletions

File tree

codegen/service/service_data.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,21 @@ func (s SchemesData) Append(d *SchemeData) SchemesData {
679679
return append(s, d)
680680
}
681681

682+
// DedupeByType returns a new SchemesData slice that is deduplicated by scheme
683+
// type.
684+
func (s SchemesData) DedupeByType() SchemesData {
685+
seen := make(map[string]struct{})
686+
uniqueSchemes := SchemesData{}
687+
for _, s := range s {
688+
if _, ok := seen[s.Type]; !ok {
689+
seen[s.Type] = struct{}{}
690+
uniqueSchemes = append(uniqueSchemes, s)
691+
}
692+
}
693+
694+
return uniqueSchemes
695+
}
696+
682697
// analyze creates the data necessary to render the code of the given service.
683698
// It records the user types needed by the service definition in userTypes.
684699
func (d ServicesData) analyze(service *expr.ServiceExpr) *Data {

codegen/service/service_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ func TestService(t *testing.T) {
5656
{"service-bidirectional-streaming-no-payload", testdata.BidirectionalStreamingNoPayloadMethodDSL, testdata.BidirectionalStreamingNoPayloadMethod},
5757
{"service-bidirectional-streaming-result-with-views", testdata.BidirectionalStreamingResultWithViewsMethodDSL, testdata.BidirectionalStreamingResultWithViewsMethod},
5858
{"service-bidirectional-streaming-result-with-explicit-view", testdata.BidirectionalStreamingResultWithExplicitViewMethodDSL, testdata.BidirectionalStreamingResultWithExplicitViewMethod},
59+
{"service-multiple-api-key-security", testdata.MultipleAPIKeySecurityDSL, testdata.MultipleAPIKeySecurity},
60+
{"service-mixed-and-multiple-api-key-security", testdata.MixedAndMultipleAPIKeySecurityDSL, testdata.MixedAndMultipleAPIKeySecurity},
5961
}
6062
for _, c := range cases {
6163
t.Run(c.Name, func(t *testing.T) {

codegen/service/templates/service.go.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type Service interface {
2929
{{- if .Schemes }}
3030
// Auther defines the authorization functions to be implemented by the service.
3131
type Auther interface {
32-
{{- range .Schemes }}
32+
{{- range .Schemes.DedupeByType }}
3333
{{ printf "%sAuth implements the authorization logic for the %s security scheme." .Type .Type | comment }}
3434
{{ .Type }}Auth(ctx context.Context, {{ if eq .Type "Basic" }}user, pass{{ else if eq .Type "APIKey" }}key{{ else }}token{{ end }} string, schema *security.{{ .Type }}Scheme) (context.Context, error)
3535
{{- end }}

codegen/service/templates/service_endpoint_method.go.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22

33
{{ printf "New%sEndpoint returns an endpoint function that calls the method %q of service %q." .VarName .Name .ServiceName | comment }}
4-
func New{{ .VarName }}Endpoint(s {{ .ServiceVarName }}{{ range .Schemes }}, auth{{ .Type }}Fn security.Auth{{ .Type }}Func{{ end }}) goa.Endpoint {
4+
func New{{ .VarName }}Endpoint(s {{ .ServiceVarName }}{{ range .Schemes.DedupeByType }}, auth{{ .Type }}Fn security.Auth{{ .Type }}Func{{ end }}) goa.Endpoint {
55
return func(ctx context.Context, req any) (any, error) {
66
{{- if or .ServerStream }}
77
ep := req.(*{{ .ServerStream.EndpointStruct }})

codegen/service/templates/service_endpoints_init.go.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func New{{ .VarName }}(s {{ .ServiceVarName }}{{ if .HasServerInterceptors }}, s
1111
return &{{ .VarName }}{
1212
{{- end }}
1313
{{- range .Methods }}
14-
{{ .VarName }}: New{{ .VarName }}Endpoint(s{{ range .Schemes }}, a.{{ .Type }}Auth{{ end }}),
14+
{{ .VarName }}: New{{ .VarName }}Endpoint(s{{ range .Schemes.DedupeByType }}, a.{{ .Type }}Auth{{ end }}),
1515
{{- end }}
1616
}
1717
{{- if .HasServerInterceptors }}

codegen/service/testdata/service_code.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3315,3 +3315,79 @@ type Foo struct {
33153315
IntField *int
33163316
}
33173317
`
3318+
3319+
const MultipleAPIKeySecurity = `
3320+
// Service is the MultipleAPIKeySecurity service interface.
3321+
type Service interface {
3322+
// A implements A.
3323+
A(context.Context, *APayload) (err error)
3324+
}
3325+
3326+
// Auther defines the authorization functions to be implemented by the service.
3327+
type Auther interface {
3328+
// APIKeyAuth implements the authorization logic for the APIKey security scheme.
3329+
APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error)
3330+
}
3331+
3332+
// APIName is the name of the API as defined in the design.
3333+
const APIName = "test api"
3334+
3335+
// APIVersion is the version of the API as defined in the design.
3336+
const APIVersion = "0.0.1"
3337+
3338+
// ServiceName is the name of the service as defined in the design. This is the
3339+
// same value that is set in the endpoint request contexts under the ServiceKey
3340+
// key.
3341+
const ServiceName = "MultipleAPIKeySecurity"
3342+
3343+
// MethodNames lists the service method names as defined in the design. These
3344+
// are the same values that are set in the endpoint request contexts under the
3345+
// MethodKey key.
3346+
var MethodNames = [1]string{"A"}
3347+
3348+
// APayload is the payload type of the MultipleAPIKeySecurity service A method.
3349+
type APayload struct {
3350+
APIKey string
3351+
TenantID string
3352+
}
3353+
`
3354+
3355+
const MixedAndMultipleAPIKeySecurity = `
3356+
// Service is the MixedAndMultipleAPIKeySecurity service interface.
3357+
type Service interface {
3358+
// A implements A.
3359+
A(context.Context, *APayload) (err error)
3360+
}
3361+
3362+
// Auther defines the authorization functions to be implemented by the service.
3363+
type Auther interface {
3364+
// JWTAuth implements the authorization logic for the JWT security scheme.
3365+
JWTAuth(ctx context.Context, token string, schema *security.JWTScheme) (context.Context, error)
3366+
// APIKeyAuth implements the authorization logic for the APIKey security scheme.
3367+
APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error)
3368+
}
3369+
3370+
// APIName is the name of the API as defined in the design.
3371+
const APIName = "test api"
3372+
3373+
// APIVersion is the version of the API as defined in the design.
3374+
const APIVersion = "0.0.1"
3375+
3376+
// ServiceName is the name of the service as defined in the design. This is the
3377+
// same value that is set in the endpoint request contexts under the ServiceKey
3378+
// key.
3379+
const ServiceName = "MixedAndMultipleAPIKeySecurity"
3380+
3381+
// MethodNames lists the service method names as defined in the design. These
3382+
// are the same values that are set in the endpoint request contexts under the
3383+
// MethodKey key.
3384+
var MethodNames = [1]string{"A"}
3385+
3386+
// APayload is the payload type of the MixedAndMultipleAPIKeySecurity service A
3387+
// method.
3388+
type APayload struct {
3389+
JWT *string
3390+
APIKey *string
3391+
TenantID *string
3392+
}
3393+
`

codegen/service/testdata/service_dsls.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,3 +981,55 @@ var EndpointWithClientInterceptorDSL = func() {
981981
})
982982
})
983983
}
984+
985+
var MultipleAPIKeySecurityDSL = func() {
986+
var APIKeyAuth = APIKeySecurity("api_key")
987+
var TenantKeyAuth = APIKeySecurity("tenant")
988+
989+
Service("MultipleAPIKeySecurity", func() {
990+
Security(APIKeyAuth, TenantKeyAuth)
991+
Method("A", func() {
992+
Payload(func() {
993+
APIKey("api_key", "api_key", String)
994+
APIKey("tenant", "tenant_id", String)
995+
Required("api_key", "tenant_id")
996+
})
997+
998+
HTTP(func() {
999+
POST("/")
1000+
Header("api_key:X-API-Key")
1001+
Header("tenant_id:X-Tenant")
1002+
})
1003+
})
1004+
})
1005+
}
1006+
1007+
var MixedAndMultipleAPIKeySecurityDSL = func() {
1008+
var Signature = JWTSecurity("jwt", func() {
1009+
Scope("api:read", "Read access to API")
1010+
})
1011+
var APIKeyAuth = APIKeySecurity("api_key")
1012+
var TenantKeyAuth = APIKeySecurity("tenant")
1013+
1014+
Service("MixedAndMultipleAPIKeySecurity", func() {
1015+
Security(Signature, func() {
1016+
Scope("api:read")
1017+
})
1018+
Security(APIKeyAuth, TenantKeyAuth)
1019+
1020+
Method("A", func() {
1021+
Payload(func() {
1022+
Token("jwt", String)
1023+
APIKey("api_key", "api_key", String)
1024+
APIKey("tenant", "tenant_id", String)
1025+
})
1026+
1027+
HTTP(func() {
1028+
POST("/")
1029+
Header("api_key:X-API-Key")
1030+
Header("tenant_id:X-Tenant")
1031+
Header("jwt:Authorization")
1032+
})
1033+
})
1034+
})
1035+
}

0 commit comments

Comments
 (0)