use std::path::PathBuf;
use super::cargo::{auth_deps, ensure_features_deps, ensure_workspace_deps};
use super::support::{finish, resolve_start, wire_into_app};
use crate::context::{Context, NestrsWorkspace};
use crate::error::{CliError, CliResult};
use crate::naming::Transport;
use crate::scaffold::{Scaffold, ensure_lines};
use crate::templates::auth;
pub struct AuthOptions {
pub path: Option<PathBuf>,
pub dry_run: bool,
}
pub fn run(opts: AuthOptions) -> CliResult<()> {
let ctx = Context::detect(&resolve_start(opts.path))?;
let ws = ctx.workspace.clone().ok_or(CliError::NotNestrsWorkspace)?;
if exists(&ws) {
return Err(CliError::Anyhow(anyhow::anyhow!(
"`{}` already has an auth adapter — edit `authz/ability.rs` to change the policy",
ws.root.display()
)));
}
let mut s = Scaffold::new();
queue(&mut s, &ws, Vec::new());
s.edit(
ws.root.join("Cargo.toml"),
ensure_workspace_deps(auth_deps()),
);
s.edit(ws.features_cargo(), ensure_features_deps(auth_deps()));
s.edit(ws.features_lib(), ensure_lines(lib_decls()));
let wired_app = wire(&ctx, &mut s);
finish(s, opts.dry_run, &ws.root, "the auth adapter")?;
print_next_steps(wired_app.is_some());
Ok(())
}
pub(super) fn lib_decls() -> Vec<String> {
[
"pub mod authn;",
"pub mod authz;",
"pub mod identity;",
"pub use identity::{Claims, Role};",
]
.map(str::to_owned)
.to_vec()
}
pub(super) fn queue(s: &mut Scaffold, ws: &NestrsWorkspace, authz_decls: Vec<String>) {
let src = ws.features_root();
s.create(src.join("identity/mod.rs"), auth::IDENTITY_MOD.to_string());
s.create(
src.join("identity/claims.rs"),
auth::IDENTITY_CLAIMS.to_string(),
);
s.create(src.join("authn/mod.rs"), auth::AUTHN_MOD.to_string());
s.create(src.join("authn/module.rs"), auth::AUTHN_MODULE.to_string());
s.create(
src.join("authn/strategy.rs"),
auth::AUTHN_STRATEGY.to_string(),
);
let authz_mod =
ensure_lines(authz_decls)(auth::AUTHZ_MOD).unwrap_or_else(|| auth::AUTHZ_MOD.to_string());
s.create(src.join("authz/mod.rs"), authz_mod);
s.create(
src.join("authz/ability.rs"),
auth::AUTHZ_ABILITY.to_string(),
);
s.create(src.join("authz/module.rs"), auth::AUTHZ_MODULE.to_string());
HTTP_BRIDGE.queue(s, ws);
let env = ws.root.join(".env");
if env.is_file() {
s.edit(env, append_authn_secret());
} else {
s.create(env, auth::ENV_AUTHN.trim_start().to_string());
}
}
pub(super) const APP_IMPORTS: [(&str, &str); 2] = [
("features::authn::AuthnModule", "AuthnModule"),
("features::authz::AuthzHttpModule", "AuthzHttpModule"),
];
fn wire(ctx: &Context, s: &mut Scaffold) -> Option<PathBuf> {
wire_into_app(ctx, s, &APP_IMPORTS, None)
}
pub(super) fn exists(ws: &NestrsWorkspace) -> bool {
ws.features_root().join("authz").is_dir()
}
pub(super) struct AuthzBridge {
pub dir: &'static str,
pub module: &'static str,
pub feature_path: &'static str,
pub app_path: &'static str,
pub files: &'static [(&'static str, &'static str)],
pub rationale: &'static [&'static str],
pub deps: &'static [&'static super::cargo::Dep],
pub written_by_g_auth: bool,
}
impl AuthzBridge {
pub(super) fn exists(&self, ws: &NestrsWorkspace) -> bool {
ws.features_root().join("authz").join(self.dir).is_dir()
}
pub(super) fn decls(&self) -> Vec<String> {
vec![
format!("pub mod {};", self.dir),
format!("pub use {}::{};", self.dir, self.module),
]
}
pub(super) fn queue(&self, s: &mut Scaffold, ws: &NestrsWorkspace) {
let dir = ws.features_root().join("authz").join(self.dir);
for (name, body) in self.files {
s.create(dir.join(name), (*body).to_string());
}
}
}
pub(super) fn bridge_for(transport: Transport) -> Option<&'static AuthzBridge> {
match transport {
Transport::Http => Some(&HTTP_BRIDGE),
Transport::Graphql => Some(&GRAPHQL_BRIDGE),
Transport::Ws => Some(&WS_BRIDGE),
Transport::Mcp => Some(&MCP_BRIDGE),
Transport::Queue | Transport::Schedule => None,
}
}
static HTTP_BRIDGE: AuthzBridge = AuthzBridge {
dir: "http",
module: "AuthzHttpModule",
feature_path: "crate::authz::AuthzHttpModule",
app_path: "features::authz::AuthzHttpModule",
files: &[
("mod.rs", auth::AUTHZ_HTTP_MOD),
("guard.rs", auth::AUTHZ_HTTP_GUARD),
("module.rs", auth::AUTHZ_HTTP_MODULE),
],
rationale: &[
"The guard runs on the HTTP request and attaches the caller's Ability, which",
"every other transport's bridge re-runs. Controllers serving rows bind",
"#[use_guards(AuthnGuard, AuthzGuard)] and import AuthzHttpModule.",
],
deps: &[&super::cargo::AUTHZ],
written_by_g_auth: true,
};
static GRAPHQL_BRIDGE: AuthzBridge = AuthzBridge {
dir: "graphql",
module: "AuthzGraphqlModule",
feature_path: "crate::authz::AuthzGraphqlModule",
app_path: "features::authz::AuthzGraphqlModule",
files: &[
("mod.rs", auth::AUTHZ_GRAPHQL_MOD),
("bridge.rs", auth::AUTHZ_GRAPHQL_BRIDGE),
("guard.rs", auth::AUTHZ_GRAPHQL_GUARD),
("module.rs", auth::AUTHZ_GRAPHQL_MODULE),
],
rationale: &[
"/graphql has no guard at the HTTP edge — authn and the ability run in band,",
"per operation, through AuthzGraphqlModule. Every resolver serving rows",
"imports it and declares #[authorize(Action, Entity)] or #[public].",
],
deps: &[
&super::cargo::AUTHZ,
&super::cargo::SEAORM,
&super::cargo::GRAPHQL,
],
written_by_g_auth: false,
};
static WS_BRIDGE: AuthzBridge = AuthzBridge {
dir: "ws",
module: "AuthzWsModule",
feature_path: "crate::authz::AuthzWsModule",
app_path: "features::authz::AuthzWsModule",
files: &[
("mod.rs", auth::AUTHZ_WS_MOD),
("module.rs", auth::AUTHZ_WS_MODULE),
],
rationale: &[
"A gateway reuses the HTTP guards — bind #[use_guards(AuthnGuard, AuthzGuard)]",
"on the struct and import AuthzWsModule in the adapter's module.rs. It carries",
"the dyn SocketContext that scopes the connection's rows to the caller.",
],
deps: &[
&super::cargo::AUTHZ,
&super::cargo::SEAORM,
&super::cargo::WS,
],
written_by_g_auth: false,
};
static MCP_BRIDGE: AuthzBridge = AuthzBridge {
dir: "mcp",
module: "AuthzMcpModule",
feature_path: "crate::authz::AuthzMcpModule",
app_path: "features::authz::AuthzMcpModule",
files: &[
("mod.rs", auth::AUTHZ_MCP_MOD),
("bridge.rs", auth::AUTHZ_MCP_BRIDGE),
("module.rs", auth::AUTHZ_MCP_MODULE),
],
rationale: &[
"/mcp denies every request until an McpOperationGuard is bound. AuthzMcpModule",
"binds one: callers are authenticated and the ambient Ability is installed, so",
"a tool can return entity rows through nest_rs::authz::masked_output_ambient.",
],
deps: &[
&super::cargo::AUTHZ,
&super::cargo::SEAORM,
&super::cargo::MCP,
],
written_by_g_auth: false,
};
fn append_authn_secret() -> crate::scaffold::Transform {
Box::new(|content: &str| {
if content.contains("NESTRS_AUTHN__") {
return None;
}
Some(format!("{content}{}", auth::ENV_AUTHN))
})
}
fn print_next_steps(wired: bool) {
println!();
println!("Next steps:");
println!(" 1. Add your rules in `crates/features/src/authz/ability.rs` — nothing is");
println!(" granted until you do, so guarded routes answer 403.");
if wired {
println!(" 2. AuthnModule + AuthzHttpModule are wired into the current app.");
} else {
println!(" 2. Import `features::authn::AuthnModule` and");
println!(" `features::authz::AuthzHttpModule` in your app's `module.rs`.");
}
println!(" 3. `.env` carries a development HS256 secret — replace it through the");
println!(" real environment before deploying (NESTRS_AUTHN__SECRET).");
}