Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/env-set/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()),
Expand Down
32 changes: 31 additions & 1 deletion packages/core/src/lib/user.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 });
};
9 changes: 9 additions & 0 deletions packages/core/src/queries/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`, `
)}
`);
28 changes: 0 additions & 28 deletions packages/core/src/queries/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
hasUserWithEmail,
hasUserWithIdentity,
hasUserWithPhone,
insertUser,
countUsers,
findUsers,
updateUserById,
Expand Down Expand Up @@ -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`
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/queries/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + '%'}`);
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/routes/admin-user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ jest.mock('@/queries/user', () => ({
})
),
deleteUserById: jest.fn(),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...user,
})
),
deleteUserIdentity: jest.fn(),
}));

Expand All @@ -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', () => ({
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/routes/admin-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,7 +16,6 @@ import {
countUsers,
findUserById,
hasUser,
insertUser,
updateUserById,
} from '@/queries/user';
import assertThat from '@/utils/assert-that';
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/routes/session/passwordless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@ import { createRequester } from '@/utils/test-utils';

import sessionPasswordlessRoutes from './passwordless';

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),
}));
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(),
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 === '[email protected]',
}));

const sendPasscode = jest.fn(async () => ({ connector: { id: 'connectorIdValue' } }));
jest.mock('@/lib/passcode', () => ({
createPasscode: async () => ({ id: 'id' }),
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/routes/session/passwordless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
37 changes: 19 additions & 18 deletions packages/core/src/routes/session/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 === '[email protected]',
hasActiveUsers: async () => hasActiveUsers(),
}));

jest.mock('@/lib/user', () => ({
async findUserByUsernameAndPassword(username: string, password: string) {
if (username !== 'username' && username !== 'admin') {
Expand All @@ -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 === '[email protected]',
hasActiveUsers: async () => hasActiveUsers(),
}));

const grantSave = jest.fn(async () => 'finalGrantId');
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/routes/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
13 changes: 8 additions & 5 deletions packages/core/src/routes/session/social.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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',
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/routes/session/social.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down