Repository: goadesign/goa
Pull request #2362 (2019-11-05 13:49:43)

v3: Create the template function for authentication in the file for each service
see #2361 

## Example

```go
package design

import (
	. "goa.design/goa/v3/dsl"
)

var BasicAuth = BasicAuthSecurity("basicauth")

var _ = Service("calc1", func() {
	Security(BasicAuth)
	Method("add", func() {
		Payload(func() {
			Username("username")
			Password("password")
			Field(1, "a", Int, "Left operand")
			Field(2, "b", Int, "Right operand")
			Required("a", "b")
		})
		Result(Int)
		HTTP(func() {
			GET("/add/{a}/{b}")
		})
	})
})

var _ = Service("calc2", func() {
	Security(BasicAuth)
	Method("mul", func() {
		Payload(func() {
			Username("username")
			Password("password")
			Field(1, "a", Int, "Left operand")
			Field(2, "b", Int, "Right operand")
			Required("a", "b")
		})
		Result(Int)
		HTTP(func() {
			GET("/mul/{a}/{b}")
		})
	})
})

```

## goa gen

The authentication method is in each service file.

calc1.go
```go
package calc1api

import (
	calc1 "calc/gen/calc1"
	"context"
	"fmt"
	"log"

	"goa.design/goa/v3/security"
)

// calc1 service example implementation.
// The example methods log the requests and return zero values.
type calc1srvc struct {
	logger *log.Logger
}

// NewCalc1 returns the calc1 service implementation.
func NewCalc1(logger *log.Logger) calc1.Service {
	return &calc1srvc{logger}
}

// BasicAuth implements the authorization logic for service "calc1" for the
// "basicauth" security scheme.
func (s *calc1srvc) BasicAuth(ctx context.Context, user, pass string, scheme *security.BasicScheme) (context.Context, error) {
	//
	// TBD: add authorization logic.
	//
	// In case of authorization failure this function should return
	// one of the generated error structs, e.g.:
	//
	//    return ctx, myservice.MakeUnauthorizedError("invalid token")
	//
	// Alternatively this function may return an instance of
	// goa.ServiceError with a Name field value that matches one of
	// the design error names, e.g:
	//
	//    return ctx, goa.PermanentError("unauthorized", "invalid token")
	//
	return ctx, fmt.Errorf("not implemented")
}

// Add implements add.
func (s *calc1srvc) Add(ctx context.Context, p *calc1.AddPayload) (res int, err error) {
	s.logger.Print("calc1.add")
	return
}
```
calc2.go
```go
package calc1api

import (
	calc2 "calc/gen/calc2"
	"context"
	"fmt"
	"log"

	"goa.design/goa/v3/security"
)

// calc2 service example implementation.
// The example methods log the requests and return zero values.
type calc2srvc struct {
	logger *log.Logger
}

// NewCalc2 returns the calc2 service implementation.
func NewCalc2(logger *log.Logger) calc2.Service {
	return &calc2srvc{logger}
}

// BasicAuth implements the authorization logic for service "calc2" for the
// "basicauth" security scheme.
func (s *calc2srvc) BasicAuth(ctx context.Context, user, pass string, scheme *security.BasicScheme) (context.Context, error) {
	//
	// TBD: add authorization logic.
	//
	// In case of authorization failure this function should return
	// one of the generated error structs, e.g.:
	//
	//    return ctx, myservice.MakeUnauthorizedError("invalid token")
	//
	// Alternatively this function may return an instance of
	// goa.ServiceError with a Name field value that matches one of
	// the design error names, e.g:
	//
	//    return ctx, goa.PermanentError("unauthorized", "invalid token")
	//
	return ctx, fmt.Errorf("not implemented")
}

// Mul implements mul.
func (s *calc2srvc) Mul(ctx context.Context, p *calc2.MulPayload) (res int, err error) {
	s.logger.Print("calc2.mul")
	return
}
```




