private/apigen: Allow to customize handlers logic
The API generator doesn't have a way to customize each Go handler endpoint unless that the Go generator is modified. This commit adds a way to customize each endpoint injecting instances of types that implement an interface (Middleware) that return the code to inject. To show how it works, the commit get rid of the 2 fields that we used to customize the authentication request with the logic that the satellite/console/consoleweb/consoleapi needs and replace the hardcoded customization using this new way to customize handlers. This new way should allow to hook the satellite/admin/back-office authorization into the handlers using a Middleware implementation. Change-Id: I894aa0026b30fa2f4a5604a6c34c22e0ed582e2b
This commit is contained in:
parent
479fbb628c
commit
adcd810e37
@ -6,10 +6,12 @@ package apigen
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
@ -48,8 +50,6 @@ type Endpoint struct {
|
||||
// It must start with a lowercase letter and can only contains letters, digits, _, and $.
|
||||
// It cannot be empty.
|
||||
TypeScriptName string
|
||||
NoCookieAuth bool
|
||||
NoAPIAuth bool
|
||||
// Request is the type that defines the format of the request body.
|
||||
Request interface{}
|
||||
// Response is the type that defines the format of the response body.
|
||||
@ -63,16 +63,12 @@ type Endpoint struct {
|
||||
// It must be of the same type than Response.
|
||||
// If a mock generator is called it must not be nil unless Response is nil.
|
||||
ResponseMock interface{}
|
||||
}
|
||||
|
||||
// CookieAuth returns endpoint's cookie auth status.
|
||||
func (e *Endpoint) CookieAuth() bool {
|
||||
return !e.NoCookieAuth
|
||||
}
|
||||
|
||||
// APIAuth returns endpoint's API auth status.
|
||||
func (e *Endpoint) APIAuth() bool {
|
||||
return !e.NoAPIAuth
|
||||
// Settings is the data to pass to the middleware handlers to adapt the generated
|
||||
// code to this endpoints.
|
||||
//
|
||||
// Not all the middlware handlers need extra data. Some of them use this data to disable it in
|
||||
// some endpoints.
|
||||
Settings map[any]any
|
||||
}
|
||||
|
||||
// Validate validates the endpoint fields values are correct according to the documented constraints.
|
||||
@ -156,8 +152,8 @@ func (e *Endpoint) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fullEndpoint represents endpoint with path and method.
|
||||
type fullEndpoint struct {
|
||||
// FullEndpoint represents endpoint with path and method.
|
||||
type FullEndpoint struct {
|
||||
Endpoint
|
||||
Path string
|
||||
Method string
|
||||
@ -185,8 +181,12 @@ type EndpointGroup struct {
|
||||
// TypeScript generator uses it for composing the URL base path (lowercase).
|
||||
//
|
||||
// Document generator uses as it is.
|
||||
Prefix string
|
||||
endpoints []*fullEndpoint
|
||||
Prefix string
|
||||
// Middleware is a list of additional processing of requests that apply to all the endpoints of this group.
|
||||
Middleware []Middleware
|
||||
// endpoints is the list of endpoints added to this group through the "HTTP method" methods (e.g.
|
||||
// Get, Patch, etc.).
|
||||
endpoints []*FullEndpoint
|
||||
}
|
||||
|
||||
// Get adds new GET endpoint to endpoints group.
|
||||
@ -234,7 +234,7 @@ func (eg *EndpointGroup) addEndpoint(path, method string, endpoint *Endpoint) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ep := &fullEndpoint{*endpoint, path, method}
|
||||
ep := &FullEndpoint{*endpoint, path, method}
|
||||
for _, e := range eg.endpoints {
|
||||
if e.Path == path && e.Method == method {
|
||||
panic(fmt.Sprintf("there is already an endpoint defined with path %q and method %q", path, method))
|
||||
@ -294,3 +294,147 @@ func NewParam(name string, instance interface{}) Param {
|
||||
Type: reflect.TypeOf(instance),
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware allows to generate custom code that's executed at the beginning of the handler.
|
||||
//
|
||||
// The implementation must declare their dependencies through unexported struct fields which doesn't
|
||||
// begin with underscore (_), except fields whose name is just underscore (the blank identifier).
|
||||
// The API generator will add the import those dependencies and allow to pass them through the
|
||||
// constructor parameters of the group handler implementation, except the fields named with the
|
||||
// blank identifier that should be only used to import packages that the generated code needs.
|
||||
//
|
||||
// The limitation of using fields with the blank identifier as its names is that those packages
|
||||
// must at least to export a type, hence, it isn't possible to import packages that only export
|
||||
// constants or variables.
|
||||
//
|
||||
// Middleware implementation with the same struct field name and type will be handled as one
|
||||
// parameter, so the dependency will be shared between them. If they have the same struct field
|
||||
// name, but a different type, the API generator will panic.
|
||||
// NOTE types are compared as [package].[type name], hence, package name collision are not handled
|
||||
// and it will produce code that doesn't compile.
|
||||
type Middleware interface {
|
||||
// Generate generates the code that the API generator adds to a handler endpoint before calling
|
||||
// the service.
|
||||
//
|
||||
// All the dependencies defined as struct fields of the implementation of this interface are
|
||||
// available as fields of the struct handler. The generated code is executed inside of the methods
|
||||
// of the struct handler, hence it has access to all its fields. The handler instance is available
|
||||
// through the variable name h. For example:
|
||||
//
|
||||
// type middlewareImpl struct {
|
||||
// log *zap.Logger // Import path: "go.uber.org/zap"
|
||||
// auth api.Auth // Import path: "storj.io/storj/private/api"
|
||||
// }
|
||||
//
|
||||
// The generated code can access to log and auth through h.log and h.auth.
|
||||
//
|
||||
// Each handler method where the code is executed has access to the following variables names:
|
||||
// ctx of type context.Context, w of type http.ResponseWriter, and r of type *http.Request.
|
||||
// Make sure to not declare variable with those names in the generated code unless that's wrapped
|
||||
// in a scope.
|
||||
Generate(api *API, group *EndpointGroup, ep *FullEndpoint) string
|
||||
}
|
||||
|
||||
func middlewareImports(m any) []string {
|
||||
imports := []string{}
|
||||
middlewareWalkFields(m, func(f reflect.StructField) {
|
||||
if p := f.Type.PkgPath(); p != "" {
|
||||
imports = append(imports, p)
|
||||
}
|
||||
})
|
||||
|
||||
return imports
|
||||
}
|
||||
|
||||
// middlewareFields returns the list of fields of a middleware implementation. It panics if m isn't
|
||||
// a struct type, it has embedded fields, or it has unexported fields.
|
||||
func middlewareFields(m any) []middlewareField {
|
||||
fields := []middlewareField{}
|
||||
middlewareWalkFields(m, func(f reflect.StructField) {
|
||||
if f.Name == "_" {
|
||||
return
|
||||
}
|
||||
|
||||
psymbol := ""
|
||||
t := f.Type
|
||||
if t.Kind() == reflect.Pointer {
|
||||
psymbol = "*"
|
||||
t = f.Type.Elem()
|
||||
}
|
||||
|
||||
typeref := t.Name()
|
||||
if p := t.PkgPath(); p != "" {
|
||||
typeref = fmt.Sprintf("%s%s.%s", psymbol, filepath.Base(p), typeref)
|
||||
}
|
||||
fields = append(fields, middlewareField{Name: f.Name, Type: typeref})
|
||||
})
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
func middlewareWalkFields(m any, walk func(f reflect.StructField)) {
|
||||
t := reflect.TypeOf(m)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("middleware %q isn't a struct type", t.Name()))
|
||||
}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.FieldByIndex([]int{i})
|
||||
if f.Anonymous {
|
||||
panic(fmt.Sprintf("middleware %q has a embedded field %q", t.Name(), f.Name))
|
||||
}
|
||||
|
||||
if f.Name != "_" {
|
||||
// Disallow fields that begin with underscore.
|
||||
if !unicode.IsLetter([]rune(f.Name)[0]) {
|
||||
panic(
|
||||
fmt.Sprintf(
|
||||
"middleware %q has a field name beginning with no letter %q. Change it to begin with lower case letter",
|
||||
t.Name(),
|
||||
f.Name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if unicode.IsUpper([]rune(f.Name)[0]) {
|
||||
panic(
|
||||
fmt.Sprintf(
|
||||
"middleware %q has a field name beginning with upper case %q. Change it to begin with lower case",
|
||||
t.Name(),
|
||||
f.Name,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
walk(f)
|
||||
}
|
||||
}
|
||||
|
||||
// middlewareField has the name of the field and type for adding to handler structs that the
|
||||
// API generator generates during the generation phase.
|
||||
type middlewareField struct {
|
||||
// Name is the name of the field. It must fulfill Go identifiers specification
|
||||
// https://go.dev/ref/spec#Identifiers
|
||||
Name string
|
||||
// Type is the type's name of the field.
|
||||
Type string
|
||||
}
|
||||
|
||||
// LoadSetting returns from endpoint.Settings the value assigned to key or
|
||||
// returns defaultValue if the key doesn't exist.
|
||||
//
|
||||
// It panics if key doesn't have a value of the type T.
|
||||
func LoadSetting[T any](key any, endpoint *FullEndpoint, defaultValue T) T {
|
||||
v, ok := endpoint.Settings[key]
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
vt, vtok := v.(T)
|
||||
if !vtok {
|
||||
panic(fmt.Sprintf("expected %T got %T", vt, v))
|
||||
}
|
||||
|
||||
return vt
|
||||
}
|
||||
|
@ -50,7 +50,6 @@ type UsersHandler struct {
|
||||
log *zap.Logger
|
||||
mon *monkit.Scope
|
||||
service UsersService
|
||||
auth api.Auth
|
||||
}
|
||||
|
||||
func NewDocuments(log *zap.Logger, mon *monkit.Scope, service DocumentsService, router *mux.Router, auth api.Auth) *DocumentsHandler {
|
||||
@ -71,12 +70,11 @@ func NewDocuments(log *zap.Logger, mon *monkit.Scope, service DocumentsService,
|
||||
return handler
|
||||
}
|
||||
|
||||
func NewUsers(log *zap.Logger, mon *monkit.Scope, service UsersService, router *mux.Router, auth api.Auth) *UsersHandler {
|
||||
func NewUsers(log *zap.Logger, mon *monkit.Scope, service UsersService, router *mux.Router) *UsersHandler {
|
||||
handler := &UsersHandler{
|
||||
log: log,
|
||||
mon: mon,
|
||||
service: service,
|
||||
auth: auth,
|
||||
}
|
||||
|
||||
usersRouter := router.PathPrefix("/api/v0/users").Subrouter()
|
||||
@ -93,13 +91,6 @@ func (h *DocumentsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.Get(ctx)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -117,6 +108,13 @@ func (h *DocumentsHandler) handleGetOne(w http.ResponseWriter, r *http.Request)
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
path, ok := mux.Vars(r)["path"]
|
||||
@ -125,13 +123,6 @@ func (h *DocumentsHandler) handleGetOne(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GetOne(ctx, path)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -149,6 +140,13 @@ func (h *DocumentsHandler) handleGetTag(w http.ResponseWriter, r *http.Request)
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
path, ok := mux.Vars(r)["path"]
|
||||
@ -163,13 +161,6 @@ func (h *DocumentsHandler) handleGetTag(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GetTag(ctx, path, tagName)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -187,6 +178,13 @@ func (h *DocumentsHandler) handleGetVersions(w http.ResponseWriter, r *http.Requ
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
path, ok := mux.Vars(r)["path"]
|
||||
@ -195,13 +193,6 @@ func (h *DocumentsHandler) handleGetVersions(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GetVersions(ctx, path)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -219,6 +210,13 @@ func (h *DocumentsHandler) handleUpdateContent(w http.ResponseWriter, r *http.Re
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
idParam := r.URL.Query().Get("id")
|
||||
@ -257,13 +255,6 @@ func (h *DocumentsHandler) handleUpdateContent(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.UpdateContent(ctx, path, id, date, payload)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -283,13 +274,6 @@ func (h *UsersHandler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.Get(ctx)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -315,13 +299,6 @@ func (h *UsersHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpErr := h.service.Create(ctx, payload)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
|
@ -7,10 +7,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
|
||||
"storj.io/storj/private/api"
|
||||
"storj.io/storj/private/apigen"
|
||||
"storj.io/storj/private/apigen/example/myapi"
|
||||
)
|
||||
@ -23,6 +28,9 @@ func main() {
|
||||
}
|
||||
|
||||
g := a.Group("Documents", "docs")
|
||||
g.Middleware = append(g.Middleware,
|
||||
authMiddleware{},
|
||||
)
|
||||
|
||||
now := time.Date(2001, 02, 03, 04, 05, 06, 07, time.UTC)
|
||||
|
||||
@ -40,6 +48,10 @@ func main() {
|
||||
Tags: [][2]string{{"category", "general"}},
|
||||
},
|
||||
}},
|
||||
Settings: map[any]any{
|
||||
NoAPIKey: true,
|
||||
NoCookie: true,
|
||||
},
|
||||
})
|
||||
|
||||
g.Get("/{path}", &apigen.Endpoint{
|
||||
@ -141,3 +153,43 @@ func main() {
|
||||
a.MustWriteTSMock("client-api-mock.gen.ts")
|
||||
a.MustWriteDocs("apidocs.gen.md")
|
||||
}
|
||||
|
||||
// authMiddleware customize endpoints to authenticate requests by API Key or Cookie.
|
||||
type authMiddleware struct {
|
||||
log *zap.Logger
|
||||
auth api.Auth
|
||||
_ http.ResponseWriter // Import the http package to use its HTTP status constants
|
||||
}
|
||||
|
||||
// Generate satisfies the apigen.Middleware.
|
||||
func (a authMiddleware) Generate(api *apigen.API, group *apigen.EndpointGroup, ep *apigen.FullEndpoint) string {
|
||||
noapikey := apigen.LoadSetting(NoAPIKey, ep, false)
|
||||
nocookie := apigen.LoadSetting(NoCookie, ep, false)
|
||||
|
||||
if noapikey && nocookie {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`ctx, err = h.auth.IsAuthenticated(ctx, r, %t, %t)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}`, !nocookie, !noapikey)
|
||||
}
|
||||
|
||||
var _ apigen.Middleware = authMiddleware{}
|
||||
|
||||
type (
|
||||
tagNoAPIKey struct{}
|
||||
tagNoCookie struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// NoAPIKey is the key for endpoint settings to indicate that it doesn't use API Key
|
||||
// authentication mechanism.
|
||||
NoAPIKey tagNoAPIKey
|
||||
// NoCookie is the key for endpoint settings to indicate that it doesn't use cookie authentication
|
||||
// mechanism.
|
||||
NoCookie tagNoCookie
|
||||
)
|
||||
|
@ -111,13 +111,18 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
packageName,
|
||||
strings.ToLower(group.Prefix),
|
||||
)
|
||||
|
||||
for _, m := range group.Middleware {
|
||||
i(middlewareImports(m)...)
|
||||
}
|
||||
}
|
||||
|
||||
pf("")
|
||||
|
||||
params := make(map[*fullEndpoint][]Param)
|
||||
params := make(map[*FullEndpoint][]Param)
|
||||
|
||||
for _, group := range a.EndpointGroups {
|
||||
// Define the service interface
|
||||
pf("type %sService interface {", capitalize(group.Name))
|
||||
for _, e := range group.endpoints {
|
||||
params[e] = append(e.PathParams, e.QueryParams...)
|
||||
@ -162,7 +167,30 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
pf("log *zap.Logger")
|
||||
pf("mon *monkit.Scope")
|
||||
pf("service %sService", cname)
|
||||
pf("auth api.Auth")
|
||||
|
||||
autodefinedFields := map[string]string{"log": "*zap.Logger", "mon": "*monkit.Scope", "service": cname + "Service"}
|
||||
for _, m := range group.Middleware {
|
||||
for _, f := range middlewareFields(m) {
|
||||
if t, ok := autodefinedFields[f.Name]; ok {
|
||||
if t != f.Type {
|
||||
panic(
|
||||
fmt.Sprintf(
|
||||
"middleware %q has a field with name %q and type %q which clashes with another defined field with the same but with type %q",
|
||||
reflect.TypeOf(m).Name(),
|
||||
f.Name,
|
||||
f.Type,
|
||||
t,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
autodefinedFields[f.Name] = f.Type
|
||||
pf("%s %s", f.Name, f.Type)
|
||||
}
|
||||
}
|
||||
|
||||
pf("}")
|
||||
pf("")
|
||||
}
|
||||
@ -170,17 +198,45 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
for _, group := range a.EndpointGroups {
|
||||
cname := capitalize(group.Name)
|
||||
i("github.com/gorilla/mux")
|
||||
pf(
|
||||
"func New%s(log *zap.Logger, mon *monkit.Scope, service %sService, router *mux.Router, auth api.Auth) *%sHandler {",
|
||||
cname,
|
||||
cname,
|
||||
cname,
|
||||
)
|
||||
|
||||
autodedefined := map[string]struct{}{"log": {}, "mon": {}, "service": {}}
|
||||
middlewareArgs := make([]string, 0, len(group.Middleware))
|
||||
middlewareFieldsList := make([]string, 0, len(group.Middleware))
|
||||
for _, m := range group.Middleware {
|
||||
for _, f := range middlewareFields(m) {
|
||||
if _, ok := autodedefined[f.Name]; !ok {
|
||||
middlewareArgs = append(middlewareArgs, fmt.Sprintf("%s %s", f.Name, f.Type))
|
||||
middlewareFieldsList = append(middlewareFieldsList, fmt.Sprintf("%[1]s: %[1]s", f.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(middlewareArgs) > 0 {
|
||||
pf(
|
||||
"func New%s(log *zap.Logger, mon *monkit.Scope, service %sService, router *mux.Router, %s) *%sHandler {",
|
||||
cname,
|
||||
cname,
|
||||
strings.Join(middlewareArgs, ", "),
|
||||
cname,
|
||||
)
|
||||
} else {
|
||||
pf(
|
||||
"func New%s(log *zap.Logger, mon *monkit.Scope, service %sService, router *mux.Router) *%sHandler {",
|
||||
cname,
|
||||
cname,
|
||||
cname,
|
||||
)
|
||||
}
|
||||
|
||||
pf("handler := &%sHandler{", cname)
|
||||
pf("log: log,")
|
||||
pf("mon: mon,")
|
||||
pf("service: service,")
|
||||
pf("auth: auth,")
|
||||
|
||||
if len(middlewareFieldsList) > 0 {
|
||||
pf(strings.Join(middlewareFieldsList, ",") + ",")
|
||||
}
|
||||
|
||||
pf("}")
|
||||
pf("")
|
||||
pf(
|
||||
@ -216,6 +272,11 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
pf("defer h.mon.Task()(&ctx)(&err)")
|
||||
pf("")
|
||||
|
||||
for _, m := range group.Middleware {
|
||||
pf(m.Generate(a, group, endpoint))
|
||||
}
|
||||
|
||||
pf("")
|
||||
pf("w.Header().Set(\"Content-Type\", \"application/json\")")
|
||||
pf("")
|
||||
|
||||
@ -227,18 +288,6 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
handleBody(pf, endpoint.Request)
|
||||
}
|
||||
|
||||
if !endpoint.NoCookieAuth || !endpoint.NoAPIAuth {
|
||||
pf("ctx, err = h.auth.IsAuthenticated(ctx, r, %v, %v)", !endpoint.NoCookieAuth, !endpoint.NoAPIAuth)
|
||||
pf("if err != nil {")
|
||||
if !endpoint.NoCookieAuth {
|
||||
pf("h.auth.RemoveAuthCookie(w)")
|
||||
}
|
||||
pf("api.ServeError(h.log, w, http.StatusUnauthorized, err)")
|
||||
pf("return")
|
||||
pf("}")
|
||||
pf("")
|
||||
}
|
||||
|
||||
var methodFormat string
|
||||
if endpoint.Response != nil {
|
||||
methodFormat = "retVal, httpErr := h.service.%s(ctx, "
|
||||
|
@ -139,7 +139,7 @@ func (f *tsGenFile) createAPIClient(group *EndpointGroup) {
|
||||
f.pf("}")
|
||||
}
|
||||
|
||||
func (f *tsGenFile) getArgsAndPath(method *fullEndpoint, group *EndpointGroup) (funcArgs, path string) {
|
||||
func (f *tsGenFile) getArgsAndPath(method *FullEndpoint, group *EndpointGroup) (funcArgs, path string) {
|
||||
// remove path parameter placeholders
|
||||
path = method.Path
|
||||
i := strings.Index(path, "{")
|
||||
|
@ -32,8 +32,6 @@ func main() {
|
||||
GoName: "GetPlacements",
|
||||
TypeScriptName: "getPlacements",
|
||||
Response: []backoffice.PlacementInfo{},
|
||||
NoCookieAuth: true,
|
||||
NoAPIAuth: true,
|
||||
})
|
||||
|
||||
modroot := findModuleRootDir()
|
||||
|
@ -27,15 +27,13 @@ type PlacementManagementHandler struct {
|
||||
log *zap.Logger
|
||||
mon *monkit.Scope
|
||||
service PlacementManagementService
|
||||
auth api.Auth
|
||||
}
|
||||
|
||||
func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service PlacementManagementService, router *mux.Router, auth api.Auth) *PlacementManagementHandler {
|
||||
func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service PlacementManagementService, router *mux.Router) *PlacementManagementHandler {
|
||||
handler := &PlacementManagementHandler{
|
||||
log: log,
|
||||
mon: mon,
|
||||
service: service,
|
||||
auth: auth,
|
||||
}
|
||||
|
||||
placementsRouter := router.PathPrefix("/back-office/api/v1/placements").Subrouter()
|
||||
|
@ -57,7 +57,13 @@ type Server struct {
|
||||
// configurations.
|
||||
//
|
||||
// When listener is nil, Server.Run is a noop.
|
||||
func NewServer(log *zap.Logger, listener net.Listener, placement *overlay.PlacementDefinitions, root *mux.Router, config Config) *Server {
|
||||
func NewServer(
|
||||
log *zap.Logger,
|
||||
listener net.Listener,
|
||||
placement *overlay.PlacementDefinitions,
|
||||
root *mux.Router,
|
||||
config Config,
|
||||
) *Server {
|
||||
server := &Server{
|
||||
log: log,
|
||||
listener: listener,
|
||||
@ -71,8 +77,7 @@ func NewServer(log *zap.Logger, listener net.Listener, placement *overlay.Placem
|
||||
|
||||
// API endpoints.
|
||||
// API generator already add the PathPrefix.
|
||||
auth := &apiAuth{server}
|
||||
NewPlacementManagement(log, mon, server, root, auth)
|
||||
NewPlacementManagement(log, mon, server, root)
|
||||
|
||||
root = root.PathPrefix(PathPrefix).Subrouter()
|
||||
// Static assets for the web interface.
|
||||
@ -117,18 +122,3 @@ func (server *Server) Run(ctx context.Context) error {
|
||||
func (server *Server) Close() error {
|
||||
return Error.Wrap(server.server.Close())
|
||||
}
|
||||
|
||||
// apiAuth exposes methods to control the authentication process for each generated API endpoint.
|
||||
type apiAuth struct {
|
||||
server *Server
|
||||
}
|
||||
|
||||
// IsAuthenticated checks if request is performed with all needed authorization credentials.
|
||||
// This method is inert because the satellite admin back office uses neither cookies nor API keys to authenticate.
|
||||
func (a *apiAuth) IsAuthenticated(ctx context.Context, r *http.Request, isCookieAuth, isKeyAuth bool) (_ context.Context, err error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// RemoveAuthCookie indicates to the client that the authentication cookie should be removed.
|
||||
// This method is inert because the satellite admin back office doesn't use authentication cookies.
|
||||
func (a *apiAuth) RemoveAuthCookie(w http.ResponseWriter) {}
|
||||
|
@ -124,6 +124,13 @@ func (h *ProjectManagementHandler) handleGenCreateProject(w http.ResponseWriter,
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
payload := console.UpsertProjectInfo{}
|
||||
@ -132,13 +139,6 @@ func (h *ProjectManagementHandler) handleGenCreateProject(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GenCreateProject(ctx, payload)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -156,6 +156,13 @@ func (h *ProjectManagementHandler) handleGenUpdateProject(w http.ResponseWriter,
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
idParam, ok := mux.Vars(r)["id"]
|
||||
@ -176,13 +183,6 @@ func (h *ProjectManagementHandler) handleGenUpdateProject(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GenUpdateProject(ctx, id, payload)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -200,6 +200,13 @@ func (h *ProjectManagementHandler) handleGenDeleteProject(w http.ResponseWriter,
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
idParam, ok := mux.Vars(r)["id"]
|
||||
@ -214,13 +221,6 @@ func (h *ProjectManagementHandler) handleGenDeleteProject(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpErr := h.service.GenDeleteProject(ctx, id)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -232,8 +232,6 @@ func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWrit
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
@ -241,6 +239,8 @@ func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWrit
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
retVal, httpErr := h.service.GenGetUsersProjects(ctx)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -258,6 +258,13 @@ func (h *ProjectManagementHandler) handleGenGetSingleBucketUsageRollup(w http.Re
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
projectIDParam := r.URL.Query().Get("projectID")
|
||||
@ -302,13 +309,6 @@ func (h *ProjectManagementHandler) handleGenGetSingleBucketUsageRollup(w http.Re
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GenGetSingleBucketUsageRollup(ctx, projectID, bucket, since, before)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -326,6 +326,13 @@ func (h *ProjectManagementHandler) handleGenGetBucketUsageRollups(w http.Respons
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
projectIDParam := r.URL.Query().Get("projectID")
|
||||
@ -364,13 +371,6 @@ func (h *ProjectManagementHandler) handleGenGetBucketUsageRollups(w http.Respons
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GenGetBucketUsageRollups(ctx, projectID, since, before)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -388,6 +388,13 @@ func (h *ProjectManagementHandler) handleGenGetAPIKeys(w http.ResponseWriter, r
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
search := r.URL.Query().Get("search")
|
||||
@ -460,13 +467,6 @@ func (h *ProjectManagementHandler) handleGenGetAPIKeys(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GenGetAPIKeys(ctx, projectID, search, limit, page, order, orderDirection)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -484,6 +484,13 @@ func (h *APIKeyManagementHandler) handleGenCreateAPIKey(w http.ResponseWriter, r
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
payload := console.CreateAPIKeyRequest{}
|
||||
@ -492,13 +499,6 @@ func (h *APIKeyManagementHandler) handleGenCreateAPIKey(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GenCreateAPIKey(ctx, payload)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -516,6 +516,13 @@ func (h *APIKeyManagementHandler) handleGenDeleteAPIKey(w http.ResponseWriter, r
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
idParam, ok := mux.Vars(r)["id"]
|
||||
@ -530,13 +537,6 @@ func (h *APIKeyManagementHandler) handleGenDeleteAPIKey(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpErr := h.service.GenDeleteAPIKey(ctx, id)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
@ -548,8 +548,6 @@ func (h *UserManagementHandler) handleGenGetUser(w http.ResponseWriter, r *http.
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
@ -557,6 +555,8 @@ func (h *UserManagementHandler) handleGenGetUser(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
retVal, httpErr := h.service.GenGetUser(ctx)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
|
@ -6,11 +6,16 @@ package main
|
||||
//go:generate go run ./
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/private/api"
|
||||
"storj.io/storj/private/apigen"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
"storj.io/storj/satellite/console"
|
||||
@ -29,6 +34,7 @@ func main() {
|
||||
|
||||
{
|
||||
g := a.Group("ProjectManagement", "projects")
|
||||
g.Middleware = append(g.Middleware, AuthMiddleware{})
|
||||
|
||||
g.Post("/create", &apigen.Endpoint{
|
||||
Name: "Create new Project",
|
||||
@ -117,6 +123,7 @@ func main() {
|
||||
|
||||
{
|
||||
g := a.Group("APIKeyManagement", "apikeys")
|
||||
g.Middleware = append(g.Middleware, AuthMiddleware{})
|
||||
|
||||
g.Post("/create", &apigen.Endpoint{
|
||||
Name: "Create new macaroon API key",
|
||||
@ -140,6 +147,7 @@ func main() {
|
||||
|
||||
{
|
||||
g := a.Group("UserManagement", "users")
|
||||
g.Middleware = append(g.Middleware, AuthMiddleware{})
|
||||
|
||||
g.Get("/", &apigen.Endpoint{
|
||||
Name: "Get User",
|
||||
@ -181,3 +189,45 @@ func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// AuthMiddleware customize endpoints to authenticate requests by API Key or Cookie.
|
||||
type AuthMiddleware struct {
|
||||
//lint:ignore U1000 this field is used by the API generator to expose in the handler.
|
||||
log *zap.Logger
|
||||
//lint:ignore U1000 this field is used by the API generator to expose in the handler.
|
||||
auth api.Auth
|
||||
_ http.ResponseWriter // Import the http package to use its HTTP status constants
|
||||
}
|
||||
|
||||
// Generate satisfies the apigen.Middleware.
|
||||
func (a AuthMiddleware) Generate(api *apigen.API, group *apigen.EndpointGroup, ep *apigen.FullEndpoint) string {
|
||||
noapikey := apigen.LoadSetting(NoAPIKey, ep, false)
|
||||
nocookie := apigen.LoadSetting(NoCookie, ep, false)
|
||||
|
||||
if noapikey && nocookie {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`ctx, err = h.auth.IsAuthenticated(ctx, r, %t, %t)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}`, !nocookie, !noapikey)
|
||||
}
|
||||
|
||||
var _ apigen.Middleware = AuthMiddleware{}
|
||||
|
||||
type (
|
||||
tagNoAPIKey struct{}
|
||||
tagNoCookie struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
// NoAPIKey is the key for endpoint settings to indicate that it doesn't use API Key
|
||||
// authentication mechanism.
|
||||
NoAPIKey tagNoAPIKey
|
||||
// NoCookie is the key for endpoint settings to indicate that it doesn't use cookie authentication
|
||||
// mechanism.
|
||||
NoCookie tagNoCookie
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user