diff --git a/app/cli.go b/app/cli.go index f882823b1..06cde359f 100644 --- a/app/cli.go +++ b/app/cli.go @@ -3,6 +3,7 @@ package app import ( "context" + authCommands "github.com/cortezaproject/corteza-server/auth/commands" federationCommands "github.com/cortezaproject/corteza-server/federation/commands" "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/pkg/rbac" @@ -51,7 +52,6 @@ func (app *CortezaApp) InitCLI() { app.Command.AddCommand( systemCommands.Users(app), systemCommands.Roles(app), - systemCommands.Auth(app, app.Opt.Auth), systemCommands.RBAC(app), systemCommands.Sink(app), systemCommands.Settings(app), @@ -60,6 +60,7 @@ func (app *CortezaApp) InitCLI() { serveCmd, upgradeCmd, provisionCmd, + authCommands.General(app, app.Opt.Auth), federationCommands.Sync(app), cli.VersionCommand(), ) diff --git a/auth/auth.go b/auth/auth.go index 12f3db5dc..f369a7d89 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -22,6 +22,7 @@ import ( "go.uber.org/zap" "html/template" "net/http" + "os" "strconv" "strings" "time" @@ -38,7 +39,7 @@ type ( ) //go:embed assets/public -var publicAssets embed.FS +var PublicAssets embed.FS // New initializes Auth service that orchestrates session manager, oauth2 manager and http request handlers func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthOpt) (svc *service, err error) { @@ -52,12 +53,12 @@ func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthO svc = &service{ opt: opt, - log: log, + log: log.WithOptions(zap.AddStacktrace(zap.PanicLevel)), store: s, settings: &settings.Settings{ /* all disabled by default. */ }, } - // use modified log ger for the resrt + // use modified logger for the rest if opt.LogEnabled { log = log.WithOptions(zap.AddStacktrace(zap.PanicLevel)) } else { @@ -157,6 +158,8 @@ func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthO } var ( + tplLoader templateLoader + tplBase = template.New(""). Funcs(sprig.FuncMap()). Funcs(template.FuncMap{ @@ -164,10 +167,14 @@ func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthO "buildtime": func() string { return version.BuildTime }, "links": handlers.GetLinks, }) - tplLoader templateLoader + + useEmbedded = len(opt.AssetsPath) == 0 ) - if len(opt.AssetsPath) > 0 { + if useEmbedded { + tplLoader = EmbeddedTemplates + log.Info("using embedded templates") + } else { tplLoader = func(t *template.Template) (tpl *template.Template, err error) { if tpl, err = t.Clone(); err != nil { return nil, fmt.Errorf("cannot clone templates: %w", err) @@ -175,21 +182,27 @@ func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthO return tpl.ParseGlob(opt.AssetsPath + "/templates/*.tpl") } } - log.Info("loading assets from filesystem", zap.String("path", opt.AssetsPath)) - } else { - tplLoader = EmbeddedTemplates - log.Info("using embedded assets") + log.Info( + "loading templates from filesystem", + zap.String("AUTH_ASSETS_PATH", svc.opt.AssetsPath), + ) } - if !opt.DevelopmentMode || len(opt.AssetsPath) == 0 { - log.Info("initializing templates without reloading (production mode)") + if !useEmbedded && opt.DevelopmentMode { + log.Info( + "initializing reloadable templates", + zap.Bool("AUTH_DEVELOPMENT_MODE", opt.DevelopmentMode), + ) + tpls = NewReloadableTemplates(tplBase, tplLoader) + } else { + log.Info( + "initializing templates without reloading", + zap.Bool("AUTH_DEVELOPMENT_MODE", opt.DevelopmentMode), + ) tpls, err = NewStaticTemplates(tplBase, tplLoader) if err != nil { return nil, fmt.Errorf("cannot load templates: %w", err) } - } else { - log.Info("initializing reloadable templates (development mode)") - tpls = NewReloadableTemplates(tplBase, tplLoader) } svc.handlers = &handlers.AuthHandlers{ @@ -294,12 +307,33 @@ func (svc service) MountHttpRoutes(r chi.Router) { svc.handlers.MountHttpRoutes(r) const uriRoot = "/auth/assets/public" - if len(svc.opt.AssetsPath) == 0 && !svc.opt.DevelopmentMode { - r.Handle(uriRoot+"/*", http.StripPrefix("/auth/", http.FileServer(http.FS(publicAssets)))) - } else { + if svc.opt.DevelopmentMode { var root = strings.TrimRight(svc.opt.AssetsPath, "/") + "/public" - r.Handle(uriRoot+"/*", http.StripPrefix(uriRoot, http.FileServer(http.Dir(root)))) + + if err := dirCheck(root); err != nil { + svc.log.Error( + "failed to run in development mode (AUTH_DEVELOPMENT_MODE=true)", + zap.Error(err), + zap.String("AUTH_ASSETS_PATH", svc.opt.AssetsPath), + ) + } else { + r.Handle(uriRoot+"/*", http.StripPrefix(uriRoot, http.FileServer(http.Dir(root)))) + return + } } + + // fallback to embedded assets + r.Handle(uriRoot+"/*", http.StripPrefix("/auth/", http.FileServer(http.FS(PublicAssets)))) +} + +// checks if directory exists & is readable +func dirCheck(path string) (err error) { + _, err = os.Stat(path) + if !os.IsNotExist(err) { + return + } + + return } //func (svc service) WellKnownOpenIDConfiguration() http.HandlerFunc { diff --git a/auth/commands/assets.go b/auth/commands/assets.go new file mode 100644 index 000000000..a5da26f7e --- /dev/null +++ b/auth/commands/assets.go @@ -0,0 +1,109 @@ +package commands + +import ( + "embed" + "github.com/cortezaproject/corteza-server/auth" + "github.com/cortezaproject/corteza-server/pkg/cli" + "github.com/cortezaproject/corteza-server/pkg/options" + "github.com/spf13/cobra" + "os" + "path" +) + +func assets(opt options.AuthOpt) *cobra.Command { + cmd := &cobra.Command{ + Use: "assets", + Short: "Authentication flow assets (styling, images) and templates", + } + + exportCmd := &cobra.Command{ + Use: "export", + Short: "Exports embedded assets into provided path (must exists)", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + assetsRoot := opt.AssetsPath + if len(args) > 0 { + assetsRoot = args[0] + } + + if len(assetsRoot) == 0 { + cmd.Println("can not export, no path provided and AUTH_ASSETS_PATH is empty") + return + } + + if _, err := os.Stat(assetsRoot); err != nil { + cli.HandleError(err) + } + + emb := map[string]embed.FS{ + "public": auth.PublicAssets, + "templates": auth.Templates, + } + + var ( + fh *os.File + buf []byte + dst string + src string + assetsSub string + ) + for dir, efs := range emb { + assetsSub = path.Join(assetsRoot, dir) + + cmd.Printf("exporting auth assets to %s\n", dir) + if _, err := os.Stat(assetsSub); os.IsNotExist(err) { + cli.HandleError(os.Mkdir(assetsSub, 0755)) + cmd.Println("directory created") + } else if err != nil { + cli.HandleError(err) + } + + cc, err := efs.ReadDir(path.Join("assets", dir)) + if err != nil { + cli.HandleError(err) + } + + for _, c := range cc { + src = path.Join("assets", dir, c.Name()) + dst = path.Join(assetsSub, c.Name()) + + if c.IsDir() { + cmd.Println("skipping directory:", assetsRoot) + continue + } + + cmd.Print("exporting asset ", dst, ": ") + + buf, err = efs.ReadFile(src) + if err != nil { + cmd.Println("\n => error:", err.Error()) + continue + } + + fh, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) + if os.IsExist(err) { + cmd.Println("exists") + continue + } + if err != nil { + cmd.Println("\n => error:", err.Error()) + continue + } + + _, err = fh.Write(buf) + if err != nil { + cmd.Println("\n => error:", err.Error()) + continue + } + + cmd.Println("ok") + } + } + + }, + } + + cmd.AddCommand(exportCmd) + + return cmd +} diff --git a/system/commands/auth.go b/auth/commands/commands.go similarity index 87% rename from system/commands/auth.go rename to auth/commands/commands.go index 289ad63c7..1aea9f589 100644 --- a/system/commands/auth.go +++ b/auth/commands/commands.go @@ -1,6 +1,7 @@ package commands import ( + "context" "github.com/cortezaproject/corteza-server/auth/external" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/cli" @@ -10,8 +11,20 @@ import ( "github.com/spf13/cobra" ) +type ( + serviceInitializer interface { + InitServices(ctx context.Context) error + } +) + +func commandPreRunInitService(app serviceInitializer) func(*cobra.Command, []string) error { + return func(_ *cobra.Command, _ []string) error { + return app.InitServices(cli.Context()) + } +} + // Will perform OpenID connect auto-configuration -func Auth(app serviceInitializer, opt options.AuthOpt) *cobra.Command { +func General(app serviceInitializer, opt options.AuthOpt) *cobra.Command { var ( enableDiscoveredProvider bool skipValidationOnAutoDiscoveredProvider bool @@ -19,7 +32,7 @@ func Auth(app serviceInitializer, opt options.AuthOpt) *cobra.Command { cmd := &cobra.Command{ Use: "auth", - Short: "External authentication", + Short: "Authentication management", } autoDiscoverCmd := &cobra.Command{ @@ -111,6 +124,7 @@ func Auth(app serviceInitializer, opt options.AuthOpt) *cobra.Command { autoDiscoverCmd, testEmails, jwtCmd, + assets(opt), ) return cmd diff --git a/auth/templates.go b/auth/templates.go index ce81d0f25..aa7b65b7c 100644 --- a/auth/templates.go +++ b/auth/templates.go @@ -7,7 +7,7 @@ import ( ) //go:embed assets/templates/*.tpl -var embeddedTemplates embed.FS +var Templates embed.FS type ( templateLoader func(tpls *template.Template) (tpl *template.Template, err error) @@ -62,5 +62,5 @@ func (t templateStatic) ExecuteTemplate(w io.Writer, name string, data interface // EmbeddedTemplates returns embedded templates. func EmbeddedTemplates(t *template.Template) (tpl *template.Template, err error) { - return t.ParseFS(embeddedTemplates, "assets/templates/*.html.tpl") + return t.ParseFS(Templates, "assets/templates/*.html.tpl") } diff --git a/pkg/options/auth.yaml b/pkg/options/auth.yaml index 17ddf94de..5cca1c648 100644 --- a/pkg/options/auth.yaml +++ b/pkg/options/auth.yaml @@ -142,6 +142,9 @@ props: When corteza starts, if path exists it tries to load template files from it. If not it uses statically embedded files. + When empty path is set (default value), embedded files are used. + + - name: developmentMode type: bool description: |-