feat: add basic local login handler in dev cli
This commit is contained in:
50
cmd/cli/command/app/default-accounts.json
Normal file
50
cmd/cli/command/app/default-accounts.json
Normal file
@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"username": "superadmin",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$cWOxfEyBy4EyKZR5usB2Pw$xG+Z/E2DUJP9kF0s1fhZjIuP03gFQ65dP7pHRJz7eR8",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "superadmin",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "SuperAdmin",
|
||||
"sub": "superadmin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "admin",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$WXXc4ECnkej6WO7f0Xya6Q$UG2wcGltJcuW0cNTR85mAl65tI1kFWMMw7ADS2FMOvY",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "admin",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "Admin",
|
||||
"sub": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "superuser",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$gkDAWCzfU23+un3x0ny+YA$L/NSPrd5iKPK/UnSCKfSz4EO+v94N3LTLky4QGJOfpI",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "superuser",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "SuperUser",
|
||||
"sub": "superuser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "user",
|
||||
"algo": "argon2id",
|
||||
"password": "$argon2id$v=19$m=65536,t=3,p=2$DhUm9qXUKP35Lzp5M37eZA$2+h6yDxSTHZqFZIuI7JZfFWozwrObna8a8yCgEEPlPE",
|
||||
"claims": {
|
||||
"arcad_entrypoint": "edge",
|
||||
"arcad_role": "user",
|
||||
"arcad_tenant": "dev.cli",
|
||||
"preferred_username": "User",
|
||||
"sub": "user"
|
||||
}
|
||||
}
|
||||
]
|
47
cmd/cli/command/app/hash-password.go
Normal file
47
cmd/cli/command/app/hash-password.go
Normal file
@ -0,0 +1,47 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
|
||||
)
|
||||
|
||||
func HashPasswordCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "hash-password",
|
||||
Usage: "Hash the provided password with the specified algorithm",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "hash `PASSWORD`",
|
||||
Aliases: []string{"p"},
|
||||
Value: "",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "algorithm",
|
||||
Usage: fmt.Sprintf("use `ALGORITHM` to hash password (available: %v)", passwd.Algorithms()),
|
||||
Aliases: []string{"a"},
|
||||
Value: string(argon2id.Algo),
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
algo := ctx.String("algorithm")
|
||||
password := ctx.String("password")
|
||||
|
||||
hash, err := passwd.Hash(passwd.Algo(algo), password)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
fmt.Println(hash)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ func Root() *cli.Command {
|
||||
Subcommands: []*cli.Command{
|
||||
RunCommand(),
|
||||
PackageCommand(),
|
||||
HashPasswordCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
@ -12,6 +14,7 @@ import (
|
||||
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||
"forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||
@ -22,9 +25,15 @@ import (
|
||||
"github.com/dop251/goja"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
_ "embed"
|
||||
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
|
||||
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
|
||||
)
|
||||
|
||||
func RunCommand() *cli.Command {
|
||||
@ -57,22 +66,12 @@ func RunCommand() *cli.Command {
|
||||
&cli.StringFlag{
|
||||
Name: "storage-file",
|
||||
Usage: "use `FILE` for SQLite storage database",
|
||||
Value: "data.sqlite",
|
||||
Value: ".edge/%APPID%/data.sqlite",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth-subject",
|
||||
Usage: "set the `SUBJECT` associated with the simulated connected user",
|
||||
Value: "jdoe",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth-role",
|
||||
Usage: "set the `ROLE` associated with the simulated connected user",
|
||||
Value: "user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth-preferred-username",
|
||||
Usage: "set the `PREFERRED_USERNAME` associated with the simulated connected user",
|
||||
Value: "Jane Doe",
|
||||
Name: "accounts-file",
|
||||
Usage: "use `FILE` as local accounts",
|
||||
Value: ".edge/%APPID%/accounts.json",
|
||||
},
|
||||
},
|
||||
Action: func(ctx *cli.Context) error {
|
||||
@ -82,12 +81,6 @@ func RunCommand() *cli.Command {
|
||||
logFormat := ctx.String("log-format")
|
||||
logLevel := ctx.Int("log-level")
|
||||
|
||||
storageFile := ctx.String("storage-file")
|
||||
|
||||
authSubject := ctx.String("auth-subject")
|
||||
authRole := ctx.String("auth-role")
|
||||
authPreferredUsername := ctx.String("auth-preferred-username")
|
||||
|
||||
logger.SetFormat(logger.Format(logFormat))
|
||||
logger.SetLevel(logger.Level(logLevel))
|
||||
|
||||
@ -105,12 +98,12 @@ func RunCommand() *cli.Command {
|
||||
return errors.Wrapf(err, "could not open path '%s' as an app bundle", path)
|
||||
}
|
||||
|
||||
mux := chi.NewMux()
|
||||
manifest, err := app.LoadManifest(bundle)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load manifest from app bundle")
|
||||
}
|
||||
|
||||
mux.Use(middleware.Logger)
|
||||
mux.Use(dummyAuthMiddleware(authSubject, authRole, authPreferredUsername))
|
||||
|
||||
bus := memory.NewBus()
|
||||
storageFile := injectAppID(ctx.String("storage-file"), manifest.ID)
|
||||
|
||||
db, err := sqlite.Open(storageFile)
|
||||
if err != nil {
|
||||
@ -119,6 +112,7 @@ func RunCommand() *cli.Command {
|
||||
|
||||
ds := sqlite.NewDocumentStoreWithDB(db)
|
||||
bs := sqlite.NewBlobStoreWithDB(db)
|
||||
bus := memory.NewBus()
|
||||
|
||||
handler := appHTTP.NewHandler(
|
||||
appHTTP.WithBus(bus),
|
||||
@ -128,11 +122,33 @@ func RunCommand() *cli.Command {
|
||||
return errors.Wrap(err, "could not load app bundle")
|
||||
}
|
||||
|
||||
mux.Handle("/*", handler)
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
accountsFile := injectAppID(ctx.String("accounts-file"), manifest.ID)
|
||||
|
||||
accounts, err := loadLocalAccounts(accountsFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not load local accounts")
|
||||
}
|
||||
|
||||
// Add auth handler
|
||||
key, err := dummyKey()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
router.Handle("/auth/*", authHTTP.NewLocalHandler(
|
||||
jwa.HS256, key,
|
||||
authHTTP.WithRoutePrefix("/auth"),
|
||||
authHTTP.WithAccounts(accounts...),
|
||||
))
|
||||
|
||||
// Add app handler
|
||||
router.Handle("/*", handler)
|
||||
|
||||
logger.Info(cmdCtx, "listening", logger.F("address", address))
|
||||
|
||||
if err := http.ListenAndServe(address, mux); err != nil {
|
||||
if err := http.ListenAndServe(address, router); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@ -153,10 +169,18 @@ func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStor
|
||||
module.BlobModuleFactory(bus, bs),
|
||||
module.Extends(
|
||||
auth.ModuleFactory(
|
||||
auth.WithJWT(dummyKeyFunc),
|
||||
auth.WithJWT(dummyKeySet),
|
||||
),
|
||||
func(o *goja.Object) {
|
||||
if err := o.Set("CLAIM_ROLE", "role"); err != nil {
|
||||
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_TENANT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
|
||||
}
|
||||
|
||||
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
|
||||
panic(errors.New("could not set 'CLAIM_ROLE' property"))
|
||||
}
|
||||
|
||||
@ -170,59 +194,72 @@ func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStor
|
||||
|
||||
var dummySecret = []byte("not_so_secret")
|
||||
|
||||
func dummyKeyFunc(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||
func dummyKey() (jwk.Key, error) {
|
||||
key, err := jwk.FromRaw(dummySecret)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return dummySecret, nil
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func dummyAuthMiddleware(subject, role, username string) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
unauthenticated := subject == "" && role == "" && username == ""
|
||||
func dummyKeySet() (jwk.Set, error) {
|
||||
key, err := dummyKey()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if unauthenticated {
|
||||
h.ServeHTTP(w, r)
|
||||
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return
|
||||
set := jwk.NewSet()
|
||||
|
||||
if err := set.AddKey(key); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
func ensureDir(path string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func injectAppID(str string, appID app.ID) string {
|
||||
return strings.ReplaceAll(str, "%APPID%", string(appID))
|
||||
}
|
||||
|
||||
//go:embed default-accounts.json
|
||||
var defaultAccounts []byte
|
||||
|
||||
func loadLocalAccounts(path string) ([]authHTTP.LocalAccount, error) {
|
||||
if err := ensureDir(path); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if err := ioutil.WriteFile(path, defaultAccounts, 0o640); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"nbf": time.Now().UTC().Unix(),
|
||||
}
|
||||
|
||||
if subject != "" {
|
||||
claims["sub"] = subject
|
||||
}
|
||||
|
||||
if role != "" {
|
||||
claims["role"] = role
|
||||
}
|
||||
|
||||
if username != "" {
|
||||
claims["preferred_username"] = username
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
rawToken, err := token.SignedString(dummySecret)
|
||||
if err != nil {
|
||||
logger.Error(ctx, "could not sign token", logger.E(errors.WithStack(err)))
|
||||
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
r.Header.Add("Authorization", "Bearer "+rawToken)
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
data = defaultAccounts
|
||||
} else {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
var accounts []authHTTP.LocalAccount
|
||||
|
||||
if err := json.Unmarshal(data, &accounts); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user