satellite/admin/back-office: Add auth middleware

Create an API generator middleware for being able to hook the new
satellite admin authorization in the endpoints.

The commit fixes a bug found in the API generator that caused that
fields of types of the same package of the generated code where wrongly
added. Concretely:

- The package matching was missing in the function middlewareFields,
  hence it was generating code that referenced types with the package
  name.
- middlewareFields function was not adding the pointer symbol (*) when
  the type was from the same package where the generated code is
  written.

There is also an accidental enhancement in the API generator because I
thought that the bug commented above corresponded to it, rather than
removing it, I though that was worthwhile to keep it because it was
already implemented. This enhancement allows to use fields in the
middleware with packages whose last path part contains `-` or `.`, using
a package rename in the import statement.

Change-Id: Ie98b303226a8e8845e494f25054867f95a283aa0
This commit is contained in:
Ivan Fraixedes 2023-11-24 13:23:01 +01:00 committed by Storj Robot
parent 0467903b52
commit fb31761bad
5 changed files with 100 additions and 25 deletions

View File

@ -6,7 +6,6 @@ package apigen
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"strings" "strings"
@ -348,7 +347,7 @@ func middlewareImports(m any) []string {
// middlewareFields returns the list of fields of a middleware implementation. It panics if m isn't // 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. // a struct type, it has embedded fields, or it has unexported fields.
func middlewareFields(m any) []middlewareField { func middlewareFields(api *API, m any) []middlewareField {
fields := []middlewareField{} fields := []middlewareField{}
middlewareWalkFields(m, func(f reflect.StructField) { middlewareWalkFields(m, func(f reflect.StructField) {
if f.Name == "_" { if f.Name == "_" {
@ -362,9 +361,10 @@ func middlewareFields(m any) []middlewareField {
t = f.Type.Elem() t = f.Type.Elem()
} }
typeref := t.Name() typeref := psymbol + t.Name()
if p := t.PkgPath(); p != "" { if p := t.PkgPath(); p != "" && p != api.PackagePath {
typeref = fmt.Sprintf("%s%s.%s", psymbol, filepath.Base(p), typeref) pn, _ := importPath(p).PkgName()
typeref = fmt.Sprintf("%s%s.%s", psymbol, pn, t.Name())
} }
fields = append(fields, middlewareField{Name: f.Name, Type: typeref}) fields = append(fields, middlewareField{Name: f.Name, Type: typeref})
}) })

View File

@ -7,8 +7,9 @@ import (
"fmt" "fmt"
"go/format" "go/format"
"os" "os"
"path/filepath"
"reflect" "reflect"
"sort" "slices"
"strings" "strings"
"time" "time"
@ -50,12 +51,12 @@ func (a *API) generateGo() ([]byte, error) {
} }
imports := struct { imports := struct {
All map[string]bool All map[importPath]bool
Standard []string Standard []importPath
External []string External []importPath
Internal []string Internal []importPath
}{ }{
All: make(map[string]bool), All: make(map[importPath]bool),
} }
i := func(paths ...string) { i := func(paths ...string) {
@ -64,12 +65,13 @@ func (a *API) generateGo() ([]byte, error) {
continue continue
} }
if _, ok := imports.All[path]; ok { ipath := importPath(path)
if _, ok := imports.All[ipath]; ok {
continue continue
} }
imports.All[path] = true imports.All[ipath] = true
var slice *[]string var slice *[]importPath
switch { switch {
case !strings.Contains(path, "."): case !strings.Contains(path, "."):
slice = &imports.Standard slice = &imports.Standard
@ -78,7 +80,7 @@ func (a *API) generateGo() ([]byte, error) {
default: default:
slice = &imports.External slice = &imports.External
} }
*slice = append(*slice, path) *slice = append(*slice, ipath)
} }
} }
@ -170,12 +172,12 @@ func (a *API) generateGo() ([]byte, error) {
autodefinedFields := map[string]string{"log": "*zap.Logger", "mon": "*monkit.Scope", "service": cname + "Service"} autodefinedFields := map[string]string{"log": "*zap.Logger", "mon": "*monkit.Scope", "service": cname + "Service"}
for _, m := range group.Middleware { for _, m := range group.Middleware {
for _, f := range middlewareFields(m) { for _, f := range middlewareFields(a, m) {
if t, ok := autodefinedFields[f.Name]; ok { if t, ok := autodefinedFields[f.Name]; ok {
if t != f.Type { if t != f.Type {
panic( panic(
fmt.Sprintf( 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", "middleware %q has a field with name %q and type %q which clashes with another defined field with the same name but with type %q",
reflect.TypeOf(m).Name(), reflect.TypeOf(m).Name(),
f.Name, f.Name,
f.Type, f.Type,
@ -203,7 +205,7 @@ func (a *API) generateGo() ([]byte, error) {
middlewareArgs := make([]string, 0, len(group.Middleware)) middlewareArgs := make([]string, 0, len(group.Middleware))
middlewareFieldsList := make([]string, 0, len(group.Middleware)) middlewareFieldsList := make([]string, 0, len(group.Middleware))
for _, m := range group.Middleware { for _, m := range group.Middleware {
for _, f := range middlewareFields(m) { for _, f := range middlewareFields(a, m) {
if _, ok := autodedefined[f.Name]; !ok { if _, ok := autodedefined[f.Name]; !ok {
middlewareArgs = append(middlewareArgs, fmt.Sprintf("%s %s", f.Name, f.Type)) middlewareArgs = append(middlewareArgs, fmt.Sprintf("%s %s", f.Name, f.Type))
middlewareFieldsList = append(middlewareFieldsList, fmt.Sprintf("%[1]s: %[1]s", f.Name)) middlewareFieldsList = append(middlewareFieldsList, fmt.Sprintf("%[1]s: %[1]s", f.Name))
@ -339,12 +341,17 @@ func (a *API) generateGo() ([]byte, error) {
pf("") pf("")
pf("import (") pf("import (")
slices := [][]string{imports.Standard, imports.External, imports.Internal} all := [][]importPath{imports.Standard, imports.External, imports.Internal}
for sn, slice := range slices { for sn, slice := range all {
sort.Strings(slice) slices.Sort(slice)
for pn, path := range slice { for pn, path := range slice {
pf(`"%s"`, path) if r, ok := path.PkgName(); ok {
if pn == len(slice)-1 && sn < len(slices)-1 { pf(`%s "%s"`, r, path)
} else {
pf(`"%s"`, path)
}
if pn == len(slice)-1 && sn < len(all)-1 {
pf("") pf("")
} }
} }
@ -469,3 +476,20 @@ func handleBody(pf func(format string, a ...interface{}), body interface{}) {
pf("}") pf("}")
pf("") pf("")
} }
type importPath string
// PkgName returns the name of the package based of the last part of the import
// path and false if the name isn't a rename, otherwise it returns true.
//
// The package name is renamed when the last part of the path contains hyphen
// (-) or dot (.) and the rename is this part with the hyphens and dots
// stripped.
func (i importPath) PkgName() (rename string, ok bool) {
b := filepath.Base(string(i))
if strings.Contains(b, "-") || strings.Contains(b, ".") {
return strings.ReplaceAll(strings.ReplaceAll(b, "-", ""), ".", ""), true
}
return b, false
}

View File

@ -8,9 +8,11 @@ package main
//go:generate go run $GOFILE //go:generate go run $GOFILE
import ( import (
"fmt"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"storj.io/storj/private/apigen" "storj.io/storj/private/apigen"
backoffice "storj.io/storj/satellite/admin/back-office" backoffice "storj.io/storj/satellite/admin/back-office"
@ -35,6 +37,7 @@ func main() {
}) })
group = api.Group("UserManagement", "users") group = api.Group("UserManagement", "users")
group.Middleware = append(group.Middleware, authMiddleware{})
group.Get("/{email}", &apigen.Endpoint{ group.Get("/{email}", &apigen.Endpoint{
Name: "Get user", Name: "Get user",
@ -45,6 +48,9 @@ func main() {
apigen.NewParam("email", ""), apigen.NewParam("email", ""),
}, },
Response: backoffice.User{}, Response: backoffice.User{},
Settings: map[any]any{
authPermsKey: []backoffice.Permission{backoffice.PermAccountView},
},
}) })
modroot := findModuleRootDir() modroot := findModuleRootDir()
@ -53,6 +59,37 @@ func main() {
api.MustWriteDocs(filepath.Join(modroot, "satellite", "admin", "back-office", "api-docs.gen.md")) api.MustWriteDocs(filepath.Join(modroot, "satellite", "admin", "back-office", "api-docs.gen.md"))
} }
type authMiddleware struct {
//lint:ignore U1000 this field is used by the API generator to expose in the handler.
auth *backoffice.Authorizer
}
func (a authMiddleware) Generate(api *apigen.API, group *apigen.EndpointGroup, ep *apigen.FullEndpoint) string {
perms := apigen.LoadSetting(authPermsKey, ep, []backoffice.Permission{})
if len(perms) == 0 {
return ""
}
verbs := make([]string, 0, len(perms))
values := make([]any, 0, len(perms))
for _, p := range perms {
verbs = append(verbs, "%d")
values = append(values, p)
}
format := fmt.Sprintf(`if h.auth.IsRejected(w, r, %s) {
return
}`, strings.Join(verbs, ", "))
return fmt.Sprintf(format, values...)
}
var _ apigen.Middleware = authMiddleware{}
type tagAuthPerms struct{}
var authPermsKey = tagAuthPerms{}
func findModuleRootDir() string { func findModuleRootDir() string {
dir, err := os.Getwd() dir, err := os.Getwd()
if err != nil { if err != nil {

View File

@ -39,6 +39,7 @@ type UserManagementHandler struct {
log *zap.Logger log *zap.Logger
mon *monkit.Scope mon *monkit.Scope
service UserManagementService service UserManagementService
auth *Authorizer
} }
func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service PlacementManagementService, router *mux.Router) *PlacementManagementHandler { func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service PlacementManagementService, router *mux.Router) *PlacementManagementHandler {
@ -54,11 +55,12 @@ func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service Placemen
return handler return handler
} }
func NewUserManagement(log *zap.Logger, mon *monkit.Scope, service UserManagementService, router *mux.Router) *UserManagementHandler { func NewUserManagement(log *zap.Logger, mon *monkit.Scope, service UserManagementService, router *mux.Router, auth *Authorizer) *UserManagementHandler {
handler := &UserManagementHandler{ handler := &UserManagementHandler{
log: log, log: log,
mon: mon, mon: mon,
service: service, service: service,
auth: auth,
} }
usersRouter := router.PathPrefix("/back-office/api/v1/users").Subrouter() usersRouter := router.PathPrefix("/back-office/api/v1/users").Subrouter()
@ -99,6 +101,10 @@ func (h *UserManagementHandler) handleGetUserByEmail(w http.ResponseWriter, r *h
return return
} }
if h.auth.IsRejected(w, r, 1) {
return
}
retVal, httpErr := h.service.GetUserByEmail(ctx, email) retVal, httpErr := h.service.GetUserByEmail(ctx, email)
if httpErr.Err != nil { if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err) api.ServeError(h.log, w, httpErr.Status, httpErr.Err)

View File

@ -72,10 +72,18 @@ func NewServer(
root = mux.NewRouter() root = mux.NewRouter()
} }
auth := NewAuthorizer(
log,
config.UserGroupsRoleAdmin,
config.UserGroupsRoleViewer,
config.UserGroupsRoleCustomerSupport,
config.UserGroupsRoleFinanceManager,
)
// API endpoints. // API endpoints.
// API generator already add the PathPrefix. // API generator already add the PathPrefix.
NewPlacementManagement(log, mon, service, root) NewPlacementManagement(log, mon, service, root)
NewUserManagement(log, mon, service, root) NewUserManagement(log, mon, service, root, auth)
root = root.PathPrefix(PathPrefix).Subrouter() root = root.PathPrefix(PathPrefix).Subrouter()
// Static assets for the web interface. // Static assets for the web interface.