/* * Copyright (C) 2024-present Puter Technologies Inc. * * This file is part of Puter. * * Puter is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import bcrypt from 'bcrypt'; import crypto from 'node:crypto'; import { v4 as uuidv4 } from 'uuid'; import { PuterService } from '../types.js'; import type { UserRow } from '../../stores/user/UserStore.js'; import { provisionUser } from '../../util/userProvisioning.js'; import { LOCAL_UNLIMITED_USER } from '../../data/subPolicies/localUnlimitedUserPolicy.js'; import { UNLIMITED_SUBSCRIPTION } from '../metering/consts.js'; const USERNAME = 'admin'; export const ADMIN_GROUP_UID = 'ca342a5e-b13d-4dee-9048-58b11a57cc55'; const ADMIN_STORAGE_BYTES = 10 * 1024 * 1024 * 1024; /** * Bootstraps the `admin` user on first boot for self-hosted deployments. * * If no admin exists, creates one with a random 8-char hex password, places * them in the admin group, and stashes the plaintext under * `metadata.tmp_password` so we can detect on later boots whether the operator * has rotated it yet. * * Each boot where the current password hash still matches the stashed * plaintext, the credentials are re-printed to stdout (CI scrapes this line to * extract the default password). Once rotation is detected the stash is deleted * so the plaintext isn't retained longer than needed. */ export class DefaultUserService extends PuterService { override async onServerStart(): Promise { // Dev convenience: grant the bootstrap `admin` user unlimited metering. // Gated to env === 'dev' so prod deployments never get a free-usage // actor. Registering the policy makes its `'unlimited'` id resolvable // (extraPolicies is always in the available set); the resolver returns // null for everyone else, so all other users keep their normal tier. if (this.config.env === 'dev') { this.services.metering.registerPolicy(LOCAL_UNLIMITED_USER); this.services.metering.registerSubscriptionResolver((actor) => actor.user?.username === USERNAME ? UNLIMITED_SUBSCRIPTION : null, ); } if (this.config.no_default_user) return; let user = await this.stores.user.getByUsername(USERNAME); let tmpPassword: string; if (!user) { // 16 bytes (128 bits) — the old 4 bytes was only 32 bits, // brute-forceable if the box is reachable before the admin // rotates the printed bootstrap password. tmpPassword = crypto.randomBytes(16).toString('hex'); user = await this.#createAdminUser(tmpPassword); } else { const metadata = (user.metadata ?? {}) as Record; const stashed = metadata.tmp_password; if (typeof stashed !== 'string' || stashed === '') return; tmpPassword = stashed; } if (!user.password) return; const isDefault = await bcrypt.compare( tmpPassword, String(user.password), ); if (!isDefault) { // Password was rotated — stop retaining the plaintext stash. await this.stores.user.updateMetadata(user.id, { tmp_password: null, }); return; } this.#printCredentials(tmpPassword); } async #createAdminUser(tmpPassword: string): Promise { const passwordHash = await bcrypt.hash(tmpPassword, 8); const created = await provisionUser( { db: this.clients.db, userStore: this.stores.user, groupStore: this.stores.group, }, { username: USERNAME, uuid: uuidv4(), password: passwordHash, email: null, free_storage: ADMIN_STORAGE_BYTES, requires_email_confirmation: false, }, ADMIN_GROUP_UID, ); await this.stores.user.updateMetadata(created.id, { tmp_password: tmpPassword, }); return (await this.stores.user.getById(created.id)) ?? created; } #printCredentials(tmpPassword: string): void { console.log(`password for admin is: ${tmpPassword}`); console.log( '\n************************************************************', ); console.log('* Your default login credentials are:'); console.log('* Username: admin'); console.log(`* Password: ${tmpPassword}`); console.log('* (change the password to remove this message)'); console.log( '************************************************************\n', ); } }