126 lines
3.9 KiB
Go
126 lines
3.9 KiB
Go
// platform composes storage, authentication, permission policy, tracing and
|
|
// OpenAPI using provider-neutral business dependencies.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
omni "git.yuntuo.hk/k2safe/go-omni"
|
|
"git.yuntuo.hk/k2safe/go-omni/auth"
|
|
"git.yuntuo.hk/k2safe/go-omni/config"
|
|
"git.yuntuo.hk/k2safe/go-omni/openapi"
|
|
"git.yuntuo.hk/k2safe/go-omni/storage"
|
|
"git.yuntuo.hk/k2safe/go-omni/storage/local"
|
|
"git.yuntuo.hk/k2safe/go-omni/telemetry"
|
|
)
|
|
|
|
type dependencies struct {
|
|
Files storage.Store
|
|
Auth auth.Authenticator
|
|
Tracing *telemetry.Runtime
|
|
}
|
|
type upload struct {
|
|
Name string `json:"name" binding:"required,max=128"`
|
|
Content string `json:"content" binding:"max=4096"`
|
|
}
|
|
|
|
// Dependencies are borrowed; the composition root owns their shutdown.
|
|
func newApp(cfg omni.Config, d dependencies) (*omni.App, error) {
|
|
if d.Files == nil || d.Auth == nil || d.Tracing == nil {
|
|
return nil, errors.New("platform: dependencies required")
|
|
}
|
|
app := omni.New(omni.WithConfig(cfg))
|
|
app.Use(d.Tracing.Middleware())
|
|
api, err := openapi.New("File service", "1")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
input, err := openapi.SchemaOf[upload]()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
output, err := openapi.SchemaOf[storage.Info]()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
policy := auth.PolicyFunc(func(_ context.Context, p auth.Principal, action, resource string) error {
|
|
if p.TenantID != "" && p.HasRole("writer") && action == "create" && resource == "files" {
|
|
return nil
|
|
}
|
|
return errors.New("denied")
|
|
})
|
|
err = api.Handle(app, openapi.Operation{Method: "POST", Path: "/api/files", OperationID: "createFile", Summary: "Store a small text file", Request: input, BearerAuth: true, Responses: map[int]openapi.Response{201: {Description: "Created", Schema: openapi.Envelope(output)}}}, func(c *omni.Context) error {
|
|
var in upload
|
|
if err := c.BindJSON(&in); err != nil {
|
|
return err
|
|
}
|
|
p, _ := auth.FromContext(c)
|
|
if strings.ContainsAny(in.Name, "/\\") {
|
|
return omni.BadRequest("INVALID_NAME", "name must be one path segment")
|
|
}
|
|
key := p.TenantID + "/" + in.Name
|
|
if err := storage.Check(c.Context(), key); err != nil {
|
|
return omni.BadRequest("INVALID_KEY", "invalid storage key")
|
|
}
|
|
var info storage.Info
|
|
err := d.Tracing.Do(c.Context(), "files.put", func(ctx context.Context) error {
|
|
var err error
|
|
info, err = d.Files.Put(ctx, key, strings.NewReader(in.Content), int64(len(in.Content)), "text/plain; charset=utf-8")
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.Created(info)
|
|
}, auth.RequireAuth(d.Auth), auth.RequirePermission(policy, "create", func(*omni.Context) string { return "files" }))
|
|
if err != nil {
|
|
app.Close()
|
|
return nil, err
|
|
}
|
|
app.GET("/openapi.json", api.Handler(), auth.RequireAuth(d.Auth))
|
|
return app, nil
|
|
}
|
|
func run(ctx context.Context, configFile, root string) error {
|
|
defaults := config.Default()
|
|
defaults.Address = "127.0.0.1:8080"
|
|
cfg, err := config.LoadProject(configFile, defaults)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token := os.Getenv("GO_OMNI_DEMO_TOKEN")
|
|
if token == "" {
|
|
return errors.New("set GO_OMNI_DEMO_TOKEN for the demonstration credential")
|
|
}
|
|
files, err := local.New(root, 64<<10)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tracing, err := telemetry.New(telemetry.Config{Service: "fileService", SampleRatio: 1})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tracing.Close()
|
|
app, err := newApp(cfg, dependencies{Files: files, Auth: auth.StaticAuthenticator{token: {UserID: "demoUser", TenantID: "demoTenant", Roles: []string{"writer"}}}, Tracing: tracing})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return app.RunContext(ctx)
|
|
}
|
|
func main() {
|
|
file := flag.String("config", "", "optional JSON/YAML config")
|
|
root := flag.String("root", "./fileData", "private local object directory")
|
|
flag.Parse()
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
if err := run(ctx, *file, *root); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|