feat: initial commit
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good

This commit is contained in:
2023-04-24 20:52:12 +02:00
commit ac21629d28
90 changed files with 5730 additions and 0 deletions

View File

@ -0,0 +1,127 @@
package admin
import (
"context"
"fmt"
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/auth"
"forge.cadoles.com/cadoles/bouncer/internal/auth/thirdparty"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
)
var ErrCodeForbidden api.ErrorCode = "forbidden"
func assertGlobalReadAccess(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 *thirdparty.User:
role := user.Role()
if role == thirdparty.RoleReader || role == thirdparty.RoleWriter {
h.ServeHTTP(w, r)
return
}
default:
logUnexpectedUserType(r.Context(), reqUser)
}
forbidden(w, r)
}
return http.HandlerFunc(fn)
}
func assertInboundWriteAccess(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 *thirdparty.User:
role := user.Role()
if role == thirdparty.RoleWriter {
h.ServeHTTP(w, r)
return
}
default:
logUnexpectedUserType(r.Context(), reqUser)
}
forbidden(w, r)
}
return http.HandlerFunc(fn)
}
func assertInboundReadAccess(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 *thirdparty.User:
role := user.Role()
if role == thirdparty.RoleReader || role == thirdparty.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)),
)
}

19
internal/admin/init.go Normal file
View File

@ -0,0 +1,19 @@
package admin
import (
"context"
"forge.cadoles.com/cadoles/bouncer/internal/setup"
"github.com/pkg/errors"
)
func (s *Server) initRepositories(ctx context.Context) error {
proxyRepo, err := setup.NewProxyRepository(ctx, s.databaseConfig)
if err != nil {
return errors.WithStack(err)
}
s.repo = proxyRepo
return nil
}

31
internal/admin/option.go Normal file
View File

@ -0,0 +1,31 @@
package admin
import (
"forge.cadoles.com/cadoles/bouncer/internal/config"
)
type Option struct {
ServerConfig config.AdminServerConfig
DatabaseConfig config.DatabaseConfig
}
type OptionFunc func(*Option)
func defaultOption() *Option {
return &Option{
ServerConfig: config.NewDefaultAdminServerConfig(),
DatabaseConfig: config.NewDefaultDatabaseConfig(),
}
}
func WithServerConfig(conf config.AdminServerConfig) OptionFunc {
return func(opt *Option) {
opt.ServerConfig = conf
}
}
func WithDatabaseConfig(conf config.DatabaseConfig) OptionFunc {
return func(opt *Option) {
opt.DatabaseConfig = conf
}
}

138
internal/admin/server.go Normal file
View File

@ -0,0 +1,138 @@
package admin
import (
"context"
"fmt"
"log"
"net"
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/auth"
"forge.cadoles.com/cadoles/bouncer/internal/auth/thirdparty"
"forge.cadoles.com/cadoles/bouncer/internal/config"
"forge.cadoles.com/cadoles/bouncer/internal/datastore"
"forge.cadoles.com/cadoles/bouncer/internal/jwk"
"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
databaseConfig config.DatabaseConfig
repo datastore.ProxyRepository
}
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(
thirdparty.NewAuthenticator(keys, string(s.serverConfig.Auth.Issuer), thirdparty.DefaultAcceptableSkew),
))
r.Route("/inbounds", func(r chi.Router) {
// r.With(assertGlobalReadAccess).Get("/", s.queryInbounds)
})
r.Route("/outbounds", func(r chi.Router) {
// r.With(assertGlobalReadAccess).Get("/", s.queryOutbounds)
})
})
})
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,
databaseConfig: opt.DatabaseConfig,
}
}