Merge pull request #15793 from Budibase/BUDI-9127/display-grant-type

Display grant type field
This commit is contained in:
Adria Navarro 2025-03-24 15:00:22 +01:00 committed by GitHub
commit 234d23d4f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 220 additions and 151 deletions

View File

@ -15,6 +15,7 @@
import type { InsertOAuth2ConfigRequest } from "@budibase/types"
import {
OAuth2CredentialsMethod,
OAuth2GrantType,
PASSWORD_REPLACEMENT,
} from "@budibase/types"
import type { ZodType } from "zod"
@ -27,6 +28,8 @@
$: data = (config as Partial<OAuth2Config>) ?? {}
$: data.grantType ??= OAuth2GrantType.CLIENT_CREDENTIALS
$: isCreation = !config
$: title = isCreation
? "Create new OAuth2 connection"
@ -64,6 +67,9 @@
method: z.nativeEnum(OAuth2CredentialsMethod, {
message: "Authentication method is required.",
}),
grantType: z.nativeEnum(OAuth2GrantType, {
message: "Grant type is required.",
}),
}) satisfies ZodType<InsertOAuth2ConfigRequest>
const validationResult = validator.safeParse(config)
@ -97,6 +103,7 @@
clientId: configData.clientId,
clientSecret: configData.clientSecret,
method: configData.method,
grantType: configData.grantType,
})
if (!connectionValidation.valid) {
let message = "Connection settings could not be validated"
@ -158,6 +165,24 @@
access_token property.
</Body>
</div>
<Select
label="Grant type*"
options={[
{
label: "Client credentials",
value: OAuth2GrantType.CLIENT_CREDENTIALS,
},
]}
bind:value={data.grantType}
error={errors.grantType}
disabled
/>
<div class="field-info">
<Body size="XS" color="var(--spectrum-global-color-gray-700)">
Only client credentials mode is supported currently.
</Body>
</div>
<Input
label="Service URL*"
placeholder="E.g. www.google.com"

View File

@ -38,6 +38,7 @@ export class OAuth2Store extends BudiStore<OAuth2StoreState> {
clientId: c.clientId,
clientSecret: c.clientSecret,
method: c.method,
grantType: c.grantType,
lastUsage: c.lastUsage,
})),
loading: false,

View File

@ -24,6 +24,7 @@ function toFetchOAuth2ConfigsResponse(
clientId: config.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: config.method,
grantType: config.grantType,
}
}
@ -54,6 +55,7 @@ export async function create(
clientId: body.clientId,
clientSecret: body.clientSecret,
method: body.method,
grantType: body.grantType,
}
const config = await sdk.oauth2.create(newConfig)
@ -80,6 +82,7 @@ export async function edit(
clientId: body.clientId,
clientSecret: body.clientSecret,
method: body.method,
grantType: body.grantType,
}
const config = await sdk.oauth2.update(toUpdate)
@ -104,6 +107,7 @@ export async function validate(
clientId: body.clientId,
clientSecret: body.clientSecret,
method: body.method,
grantType: body.grantType,
}
if (config.clientSecret === PASSWORD_REPLACEMENT && body._id) {

View File

@ -1,5 +1,9 @@
import Router from "@koa/router"
import { OAuth2CredentialsMethod, PermissionType } from "@budibase/types"
import {
OAuth2CredentialsMethod,
OAuth2GrantType,
PermissionType,
} from "@budibase/types"
import { middleware } from "@budibase/backend-core"
import authorized from "../../middleware/authorized"
@ -13,6 +17,9 @@ const baseSchema = {
method: Joi.string()
.required()
.valid(...Object.values(OAuth2CredentialsMethod)),
grantType: Joi.string()
.required()
.valid(...Object.values(OAuth2GrantType)),
}
const insertSchema = Joi.object({

View File

@ -3,6 +3,7 @@ import {
InsertOAuth2ConfigRequest,
OAuth2ConfigResponse,
OAuth2CredentialsMethod,
OAuth2GrantType,
PASSWORD_REPLACEMENT,
} from "@budibase/types"
import * as setup from "./utilities"
@ -19,6 +20,7 @@ describe("/oauth2", () => {
clientId: generator.guid(),
clientSecret: generator.hash(),
method: generator.pickone(Object.values(OAuth2CredentialsMethod)),
grantType: generator.pickone(Object.values(OAuth2GrantType)),
}
}
@ -58,6 +60,7 @@ describe("/oauth2", () => {
clientId: c.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: c.method,
grantType: c.grantType,
}))
),
})
@ -80,6 +83,7 @@ describe("/oauth2", () => {
clientId: oauth2Config.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: oauth2Config.method,
grantType: oauth2Config.grantType,
},
],
})
@ -102,6 +106,7 @@ describe("/oauth2", () => {
clientId: oauth2Config.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: oauth2Config.method,
grantType: oauth2Config.grantType,
},
{
_id: expectOAuth2ConfigId,
@ -111,6 +116,7 @@ describe("/oauth2", () => {
clientId: oauth2Config2.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: oauth2Config2.method,
grantType: oauth2Config2.grantType,
},
])
)
@ -139,6 +145,7 @@ describe("/oauth2", () => {
clientId: oauth2Config.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: oauth2Config.method,
grantType: oauth2Config.grantType,
},
])
})
@ -177,6 +184,7 @@ describe("/oauth2", () => {
clientId: configData.clientId,
clientSecret: PASSWORD_REPLACEMENT,
method: configData.method,
grantType: configData.grantType,
},
])
)

