feat(server): allow skipping internal auth
Add --dangerously-skip-auth / CODENOMAD_SKIP_AUTH for trusted-perimeter deployments so users behind SSO/VPN don't need a second login.
This commit is contained in:
@@ -15,15 +15,25 @@ export interface AuthManagerInit {
|
||||
username: string
|
||||
password?: string
|
||||
generateToken: boolean
|
||||
dangerouslySkipAuth?: boolean
|
||||
}
|
||||
|
||||
export class AuthManager {
|
||||
private readonly authStore: AuthStore
|
||||
private readonly authStore: AuthStore | null
|
||||
private readonly tokenManager: TokenManager | null
|
||||
private readonly sessionManager = new SessionManager()
|
||||
private readonly cookieName = DEFAULT_AUTH_COOKIE_NAME
|
||||
private readonly authEnabled: boolean
|
||||
|
||||
constructor(private readonly init: AuthManagerInit, private readonly logger: Logger) {
|
||||
this.authEnabled = !Boolean(init.dangerouslySkipAuth)
|
||||
|
||||
if (!this.authEnabled) {
|
||||
this.authStore = null
|
||||
this.tokenManager = null
|
||||
return
|
||||
}
|
||||
|
||||
const authFilePath = resolveAuthFilePath(init.configPath)
|
||||
this.authStore = new AuthStore(authFilePath, logger.child({ component: "auth" }))
|
||||
|
||||
@@ -37,6 +47,10 @@ export class AuthManager {
|
||||
this.tokenManager = init.generateToken ? new TokenManager(60_000) : null
|
||||
}
|
||||
|
||||
isAuthEnabled(): boolean {
|
||||
return this.authEnabled
|
||||
}
|
||||
|
||||
getCookieName(): string {
|
||||
return this.cookieName
|
||||
}
|
||||
@@ -56,19 +70,31 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
validateLogin(username: string, password: string): boolean {
|
||||
return this.authStore.validateCredentials(username, password)
|
||||
if (!this.authEnabled) {
|
||||
return true
|
||||
}
|
||||
return this.requireAuthStore().validateCredentials(username, password)
|
||||
}
|
||||
|
||||
createSession(username: string) {
|
||||
if (!this.authEnabled) {
|
||||
return { id: "auth-disabled", createdAt: Date.now(), username: this.init.username }
|
||||
}
|
||||
return this.sessionManager.createSession(username)
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.authStore.getStatus()
|
||||
if (!this.authEnabled) {
|
||||
return { username: this.init.username, passwordUserProvided: false }
|
||||
}
|
||||
return this.requireAuthStore().getStatus()
|
||||
}
|
||||
|
||||
setPassword(password: string) {
|
||||
return this.authStore.setPassword({ password, markUserProvided: true })
|
||||
if (!this.authEnabled) {
|
||||
throw new Error("Internal authentication is disabled")
|
||||
}
|
||||
return this.requireAuthStore().setPassword({ password, markUserProvided: true })
|
||||
}
|
||||
|
||||
isLoopbackRequest(request: FastifyRequest): boolean {
|
||||
@@ -76,6 +102,12 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
getSessionFromRequest(request: FastifyRequest): { username: string; sessionId: string } | null {
|
||||
if (!this.authEnabled) {
|
||||
// When auth is disabled, treat all requests as authenticated.
|
||||
// We still return a stable username so callers can display it.
|
||||
return { username: this.init.username, sessionId: "auth-disabled" }
|
||||
}
|
||||
|
||||
const cookies = parseCookies(request.headers.cookie)
|
||||
const sessionId = cookies[this.cookieName]
|
||||
const session = this.sessionManager.getSession(sessionId)
|
||||
@@ -90,6 +122,13 @@ export class AuthManager {
|
||||
clearSessionCookie(reply: FastifyReply) {
|
||||
reply.header("Set-Cookie", buildSessionCookie(this.cookieName, "", { maxAgeSeconds: 0 }))
|
||||
}
|
||||
|
||||
private requireAuthStore(): AuthStore {
|
||||
if (!this.authStore) {
|
||||
throw new Error("Auth store is unavailable")
|
||||
}
|
||||
return this.authStore
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAuthFilePath(configPath: string) {
|
||||
|
||||
@@ -44,6 +44,7 @@ interface CliOptions {
|
||||
authUsername: string
|
||||
authPassword?: string
|
||||
generateToken: boolean
|
||||
dangerouslySkipAuth: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_PORT = 9898
|
||||
@@ -84,6 +85,14 @@ function parseCliOptions(argv: string[]): CliOptions {
|
||||
.env("CODENOMAD_GENERATE_TOKEN")
|
||||
.default(false),
|
||||
)
|
||||
.addOption(
|
||||
new Option(
|
||||
"--dangerously-skip-auth",
|
||||
"Disable CodeNomad's internal auth. Use only behind a trusted perimeter (SSO/VPN/etc).",
|
||||
)
|
||||
.env("CODENOMAD_SKIP_AUTH")
|
||||
.default(false),
|
||||
)
|
||||
|
||||
program.parse(argv, { from: "user" })
|
||||
const parsed = program.opts<{
|
||||
@@ -104,8 +113,14 @@ function parseCliOptions(argv: string[]): CliOptions {
|
||||
username: string
|
||||
password?: string
|
||||
generateToken?: boolean
|
||||
dangerouslySkipAuth?: boolean
|
||||
}>()
|
||||
|
||||
const parseBooleanEnv = (value: string | undefined): boolean => {
|
||||
const normalized = (value ?? "").trim().toLowerCase()
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "y" || normalized === "on"
|
||||
}
|
||||
|
||||
const resolvedRoot = parsed.workspaceRoot ?? parsed.root ?? process.cwd()
|
||||
|
||||
const normalizedHost = resolveHost(parsed.host)
|
||||
@@ -130,6 +145,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
||||
authUsername: parsed.username,
|
||||
authPassword: parsed.password,
|
||||
generateToken: Boolean(parsed.generateToken),
|
||||
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +190,12 @@ async function main() {
|
||||
|
||||
logger.info({ options: logOptions }, "Starting CodeNomad CLI server")
|
||||
|
||||
if (options.dangerouslySkipAuth) {
|
||||
logger.warn(
|
||||
"DANGEROUS: internal authentication is disabled (--dangerously-skip-auth / CODENOMAD_SKIP_AUTH).",
|
||||
)
|
||||
}
|
||||
|
||||
const eventBus = new EventBus(eventLogger)
|
||||
|
||||
const isLoopbackHost = (host: string) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.")
|
||||
@@ -195,11 +217,12 @@ async function main() {
|
||||
username: options.authUsername,
|
||||
password: options.authPassword,
|
||||
generateToken: options.generateToken,
|
||||
dangerouslySkipAuth: options.dangerouslySkipAuth,
|
||||
},
|
||||
logger.child({ component: "auth" }),
|
||||
)
|
||||
|
||||
if (options.generateToken) {
|
||||
if (options.generateToken && !options.dangerouslySkipAuth) {
|
||||
const token = authManager.issueBootstrapToken()
|
||||
if (token) {
|
||||
console.log(`${BOOTSTRAP_TOKEN_STDOUT_PREFIX}${token}`)
|
||||
|
||||
Reference in New Issue
Block a user