mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
c0bcc50b28
https://linear.app/unleash/issue/2-1071/prevent-users-from-disabling-password-authentication-when-there-are-no Improves the behavior of disabling password based login by adding some relevant information and a confirmation dialog with a warning. This felt better than trying to disable the toggle, by still allowing the end users to make the decision, except now it should be a properly informed decision with confirmation. ![image](https://github.com/Unleash/unleash/assets/14320932/2ca754d8-cfa2-4fda-984d-0c34b89750f3) - **Password based administrators**: Admin accounts that have a password set; - **Other administrators**: Other admin users that do not have a password. May be SSO, but may also be users that did not set a password yet; - **Admin service accounts**: Service accounts that have the admin root role. Depending on how you're using the SA this may not necessarily mean locking yourself out of an admin account, especially if you secured its token beforehand; - **Admin API tokens**: Similar to the above. If you secured an admin API token beforehand, you still have access to all features through the API; Each one of them link to the respective page inside Unleash (e.g. users page, service accounts page, tokens page...); If you try to disable and press "save", and only in that scenario, you are presented with the following confirmation dialog: ![image](https://github.com/Unleash/unleash/assets/14320932/5ad6d105-ad47-4d31-a1df-04737aed4e00)
201 lines
5.9 KiB
TypeScript
201 lines
5.9 KiB
TypeScript
import { Logger, LogProvider } from '../logger';
|
|
import User from '../types/user';
|
|
|
|
import NotFoundError from '../error/notfound-error';
|
|
import { IUserLookup } from '../types/stores/user-store';
|
|
import { IAdminCount } from '../types/stores/account-store';
|
|
import { IAccountStore } from '../types';
|
|
import { Db } from './db';
|
|
|
|
const TABLE = 'users';
|
|
|
|
const USER_COLUMNS_PUBLIC = [
|
|
'id',
|
|
'name',
|
|
'username',
|
|
'email',
|
|
'image_url',
|
|
'seen_at',
|
|
'is_service',
|
|
];
|
|
|
|
const USER_COLUMNS = [...USER_COLUMNS_PUBLIC, 'login_attempts', 'created_at'];
|
|
|
|
const emptify = (value) => {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
const safeToLower = (s?: string) => (s ? s.toLowerCase() : s);
|
|
|
|
const rowToUser = (row) => {
|
|
if (!row) {
|
|
throw new NotFoundError('No user found');
|
|
}
|
|
return new User({
|
|
id: row.id,
|
|
name: emptify(row.name),
|
|
username: emptify(row.username),
|
|
email: emptify(row.email),
|
|
imageUrl: emptify(row.image_url),
|
|
loginAttempts: row.login_attempts,
|
|
seenAt: row.seen_at,
|
|
createdAt: row.created_at,
|
|
isService: row.is_service,
|
|
});
|
|
};
|
|
|
|
export class AccountStore implements IAccountStore {
|
|
private db: Db;
|
|
|
|
private logger: Logger;
|
|
|
|
constructor(db: Db, getLogger: LogProvider) {
|
|
this.db = db;
|
|
this.logger = getLogger('account-store.ts');
|
|
}
|
|
|
|
buildSelectAccount(q: IUserLookup): any {
|
|
const query = this.activeAccounts();
|
|
if (q.id) {
|
|
return query.where('id', q.id);
|
|
}
|
|
if (q.email) {
|
|
return query.where('email', safeToLower(q.email));
|
|
}
|
|
if (q.username) {
|
|
return query.where('username', q.username);
|
|
}
|
|
throw new Error('Can only find users with id, username or email.');
|
|
}
|
|
|
|
activeAccounts(): any {
|
|
return this.db(TABLE).where({
|
|
deleted_at: null,
|
|
});
|
|
}
|
|
|
|
async hasAccount(idQuery: IUserLookup): Promise<number | undefined> {
|
|
const query = this.buildSelectAccount(idQuery);
|
|
const item = await query.first('id');
|
|
return item ? item.id : undefined;
|
|
}
|
|
|
|
async getAll(): Promise<User[]> {
|
|
const users = await this.activeAccounts().select(USER_COLUMNS);
|
|
return users.map(rowToUser);
|
|
}
|
|
|
|
async search(query: string): Promise<User[]> {
|
|
const users = await this.activeAccounts()
|
|
.select(USER_COLUMNS_PUBLIC)
|
|
.where('name', 'ILIKE', `%${query}%`)
|
|
.orWhere('username', 'ILIKE', `${query}%`)
|
|
.orWhere('email', 'ILIKE', `${query}%`);
|
|
return users.map(rowToUser);
|
|
}
|
|
|
|
async getAllWithId(userIdList: number[]): Promise<User[]> {
|
|
const users = await this.activeAccounts()
|
|
.select(USER_COLUMNS_PUBLIC)
|
|
.whereIn('id', userIdList);
|
|
return users.map(rowToUser);
|
|
}
|
|
|
|
async getByQuery(idQuery: IUserLookup): Promise<User> {
|
|
const row = await this.buildSelectAccount(idQuery).first(USER_COLUMNS);
|
|
return rowToUser(row);
|
|
}
|
|
|
|
async delete(id: number): Promise<void> {
|
|
return this.activeAccounts()
|
|
.where({ id })
|
|
.update({
|
|
deleted_at: new Date(),
|
|
email: null,
|
|
username: null,
|
|
name: this.db.raw('name || ?', '(Deleted)'),
|
|
});
|
|
}
|
|
|
|
async deleteAll(): Promise<void> {
|
|
await this.activeAccounts().del();
|
|
}
|
|
|
|
async count(): Promise<number> {
|
|
return this.activeAccounts()
|
|
.count('*')
|
|
.then((res) => Number(res[0].count));
|
|
}
|
|
|
|
destroy(): void {}
|
|
|
|
async exists(id: number): Promise<boolean> {
|
|
const result = await this.db.raw(
|
|
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ? and deleted_at = null) AS present`,
|
|
[id],
|
|
);
|
|
const { present } = result.rows[0];
|
|
return present;
|
|
}
|
|
|
|
async get(id: number): Promise<User> {
|
|
const row = await this.activeAccounts().where({ id }).first();
|
|
return rowToUser(row);
|
|
}
|
|
|
|
async getAccountByPersonalAccessToken(secret: string): Promise<User> {
|
|
const row = await this.activeAccounts()
|
|
.select(USER_COLUMNS.map((column) => `${TABLE}.${column}`))
|
|
.leftJoin(
|
|
'personal_access_tokens',
|
|
'personal_access_tokens.user_id',
|
|
`${TABLE}.id`,
|
|
)
|
|
.where('secret', secret)
|
|
.andWhere('expires_at', '>', 'now()')
|
|
.first();
|
|
return rowToUser(row);
|
|
}
|
|
|
|
async markSeenAt(secrets: string[]): Promise<void> {
|
|
const now = new Date();
|
|
try {
|
|
await this.db('personal_access_tokens')
|
|
.whereIn('secret', secrets)
|
|
.update({ seen_at: now });
|
|
} catch (err) {
|
|
this.logger.error('Could not update lastSeen, error: ', err);
|
|
}
|
|
}
|
|
|
|
async getAdminCount(): Promise<IAdminCount> {
|
|
const adminCount = await this.activeAccounts()
|
|
.join('role_user as ru', 'users.id', 'ru.user_id')
|
|
.where(
|
|
'ru.role_id',
|
|
'=',
|
|
this.db.raw('(SELECT id FROM roles WHERE name = ?)', ['Admin']),
|
|
)
|
|
.select(
|
|
this.db.raw(
|
|
'COUNT(CASE WHEN users.password_hash IS NOT NULL AND users.is_service = false THEN 1 END)::integer AS password',
|
|
),
|
|
this.db.raw(
|
|
'COUNT(CASE WHEN users.password_hash IS NULL AND users.is_service = false THEN 1 END)::integer AS no_password',
|
|
),
|
|
this.db.raw(
|
|
'COUNT(CASE WHEN users.is_service = true THEN 1 END)::integer AS service',
|
|
),
|
|
);
|
|
|
|
return {
|
|
password: adminCount[0].password,
|
|
noPassword: adminCount[0].no_password,
|
|
service: adminCount[0].service,
|
|
};
|
|
}
|
|
}
|