diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index 26c7cd4e26..fb2fd2cf51 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -199,7 +199,6 @@ export async function platformLogout(opts: PlatformLogoutOpts) { } else { // clear cookies clearCookie(ctx, Cookie.Auth) - clearCookie(ctx, Cookie.CurrentApp) } const sessionIds = sessions.map(({ sessionId }) => sessionId) diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index e25c90575f..15cec7a6b9 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -4,7 +4,6 @@ export enum UserStatus { } export enum Cookie { - CurrentApp = "budibase:currentapp", Auth = "budibase:auth", Init = "budibase:init", ACCOUNT_RETURN_URL = "budibase:account:returnurl", diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts index ca77f476d0..ff2e5b147f 100644 --- a/packages/backend-core/tests/utilities/structures/index.ts +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -8,4 +8,5 @@ export * as plugins from "./plugins" export * as sso from "./sso" export * as tenant from "./tenants" export * as users from "./users" +export * as userGroups from "./userGroups" export { generator } from "./generator" diff --git a/packages/backend-core/tests/utilities/structures/userGroups.ts b/packages/backend-core/tests/utilities/structures/userGroups.ts new file mode 100644 index 0000000000..4dc870a00a --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/userGroups.ts @@ -0,0 +1,10 @@ +import { UserGroup } from "@budibase/types" +import { generator } from "./generator" + +export function userGroup(): UserGroup { + return { + name: generator.word(), + icon: generator.word(), + color: generator.word(), + } +} diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 162240e12c..0c5adfc18c 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -136,6 +136,7 @@ const onUpdateColumns = () => { selectedRows = [] fetch.refresh() + tables.fetchTable(id) } // Fetch data whenever rows are modified. Unfortunately we have to lose diff --git a/packages/builder/src/pages/builder/_layout.svelte b/packages/builder/src/pages/builder/_layout.svelte index a2b480f742..b216958045 100644 --- a/packages/builder/src/pages/builder/_layout.svelte +++ b/packages/builder/src/pages/builder/_layout.svelte @@ -79,7 +79,7 @@ } // Validate tenant if in a multi-tenant env - if (useAccountPortal && multiTenancyEnabled) { + if (multiTenancyEnabled) { await validateTenantId() } } catch (error) { diff --git a/packages/builder/src/stores/backend/tables.js b/packages/builder/src/stores/backend/tables.js index ee38504dd1..3b7ce35dde 100644 --- a/packages/builder/src/stores/backend/tables.js +++ b/packages/builder/src/stores/backend/tables.js @@ -22,6 +22,18 @@ export function createTablesStore() { })) } + const fetchTable = async tableId => { + const table = await API.fetchTableDefinition(tableId) + + store.update(state => { + const indexToUpdate = state.list.findIndex(t => t._id === table._id) + state.list[indexToUpdate] = table + return { + ...state, + } + }) + } + const select = tableId => { store.update(state => ({ ...state, @@ -126,6 +138,7 @@ export function createTablesStore() { return { subscribe: derivedStore.subscribe, fetch, + fetchTable, init: fetch, select, save, diff --git a/packages/server/src/middleware/currentapp.ts b/packages/server/src/middleware/currentapp.ts index 7621bf61ee..efafc59e21 100644 --- a/packages/server/src/middleware/currentapp.ts +++ b/packages/server/src/middleware/currentapp.ts @@ -2,7 +2,6 @@ import { utils, constants, roles, - db as dbCore, tenancy, context, } from "@budibase/backend-core" @@ -15,29 +14,10 @@ import { UserCtx } from "@budibase/types" export default async (ctx: UserCtx, next: any) => { // try to get the appID from the request let requestAppId = await utils.getAppIdFromCtx(ctx) - // get app cookie if it exists - let appCookie: { appId?: string } | undefined - try { - appCookie = utils.getCookie(ctx, constants.Cookie.CurrentApp) - } catch (err) { - utils.clearCookie(ctx, constants.Cookie.CurrentApp) - } - if (!appCookie && !requestAppId) { + if (!requestAppId) { return next() } - // check the app exists referenced in cookie - if (appCookie) { - const appId = appCookie.appId - const exists = await dbCore.dbExists(appId) - if (!exists) { - utils.clearCookie(ctx, constants.Cookie.CurrentApp) - return next() - } - // if the request app ID wasn't set, update it with the cookie - requestAppId = requestAppId || appId - } - // deny access to application preview if (!env.isTest()) { if ( @@ -45,7 +25,6 @@ export default async (ctx: UserCtx, next: any) => { !isWebhookEndpoint(ctx) && (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) ) { - utils.clearCookie(ctx, constants.Cookie.CurrentApp) return ctx.redirect("/") } } @@ -127,14 +106,6 @@ export default async (ctx: UserCtx, next: any) => { role: await roles.getRole(roleId), } } - if ( - (requestAppId !== appId || - appCookie == null || - appCookie.appId !== requestAppId) && - !skipCookie - ) { - utils.setCookie(ctx, { appId }, constants.Cookie.CurrentApp) - } return next() }) diff --git a/packages/server/src/middleware/tests/currentapp.spec.js b/packages/server/src/middleware/tests/currentapp.spec.js index 8770118da2..b80800fd96 100644 --- a/packages/server/src/middleware/tests/currentapp.spec.js +++ b/packages/server/src/middleware/tests/currentapp.spec.js @@ -158,27 +158,22 @@ describe("Current app middleware", () => { }) describe("check functionality when logged in", () => { - async function checkExpected(setCookie) { + async function checkExpected() { config.setUser() await config.executeMiddleware() - let { utils } = require("@budibase/backend-core") - if (setCookie) { - expect(utils.setCookie).toHaveBeenCalled() - } else { - expect(utils.setCookie).not.toHaveBeenCalled() - } + expect(config.ctx.roleId).toEqual("PUBLIC") expect(config.ctx.user.role._id).toEqual("PUBLIC") expect(config.ctx.appId).toEqual("app_test") expect(config.next).toHaveBeenCalled() } - it("should be able to setup an app token when cookie not setup", async () => { + it("should be able to setup an app token on a first call", async () => { mockAuthWithCookie() - await checkExpected(true) + await checkExpected() }) - it("should perform correct when no cookie exists", async () => { + it("should perform correct on a first call", async () => { mockReset() jest.mock("@budibase/backend-core", () => { const core = jest.requireActual("@budibase/backend-core") @@ -206,38 +201,7 @@ describe("Current app middleware", () => { }, } }) - await checkExpected(true) - }) - - it("lastly check what occurs when cookie doesn't need updated", async () => { - mockReset() - jest.mock("@budibase/backend-core", () => { - const core = jest.requireActual("@budibase/backend-core") - return { - ...core, - db: { - ...core.db, - dbExists: () => true, - }, - utils: { - getAppIdFromCtx: () => { - return "app_test" - }, - setCookie: jest.fn(), - getCookie: () => ({ appId: "app_test", roleId: "PUBLIC" }), - }, - cache: { - user: { - getUser: async id => { - return { - _id: "us_uuid1", - } - }, - }, - }, - } - }) - await checkExpected(false) + await checkExpected() }) }) }) diff --git a/packages/server/src/sdk/users/tests/utils.spec.ts b/packages/server/src/sdk/users/tests/utils.spec.ts new file mode 100644 index 0000000000..11c2c53643 --- /dev/null +++ b/packages/server/src/sdk/users/tests/utils.spec.ts @@ -0,0 +1,159 @@ +import { db, roles } from "@budibase/backend-core" +import { structures } from "@budibase/backend-core/tests" +import { sdk as proSdk } from "@budibase/pro" + +import TestConfiguration from "../../../tests/utilities/TestConfiguration" +import { rawUserMetadata, syncGlobalUsers } from "../utils" + +describe("syncGlobalUsers", () => { + const config = new TestConfiguration() + + beforeEach(async () => { + await config.init() + }) + + afterAll(config.end) + + it("the default user is synced", async () => { + await config.doInContext(config.appId, async () => { + await syncGlobalUsers() + + const metadata = await rawUserMetadata() + expect(metadata).toHaveLength(1) + expect(metadata).toEqual([ + expect.objectContaining({ + _id: db.generateUserMetadataID(config.user._id), + }), + ]) + }) + }) + + it("admin and builders users are synced", async () => { + const user1 = await config.createUser({ admin: true }) + const user2 = await config.createUser({ admin: false, builder: true }) + await config.doInContext(config.appId, async () => { + expect(await rawUserMetadata()).toHaveLength(1) + await syncGlobalUsers() + + const metadata = await rawUserMetadata() + expect(metadata).toHaveLength(3) + expect(metadata).toContainEqual( + expect.objectContaining({ + _id: db.generateUserMetadataID(user1._id), + }) + ) + expect(metadata).toContainEqual( + expect.objectContaining({ + _id: db.generateUserMetadataID(user2._id), + }) + ) + }) + }) + + it("app users are not synced if not specified", async () => { + const user = await config.createUser({ admin: false, builder: false }) + await config.doInContext(config.appId, async () => { + await syncGlobalUsers() + + const metadata = await rawUserMetadata() + expect(metadata).toHaveLength(1) + expect(metadata).not.toContainEqual( + expect.objectContaining({ + _id: db.generateUserMetadataID(user._id), + }) + ) + }) + }) + + it("app users are added when group is assigned to app", async () => { + await config.doInTenant(async () => { + const group = await proSdk.groups.save(structures.userGroups.userGroup()) + const user1 = await config.createUser({ admin: false, builder: false }) + const user2 = await config.createUser({ admin: false, builder: false }) + await proSdk.groups.addUsers(group.id, [user1._id, user2._id]) + + await config.doInContext(config.appId, async () => { + await syncGlobalUsers() + expect(await rawUserMetadata()).toHaveLength(1) + + await proSdk.groups.updateGroupApps(group.id, { + appsToAdd: [ + { appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC }, + ], + }) + await syncGlobalUsers() + + const metadata = await rawUserMetadata() + expect(metadata).toHaveLength(3) + expect(metadata).toContainEqual( + expect.objectContaining({ + _id: db.generateUserMetadataID(user1._id), + }) + ) + expect(metadata).toContainEqual( + expect.objectContaining({ + _id: db.generateUserMetadataID(user2._id), + }) + ) + }) + }) + }) + + it("app users are removed when app is removed from user group", async () => { + await config.doInTenant(async () => { + const group = await proSdk.groups.save(structures.userGroups.userGroup()) + const user1 = await config.createUser({ admin: false, builder: false }) + const user2 = await config.createUser({ admin: false, builder: false }) + await proSdk.groups.updateGroupApps(group.id, { + appsToAdd: [ + { appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC }, + ], + }) + await proSdk.groups.addUsers(group.id, [user1._id, user2._id]) + + await config.doInContext(config.appId, async () => { + await syncGlobalUsers() + expect(await rawUserMetadata()).toHaveLength(3) + + await proSdk.groups.updateGroupApps(group.id, { + appsToRemove: [{ appId: config.prodAppId! }], + }) + await syncGlobalUsers() + + const metadata = await rawUserMetadata() + expect(metadata).toHaveLength(1) + }) + }) + }) + + it("app users are removed when app is removed from user group", async () => { + await config.doInTenant(async () => { + const group = await proSdk.groups.save(structures.userGroups.userGroup()) + const user1 = await config.createUser({ admin: false, builder: false }) + const user2 = await config.createUser({ admin: false, builder: false }) + await proSdk.groups.updateGroupApps(group.id, { + appsToAdd: [ + { appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC }, + ], + }) + await proSdk.groups.addUsers(group.id, [user1._id, user2._id]) + + await config.doInContext(config.appId, async () => { + await syncGlobalUsers() + expect(await rawUserMetadata()).toHaveLength(3) + + await proSdk.groups.removeUsers(group.id, [user1._id]) + await syncGlobalUsers() + + const metadata = await rawUserMetadata() + expect(metadata).toHaveLength(2) + + expect(metadata).not.toContainEqual( + expect.objectContaining({ + _id: db.generateUserMetadataID(user1._id), + }) + ) + }) + }) + }) +}) diff --git a/packages/server/src/sdk/users/utils.ts b/packages/server/src/sdk/users/utils.ts index 5c369754a1..9b9ea04c56 100644 --- a/packages/server/src/sdk/users/utils.ts +++ b/packages/server/src/sdk/users/utils.ts @@ -6,25 +6,33 @@ import { InternalTables, } from "../../db/utils" import { isEqual } from "lodash" +import { ContextUser, UserMetadata } from "@budibase/types" -export function combineMetadataAndUser(user: any, metadata: any) { +export function combineMetadataAndUser( + user: ContextUser, + metadata: UserMetadata | UserMetadata[] +) { + const metadataId = generateUserMetadataID(user._id!) + const found = Array.isArray(metadata) + ? metadata.find(doc => doc._id === metadataId) + : metadata // skip users with no access if ( user.roleId == null || user.roleId === rolesCore.BUILTIN_ROLE_IDS.PUBLIC ) { + // If it exists and it should not, we must remove it + if (found?._id) { + return { ...found, _deleted: true } + } return null } delete user._rev - const metadataId = generateUserMetadataID(user._id) const newDoc = { ...user, _id: metadataId, tableId: InternalTables.USER_METADATA, } - const found = Array.isArray(metadata) - ? metadata.find(doc => doc._id === metadataId) - : metadata // copy rev over for the purposes of equality check if (found) { newDoc._rev = found._rev @@ -58,7 +66,7 @@ export async function syncGlobalUsers() { ]) const toWrite = [] for (let user of users) { - const combined = await combineMetadataAndUser(user, metadata) + const combined = combineMetadataAndUser(user, metadata) if (combined) { toWrite.push(combined) } diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index cf337c689f..ca48dd7f86 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -47,6 +47,7 @@ import { SourceName, Table, SearchFilters, + UserRoles, } from "@budibase/types" type DefaultUserValues = { @@ -277,7 +278,7 @@ class TestConfiguration { email?: string builder?: boolean admin?: boolean - roles?: any + roles?: UserRoles } = {} ) { let { id, firstName, lastName, email, builder, admin, roles } = user @@ -330,21 +331,13 @@ class TestConfiguration { sessionId: "sessionid", tenantId: this.getTenantId(), } - const app = { - roleId: roleId, - appId, - } const authToken = auth.jwt.sign(authObj, coreEnv.JWT_SECRET) - const appToken = auth.jwt.sign(app, coreEnv.JWT_SECRET) // returning necessary request headers await cache.user.invalidateUser(userId) return { Accept: "application/json", - Cookie: [ - `${constants.Cookie.Auth}=${authToken}`, - `${constants.Cookie.CurrentApp}=${appToken}`, - ], + Cookie: [`${constants.Cookie.Auth}=${authToken}`], [constants.Header.APP_ID]: appId, } }) @@ -359,18 +352,11 @@ class TestConfiguration { sessionId: "sessionid", tenantId, } - const app = { - roleId: roles.BUILTIN_ROLE_IDS.ADMIN, - appId: this.appId, - } const authToken = auth.jwt.sign(authObj, coreEnv.JWT_SECRET) - const appToken = auth.jwt.sign(app, coreEnv.JWT_SECRET) + const headers: any = { Accept: "application/json", - Cookie: [ - `${constants.Cookie.Auth}=${authToken}`, - `${constants.Cookie.CurrentApp}=${appToken}`, - ], + Cookie: [`${constants.Cookie.Auth}=${authToken}`], [constants.Header.CSRF_TOKEN]: this.defaultUserValues.csrfToken, Host: this.tenantHost(), ...extras, diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 1286fd519d..c8f75b3610 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -50,11 +50,6 @@ async function passportCallback( setCookie(ctx, token, Cookie.Auth, { sign: false }) // set the token in a header as well for APIs ctx.set(Header.TOKEN, token) - // get rid of any app cookies on login - // have to check test because this breaks cypress - if (!env.isTest()) { - clearCookie(ctx, Cookie.CurrentApp) - } } export const login = async (ctx: Ctx, next: any) => { diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts index 78e5bf7164..d0232bee60 100644 --- a/packages/worker/src/api/controllers/global/self.ts +++ b/packages/worker/src/api/controllers/global/self.ts @@ -2,7 +2,6 @@ import * as userSdk from "../../../sdk/users" import { featureFlags, tenancy, - constants, db as dbCore, utils, encryption, @@ -11,7 +10,7 @@ import { import env from "../../../environment" import { groups } from "@budibase/pro" import { UpdateSelfRequest, UpdateSelfResponse, UserCtx } from "@budibase/types" -const { getCookie, clearCookie, newid } = utils +const { newid } = utils function newTestApiKey() { return env.ENCRYPTED_TEST_PUBLIC_API_KEY @@ -71,16 +70,6 @@ export async function fetchAPIKey(ctx: any) { ctx.body = cleanupDevInfo(devInfo) } -const checkCurrentApp = (ctx: any) => { - const appCookie = getCookie(ctx, constants.Cookie.CurrentApp) - if (appCookie && !tenancy.isUserInAppTenant(appCookie.appId)) { - // there is a currentapp cookie from another tenant - // remove the cookie as this is incompatible with the builder - // due to builder and admin permissions being removed - clearCookie(ctx, constants.Cookie.CurrentApp) - } -} - /** * Add the attributes that are session based to the current user. */ @@ -101,8 +90,6 @@ export async function getSelf(ctx: any) { id: userId, } - checkCurrentApp(ctx) - // get the main body of the user const user = await userSdk.getUser(userId) ctx.body = await groups.enrichUserRolesFromGroups(user)