// CLI command implementations.
///|
fn usage() -> String {
(
#|MoonFGA - OpenFGA-compatible authorization engine
#|
#|Usage:
#| moonfga validate [--json]
#| moonfga transform --to json|dsl [--out ]
#| moonfga check --model --object --relation --user
#| [--store ] [--context ] [--contextual ]
#| [--explain] [--json]
#| moonfga list-objects --model --type --relation --user
#| [--store ] [--context ] [--contextual ]
#| [--json]
#| moonfga list-users --model --object --relation
#| [--filters user,group#member] [--store ]
#| [--context ] [--contextual ] [--json]
#| moonfga expand --model --object --relation
#| [--contextual ]
#| moonfga test
#|
#|Tuple files accept JSON (`[{"object":...}]`), YAML (`tuples:`),
#|or one `object#relation@user` per line.
)
}
///|
fn positional(
args : CliArgs,
index : Int32,
what : String,
) -> String raise @fga.FgaError {
if index >= args.positionals.length() {
raise @fga.fail(@fga.ERR_VALIDATION, "usage: missing " + what)
}
args.positionals[index]
}
///|
fn issues_json(issues : Array[@validate.ValidationIssue]) -> Json {
let items : Array[Json] = []
for issue in issues {
let fields : Map[String, Json] = Map([])
fields["code"] = Json::number(issue.code.to_double())
fields["message"] = Json::string(issue.message)
fields["line"] = Json::number(issue.line.to_double())
fields["column"] = Json::number(issue.column.to_double())
fields["symbol"] = Json::string(issue.symbol)
items.push(Json::object(fields))
}
Json::array(items)
}
///|
fn command_validate(args : CliArgs) -> Int32 raise @fga.FgaError {
let path = positional(args, 1, "model file")
let model = load_model_file(path)
let issues = @validate.validate_model(model)
if has_flag(args, "--json") {
println(issues_json(issues).stringify(indent=2))
} else if issues.length() == 0 {
println("model is valid")
} else {
for issue in issues {
println(@validate.format_issue(issue))
}
}
if issues.length() == 0 {
0
} else {
1
}
}
///|
fn command_transform(args : CliArgs) -> Int32 raise @fga.FgaError {
let path = positional(args, 1, "model file")
let target = require_value(args, "--to")
let model = load_model_file(path)
let output = match target {
"json" => @model.model_to_json_string(model)
"dsl" => @dsl.to_dsl(model)
_ => raise @fga.fail(@fga.ERR_VALIDATION, "usage: --to must be json or dsl")
}
match flag_value(args, "--out") {
Some(out) => {
write_text(out, output + "\n")
println("wrote " + out)
}
None => println(output)
}
0
}
///|
fn command_check(args : CliArgs) -> Int32 raise @fga.FgaError {
let model = load_model_file(require_value(args, "--model"))
let object = require_value(args, "--object")
let relation = require_value(args, "--relation")
let user = require_value(args, "--user")
let context = load_context(args)
let contextual = load_optional_tuples(args, "--contextual")
let store = @store.MemoryStore::new()
let _ = store.write_all(load_optional_tuples(args, "--store"))
let engine = @engine.Engine::new(model, store as &@fga.TupleReader)
if has_flag(args, "--explain") {
let result = engine.explain_check(
object, relation, user, context, contextual,
)
if has_flag(args, "--json") {
println(result.to_json().stringify(indent=2))
} else {
println("allowed: " + result.allowed.to_string())
for path in result.paths {
println(" " + path)
}
match result.explain_error {
Some(message) => println("explanation incomplete: " + message)
None => ()
}
}
} else {
let allowed = engine.check_with(object, relation, user, context, contextual)
if has_flag(args, "--json") {
let fields : Map[String, Json] = Map([])
fields["allowed"] = Json::boolean(allowed)
println(Json::object(fields).stringify(indent=2))
} else {
println(allowed.to_string())
}
}
0
}
///|
fn print_strings(values : Array[String], as_json : Bool) -> Unit {
if as_json {
let items : Array[Json] = []
for value in values {
items.push(Json::string(value))
}
println(Json::array(items).stringify(indent=2))
} else {
for value in values {
println(value)
}
}
}
///|
fn command_list_objects(args : CliArgs) -> Int32 raise @fga.FgaError {
let model = load_model_file(require_value(args, "--model"))
let type_name = require_value(args, "--type")
let relation = require_value(args, "--relation")
let user = require_value(args, "--user")
let context = load_context(args)
let contextual = load_optional_tuples(args, "--contextual")
let store = @store.MemoryStore::new()
let _ = store.write_all(load_optional_tuples(args, "--store"))
let engine = @engine.Engine::new(model, store as &@fga.TupleReader)
let objects = engine.list_objects(
type_name, relation, user, context, contextual,
)
print_strings(objects, has_flag(args, "--json"))
0
}
///|
fn command_list_users(args : CliArgs) -> Int32 raise @fga.FgaError {
let model = load_model_file(require_value(args, "--model"))
let object = require_value(args, "--object")
let relation = require_value(args, "--relation")
let filters = parse_filters(args, model)
let context = load_context(args)
let contextual = load_optional_tuples(args, "--contextual")
let store = @store.MemoryStore::new()
let _ = store.write_all(load_optional_tuples(args, "--store"))
let engine = @engine.Engine::new(model, store as &@fga.TupleReader)
let users = engine.list_users(object, relation, filters, context, contextual)
print_strings(users, has_flag(args, "--json"))
0
}
///|
fn command_expand(args : CliArgs) -> Int32 raise @fga.FgaError {
let model = load_model_file(require_value(args, "--model"))
let object = require_value(args, "--object")
let relation = require_value(args, "--relation")
let contextual = load_optional_tuples(args, "--contextual")
let store = @store.MemoryStore::new()
let _ = store.write_all(load_optional_tuples(args, "--store"))
let engine = @engine.Engine::new(model, store as &@fga.TupleReader)
let tree = engine.expand_json(object, relation, contextual)
println(tree.stringify(indent=2))
0
}
///|
fn command_test(args : CliArgs) -> Int32 raise @fga.FgaError {
let path = positional(args, 1, "store test file")
let suite = @storetest.load_store_test(
read_text(path),
dirname(path),
read_file=read_text,
)
let report = @storetest.run_store_test(suite)
println(@storetest.format_store_test_report(report))
if report.failures.length() == 0 {
0
} else {
1
}
}