Skip to content

Commit

Permalink
Merge branch 'fix/budi-8715-sql-relationships-many-side' of github.co…
Browse files Browse the repository at this point in the history
…m:Budibase/budibase into fix/budi-8715-sql-relationships-many-side
  • Loading branch information
mike12345567 committed Oct 9, 2024
2 parents 00048a2 + 01e458b commit 6a25f66
Show file tree
Hide file tree
Showing 27 changed files with 616 additions and 244 deletions.
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2.32.14",
"version": "2.32.15",
"npmClient": "yarn",
"packages": [
"packages/*",
Expand Down
9 changes: 9 additions & 0 deletions packages/backend-core/src/context/mainContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ export function getAppId(): string | undefined {
}
}

export function getIP(): string | undefined {
const context = Context.get()
return context?.ip
}

export const getProdAppId = () => {
const appId = getAppId()
if (!appId) {
Expand Down Expand Up @@ -281,6 +286,10 @@ export function doInScimContext(task: any) {
return newContext(updates, task)
}

export function doInIPContext(ip: string, task: any) {
return newContext({ ip }, task)
}

export async function ensureSnippetContext(enabled = !env.isTest()) {
const ctx = getCurrentContext()

Expand Down
1 change: 1 addition & 0 deletions packages/backend-core/src/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ContextMap = {
identity?: IdentityContext
environmentVariables?: Record<string, string>
isScim?: boolean
ip?: string
automationId?: string
isMigrating?: boolean
vm?: VM
Expand Down
72 changes: 25 additions & 47 deletions packages/backend-core/src/features/features.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import env from "../environment"
import * as crypto from "crypto"
import * as context from "../context"
import { PostHog, PostHogOptions } from "posthog-node"
import { FeatureFlag, IdentityType, UserCtx } from "@budibase/types"
import { FeatureFlag } from "@budibase/types"
import tracer from "dd-trace"
import { Duration } from "../utils"

Expand Down Expand Up @@ -141,23 +142,17 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
return this.flagSchema[name as keyof T] !== undefined
}

async get<K extends keyof T>(
key: K,
ctx?: UserCtx
): Promise<FlagValues<T>[K]> {
const flags = await this.fetch(ctx)
async get<K extends keyof T>(key: K): Promise<FlagValues<T>[K]> {
const flags = await this.fetch()
return flags[key]
}

async isEnabled<K extends KeysOfType<T, boolean>>(
key: K,
ctx?: UserCtx
): Promise<boolean> {
const flags = await this.fetch(ctx)
async isEnabled<K extends KeysOfType<T, boolean>>(key: K): Promise<boolean> {
const flags = await this.fetch()
return flags[key]
}

async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
async fetch(): Promise<FlagValues<T>> {
return await tracer.trace("features.fetch", async span => {
const cachedFlags = context.getFeatureFlags<FlagValues<T>>(this.setId)
if (cachedFlags) {
Expand Down Expand Up @@ -198,50 +193,33 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
tags[`flags.${key}.source`] = "environment"
}

const license = ctx?.user?.license
if (license) {
tags[`readFromLicense`] = true

for (const feature of license.features) {
if (!this.isFlagName(feature)) {
continue
}

if (
flagValues[feature] === true ||
specificallySetFalse.has(feature)
) {
// If the flag is already set to through environment variables, we
// don't want to override it back to false here.
continue
}
const identity = context.getIdentity()

// @ts-expect-error - TS does not like you writing into a generic type,
// but we know that it's okay in this case because it's just an object.
flagValues[feature] = true
tags[`flags.${feature}.source`] = "license"
let userId = identity?._id
if (!userId) {
const ip = context.getIP()
if (ip) {
userId = crypto.createHash("sha512").update(ip).digest("hex")
}
}

const identity = context.getIdentity()
let tenantId = identity?.tenantId
if (!tenantId) {
tenantId = currentTenantId
}

tags[`identity.type`] = identity?.type
tags[`identity.tenantId`] = identity?.tenantId
tags[`identity._id`] = identity?._id
tags[`tenantId`] = tenantId
tags[`userId`] = userId

if (posthog && identity?.type === IdentityType.USER) {
if (posthog && userId) {
tags[`readFromPostHog`] = true

const personProperties: Record<string, string> = {}
if (identity.tenantId) {
personProperties.tenantId = identity.tenantId
}

const posthogFlags = await posthog.getAllFlagsAndPayloads(
identity._id,
{
personProperties,
}
)
const personProperties: Record<string, string> = { tenantId }
const posthogFlags = await posthog.getAllFlagsAndPayloads(userId, {
personProperties,
})

for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
if (!this.isFlagName(name)) {
Expand Down
90 changes: 59 additions & 31 deletions packages/backend-core/src/features/tests/features.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { IdentityContext, IdentityType, UserCtx } from "@budibase/types"
import { IdentityContext, IdentityType } from "@budibase/types"
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
import * as context from "../../context"
import environment, { withEnv } from "../../environment"
import nodeFetch from "node-fetch"
import nock from "nock"
import * as crypto from "crypto"

const schema = {
TEST_BOOLEAN: Flag.boolean(false),
Expand All @@ -17,7 +18,6 @@ interface TestCase {
identity?: Partial<IdentityContext>
environmentFlags?: string
posthogFlags?: PostHogFlags
licenseFlags?: Array<string>
expected?: Partial<FlagValues<typeof schema>>
errorMessage?: string | RegExp
}
Expand All @@ -27,10 +27,14 @@ interface PostHogFlags {
featureFlagPayloads?: Record<string, string>
}

function mockPosthogFlags(flags: PostHogFlags) {
function mockPosthogFlags(
flags: PostHogFlags,
opts?: { token?: string; distinct_id?: string }
) {
const { token = "test", distinct_id = "us_1234" } = opts || {}
nock("https://us.i.posthog.com")
.post("/decide/?v=3", body => {
return body.token === "test" && body.distinct_id === "us_1234"
return body.token === token && body.distinct_id === distinct_id
})
.reply(200, flags)
.persist()
Expand Down Expand Up @@ -112,36 +116,19 @@ describe("feature flags", () => {
},
expected: { TEST_BOOLEAN: true },
},
{
it: "should be able to set boolean flags through the license",
licenseFlags: ["TEST_BOOLEAN"],
expected: { TEST_BOOLEAN: true },
},
{
it: "should not be able to override a negative environment flag from license",
environmentFlags: "default:!TEST_BOOLEAN",
licenseFlags: ["TEST_BOOLEAN"],
expected: { TEST_BOOLEAN: false },
},
{
it: "should not error on unrecognised PostHog flag",
posthogFlags: {
featureFlags: { UNDEFINED: true },
},
expected: flags.defaults(),
},
{
it: "should not error on unrecognised license flag",
licenseFlags: ["UNDEFINED"],
expected: flags.defaults(),
},
])(
"$it",
async ({
identity,
environmentFlags,
posthogFlags,
licenseFlags,
expected,
errorMessage,
}) => {
Expand All @@ -157,8 +144,6 @@ describe("feature flags", () => {
env.POSTHOG_API_HOST = "https://us.i.posthog.com"
}

const ctx = { user: { license: { features: licenseFlags || [] } } }

await withEnv(env, async () => {
// We need to pass in node-fetch here otherwise nock won't get used
// because posthog-node uses axios under the hood.
Expand All @@ -180,18 +165,13 @@ describe("feature flags", () => {

await context.doInIdentityContext(fullIdentity, async () => {
if (errorMessage) {
await expect(flags.fetch(ctx as UserCtx)).rejects.toThrow(
errorMessage
)
await expect(flags.fetch()).rejects.toThrow(errorMessage)
} else if (expected) {
const values = await flags.fetch(ctx as UserCtx)
const values = await flags.fetch()
expect(values).toMatchObject(expected)

for (const [key, expectedValue] of Object.entries(expected)) {
const value = await flags.get(
key as keyof typeof schema,
ctx as UserCtx
)
const value = await flags.get(key as keyof typeof schema)
expect(value).toBe(expectedValue)
}
} else {
Expand All @@ -214,6 +194,14 @@ describe("feature flags", () => {
lastName: "User",
}

// We need to pass in node-fetch here otherwise nock won't get used
// because posthog-node uses axios under the hood.
init({
fetch: (url, opts) => {
return nodeFetch(url, opts)
},
})

nock("https://us.i.posthog.com")
.post("/decide/?v=3", body => {
return body.token === "test" && body.distinct_id === "us_1234"
Expand All @@ -230,4 +218,44 @@ describe("feature flags", () => {
}
)
})

it("should still get flags when user is logged out", async () => {
const env: Partial<typeof environment> = {
SELF_HOSTED: false,
POSTHOG_FEATURE_FLAGS_ENABLED: "true",
POSTHOG_API_HOST: "https://us.i.posthog.com",
POSTHOG_TOKEN: "test",
}

const ip = "127.0.0.1"
const hashedIp = crypto.createHash("sha512").update(ip).digest("hex")

await withEnv(env, async () => {
mockPosthogFlags(
{
featureFlags: { TEST_BOOLEAN: true },
},
{
distinct_id: hashedIp,
}
)

// We need to pass in node-fetch here otherwise nock won't get used
// because posthog-node uses axios under the hood.
init({
fetch: (url, opts) => {
return nodeFetch(url, opts)
},
})

await context.doInIPContext(ip, async () => {
await context.doInTenant("default", async () => {
const result = await flags.fetch()
expect(result.TEST_BOOLEAN).toBe(true)
})
})

shutdown()
})
})
})
1 change: 1 addition & 0 deletions packages/backend-core/src/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export { default as correlation } from "../logging/correlation/middleware"
export { default as errorHandling } from "./errorHandling"
export { default as querystringToBody } from "./querystringToBody"
export * as joiValidator from "./joi-validator"
export { default as ip } from "./ip"
12 changes: 12 additions & 0 deletions packages/backend-core/src/middleware/ip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Ctx } from "@budibase/types"
import { doInIPContext } from "../context"

export default async (ctx: Ctx, next: any) => {
if (ctx.ip) {
return await doInIPContext(ctx.ip, () => {
return next()
})
} else {
return next()
}
}
8 changes: 7 additions & 1 deletion packages/backend-core/src/security/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ export enum BuiltinPermissionID {
POWER = "power",
}

export const BUILTIN_PERMISSIONS = {
export const BUILTIN_PERMISSIONS: {
[key in keyof typeof BuiltinPermissionID]: {
_id: (typeof BuiltinPermissionID)[key]
name: string
permissions: Permission[]
}
} = {
PUBLIC: {
_id: BuiltinPermissionID.PUBLIC,
name: "Public",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
loadDependantInfo()
</script>
<ModalContent showCancelButton={false} confirmText="Done">
<ModalContent showCancelButton={false} showConfirmButton={false}>
<span slot="header">
Manage Access
{#if requiresPlanToModify}
Expand Down
Loading

0 comments on commit 6a25f66

Please sign in to comment.