Merge branch 'master' into fix/role-permission-update

This commit is contained in:
Michael Drury 2024-09-12 13:30:05 +01:00 committed by GitHub
commit cdcefc397c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 111 additions and 52 deletions

View File

@ -117,9 +117,9 @@ jobs:
- name: Test - name: Test
run: | run: |
if ${{ env.ONLY_AFFECTED_TASKS }}; then if ${{ env.ONLY_AFFECTED_TASKS }}; then
yarn test --ignore=@budibase/worker --ignore=@budibase/server --since=${{ env.NX_BASE_BRANCH }} yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore @budibase/account-portal-server --since=${{ env.NX_BASE_BRANCH }}
else else
yarn test --ignore=@budibase/worker --ignore=@budibase/server yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore @budibase/account-portal-server
fi fi
test-worker: test-worker:

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2.32.0", "version": "2.32.1",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -63,14 +63,25 @@ async function populateUsersFromDB(
* If not present fallback to loading the user directly and re-caching. * If not present fallback to loading the user directly and re-caching.
* @param userId the id of the user to get * @param userId the id of the user to get
* @param tenantId the tenant of the user to get * @param tenantId the tenant of the user to get
* @param email the email of the user to populate from account if needed
* @param populateUser function to provide the user for re-caching. default to couch db * @param populateUser function to provide the user for re-caching. default to couch db
* @returns * @returns
*/ */
export async function getUser( export async function getUser({
userId: string, userId,
tenantId?: string, tenantId,
populateUser?: (userId: string, tenantId: string) => Promise<User> email,
) { populateUser,
}: {
userId: string
email?: string
tenantId?: string
populateUser?: (
userId: string,
tenantId: string,
email?: string
) => Promise<User>
}) {
if (!populateUser) { if (!populateUser) {
populateUser = populateFromDB populateUser = populateFromDB
} }
@ -85,7 +96,7 @@ export async function getUser(
// try cache // try cache
let user: User = await client.get(userId) let user: User = await client.get(userId)
if (!user) { if (!user) {
user = await populateUser(userId, tenantId) user = await populateUser(userId, tenantId, email)
await client.store(userId, user, EXPIRY_SECONDS) await client.store(userId, user, EXPIRY_SECONDS)
} }
if (user && !user.tenantId && tenantId) { if (user && !user.tenantId && tenantId) {

View File

@ -43,7 +43,11 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) {
async function checkApiKey( async function checkApiKey(
apiKey: string, apiKey: string,
populateUser?: (userId: string, tenantId: string) => Promise<User> populateUser?: (
userId: string,
tenantId: string,
email?: string
) => Promise<User>
) { ) {
// check both the primary and the fallback internal api keys // check both the primary and the fallback internal api keys
// this allows for rotation // this allows for rotation
@ -70,7 +74,11 @@ async function checkApiKey(
if (userId) { if (userId) {
return { return {
valid: true, valid: true,
user: await getUser(userId, tenantId, populateUser), user: await getUser({
userId,
tenantId,
populateUser,
}),
} }
} else { } else {
throw new InvalidAPIKeyError() throw new InvalidAPIKeyError()
@ -123,13 +131,18 @@ export default function (
// getting session handles error checking (if session exists etc) // getting session handles error checking (if session exists etc)
session = await getSession(userId, sessionId) session = await getSession(userId, sessionId)
if (opts && opts.populateUser) { if (opts && opts.populateUser) {
user = await getUser( user = await getUser({
userId, userId,
session.tenantId, tenantId: session.tenantId,
opts.populateUser(ctx) email: session.email,
) populateUser: opts.populateUser(ctx),
})
} else { } else {
user = await getUser(userId, session.tenantId) user = await getUser({
userId,
tenantId: session.tenantId,
email: session.email,
})
} }
// @ts-ignore // @ts-ignore
user.csrfToken = session.csrfToken user.csrfToken = session.csrfToken
@ -148,7 +161,11 @@ export default function (
} }
// this is an internal request, no user made it // this is an internal request, no user made it
if (!authenticated && apiKey) { if (!authenticated && apiKey) {
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null const populateUser: (
userId: string,
tenantId: string,
email?: string
) => Promise<User> = opts.populateUser ? opts.populateUser(ctx) : null
const { valid, user: foundUser } = await checkApiKey( const { valid, user: foundUser } = await checkApiKey(
apiKey, apiKey,
populateUser populateUser

View File

@ -1,4 +1,4 @@
FROM node:20-slim FROM node:20-alpine
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh" LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh" LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
@ -15,37 +15,35 @@ ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
ENV TOP_LEVEL_PATH=/ ENV TOP_LEVEL_PATH=/
# handle node-gyp # handle node-gyp and install postgres client for pg_dump utils
RUN apt-get update \ RUN apk add --no-cache \
&& apt-get install -y --no-install-recommends g++ make python3 jq g++ \
RUN yarn global add pm2 make \
python3 \
jq \
bash \
postgresql-client \
git
# Install postgres client for pg_dump utils RUN yarn global add pm2
RUN apt update && apt upgrade -y \
&& apt install software-properties-common apt-transport-https curl gpg -y \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
&& apt install postgresql-client-15 -y \
&& apt remove software-properties-common apt-transport-https curl gpg -y
WORKDIR / WORKDIR /
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /app WORKDIR /app
COPY packages/server/package.json . COPY packages/server/package.json .
COPY packages/server/dist/yarn.lock . COPY packages/server/dist/yarn.lock .
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh package.json RUN ./scripts/removeWorkspaceDependencies.sh package.json
# Install yarn packages with caching
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 \ RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true --network-timeout 1000000 \
# Remove unneeded data from file system to reduce image size && yarn cache clean \
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \ && apk del g++ make python3 jq \
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp && rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
COPY packages/server/dist/ dist/ COPY packages/server/dist/ dist/
@ -69,7 +67,7 @@ EXPOSE 4001
# due to this causing yarn to stop installing dev dependencies # due to this causing yarn to stop installing dev dependencies
# which are actually needed to get this environment up and running # which are actually needed to get this environment up and running
ENV NODE_ENV=production ENV NODE_ENV=production
# this is required for isolated-vm to work on Node 20+ # This is required for isolated-vm to work on Node 20+
ENV NODE_OPTIONS="--no-node-snapshot" ENV NODE_OPTIONS="--no-node-snapshot"
ENV CLUSTER_MODE=${CLUSTER_MODE} ENV CLUSTER_MODE=${CLUSTER_MODE}
ENV TOP_LEVEL_PATH=/app ENV TOP_LEVEL_PATH=/app

View File

@ -333,6 +333,7 @@ export default class TestConfiguration {
sessionId: this.sessionIdForUser(_id), sessionId: this.sessionIdForUser(_id),
tenantId: this.getTenantId(), tenantId: this.getTenantId(),
csrfToken: this.csrfToken, csrfToken: this.csrfToken,
email,
}) })
const resp = await db.put(user) const resp = await db.put(user)
await cache.user.invalidateUser(_id) await cache.user.invalidateUser(_id)
@ -396,16 +397,17 @@ export default class TestConfiguration {
} }
// make sure the user exists in the global DB // make sure the user exists in the global DB
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) { if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser({ const user = await this.globalUser({
_id: userId, _id: userId,
builder: { global: builder }, builder: { global: builder },
roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC }, roles: { [appId]: roleId || roles.BUILTIN_ROLE_IDS.BASIC },
}) })
await sessions.createASession(userId, {
sessionId: this.sessionIdForUser(userId),
tenantId: this.getTenantId(),
email: user.email,
})
} }
await sessions.createASession(userId, {
sessionId: this.sessionIdForUser(userId),
tenantId: this.getTenantId(),
})
// have to fake this // have to fake this
const authObj = { const authObj = {
userId, userId,

View File

@ -247,7 +247,9 @@ class QueryRunner {
if (!resp.err) { if (!resp.err) {
const globalUserId = getGlobalIDFromUserMetadataID(_id) const globalUserId = getGlobalIDFromUserMetadataID(_id)
await auth.updateUserOAuth(globalUserId, resp) await auth.updateUserOAuth(globalUserId, resp)
this.ctx.user = await cache.user.getUser(globalUserId) this.ctx.user = await cache.user.getUser({
userId: globalUserId,
})
} else { } else {
// In this event the user may have oAuth issues that // In this event the user may have oAuth issues that
// could require re-authenticating with their provider. // could require re-authenticating with their provider.

View File

@ -77,7 +77,9 @@ export async function getCachedSelf(
): Promise<ContextUser> { ): Promise<ContextUser> {
// this has to be tenant aware, can't depend on the context to find it out // this has to be tenant aware, can't depend on the context to find it out
// running some middlewares before the tenancy causes context to break // running some middlewares before the tenancy causes context to break
const user = await cache.user.getUser(ctx.user?._id!) const user = await cache.user.getUser({
userId: ctx.user?._id!,
})
return processUser(user, { appId }) return processUser(user, { appId })
} }

View File

@ -35,7 +35,9 @@ export async function processInputBBReference(
} }
try { try {
await cache.user.getUser(id) await cache.user.getUser({
userId: id,
})
return id return id
} catch (e: any) { } catch (e: any) {
if (e.statusCode === 404) { if (e.statusCode === 404) {
@ -125,7 +127,9 @@ export async function processOutputBBReference(
case BBReferenceFieldSubType.USER: { case BBReferenceFieldSubType.USER: {
let user let user
try { try {
user = await cache.user.getUser(value as string) user = await cache.user.getUser({
userId: value as string,
})
} catch (err: any) { } catch (err: any) {
if (err.statusCode !== 404) { if (err.statusCode !== 404) {
throw err throw err

View File

@ -110,7 +110,9 @@ async function processDefaultValues(table: Table, row: Row) {
const identity = context.getIdentity() const identity = context.getIdentity()
if (identity?._id && identity.type === IdentityType.USER) { if (identity?._id && identity.type === IdentityType.USER) {
const user = await cache.user.getUser(identity._id) const user = await cache.user.getUser({
userId: identity._id,
})
delete user.password delete user.password
ctx["Current User"] = user ctx["Current User"] = user

View File

@ -74,7 +74,9 @@ describe("bbReferenceProcessor", () => {
expect(result).toEqual(userId) expect(result).toEqual(userId)
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1) expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId) expect(cacheGetUserSpy).toHaveBeenCalledWith({
userId,
})
}) })
it("throws an error given an invalid id", async () => { it("throws an error given an invalid id", async () => {
@ -98,7 +100,9 @@ describe("bbReferenceProcessor", () => {
expect(result).toEqual(userId) expect(result).toEqual(userId)
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1) expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId) expect(cacheGetUserSpy).toHaveBeenCalledWith({
userId,
})
}) })
it("empty strings will return null", async () => { it("empty strings will return null", async () => {
@ -243,7 +247,9 @@ describe("bbReferenceProcessor", () => {
lastName: user.lastName, lastName: user.lastName,
}) })
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1) expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId) expect(cacheGetUserSpy).toHaveBeenCalledWith({
userId,
})
}) })
it("returns undefined given an unexisting user", async () => { it("returns undefined given an unexisting user", async () => {
@ -255,7 +261,9 @@ describe("bbReferenceProcessor", () => {
expect(result).toBeUndefined() expect(result).toBeUndefined()
expect(cacheGetUserSpy).toHaveBeenCalledTimes(1) expect(cacheGetUserSpy).toHaveBeenCalledTimes(1)
expect(cacheGetUserSpy).toHaveBeenCalledWith(userId) expect(cacheGetUserSpy).toHaveBeenCalledWith({
userId,
})
}) })
}) })
}) })

View File

@ -10,6 +10,7 @@ export interface AuthToken {
export interface CreateSession { export interface CreateSession {
sessionId: string sessionId: string
tenantId: string tenantId: string
email: string
csrfToken?: string csrfToken?: string
hosting?: Hosting hosting?: Hosting
} }

View File

@ -19,12 +19,17 @@ import { EmailTemplatePurpose } from "../../constants"
export async function loginUser(user: User) { export async function loginUser(user: User) {
const sessionId = coreUtils.newid() const sessionId = coreUtils.newid()
const tenantId = tenancy.getTenantId() const tenantId = tenancy.getTenantId()
await sessions.createASession(user._id!, { sessionId, tenantId }) await sessions.createASession(user._id!, {
sessionId,
tenantId,
email: user.email,
})
return jwt.sign( return jwt.sign(
{ {
userId: user._id, userId: user._id,
sessionId, sessionId,
tenantId, tenantId,
email: user.email,
}, },
coreEnv.JWT_SECRET! coreEnv.JWT_SECRET!
) )

View File

@ -170,19 +170,26 @@ class TestConfiguration {
async _createSession({ async _createSession({
userId, userId,
tenantId, tenantId,
email,
}: { }: {
userId: string userId: string
tenantId: string tenantId: string
email: string
}) { }) {
await sessions.createASession(userId!, { await sessions.createASession(userId!, {
sessionId: "sessionid", sessionId: "sessionid",
tenantId: tenantId, tenantId,
csrfToken: CSRF_TOKEN, csrfToken: CSRF_TOKEN,
email,
}) })
} }
async createSession(user: User) { async createSession(user: User) {
return this._createSession({ userId: user._id!, tenantId: user.tenantId }) return this._createSession({
userId: user._id!,
tenantId: user.tenantId,
email: user.email,
})
} }
cookieHeader(cookies: any) { cookieHeader(cookies: any) {