Merge branch 'develop' of github.com:Budibase/budibase into design-section-feature-branch

This commit is contained in:
Andrew Kingston 2023-08-23 18:45:35 +01:00
commit f421ef54f3
15 changed files with 127 additions and 111 deletions

View File

@ -9,18 +9,8 @@ module.exports = () => {
}, },
wait: { wait: {
type: "ports", type: "ports",
timeout: 10000, timeout: 20000,
} }
} }
} }
} }
// module.exports = () => {
// return {
// dockerCompose: {
// composeFilePath: "../../hosting",
// composeFile: "docker-compose.test.yaml",
// startupTimeout: 10000,
// },
// }
// }

View File

@ -1,5 +1,5 @@
{ {
"version": "2.9.30-alpha.10", "version": "2.9.30-alpha.11",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -4,6 +4,8 @@ import * as context from "../context"
import * as platform from "../platform" import * as platform from "../platform"
import env from "../environment" import env from "../environment"
import * as accounts from "../accounts" import * as accounts from "../accounts"
import { UserDB } from "../users"
import { sdk } from "@budibase/shared-core"
const EXPIRY_SECONDS = 3600 const EXPIRY_SECONDS = 3600
@ -60,6 +62,18 @@ export async function getUser(
// make sure the tenant ID is always correct/set // make sure the tenant ID is always correct/set
user.tenantId = tenantId user.tenantId = tenantId
} }
// if has groups, could have builder permissions granted by a group
if (user.userGroups && !sdk.users.isGlobalBuilder(user)) {
await context.doInTenant(tenantId, async () => {
const appIds = await UserDB.getGroupBuilderAppIds(user)
if (appIds.length) {
const existing = user.builder?.apps || []
user.builder = {
apps: [...new Set(existing.concat(appIds))],
}
}
})
}
return user return user
} }

View File

