feat: initial commit
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
This commit is contained in:
101
internal/admin/authz.go
Normal file
101
internal/admin/authz.go
Normal file
@ -0,0 +1,101 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
var ErrCodeForbidden api.ErrorCode = "forbidden"
|
||||
|
||||
func assertReadAccess(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
reqUser, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch user := reqUser.(type) {
|
||||
case *jwt.User:
|
||||
role := user.Role()
|
||||
if role == jwt.RoleReader || role == jwt.RoleWriter {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
logUnexpectedUserType(r.Context(), reqUser)
|
||||
}
|
||||
|
||||
forbidden(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func assertWriteAccess(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
reqUser, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch user := reqUser.(type) {
|
||||
case *jwt.User:
|
||||
role := user.Role()
|
||||
if role == jwt.RoleWriter {
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
logUnexpectedUserType(r.Context(), reqUser)
|
||||
}
|
||||
|
||||
forbidden(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool) {
|
||||
ctx := r.Context()
|
||||
user, err := auth.CtxUser(ctx)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not retrieve user", logger.E(errors.WithStack(err)))
|
||||
|
||||
forbidden(w, r)
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
forbidden(w, r)
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return user, true
|
||||
}
|
||||
|
||||
func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||
logger.Warn(r.Context(), "forbidden", logger.F("path", r.URL.Path))
|
||||
|
||||
api.ErrorResponse(w, http.StatusForbidden, ErrCodeForbidden, nil)
|
||||
}
|
||||
|
||||
func logUnexpectedUserType(ctx context.Context, user auth.User) {
|
||||
logger.Error(
|
||||
ctx, "unexpected user type",
|
||||
logger.F("subject", user.Subject()),
|
||||
logger.F("type", fmt.Sprintf("%T", user)),
|
||||
)
|
||||
}
|
31
internal/admin/error.go
Normal file
31
internal/admin/error.go
Normal file
@ -0,0 +1,31 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
)
|
||||
|
||||
const ErrCodeAlreadyExist api.ErrorCode = "already-exist"
|
||||
|
||||
func invalidDataErrorResponse(w http.ResponseWriter, r *http.Request, err *schema.InvalidDataError) {
|
||||
keyErrors := err.KeyErrors()
|
||||
|
||||
message := ""
|
||||
for idx, err := range keyErrors {
|
||||
if idx != 0 {
|
||||
message += ", "
|
||||
}
|
||||
message += fmt.Sprintf("Path [%s]: %s", err.PropertyPath, err.Message)
|
||||
}
|
||||
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeInvalidRequest, &struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
42
internal/admin/init.go
Normal file
42
internal/admin/init.go
Normal file
@ -0,0 +1,42 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Server) initRepositories(ctx context.Context) error {
|
||||
if err := s.initLayerRepository(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := s.initProxyRepository(ctx); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) initLayerRepository(ctx context.Context) error {
|
||||
layerRepository, err := setup.NewLayerRepository(ctx, s.redisConfig)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.layerRepository = layerRepository
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) initProxyRepository(ctx context.Context) error {
|
||||
proxyRepository, err := setup.NewProxyRepository(ctx, s.redisConfig)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.proxyRepository = proxyRepository
|
||||
|
||||
return nil
|
||||
}
|
302
internal/admin/layer_route.go
Normal file
302
internal/admin/layer_route.go
Normal file
@ -0,0 +1,302 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type QueryLayerResponse struct {
|
||||
Layers []*store.LayerHeader `json:"layers"`
|
||||
}
|
||||
|
||||
func (s *Server) queryLayer(w http.ResponseWriter, r *http.Request) {
|
||||
proxyName, ok := getProxyName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
options := []store.QueryLayerOptionFunc{}
|
||||
|
||||
name := r.URL.Query().Get("name")
|
||||
if name != "" {
|
||||
options = append(options, store.WithLayerQueryName(store.LayerName(name)))
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
layers, err := s.layerRepository.QueryLayers(
|
||||
ctx,
|
||||
proxyName,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not list layers", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sort.Sort(store.ByLayerWeight(layers))
|
||||
|
||||
api.DataResponse(w, http.StatusOK, QueryLayerResponse{
|
||||
Layers: layers,
|
||||
})
|
||||
}
|
||||
|
||||
func validateLayerName(v string) (store.LayerName, error) {
|
||||
name, err := store.ValidateName(v)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return store.LayerName(name), nil
|
||||
}
|
||||
|
||||
type GetLayerResponse struct {
|
||||
Layer *store.Layer `json:"layer"`
|
||||
}
|
||||
|
||||
func (s *Server) getLayer(w http.ResponseWriter, r *http.Request) {
|
||||
proxyName, ok := getProxyName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
layerName, ok := getLayerName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
layer, err := s.layerRepository.GetLayer(ctx, proxyName, layerName)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, GetLayerResponse{
|
||||
Layer: layer,
|
||||
})
|
||||
}
|
||||
|
||||
type DeleteLayerResponse struct {
|
||||
LayerName store.LayerName `json:"layerName"`
|
||||
}
|
||||
|
||||
func (s *Server) deleteLayer(w http.ResponseWriter, r *http.Request) {
|
||||
proxyName, ok := getProxyName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
layerName, ok := getLayerName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
if err := s.layerRepository.DeleteLayer(ctx, proxyName, layerName); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not delete layer", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, DeleteLayerResponse{
|
||||
LayerName: layerName,
|
||||
})
|
||||
}
|
||||
|
||||
type CreateLayerRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
Options map[string]any `json:"options"`
|
||||
}
|
||||
|
||||
type CreateLayerResponse struct {
|
||||
Layer *store.Layer `json:"layer"`
|
||||
}
|
||||
|
||||
func (s *Server) createLayer(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
proxyName, ok := getProxyName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
createLayerReq := &CreateLayerRequest{}
|
||||
if ok := api.Bind(w, r, createLayerReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
layerName, err := store.ValidateName(createLayerReq.Name)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
layer, err := s.layerRepository.CreateLayer(ctx, proxyName, store.LayerName(layerName), store.LayerType(createLayerReq.Type), createLayerReq.Options)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrAlreadyExist) {
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyExist, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not create layer", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Layer *store.Layer `json:"layer"`
|
||||
}{
|
||||
Layer: layer,
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateLayerRequest struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
Weight *int `json:"weight"`
|
||||
Options *store.LayerOptions `json:"options"`
|
||||
}
|
||||
|
||||
type UpdateLayerResponse struct {
|
||||
Layer *store.Layer `json:"layer"`
|
||||
}
|
||||
|
||||
func (s *Server) updateLayer(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
proxyName, ok := getProxyName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
layerName, ok := getLayerName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
layer, err := s.layerRepository.GetLayer(ctx, proxyName, layerName)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get layer", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
updateLayerReq := &UpdateLayerRequest{}
|
||||
if ok := api.Bind(w, r, updateLayerReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
options := make([]store.UpdateLayerOptionFunc, 0)
|
||||
|
||||
if updateLayerReq.Enabled != nil {
|
||||
options = append(options, store.WithLayerUpdateEnabled(*updateLayerReq.Enabled))
|
||||
}
|
||||
|
||||
if updateLayerReq.Weight != nil {
|
||||
options = append(options, store.WithLayerUpdateWeight(*updateLayerReq.Weight))
|
||||
}
|
||||
|
||||
if updateLayerReq.Options != nil {
|
||||
if err := schema.ValidateLayerOptions(ctx, layer.Type, updateLayerReq.Options); err != nil {
|
||||
logger.Error(r.Context(), "could not validate layer options", logger.E(errors.WithStack(err)))
|
||||
|
||||
var invalidDataErr *schema.InvalidDataError
|
||||
if errors.As(err, &invalidDataErr) {
|
||||
invalidDataErrorResponse(w, r, invalidDataErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
options = append(options, store.WithLayerUpdateOptions(*updateLayerReq.Options))
|
||||
}
|
||||
|
||||
layer, err = s.layerRepository.UpdateLayer(
|
||||
ctx, proxyName, layerName,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not update layer", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, UpdateLayerResponse{Layer: layer})
|
||||
}
|
||||
|
||||
func getLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool) {
|
||||
rawLayerName := chi.URLParam(r, "layerName")
|
||||
|
||||
name, err := store.ValidateName(rawLayerName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return store.LayerName(name), true
|
||||
}
|
||||
|
||||
func geLayerName(w http.ResponseWriter, r *http.Request) (store.LayerName, bool) {
|
||||
rawLayerName := chi.URLParam(r, "layerName")
|
||||
|
||||
name, err := store.ValidateName(rawLayerName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse layer name", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return store.LayerName(name), true
|
||||
}
|
31
internal/admin/option.go
Normal file
31
internal/admin/option.go
Normal file
@ -0,0 +1,31 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
ServerConfig config.AdminServerConfig
|
||||
RedisConfig config.RedisConfig
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
|
||||
func defaultOption() *Option {
|
||||
return &Option{
|
||||
ServerConfig: config.NewDefaultAdminServerConfig(),
|
||||
RedisConfig: config.NewDefaultRedisConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func WithServerConfig(conf config.AdminServerConfig) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.ServerConfig = conf
|
||||
}
|
||||
}
|
||||
|
||||
func WithRedisConfig(conf config.RedisConfig) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.RedisConfig = conf
|
||||
}
|
||||
}
|
312
internal/admin/proxy_route.go
Normal file
312
internal/admin/proxy_route.go
Normal file
@ -0,0 +1,312 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type QueryProxyResponse struct {
|
||||
Proxies []*store.ProxyHeader `json:"proxies"`
|
||||
}
|
||||
|
||||
func (s *Server) queryProxy(w http.ResponseWriter, r *http.Request) {
|
||||
options := []store.QueryProxyOptionFunc{}
|
||||
|
||||
names, ok := getStringableSliceValues(w, r, "names", nil, validateProxyName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if names != nil {
|
||||
options = append(options, store.WithProxyQueryNames(names...))
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
proxies, err := s.proxyRepository.QueryProxy(
|
||||
ctx,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not list proxies", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sort.Sort(store.ByProxyWeight(proxies))
|
||||
|
||||
api.DataResponse(w, http.StatusOK, QueryProxyResponse{
|
||||
Proxies: proxies,
|
||||
})
|
||||
}
|
||||
|
||||
func validateProxyName(v string) (store.ProxyName, error) {
|
||||
name, err := store.ValidateName(v)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return store.ProxyName(name), nil
|
||||
}
|
||||
|
||||
type GetProxyResponse struct {
|
||||
Proxy *store.Proxy `json:"proxy"`
|
||||
}
|
||||
|
||||
func (s *Server) getProxy(w http.ResponseWriter, r *http.Request) {
|
||||
proxyName, ok := getProxyName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
proxy, err := s.proxyRepository.GetProxy(ctx, proxyName)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not get proxy", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, GetProxyResponse{
|
||||
Proxy: proxy,
|
||||
})
|
||||
}
|
||||
|
||||
type DeleteProxyResponse struct {
|
||||
ProxyName store.ProxyName `json:"proxyName"`
|
||||
}
|
||||
|
||||
func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||
proxyName, ok := getProxyName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
if err := s.proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not delete proxy", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, DeleteProxyResponse{
|
||||
ProxyName: proxyName,
|
||||
})
|
||||
}
|
||||
|
||||
type CreateProxyRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
To string `json:"to" validate:"required"`
|
||||
From []string `json:"from" validate:"required"`
|
||||
}
|
||||
|
||||
type CreateProxyResponse struct {
|
||||
Proxy *store.Proxy `json:"proxy"`
|
||||
}
|
||||
|
||||
func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
createProxyReq := &CreateProxyRequest{}
|
||||
if ok := api.Bind(w, r, createProxyReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
name, err := store.ValidateName(createProxyReq.Name)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'name' parameter", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := url.Parse(createProxyReq.To); err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxy, err := s.proxyRepository.CreateProxy(ctx, store.ProxyName(name), createProxyReq.To, createProxyReq.From...)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrAlreadyExist) {
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyExist, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not create proxy", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Proxy *store.Proxy `json:"proxy"`
|
||||
}{
|
||||
Proxy: proxy,
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateProxyRequest struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
Weight *int `json:"weight"`
|
||||
To *string `json:"to"`
|
||||
From []string `json:"from"`
|
||||
}
|
||||
|
||||
type UpdateProxyResponse struct {
|
||||
Proxy *store.Proxy `json:"proxy"`
|
||||
}
|
||||
|
||||
func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
proxyName, ok := getProxyName(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
updateProxyReq := &UpdateProxyRequest{}
|
||||
if ok := api.Bind(w, r, updateProxyReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
options := make([]store.UpdateProxyOptionFunc, 0)
|
||||
|
||||
if updateProxyReq.Enabled != nil {
|
||||
options = append(options, store.WithProxyUpdateEnabled(*updateProxyReq.Enabled))
|
||||
}
|
||||
|
||||
if updateProxyReq.To != nil {
|
||||
_, err := url.Parse(*updateProxyReq.To)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse 'to' parameter", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
options = append(options, store.WithProxyUpdateTo(*updateProxyReq.To))
|
||||
}
|
||||
|
||||
if updateProxyReq.From != nil {
|
||||
options = append(options, store.WithProxyUpdateFrom(updateProxyReq.From...))
|
||||
}
|
||||
|
||||
if updateProxyReq.Weight != nil {
|
||||
options = append(options, store.WithProxyUpdateWeight(*updateProxyReq.Weight))
|
||||
}
|
||||
|
||||
proxy, err := s.proxyRepository.UpdateProxy(
|
||||
ctx, proxyName,
|
||||
options...,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not update proxy", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, UpdateProxyResponse{Proxy: proxy})
|
||||
}
|
||||
|
||||
func getProxyName(w http.ResponseWriter, r *http.Request) (store.ProxyName, bool) {
|
||||
rawProxyName := chi.URLParam(r, "proxyName")
|
||||
|
||||
name, err := store.ValidateName(rawProxyName)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse proxy name", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return store.ProxyName(name), true
|
||||
}
|
||||
|
||||
func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
if rawValue != "" {
|
||||
value, err := strconv.ParseInt(rawValue, 10, 64)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse int param", logger.F("param", param), logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
||||
|
||||
func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
if rawValue != "" {
|
||||
values := strings.Split(rawValue, ",")
|
||||
|
||||
return values, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
||||
|
||||
func getStringableSliceValues[T ~string](w http.ResponseWriter, r *http.Request, param string, defaultValue []T, validate func(string) (T, error)) ([]T, bool) {
|
||||
rawValue := r.URL.Query().Get(param)
|
||||
|
||||
if rawValue != "" {
|
||||
rawValues := strings.Split(rawValue, ",")
|
||||
values := make([]T, 0, len(rawValues))
|
||||
|
||||
for _, rv := range rawValues {
|
||||
v, err := validate(rv)
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse ids slice param", logger.F("param", param), logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
return values, true
|
||||
}
|
||||
|
||||
return defaultValue, true
|
||||
}
|
145
internal/admin/server.go
Normal file
145
internal/admin/server.go
Normal file
@ -0,0 +1,145 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/auth/jwt"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
serverConfig config.AdminServerConfig
|
||||
redisConfig config.RedisConfig
|
||||
proxyRepository store.ProxyRepository
|
||||
layerRepository store.LayerRepository
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context) (<-chan net.Addr, <-chan error) {
|
||||
errs := make(chan error)
|
||||
addrs := make(chan net.Addr)
|
||||
|
||||
go s.run(ctx, addrs, errs)
|
||||
|
||||
return addrs, errs
|
||||
}
|
||||
|
||||
func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan error) {
|
||||
defer func() {
|
||||
close(errs)
|
||||
close(addrs)
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
defer cancel()
|
||||
|
||||
if err := s.initRepositories(ctx); err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.serverConfig.HTTP.Host, s.serverConfig.HTTP.Port))
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
addrs <- listener.Addr()
|
||||
|
||||
defer func() {
|
||||
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
errs <- errors.WithStack(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
|
||||
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
log.Printf("%+v", errors.WithStack(err))
|
||||
}
|
||||
}()
|
||||
|
||||
key, err := jwk.LoadOrGenerate(string(s.serverConfig.Auth.PrivateKey), jwk.DefaultKeySize)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := jwk.PublicKeySet(key)
|
||||
if err != nil {
|
||||
errs <- errors.WithStack(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
corsMiddleware := cors.New(cors.Options{
|
||||
AllowedOrigins: s.serverConfig.CORS.AllowedOrigins,
|
||||
AllowedMethods: s.serverConfig.CORS.AllowedMethods,
|
||||
AllowCredentials: bool(s.serverConfig.CORS.AllowCredentials),
|
||||
AllowedHeaders: s.serverConfig.CORS.AllowedHeaders,
|
||||
Debug: bool(s.serverConfig.CORS.Debug),
|
||||
})
|
||||
|
||||
router.Use(corsMiddleware.Handler)
|
||||
|
||||
router.Route("/api/v1", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.Middleware(
|
||||
jwt.NewAuthenticator(keys, string(s.serverConfig.Auth.Issuer), jwt.DefaultAcceptableSkew),
|
||||
))
|
||||
|
||||
r.Route("/proxies", func(r chi.Router) {
|
||||
r.With(assertReadAccess).Get("/", s.queryProxy)
|
||||
r.With(assertWriteAccess).Post("/", s.createProxy)
|
||||
r.With(assertReadAccess).Get("/{proxyName}", s.getProxy)
|
||||
r.With(assertWriteAccess).Put("/{proxyName}", s.updateProxy)
|
||||
r.With(assertWriteAccess).Delete("/{proxyName}", s.deleteProxy)
|
||||
|
||||
r.With(assertReadAccess).Get("/{proxyName}/layers", s.queryLayer)
|
||||
r.With(assertWriteAccess).Post("/{proxyName}/layers", s.createLayer)
|
||||
r.With(assertReadAccess).Get("/{proxyName}/layers/{layerName}", s.getLayer)
|
||||
r.With(assertWriteAccess).Put("/{proxyName}/layers/{layerName}", s.updateLayer)
|
||||
r.With(assertWriteAccess).Delete("/{proxyName}/layers/{layerName}", s.deleteLayer)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
logger.Info(ctx, "http server listening")
|
||||
|
||||
if err := http.Serve(listener, router); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
errs <- errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "http server exiting")
|
||||
}
|
||||
|
||||
func NewServer(funcs ...OptionFunc) *Server {
|
||||
opt := defaultOption()
|
||||
for _, fn := range funcs {
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
return &Server{
|
||||
serverConfig: opt.ServerConfig,
|
||||
redisConfig: opt.RedisConfig,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user