Patch:
diff --git a/codegen/generator/example.go b/codegen/generator/example.go
index f520ed9f..c78545a3 100644
--- a/codegen/generator/example.go
+++ b/codegen/generator/example.go
@@ -25,11 +25,6 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
 			files = append(files, fs...)
 		}
 
-		// example auth file
-		if f := service.AuthFuncsFile(genpkg, r); f != nil {
-			files = append(files, f)
-		}
-
 		// server main
 		if fs := example.ServerFiles(genpkg, r); len(fs) != 0 {
 			files = append(files, fs...)
diff --git a/codegen/service/auth_funcs.go b/codegen/service/auth_funcs.go
index d854072e..e97a42ff 100644
--- a/codegen/service/auth_funcs.go
+++ b/codegen/service/auth_funcs.go
@@ -1,69 +1,5 @@
 package service
 
-import (
-	"os"
-	"path"
-	"strings"
-
-	"goa.design/goa/v3/codegen"
-	"goa.design/goa/v3/expr"
-)
-
-// AuthFuncsFile returns a file that contains a dummy implementation of the
-// authorization functions needed to instantiate the service endpoints.
-func AuthFuncsFile(genpkg string, root *expr.RootExpr) *codegen.File {
-	filepath := "auth.go"
-	if _, err := os.Stat(filepath); !os.IsNotExist(err) {
-		return nil // file already exists, skip it.
-	}
-
-	var (
-		sections []*codegen.SectionTemplate
-		generate bool
-
-		scope = codegen.NewNameScope()
-	)
-	{
-		specs := []*codegen.ImportSpec{
-			{Path: "context"},
-			{Path: "fmt"},
-			codegen.GoaImport(""),
-			codegen.GoaImport("security"),
-		}
-		for _, svc := range root.Services {
-			sd := Services.Get(svc.Name)
-			specs = append(specs, &codegen.ImportSpec{
-				Path: path.Join(genpkg, codegen.SnakeCase(sd.VarName)),
-				Name: scope.Unique(sd.PkgName),
-			})
-		}
-
-		apiPkg := scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
-		header := codegen.Header("", apiPkg, specs)
-		sections = []*codegen.SectionTemplate{header}
-		for _, s := range root.Services {
-			svc := Services.Get(s.Name)
-			if len(svc.Schemes) > 0 {
-				generate = true
-				sections = append(sections, &codegen.SectionTemplate{
-					Name:   "security-authfuncs",
-					Source: dummyAuthFuncsT,
-					Data:   svc,
-				})
-			}
-		}
-	}
-	if len(sections) == 0 || !generate {
-		return nil
-	}
-
-	return &codegen.File{
-		Path:             filepath,
-		SectionTemplates: sections,
-		SkipExist:        true,
-	}
-}
-
 // data: Data
 const dummyAuthFuncsT = `{{ range .Schemes }}
 {{ printf "%sAuth implements the authorization logic for service %q for the %q security scheme." .Type $.Name .SchemeName | comment }}
diff --git a/codegen/service/example_svc.go b/codegen/service/example_svc.go
index e1442b95..2679b458 100644
--- a/codegen/service/example_svc.go
+++ b/codegen/service/example_svc.go
@@ -68,13 +68,22 @@ func exampleServiceFile(genpkg string, root *expr.RootExpr, svc *expr.ServiceExp
 	specs := []*codegen.ImportSpec{
 		{Path: "context"},
 		{Path: "log"},
+		{Path: "fmt"},
 		{Path: path.Join(genpkg, codegen.SnakeCase(svcName)), Name: data.PkgName},
+		{Path: "goa.design/goa/v3/security"},
 	}
 	sections := []*codegen.SectionTemplate{
 		codegen.Header("", apipkg, specs),
 		{Name: "basic-service-struct", Source: svcStructT, Data: data},
 		{Name: "basic-service-init", Source: svcInitT, Data: data},
 	}
+	if len(data.Schemes) > 0 {
+		sections = append(sections, &codegen.SectionTemplate{
+			Name:   "security-authfuncs",
+			Source: dummyAuthFuncsT,
+			Data:   data,
+		})
+	}
 	for _, m := range svc.Methods {
 		sections = append(sections, basicEndpointSection(m, data))
 	}
diff --git a/expr/method.go b/expr/method.go
index ada71ddd..fc97affe 100644
--- a/expr/method.go
+++ b/expr/method.go
@@ -228,7 +228,9 @@ func hasTag(p *AttributeExpr, tag string) bool {
 		if !ok {
 			continue
 		}
-		return hasTag(ut.Attribute(), tag)
+		if hasTag(ut.Attribute(), tag) {
+			return true
+		}
 	}
 	if ut, ok := p.Type.(UserType); ok {
 		return hasTag(ut.Attribute(), tag)
@@ -248,7 +250,9 @@ func hasTagPrefix(p *AttributeExpr, prefix string) bool {
 		if !ok {
 			continue
 		}
-		return hasTagPrefix(ut.Attribute(), prefix)
+		if hasTagPrefix(ut.Attribute(), prefix) {
+			return true
+		}
 	}
 	if ut, ok := p.Type.(UserType); ok {
 		return hasTagPrefix(ut.Attribute(), prefix)


Test patch:
diff --git a/expr/method_test.go b/expr/method_test.go
index 0d97b847..f17da484 100644
--- a/expr/method_test.go
+++ b/expr/method_test.go
@@ -14,6 +14,7 @@ func TestMethodExprValidate(t *testing.T) {
 		DSL   func()
 		Error string
 	}{
+		{"valid-security-schemes-extend", testdata.ValidSecuritySchemesExtendDSL, ""},
 		{"invalid-security-schemes", testdata.InvalidSecuritySchemesDSL,
 			`service "InvalidSecuritySchemesService" method "SecureMethod": payload of method "SecureMethod" of service "InvalidSecuritySchemesService" does not define a username attribute, use Username to define one
 service "InvalidSecuritySchemesService" method "SecureMethod": payload of method "SecureMethod" of service "InvalidSecuritySchemesService" does not define a password attribute, use Password to define one
@@ -34,9 +35,13 @@ service "AnotherInvalidSecuritySchemesService" method "Method": payload of metho
 	}
 	for _, tc := range cases {
 		t.Run(tc.Name, func(t *testing.T) {
-			err := expr.RunInvalidDSL(t, tc.DSL)
-			if tc.Error != err.Error() {
-				t.Errorf("invalid error:\ngot:\n%s\n\ngot vs expected:\n%s", err.Error(), expr.Diff(t, err.Error(), tc.Error))
+			if tc.Error == "" {
+				expr.RunDSL(t, tc.DSL)
+			} else {
+				err := expr.RunInvalidDSL(t, tc.DSL)
+				if tc.Error != err.Error() {
+					t.Errorf("invalid error:\ngot:\n%s\n\ngot vs expected:\n%s", err.Error(), expr.Diff(t, err.Error(), tc.Error))
+				}
 			}
 		})
 	}
diff --git a/expr/testdata/method_validate_dsls.go b/expr/testdata/method_validate_dsls.go
index 96f34d75..ed43ad8c 100644
--- a/expr/testdata/method_validate_dsls.go
+++ b/expr/testdata/method_validate_dsls.go
@@ -18,6 +18,25 @@ var OAuth2 = OAuth2Security("authCode", func() {
 	Scope("api:read", "Read access")
 })
 
+var ValidSecuritySchemesExtendDSL = func() {
+	var CommonAttr = Type("Common", func() {
+		Attribute("version", String)
+	})
+	var SecurityAttr = Type("Security", func() {
+		Username("user", String)
+		Password("pass", String)
+	})
+	Service("ValidSecuritySchemesExtendService", func() {
+		Method("SecureMethod", func() {
+			Security(BasicAuth)
+			Payload(func() {
+				Extend(CommonAttr)
+				Extend(SecurityAttr)
+			})
+		})
+	})
+}
+
 var InvalidSecuritySchemesDSL = func() {
 	Service("InvalidSecuritySchemesService", func() {
 		Security(OAuth2, APIKeyAuth, func() {