@ -5,11 +5,12 @@ import env from "../environment"
export default async (ctx: UserCtx, next: any) => { export default async (ctx: UserCtx, next: any) => {
const appId = getAppId() const appId = getAppId()
const builderFn = env.isWorker() const builderFn =
? hasBuilderPermissions env.isWorker() || !appId
: env.isApps() ? hasBuilderPermissions
? isBuilder : env.isApps()
: undefined ? isBuilder
: undefined
if (!builderFn) { if (!builderFn) {
throw new Error("Service name unknown - middleware inactive.") throw new Error("Service name unknown - middleware inactive.")
} }

View File

@ -5,11 +5,12 @@ import env from "../environment"
export default async (ctx: UserCtx, next: any) => { export default async (ctx: UserCtx, next: any) => {
const appId = getAppId() const appId = getAppId()
const builderFn = env.isWorker() const builderFn =
? hasBuilderPermissions env.isWorker() || !appId
: env.isApps() ? hasBuilderPermissions
? isBuilder : env.isApps()
: undefined ? isBuilder
: undefined
if (!builderFn) { if (!builderFn) {
throw new Error("Service name unknown - middleware inactive.") throw new Error("Service name unknown - middleware inactive.")
} }

View File

@ -20,6 +20,8 @@ import {
SaveUserOpts, SaveUserOpts,
User, User,
UserStatus, UserStatus,
UserGroup,
ContextUser,
} from "@budibase/types" } from "@budibase/types"
import { import {
getAccountHolderFromUserIds, getAccountHolderFromUserIds,
@ -32,8 +34,14 @@ import { hash } from "../utils"
type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any> type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any> type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
type FeatureFn = () => Promise<Boolean> type FeatureFn = () => Promise<Boolean>
type GroupGetFn = (ids: string[]) => Promise<UserGroup[]>
type GroupBuildersFn = (user: User) => Promise<string[]>
type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn } type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
type GroupFns = { addUsers: GroupUpdateFn } type GroupFns = {
addUsers: GroupUpdateFn
getBulk: GroupGetFn
getGroupBuilderAppIds: GroupBuildersFn
}
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn } type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
const bulkDeleteProcessing = async (dbUser: User) => { const bulkDeleteProcessing = async (dbUser: User) => {
@ -465,4 +473,12 @@ export class UserDB {
await cache.user.invalidateUser(userId) await cache.user.invalidateUser(userId)
await sessions.invalidateSessions(userId, { reason: "deletion" }) await sessions.invalidateSessions(userId, { reason: "deletion" })
} }
static async getGroups(groupIds: string[]) {
return await this.groups.getBulk(groupIds)
}
static async getGroupBuilderAppIds(user: User) {
return await this.groups.getGroupBuilderAppIds(user)
}
} }

View File

@ -2,9 +2,9 @@ import { outputProcessing } from "../../utilities/rowProcessor"
import { InternalTables } from "../../db/utils" import { InternalTables } from "../../db/utils"
import { getFullUser } from "../../utilities/users" import { getFullUser } from "../../utilities/users"
import { roles, context } from "@budibase/backend-core" import { roles, context } from "@budibase/backend-core"
import { groups } from "@budibase/pro" import { ContextUser, Row, UserCtx } from "@budibase/types"
import { ContextUser, User, Row, UserCtx } from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
import { processUser } from "../../utilities/global"
const PUBLIC_ROLE = roles.BUILTIN_ROLE_IDS.PUBLIC const PUBLIC_ROLE = roles.BUILTIN_ROLE_IDS.PUBLIC
@ -26,7 +26,7 @@ export async function fetchSelf(ctx: UserCtx) {
} }
const appId = context.getAppId() const appId = context.getAppId()
const user: ContextUser = await getFullUser(ctx, userId) let user: ContextUser = await getFullUser(ctx, userId)
// this shouldn't be returned by the app self // this shouldn't be returned by the app self
delete user.roles delete user.roles
// forward the csrf token from the session // forward the csrf token from the session
@ -36,8 +36,7 @@ export async function fetchSelf(ctx: UserCtx) {
const db = context.getAppDB() const db = context.getAppDB()
// check for group permissions // check for group permissions
if (!user.roleId || user.roleId === PUBLIC_ROLE) { if (!user.roleId || user.roleId === PUBLIC_ROLE) {
const groupRoleId = await groups.getGroupRoleId(user as User, appId) user = await processUser(user, { appId })
user.roleId = groupRoleId || user.roleId
} }
// remove the full roles structure // remove the full roles structure
delete user.roles delete user.roles

View File

@ -18,7 +18,7 @@ import {
import _ from "lodash" import _ from "lodash"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { utils } from "@budibase/backend-core" import { utils } from "@budibase/backend-core"
import { GenericContainer } from "testcontainers" import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
const config = setup.getConfig()! const config = setup.getConfig()!
@ -37,22 +37,36 @@ describe("postgres integrations", () => {
let host: string let host: string
let port: number let port: number
const containers: StartedTestContainer[] = []
beforeAll(async () => { beforeAll(async () => {
const container = await new GenericContainer("postgres") const containerPostgres = await new GenericContainer("postgres")
.withExposedPorts(5432) .withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "password") .withEnv("POSTGRES_PASSWORD", "password")
.withWaitStrategy(
Wait.forLogMessage(
"PostgreSQL init process complete; ready for start up."
)
)
.start() .start()
host = container.getContainerIpAddress() host = containerPostgres.getContainerIpAddress()
port = container.getMappedPort(5432) port = containerPostgres.getMappedPort(5432)
await config.init() await config.init()
const apiKey = await config.generateApiKey() const apiKey = await config.generateApiKey()
containers.push(containerPostgres)
makeRequest = generateMakeRequest(apiKey, true) makeRequest = generateMakeRequest(apiKey, true)
}) })
afterAll(async () => {
for (let container of containers) {
await container.stop()
}
})
function pgDatasourceConfig() { function pgDatasourceConfig() {
return { return {
datasource: { datasource: {

View File

@ -12,75 +12,65 @@ import { groups } from "@budibase/pro"
import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types" import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types"
import cloneDeep from "lodash/cloneDeep" import cloneDeep from "lodash/cloneDeep"
export function updateAppRole( export async function processUser(
user: ContextUser, user: ContextUser,
{ appId }: { appId?: string } = {} opts: { appId?: string; groups?: UserGroup[] } = {}
) { ) {
appId = appId || context.getAppId()
if (!user || (!user.roles && !user.userGroups)) { if (!user || (!user.roles && !user.userGroups)) {
return user return user
} }
// if in an multi-tenancy environment make sure roles are never updated user = cloneDeep(user)
delete user.password
const appId = opts.appId || context.getAppId()
if (!appId) {
throw new Error("Unable to process user without app ID")
}
// if in a multi-tenancy environment and in wrong tenant make sure roles are never updated
if (env.MULTI_TENANCY && appId && !tenancy.isUserInAppTenant(appId, user)) { if (env.MULTI_TENANCY && appId && !tenancy.isUserInAppTenant(appId, user)) {
user = users.removePortalUserPermissions(user) user = users.removePortalUserPermissions(user)
user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
return user return user
} }
// always use the deployed app let groupList: UserGroup[] = []
if (appId && user.roles) { if (appId && user?.userGroups?.length) {
groupList = opts.groups
? opts.groups
: await groups.getBulk(user.userGroups)
}
// check if a group provides builder access
const builderAppIds = await groups.getGroupBuilderAppIds(user, {
appId,
groups: groupList,
})
if (builderAppIds.length && !users.isBuilder(user, appId)) {
const existingApps = user.builder?.apps || []
user.builder = {
apps: [...new Set(existingApps.concat(builderAppIds))],
}
}
// builders are always admins within the app
if (users.isBuilder(user, appId)) {
user.roleId = roles.BUILTIN_ROLE_IDS.ADMIN
}
// try to get the role from the user list
if (!user.roleId && appId && user.roles) {
user.roleId = user.roles[dbCore.getProdAppID(appId)] user.roleId = user.roles[dbCore.getProdAppID(appId)]
} }
// if a role wasn't found then either set as admin (builder) or public (everyone else) // try to get the role from the group list
if (!user.roleId && users.isBuilder(user, appId)) { if (!user.roleId && groupList) {
user.roleId = roles.BUILTIN_ROLE_IDS.ADMIN user.roleId = await groups.getGroupRoleId(user, appId, {
} else if (!user.roleId && !user?.userGroups?.length) { groups: groupList,
user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
}
delete user.roles
return user
}
async function checkGroupRoles(
user: ContextUser,
opts: { appId?: string; groups?: UserGroup[] } = {}
) {
if (user.roleId && user.roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
return user
}
if (opts.appId) {
user.roleId = await groups.getGroupRoleId(user as User, opts.appId, {
groups: opts.groups,
}) })
} }
// final fallback, simply couldn't find a role - user must be public // final fallback, simply couldn't find a role - user must be public
if (!user.roleId) { if (!user.roleId) {
user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
} }
// remove the roles as it is now set
delete user.roles
return user return user
} }
export async function processUser(
user: ContextUser,
opts: { appId?: string; groups?: UserGroup[] } = {}
) {
let clonedUser = cloneDeep(user)
if (clonedUser) {
delete clonedUser.password
}
const appId = opts.appId || context.getAppId()
clonedUser = updateAppRole(clonedUser, { appId })
if (!clonedUser.roleId && clonedUser?.userGroups?.length) {
clonedUser = await checkGroupRoles(clonedUser, {
appId,
groups: opts?.groups,
})
}
return clonedUser
}
export async function getCachedSelf(ctx: UserCtx, appId: string) { export async function getCachedSelf(ctx: UserCtx, appId: string) {
// 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

View File

@ -8,10 +8,9 @@ import {
logging, logging,
env as coreEnv, env as coreEnv,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { updateAppRole } from "./global" import { Ctx, User, EmailInvite } from "@budibase/types"
import { BBContext, User, EmailInvite } from "@budibase/types"
export function request(ctx?: BBContext, request?: any) { export function request(ctx?: Ctx, request?: any) {
if (!request.headers) { if (!request.headers) {
request.headers = {} request.headers = {}
} }
@ -43,7 +42,7 @@ export function request(ctx?: BBContext, request?: any) {
async function checkResponse( async function checkResponse(
response: any, response: any,
errorMsg: string, errorMsg: string,
{ ctx }: { ctx?: BBContext } = {} { ctx }: { ctx?: Ctx } = {}
) { ) {
if (response.status !== 200) { if (response.status !== 200) {
let error let error
@ -105,21 +104,7 @@ export async function sendSmtpEmail({
return checkResponse(response, "send email") return checkResponse(response, "send email")
} }
export async function getGlobalSelf(ctx: BBContext, appId?: string) { export async function removeAppFromUserRoles(ctx: Ctx, appId: string) {
const endpoint = `/api/global/self`
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + endpoint),
// we don't want to use API key when getting self
request(ctx, { method: "GET" })
)
let json = await checkResponse(response, "get self globally", { ctx })
if (appId) {
json = updateAppRole(json)
}
return json
}
export async function removeAppFromUserRoles(ctx: BBContext, appId: string) {
const prodAppId = dbCore.getProdAppID(appId) const prodAppId = dbCore.getProdAppID(appId)
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/global/roles/${prodAppId}`), checkSlashesInUrl(env.WORKER_URL + `/api/global/roles/${prodAppId}`),
@ -130,7 +115,7 @@ export async function removeAppFromUserRoles(ctx: BBContext, appId: string) {
return checkResponse(response, "remove app role") return checkResponse(response, "remove app role")
} }
export async function allGlobalUsers(ctx: BBContext) { export async function allGlobalUsers(ctx: Ctx) {
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + "/api/global/users"), checkSlashesInUrl(env.WORKER_URL + "/api/global/users"),
// we don't want to use API key when getting self // we don't want to use API key when getting self
@ -139,7 +124,7 @@ export async function allGlobalUsers(ctx: BBContext) {
return checkResponse(response, "get users", { ctx }) return checkResponse(response, "get users", { ctx })
} }
export async function saveGlobalUser(ctx: BBContext) { export async function saveGlobalUser(ctx: Ctx) {
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + "/api/global/users"), checkSlashesInUrl(env.WORKER_URL + "/api/global/users"),
// we don't want to use API key when getting self // we don't want to use API key when getting self
@ -148,7 +133,7 @@ export async function saveGlobalUser(ctx: BBContext) {
return checkResponse(response, "save user", { ctx }) return checkResponse(response, "save user", { ctx })
} }
export async function deleteGlobalUser(ctx: BBContext) { export async function deleteGlobalUser(ctx: Ctx) {
const response = await fetch( const response = await fetch(
checkSlashesInUrl( checkSlashesInUrl(
env.WORKER_URL + `/api/global/users/${ctx.params.userId}` env.WORKER_URL + `/api/global/users/${ctx.params.userId}`
@ -159,7 +144,7 @@ export async function deleteGlobalUser(ctx: BBContext) {
return checkResponse(response, "delete user", { ctx }) return checkResponse(response, "delete user", { ctx })
} }
export async function readGlobalUser(ctx: BBContext): Promise<User> { export async function readGlobalUser(ctx: Ctx): Promise<User> {
const response = await fetch( const response = await fetch(
checkSlashesInUrl( checkSlashesInUrl(
env.WORKER_URL + `/api/global/users/${ctx.params.userId}` env.WORKER_URL + `/api/global/users/${ctx.params.userId}`

View File

@ -7,6 +7,10 @@ export interface UserGroup extends Document {
color: string color: string
users?: GroupUser[] users?: GroupUser[]
roles?: UserGroupRoles roles?: UserGroupRoles
// same structure as users
builder?: {
apps: string[]
}
createdAt?: number createdAt?: number
scimInfo?: { scimInfo?: {
externalId: string externalId: string

View File

@ -48,7 +48,7 @@ export async function generateAPIKey(ctx: any) {
} catch (err) { } catch (err) {
devInfo = { _id: id, userId } devInfo = { _id: id, userId }
} }
devInfo.apiKey = await apiKey devInfo.apiKey = apiKey
await db.put(devInfo) await db.put(devInfo)
ctx.body = cleanupDevInfo(devInfo) ctx.body = cleanupDevInfo(devInfo)
} }
@ -63,7 +63,7 @@ export async function fetchAPIKey(ctx: any) {
devInfo = { devInfo = {
_id: id, _id: id,
userId: ctx.user._id, userId: ctx.user._id,
apiKey: await newApiKey(), apiKey: newApiKey(),
} }
await db.put(devInfo) await db.put(devInfo)
} }

View File

@ -25,11 +25,11 @@ import {
import { import {
accounts, accounts,
cache, cache,
ErrorCode,
events, events,
migrations, migrations,
tenancy,
platform, platform,
ErrorCode, tenancy,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users" import { checkAnyUserExists } from "../../../utilities/users"
import { isEmailConfigured } from "../../../utilities/email" import { isEmailConfigured } from "../../../utilities/email"
@ -280,7 +280,7 @@ export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
let bulkCreateReponse = await userSdk.db.bulkCreate(users, []) let bulkCreateReponse = await userSdk.db.bulkCreate(users, [])
// Apply temporary credentials // Apply temporary credentials
let createWithCredentials = { ctx.body = {
...bulkCreateReponse, ...bulkCreateReponse,
successful: bulkCreateReponse?.successful.map(user => { successful: bulkCreateReponse?.successful.map(user => {
return { return {
@ -290,8 +290,6 @@ export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
}), }),
created: true, created: true,
} }
ctx.body = createWithCredentials
} else { } else {
ctx.throw(400, "User onboarding failed") ctx.throw(400, "User onboarding failed")
} }

View File

@ -33,7 +33,9 @@ describe("/api/global/users/:userId/app/builder", () => {
MOCK_APP_ID, MOCK_APP_ID,
400 400
) )
expect(resp.body.message).toContain("Feature not enabled") expect(resp.body.message).toContain(
"appBuilders are not currently enabled"
)
}) })
}) })

View File

@ -10,6 +10,8 @@ import {
import { TestConfiguration } from "../../../../tests" import { TestConfiguration } from "../../../../tests"
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
jest.setTimeout(30000)
mocks.licenses.useScimIntegration() mocks.licenses.useScimIntegration()
describe("scim", () => { describe("scim", () => {