0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

feat(core): set user default roles from env (#1793)

This commit is contained in:
Gao Sun 2022-08-19 16:53:19 +08:00 committed by GitHub
parent 84c0d8f845
commit 4afdf3cb4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 88 additions and 76 deletions

View file

@ -23,7 +23,6 @@ const loadEnvValues = async () => {
const port = Number(getEnv('PORT', '3001'));
const localhostUrl = `${isHttpsEnabled ? 'https' : 'http'}://localhost:${port}`;
const endpoint = getEnv('ENDPOINT', localhostUrl);
const additionalConnectorPackages = getEnvAsStringArray('ADDITIONAL_CONNECTOR_PACKAGES', []);
return Object.freeze({
isTest,
@ -35,7 +34,8 @@ const loadEnvValues = async () => {
port,
localhostUrl,
endpoint,
additionalConnectorPackages,
additionalConnectorPackages: getEnvAsStringArray('ADDITIONAL_CONNECTOR_PACKAGES'),
userDefaultRoleNames: getEnvAsStringArray('USER_DEFAULT_ROLE_NAMES'),
developmentUserId: getEnv('DEVELOPMENT_USER_ID'),
trustProxyHeader: isTrue(getEnv('TRUST_PROXY_HEADER')),
oidc: await loadOidcValues(appendPath(endpoint, '/oidc').toString()),

View file

@ -1,7 +1,10 @@
import { User, UsersPasswordEncryptionMethod } from '@logto/schemas';
import { User, CreateUser, Users, UsersPasswordEncryptionMethod } from '@logto/schemas';
import { argon2Verify } from 'hash-wasm';
import pRetry from 'p-retry';
import { buildInsertInto } from '@/database/insert-into';
import envSet from '@/env-set';
import { findRolesByRoleNames, insertRoles } from '@/queries/roles';
import { findUserByUsername, hasUserWithId, updateUserById } from '@/queries/user';
import assertThat from '@/utils/assert-that';
import { buildIdGenerator } from '@/utils/id';
@ -58,3 +61,30 @@ export const findUserByUsernameAndPassword = async (
export const updateLastSignInAt = async (userId: string) =>
updateUserById(userId, { lastSignInAt: Date.now() });
const insertUserQuery = buildInsertInto<CreateUser, User>(Users, {
returning: true,
});
// Temp solution since Hasura requires a role to proceed authn.
// The source of default roles should be guarded and moved to database once we implement RBAC.
export const insertUser: typeof insertUserQuery = async ({ roleNames, ...rest }) => {
const computedRoleNames = [
...new Set((roleNames ?? []).concat(envSet.values.userDefaultRoleNames)),
];
if (computedRoleNames.length > 0) {
const existingRoles = await findRolesByRoleNames(computedRoleNames);
const missingRoleNames = computedRoleNames.filter(
(roleName) => !existingRoles.some(({ name }) => roleName === name)
);
if (missingRoleNames.length > 0) {
await insertRoles(
missingRoleNames.map((name) => ({ name, description: 'User default role.' }))
);
}
}
return insertUserQuery({ roleNames: computedRoleNames, ...rest });
};

View file

@ -18,3 +18,12 @@ export const findRolesByRoleNames = async (roleNames: string[]) =>
from ${table}
where ${fields.name} in (${sql.join(roleNames, sql`, `)})
`);
export const insertRoles = async (roles: Role[]) =>
envSet.pool.query(sql`
insert into ${table} (${fields.name}, ${fields.description}) values
${sql.join(
roles.map(({ name, description }) => sql`(${name}, ${description})`),
sql`, `
)}
`);

View file

@ -18,7 +18,6 @@ import {
hasUserWithEmail,
hasUserWithIdentity,
hasUserWithPhone,
insertUser,
countUsers,
findUsers,
updateUserById,
@ -235,33 +234,6 @@ describe('user query', () => {
await expect(hasUserWithIdentity(target, mockUser.id)).resolves.toEqual(true);
});
it('insertUser', async () => {
const expectSql = sql`
insert into ${table} (${sql.join(Object.values(fields), sql`, `)})
values (${sql.join(
Object.values(fields)
.slice(0, -1)
.map((_, index) => `$${index + 1}`),
sql`, `
)}, to_timestamp(${Object.values(fields).length}::double precision / 1000))
returning *
`;
mockQuery.mockImplementationOnce(async (sql, values) => {
expectSqlAssert(sql, expectSql.sql);
expect(values).toEqual(
Users.fieldKeys.map((k) =>
k === 'lastSignInAt' ? mockUser[k] : convertToPrimitiveOrSql(k, mockUser[k])
)
);
return createMockQueryResult([dbvalue]);
});
await expect(insertUser(mockUser)).resolves.toEqual(dbvalue);
});
it('countUsers', async () => {
const search = 'foo';
const expectSql = sql`

View file

@ -83,10 +83,6 @@ export const hasUserWithIdentity = async (target: string, userId: string) =>
`
);
export const insertUser = buildInsertInto<CreateUser, User>(Users, {
returning: true,
});
const buildUserSearchConditionSql = (search: string) => {
const searchFields = [fields.primaryEmail, fields.primaryPhone, fields.username, fields.name];
const conditions = searchFields.map((filedName) => sql`${filedName} like ${'%' + search + '%'}`);

View file

@ -39,12 +39,6 @@ jest.mock('@/queries/user', () => ({
})
),
deleteUserById: jest.fn(),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...user,
})
),
deleteUserIdentity: jest.fn(),
}));
@ -54,6 +48,12 @@ jest.mock('@/lib/user', () => ({
passwordEncrypted: 'password',
passwordEncryptionMethod: 'Argon2i',
})),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...user,
})
),
}));
jest.mock('@/queries/roles', () => ({

View file

@ -5,7 +5,7 @@ import pick from 'lodash.pick';
import { literal, object, string } from 'zod';
import RequestError from '@/errors/RequestError';
import { encryptUserPassword, generateUserId } from '@/lib/user';
import { encryptUserPassword, generateUserId, insertUser } from '@/lib/user';
import koaGuard from '@/middleware/koa-guard';
import koaPagination from '@/middleware/koa-pagination';
import { findRolesByRoleNames } from '@/queries/roles';
@ -16,7 +16,6 @@ import {
countUsers,
findUserById,
hasUser,
insertUser,
updateUserById,
} from '@/queries/user';
import assertThat from '@/utils/assert-that';

View file

@ -7,23 +7,26 @@ import { createRequester } from '@/utils/test-utils';
import sessionPasswordlessRoutes from './passwordless';
jest.mock('@/lib/user', () => ({
generateUserId: () => 'user1',
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
}));
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const findUserById = jest.fn(async (): Promise<User> => mockUser);
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
jest.mock('@/lib/user', () => ({
generateUserId: () => 'user1',
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
insertUser: async (...args: unknown[]) => insertUser(...args),
}));
jest.mock('@/queries/user', () => ({
findUserById: async () => findUserById(),
findUserByPhone: async () => ({ id: 'id' }),
findUserByEmail: async () => ({ id: 'id' }),
insertUser: async (...args: unknown[]) => insertUser(...args),
updateUserById: async (...args: unknown[]) => updateUserById(...args),
hasUser: async (username: string) => username === 'username1',
hasUserWithPhone: async (phone: string) => phone === '13000000000',
hasUserWithEmail: async (email: string) => email === 'a@a.com',
}));
const sendPasscode = jest.fn(async () => ({ connector: { id: 'connectorIdValue' } }));
jest.mock('@/lib/passcode', () => ({
createPasscode: async () => ({ id: 'id' }),

View file

@ -6,12 +6,11 @@ import { object, string } from 'zod';
import RequestError from '@/errors/RequestError';
import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode';
import { assignInteractionResults } from '@/lib/session';
import { generateUserId, updateLastSignInAt } from '@/lib/user';
import { generateUserId, insertUser, updateLastSignInAt } from '@/lib/user';
import koaGuard from '@/middleware/koa-guard';
import {
hasUserWithEmail,
hasUserWithPhone,
insertUser,
findUserByEmail,
findUserByPhone,
} from '@/queries/user';

View file

@ -8,6 +8,25 @@ import { createRequester } from '@/utils/test-utils';
import sessionRoutes from './session';
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const findUserById = jest.fn(async (): Promise<User> => mockUser);
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const hasActiveUsers = jest.fn(async () => true);
jest.mock('@/queries/user', () => ({
findUserById: async () => findUserById(),
findUserByIdentity: async () => ({ id: 'id', identities: {} }),
findUserByPhone: async () => ({ id: 'id' }),
findUserByEmail: async () => ({ id: 'id' }),
updateUserById: async (...args: unknown[]) => updateUserById(...args),
hasUser: async (username: string) => username === 'username1',
hasUserWithIdentity: async (connectorId: string, userId: string) =>
connectorId === 'connectorId' && userId === 'id',
hasUserWithPhone: async (phone: string) => phone === '13000000000',
hasUserWithEmail: async (email: string) => email === 'a@a.com',
hasActiveUsers: async () => hasActiveUsers(),
}));
jest.mock('@/lib/user', () => ({
async findUserByUsernameAndPassword(username: string, password: string) {
if (username !== 'username' && username !== 'admin') {
@ -28,25 +47,7 @@ jest.mock('@/lib/user', () => ({
passwordEncryptionMethod: 'Argon2i',
}),
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
}));
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const findUserById = jest.fn(async (): Promise<User> => mockUser);
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const hasActiveUsers = jest.fn(async () => true);
jest.mock('@/queries/user', () => ({
findUserById: async () => findUserById(),
findUserByIdentity: async () => ({ id: 'id', identities: {} }),
findUserByPhone: async () => ({ id: 'id' }),
findUserByEmail: async () => ({ id: 'id' }),
insertUser: async (...args: unknown[]) => insertUser(...args),
updateUserById: async (...args: unknown[]) => updateUserById(...args),
hasUser: async (username: string) => username === 'username1',
hasUserWithIdentity: async (connectorId: string, userId: string) =>
connectorId === 'connectorId' && userId === 'id',
hasUserWithPhone: async (phone: string) => phone === '13000000000',
hasUserWithEmail: async (email: string) => email === 'a@a.com',
hasActiveUsers: async () => hasActiveUsers(),
}));
const grantSave = jest.fn(async () => 'finalGrantId');

View file

@ -15,9 +15,10 @@ import {
generateUserId,
findUserByUsernameAndPassword,
updateLastSignInAt,
insertUser,
} from '@/lib/user';
import koaGuard from '@/middleware/koa-guard';
import { hasUser, insertUser, hasActiveUsers } from '@/queries/user';
import { hasUser, hasActiveUsers } from '@/queries/user';
import assertThat from '@/utils/assert-that';
import { AnonymousRouter } from '../types';

View file

@ -9,10 +9,6 @@ import { createRequester } from '@/utils/test-utils';
import sessionSocialRoutes from './social';
jest.mock('@/lib/user', () => ({
generateUserId: () => 'user1',
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
}));
jest.mock('@/lib/social', () => ({
...jest.requireActual('@/lib/social'),
async findSocialRelatedUser() {
@ -39,14 +35,21 @@ jest.mock('@/lib/social', () => ({
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const findUserById = jest.fn(async (): Promise<User> => mockUser);
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
jest.mock('@/queries/user', () => ({
findUserById: async () => findUserById(),
findUserByIdentity: async () => ({ id: 'id', identities: {} }),
insertUser: async (...args: unknown[]) => insertUser(...args),
updateUserById: async (...args: unknown[]) => updateUserById(...args),
hasUserWithIdentity: async (target: string, userId: string) =>
target === 'connectorTarget' && userId === 'id',
}));
jest.mock('@/lib/user', () => ({
generateUserId: () => 'user1',
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
insertUser: async (...args: unknown[]) => insertUser(...args),
}));
const getConnectorInstanceByIdHelper = jest.fn(async (connectorId: string) => {
const connector = {
enabled: connectorId === 'social_enabled',

View file

@ -12,11 +12,10 @@ import {
getUserInfoByAuthCode,
getUserInfoFromInteractionResult,
} from '@/lib/social';
import { generateUserId, updateLastSignInAt } from '@/lib/user';
import { generateUserId, insertUser, updateLastSignInAt } from '@/lib/user';
import koaGuard from '@/middleware/koa-guard';
import {
hasUserWithIdentity,
insertUser,
findUserById,
updateUserById,
findUserByIdentity,