View File

@ -6,6 +6,7 @@ import {
BearerRestAuthConfig,
BodyType,
OAuth2CredentialsMethod,
OAuth2GrantType,
RestAuthType,
} from "@budibase/types"
import { Response } from "node-fetch"
@ -286,6 +287,7 @@ describe("REST Integration", () => {
clientId: generator.guid(),
clientSecret: secret,
method: OAuth2CredentialsMethod.HEADER,
grantType: OAuth2GrantType.CLIENT_CREDENTIALS,
})
const token = generator.guid()
@ -323,6 +325,7 @@ describe("REST Integration", () => {
clientId: generator.guid(),
clientSecret: secret,
method: OAuth2CredentialsMethod.BODY,
grantType: OAuth2GrantType.CLIENT_CREDENTIALS,
})
const token = generator.guid()

View File

@ -6,7 +6,7 @@ import { getToken } from "../utils"
import path from "path"
import { KEYCLOAK_IMAGE } from "../../../../integrations/tests/utils/images"
import { startContainer } from "../../../../integrations/tests/utils"
import { OAuth2CredentialsMethod } from "@budibase/types"
import { OAuth2CredentialsMethod, OAuth2GrantType } from "@budibase/types"
import { cache } from "@budibase/backend-core"
import tk from "timekeeper"
@ -44,169 +44,175 @@ describe("oauth2 utils", () => {
keycloakUrl = `http://127.0.0.1:${port}`
})
describe.each(Object.values(OAuth2CredentialsMethod))(
"getToken (in %s)",
method => {
it("successfully generates tokens", async () => {
const response = await config.doInContext(config.appId, async () => {
describe.each(
Object.values(OAuth2CredentialsMethod).flatMap(method =>
Object.values(OAuth2GrantType).map<
[OAuth2CredentialsMethod, OAuth2GrantType]
>(grantType => [method, grantType])
)
)("generateToken (in %s, grant type %s)", (method, grantType) => {
it("successfully generates tokens", async () => {
const response = await config.doInContext(config.appId, async () => {
const oauthConfig = await sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "my-secret",
method,
grantType,
})
const response = await getToken(oauthConfig._id)
return response
})
expect(response).toEqual(expect.stringMatching(/^Bearer .+/))
})
it("handles wrong urls", async () => {
await expect(
config.doInContext(config.appId, async () => {
const oauthConfig = await sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/wrong/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "my-secret",
method,
grantType,
})
await getToken(oauthConfig._id)
})
).rejects.toThrow("Error fetching oauth2 token: Not Found")
})
it("handles wrong client ids", async () => {
await expect(
config.doInContext(config.appId, async () => {
const oauthConfig = await sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "wrong-client-id",
clientSecret: "my-secret",
method,
grantType,
})
await getToken(oauthConfig._id)
})
).rejects.toThrow(
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
)
})
it("handles wrong secrets", async () => {
await expect(
config.doInContext(config.appId, async () => {
const oauthConfig = await sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "wrong-secret",
method,
grantType,
})
await getToken(oauthConfig._id)
})
).rejects.toThrow(
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
)
})
describe("track usages", () => {
beforeAll(() => {
tk.freeze(Date.now())
})
it("tracks usages on generation", async () => {
const oauthConfig = await config.doInContext(config.appId, () =>
sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "my-secret",
method,
grantType,
})
const response = await getToken(oauthConfig._id)
return response
})
expect(response).toEqual(expect.stringMatching(/^Bearer .+/))
})
it("handles wrong urls", async () => {
await expect(
config.doInContext(config.appId, async () => {
const oauthConfig = await sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/wrong/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "my-secret",
method,
})
await getToken(oauthConfig._id)
})
).rejects.toThrow("Error fetching oauth2 token: Not Found")
})
it("handles wrong client ids", async () => {
await expect(
config.doInContext(config.appId, async () => {
const oauthConfig = await sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "wrong-client-id",
clientSecret: "my-secret",
method,
})
await getToken(oauthConfig._id)
})
).rejects.toThrow(
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
)
})
it("handles wrong secrets", async () => {
await expect(
config.doInContext(config.appId, async () => {
const oauthConfig = await sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "wrong-secret",
method,
})
await getToken(oauthConfig._id)
})
).rejects.toThrow(
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
await config.doInContext(config.appId, () => getToken(oauthConfig._id))
await testUtils.queue.processMessages(
cache.docWritethrough.DocWritethroughProcessor.queue
)
const usageLog = await config.doInContext(config.appId, () =>
sdk.oauth2.getLastUsages([oauthConfig._id])
)
expect(usageLog[oauthConfig._id]).toEqual(Date.now())
})
describe("track usages", () => {
beforeAll(() => {
tk.freeze(Date.now())
})
it("does not track on failed usages", async () => {
const oauthConfig = await config.doInContext(config.appId, () =>
sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "wrong-client",
clientSecret: "my-secret",
method,
grantType,
})
)
it("tracks usages on generation", async () => {
const oauthConfig = await config.doInContext(config.appId, () =>
sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "my-secret",
method,
})
)
await expect(
config.doInContext(config.appId, () => getToken(oauthConfig._id))
).rejects.toThrow()
await testUtils.queue.processMessages(
cache.docWritethrough.DocWritethroughProcessor.queue
)
await config.doInContext(config.appId, () =>
getToken(oauthConfig._id)
)
await testUtils.queue.processMessages(
cache.docWritethrough.DocWritethroughProcessor.queue
)
const usageLog = await config.doInContext(config.appId, () =>
sdk.oauth2.getLastUsages([oauthConfig._id])
)
const usageLog = await config.doInContext(config.appId, () =>
expect(usageLog[oauthConfig._id]).toBeUndefined()
})
it("tracks usages between prod and dev, keeping always the latest", async () => {
const oauthConfig = await config.doInContext(config.appId, () =>
sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "my-secret",
method,
grantType,
})
)
await config.doInContext(config.appId, () => getToken(oauthConfig._id))
await config.publish()
tk.travel(Date.now() + 100)
await config.doInContext(config.prodAppId, () =>
getToken(oauthConfig._id)
)
await testUtils.queue.processMessages(
cache.docWritethrough.DocWritethroughProcessor.queue
)
for (const appId of [config.appId, config.prodAppId]) {
const usageLog = await config.doInContext(appId, () =>
sdk.oauth2.getLastUsages([oauthConfig._id])
)
expect(usageLog[oauthConfig._id]).toEqual(Date.now())
})
it("does not track on failed usages", async () => {
const oauthConfig = await config.doInContext(config.appId, () =>
sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "wrong-client",
clientSecret: "my-secret",
method,
})
)
await expect(
config.doInContext(config.appId, () => getToken(oauthConfig._id))
).rejects.toThrow()
await testUtils.queue.processMessages(
cache.docWritethrough.DocWritethroughProcessor.queue
)
const usageLog = await config.doInContext(config.appId, () =>
sdk.oauth2.getLastUsages([oauthConfig._id])
)
expect(usageLog[oauthConfig._id]).toBeUndefined()
})
it("tracks usages between prod and dev, keeping always the latest", async () => {
const oauthConfig = await config.doInContext(config.appId, () =>
sdk.oauth2.create({
name: generator.guid(),
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
clientId: "my-client",
clientSecret: "my-secret",
method,
})
)
await config.doInContext(config.appId, () =>
getToken(oauthConfig._id)
)
await config.publish()
tk.travel(Date.now() + 100)
await config.doInContext(config.prodAppId, () =>
getToken(oauthConfig._id)
)
await testUtils.queue.processMessages(
cache.docWritethrough.DocWritethroughProcessor.queue
)
for (const appId of [config.appId, config.prodAppId]) {
const usageLog = await config.doInContext(appId, () =>
sdk.oauth2.getLastUsages([oauthConfig._id])
)
expect(usageLog).toEqual({
[oauthConfig._id]: Date.now(),
})
}
})
expect(usageLog).toEqual({
[oauthConfig._id]: Date.now(),
})
}
})
}
)
})
})
})

