feat(sdk,client): add menu to help navigation between apps
All checks were successful
arcad/edge/pipeline/head This commit looks good
All checks were successful
arcad/edge/pipeline/head This commit looks good
This commit is contained in:
@ -44,6 +44,10 @@ func (r *Repository) List(ctx context.Context) ([]*app.Manifest, error) {
|
||||
}
|
||||
|
||||
func NewRepository(getURL GetURLFunc, manifests ...*app.Manifest) *Repository {
|
||||
if manifests == nil {
|
||||
manifests = make([]*app.Manifest, 0)
|
||||
}
|
||||
|
||||
return &Repository{getURL, manifests}
|
||||
}
|
||||
|
||||
|
112
pkg/module/app/mount.go
Normal file
112
pkg/module/app/mount.go
Normal file
@ -0,0 +1,112 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type MountFunc func(r chi.Router)
|
||||
|
||||
type Handler struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func (h *Handler) serveApps(w http.ResponseWriter, r *http.Request) {
|
||||
manifests, err := h.repo.List(r.Context())
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not retrieve app manifest", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Manifests []*app.Manifest `json:"manifests"`
|
||||
}{
|
||||
Manifests: manifests,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) serveApp(w http.ResponseWriter, r *http.Request) {
|
||||
appID := app.ID(chi.URLParam(r, "appID"))
|
||||
|
||||
manifest, err := h.repo.Get(r.Context(), appID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(r.Context(), "could not retrieve app manifest", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Manifest *app.Manifest `json:"manifest"`
|
||||
}{
|
||||
Manifest: manifest,
|
||||
})
|
||||
}
|
||||
|
||||
type serveAppURLRequest struct {
|
||||
From string `json:"from,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handler) serveAppURL(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req := &serveAppURLRequest{}
|
||||
if ok := api.Bind(w, r, req); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
appID := app.ID(chi.URLParam(r, "appID"))
|
||||
|
||||
from := req.From
|
||||
if from == "" {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "could not split remote address", logger.E(errors.WithStack(err)))
|
||||
} else {
|
||||
from = host
|
||||
}
|
||||
}
|
||||
|
||||
url, err := h.repo.GetURL(ctx, appID, from)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(r.Context(), "could not retrieve app url", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
URL string `json:"url"`
|
||||
}{
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
|
||||
func Mount(repository Repository) MountFunc {
|
||||
handler := &Handler{repository}
|
||||
return func(r chi.Router) {
|
||||
r.Get("/api/v1/apps", handler.serveApps)
|
||||
r.Get("/api/v1/apps/{appID}", handler.serveApp)
|
||||
r.Post("/api/v1/apps/{appID}/url", handler.serveAppURL)
|
||||
}
|
||||
}
|
@ -9,5 +9,5 @@ import (
|
||||
type Repository interface {
|
||||
List(context.Context) ([]*app.Manifest, error)
|
||||
Get(context.Context, app.ID) (*app.Manifest, error)
|
||||
GetURL(context.Context, app.ID, string) (string, error)
|
||||
GetURL(ctx context.Context, id app.ID, from string) (string, error)
|
||||
}
|
||||
|
@ -2,7 +2,4 @@ package auth
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrUnauthenticated = errors.New("unauthenticated")
|
||||
ErrClaimNotFound = errors.New("claim not found")
|
||||
)
|
||||
var ErrUnauthenticated = errors.New("unauthenticated")
|
||||
|
@ -110,6 +110,8 @@ func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
account.Claims[auth.ClaimIssuer] = "local"
|
||||
|
||||
token, err := generateSignedToken(h.algo, h.key, account.Claims)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
|
||||
|
@ -91,7 +91,7 @@
|
||||
<form method="post" action="{{ .URL }}">
|
||||
<div class="form-control">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" value="{{ .Username }}" required />
|
||||
<input type="text" id="username" name="username" value="{{ .Username }}" required autofocus />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="password">Password</label>
|
||||
|
@ -19,10 +19,10 @@ type GetKeySetFunc func() (jwk.Set, error)
|
||||
|
||||
func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
|
||||
return func(o *Option) {
|
||||
o.GetClaim = func(ctx context.Context, r *http.Request, claimName string) (string, error) {
|
||||
claim, err := getClaim[string](r, claimName, getKeySet)
|
||||
o.GetClaims = func(ctx context.Context, r *http.Request, names ...string) ([]string, error) {
|
||||
claim, err := getClaims[string](r, getKeySet, names...)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return claim, nil
|
||||
@ -76,28 +76,34 @@ func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func getClaim[T any](r *http.Request, claimAttr string, getKeySet GetKeySetFunc) (T, error) {
|
||||
func getClaims[T any](r *http.Request, getKeySet GetKeySetFunc, names ...string) ([]T, error) {
|
||||
token, err := FindToken(r, getKeySet)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
mapClaims, err := token.AsMap(ctx)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rawClaim, exists := mapClaims[claimAttr]
|
||||
if !exists {
|
||||
return *new(T), errors.WithStack(ErrClaimNotFound)
|
||||
claims := make([]T, len(names))
|
||||
|
||||
for idx, n := range names {
|
||||
rawClaim, exists := mapClaims[n]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
claim, ok := rawClaim.(T)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected claim '%s' to be of type '%T', got '%T'", n, new(T), rawClaim)
|
||||
}
|
||||
|
||||
claims[idx] = claim
|
||||
}
|
||||
|
||||
claim, ok := rawClaim.(T)
|
||||
if !ok {
|
||||
return *new(T), errors.Errorf("unexpected claim '%s' to be of type '%T', got '%T'", claimAttr, new(T), rawClaim)
|
||||
}
|
||||
|
||||
return claim, nil
|
||||
return claims, nil
|
||||
}
|
||||
|
@ -8,15 +8,21 @@ import (
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
ClaimSubject = "sub"
|
||||
ClaimSubject = "sub"
|
||||
ClaimIssuer = "iss"
|
||||
ClaimPreferredUsername = "preferred_username"
|
||||
ClaimEdgeRole = "edge_role"
|
||||
ClaimEdgeTenant = "edge_tenant"
|
||||
ClaimEdgeEntrypoint = "edge_entrypoint"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
server *app.Server
|
||||
getClaimFunc GetClaimFunc
|
||||
server *app.Server
|
||||
getClaims GetClaimsFunc
|
||||
}
|
||||
|
||||
func (m *Module) Name() string {
|
||||
@ -31,6 +37,22 @@ func (m *Module) Export(export *goja.Object) {
|
||||
if err := export.Set("CLAIM_SUBJECT", ClaimSubject); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'CLAIM_SUBJECT' property"))
|
||||
}
|
||||
|
||||
if err := export.Set("CLAIM_TENANT", ClaimEdgeTenant); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := export.Set("CLAIM_ENTRYPOINT", ClaimEdgeEntrypoint); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := export.Set("CLAIM_ROLE", ClaimEdgeRole); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
if err := export.Set("CLAIM_PREFERRED_USERNAME", ClaimPreferredUsername); err != nil {
|
||||
panic(errors.Wrap(err, "could not set 'CLAIM_PREFERRED_USERNAME' property"))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
@ -42,16 +64,21 @@ func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||
panic(rt.ToValue(errors.New("could not find http request in context")))
|
||||
}
|
||||
|
||||
claim, err := m.getClaimFunc(ctx, req, claimName)
|
||||
claim, err := m.getClaims(ctx, req, claimName)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUnauthenticated) || errors.Is(err, ErrClaimNotFound) {
|
||||
if errors.Is(err, ErrUnauthenticated) {
|
||||
return nil
|
||||
}
|
||||
|
||||
panic(rt.ToValue(errors.WithStack(err)))
|
||||
logger.Error(ctx, "could not retrieve claim", logger.E(errors.WithStack(err)))
|
||||
return nil
|
||||
}
|
||||
|
||||
return rt.ToValue(claim)
|
||||
if len(claim) == 0 || claim[0] == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return rt.ToValue(claim[0])
|
||||
}
|
||||
|
||||
func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
|
||||
@ -62,8 +89,8 @@ func ModuleFactory(funcs ...OptionFunc) app.ServerModuleFactory {
|
||||
|
||||
return func(server *app.Server) app.ServerModule {
|
||||
return &Module{
|
||||
server: server,
|
||||
getClaimFunc: opt.GetClaim,
|
||||
server: server,
|
||||
getClaims: opt.GetClaims,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
72
pkg/module/auth/mount.go
Normal file
72
pkg/module/auth/mount.go
Normal file
@ -0,0 +1,72 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type MountFunc func(r chi.Router)
|
||||
|
||||
type Handler struct {
|
||||
getClaims GetClaimsFunc
|
||||
profileClaims []string
|
||||
}
|
||||
|
||||
func (h *Handler) serveProfile(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
claims, err := h.getClaims(ctx, r, h.profileClaims...)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUnauthenticated) {
|
||||
api.ErrorResponse(
|
||||
w, http.StatusUnauthorized,
|
||||
api.ErrCodeUnauthorized,
|
||||
nil,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "could not retrieve claims", logger.E(errors.WithStack(err)))
|
||||
api.ErrorResponse(
|
||||
w, http.StatusInternalServerError,
|
||||
api.ErrCodeUnknownError,
|
||||
nil,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
profile := make(map[string]any)
|
||||
|
||||
for idx, cl := range h.profileClaims {
|
||||
profile[cl] = claims[idx]
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Profile map[string]any `json:"profile"`
|
||||
}{
|
||||
Profile: profile,
|
||||
})
|
||||
}
|
||||
|
||||
func Mount(authHandler http.Handler, funcs ...OptionFunc) MountFunc {
|
||||
opt := defaultOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
handler := &Handler{
|
||||
profileClaims: opt.ProfileClaims,
|
||||
getClaims: opt.GetClaims,
|
||||
}
|
||||
|
||||
return func(r chi.Router) {
|
||||
r.Get("/api/v1/profile", handler.serveProfile)
|
||||
r.Handle("/auth/*", authHandler)
|
||||
}
|
||||
}
|
@ -7,26 +7,41 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetClaimFunc func(ctx context.Context, r *http.Request, claimName string) (string, error)
|
||||
type GetClaimsFunc func(ctx context.Context, r *http.Request, claims ...string) ([]string, error)
|
||||
|
||||
type Option struct {
|
||||
GetClaim GetClaimFunc
|
||||
GetClaims GetClaimsFunc
|
||||
ProfileClaims []string
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
|
||||
func defaultOptions() *Option {
|
||||
return &Option{
|
||||
GetClaim: dummyGetClaim,
|
||||
GetClaims: dummyGetClaims,
|
||||
ProfileClaims: []string{
|
||||
ClaimSubject,
|
||||
ClaimIssuer,
|
||||
ClaimEdgeEntrypoint,
|
||||
ClaimEdgeRole,
|
||||
ClaimPreferredUsername,
|
||||
ClaimEdgeTenant,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dummyGetClaim(ctx context.Context, r *http.Request, claimName string) (string, error) {
|
||||
return "", errors.Errorf("dummy getclaim func cannot retrieve claim '%s'", claimName)
|
||||
func dummyGetClaims(ctx context.Context, r *http.Request, claims ...string) ([]string, error) {
|
||||
return nil, errors.Errorf("dummy getclaim func cannot retrieve claims '%s'", claims)
|
||||
}
|
||||
|
||||
func WithGetClaim(fn GetClaimFunc) OptionFunc {
|
||||
func WithGetClaims(fn GetClaimsFunc) OptionFunc {
|
||||
return func(o *Option) {
|
||||
o.GetClaim = fn
|
||||
o.GetClaims = fn
|
||||
}
|
||||
}
|
||||
|
||||
func WithProfileClaims(claims ...string) OptionFunc {
|
||||
return func(o *Option) {
|
||||
o.ProfileClaims = claims
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user