View File

@ -1,7 +1,11 @@
import fetch, { RequestInit } from "node-fetch"
import { HttpError } from "koa"
import { get } from "../oauth2"
import { Document, OAuth2CredentialsMethod } from "@budibase/types"
import {
Document,
OAuth2CredentialsMethod,
OAuth2GrantType,
} from "@budibase/types"
import { cache, context, docIds } from "@budibase/backend-core"
interface OAuth2LogDocument extends Document {
@ -15,6 +19,7 @@ async function fetchToken(config: {
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
}) {
const fetchConfig: RequestInit = {
method: "POST",
@ -37,7 +42,7 @@ async function fetchToken(config: {
}
} else {
fetchConfig.body = new URLSearchParams({
grant_type: "client_credentials",
grant_type: config.grantType,
client_id: config.clientId,
client_secret: config.clientSecret,
})
@ -82,6 +87,7 @@ export async function validateConfig(config: {
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
}): Promise<{ valid: boolean; message?: string }> {
try {
const resp = await fetchToken(config)

View File

@ -1,4 +1,4 @@
import { OAuth2CredentialsMethod } from "@budibase/types"
import { OAuth2CredentialsMethod, OAuth2GrantType } from "@budibase/types"
export interface OAuth2ConfigResponse {
_id: string
@ -8,6 +8,7 @@ export interface OAuth2ConfigResponse {
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
}
export interface FetchOAuth2ConfigsResponse {
@ -20,6 +21,7 @@ export interface InsertOAuth2ConfigRequest {
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
}
export interface InsertOAuth2ConfigResponse {
@ -34,6 +36,7 @@ export interface UpdateOAuth2ConfigRequest {
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
}
export interface UpdateOAuth2ConfigResponse {
@ -46,6 +49,7 @@ export interface ValidateConfigRequest {
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
}
export interface ValidateConfigResponse {

View File

@ -5,10 +5,15 @@ export enum OAuth2CredentialsMethod {
BODY = "BODY",
}
export enum OAuth2GrantType {
CLIENT_CREDENTIALS = "client_credentials",
}
export interface OAuth2Config extends Document {
name: string
url: string
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
grantType: OAuth2GrantType
}