feat(connect): Skatteverket data calls through the connector with a per-company canary (#2209)
CONNECT_SKV_CANARY_COMPANIES mirrors the bank canary: the listed companies' user-token Skatteverket calls (skattekonto, moms, AGI) go through the connector's data proxy while this installation still has its own credentials and keeps refreshing the tokens on its own OAuth client. Callers without a company id (OAuth start, token exchange, environment reporting) keep the plain rule: own credentials win. This is how hosted moves its Skatteverket traffic to Connect a few companies at a time before dropping its own keys. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
This commit is contained in:
co-authored by
Jakob Wennberg
parent
65bd675f43
commit
d900fea1a8
@@ -35,6 +35,7 @@ const ENV_VARS = [
|
||||
'SKATTEVERKET_AGD_PERIOD_API_BASE_URL',
|
||||
'GNUBOK_CONNECTOR_KEY',
|
||||
'GNUBOK_CONNECT_URL',
|
||||
'CONNECT_SKV_CANARY_COMPANIES',
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -316,6 +317,32 @@ describe('skvRequestWithAuth: connector mode is OFF with own credentials (direct
|
||||
expect(headers['X-Connector-Upstream-Content-Type']).toBeUndefined()
|
||||
expect(JSON.stringify(headers)).not.toContain('gnubok_ck_')
|
||||
})
|
||||
|
||||
it('CONNECT_SKV_CANARY_COMPANIES reroutes only the listed company through the proxy', async () => {
|
||||
// Hosted moving to Connect upstream by upstream: a listed company's data
|
||||
// calls go through the proxy (no gateway credentials, user Bearer in the
|
||||
// upstream-auth header); every other company stays on the direct path.
|
||||
process.env.SKATTEVERKET_APIGW_CLIENT_ID = 'gw-id'
|
||||
process.env.SKATTEVERKET_APIGW_CLIENT_SECRET = 'gw-secret'
|
||||
process.env.SKATTEVERKET_API_BASE_URL = 'https://api.test.example/moms'
|
||||
process.env.CONNECT_SKV_CANARY_COMPANIES = 'comp-1'
|
||||
const fetchMock = mockFetchStatus(200, '{"ok":true}')
|
||||
|
||||
await skvRequest(fakeSupabase, 'user-1', 'comp-1', 'GET', '/deklarationer')
|
||||
const [canaryUrl, canaryInit] = lastFetchCall(fetchMock)
|
||||
expect(canaryUrl).toBe('https://app.hosted.example/api/connect/skv/api/moms/deklarationer')
|
||||
const canaryHeaders = canaryInit.headers as Record<string, string>
|
||||
expect(canaryHeaders['Authorization']).toBe('Bearer gnubok_ck_test')
|
||||
expect(canaryHeaders['X-Connector-Upstream-Authorization']).toBe('Bearer user-skv-token')
|
||||
expect(canaryHeaders['Client_Id']).toBeUndefined()
|
||||
expect(canaryHeaders['Client_Secret']).toBeUndefined()
|
||||
|
||||
await skvRequest(fakeSupabase, 'user-1', 'comp-2', 'GET', '/deklarationer')
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
const [directUrl, directInit] = fetchMock.mock.calls[1] as unknown as [string, RequestInit]
|
||||
expect(directUrl).toBe('https://api.test.example/moms/deklarationer')
|
||||
expect((directInit.headers as Record<string, string>)['Client_Id']).toBe('gw-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('skvRequestWithAuth: system mode is never brokered', () => {
|
||||
|
||||
@@ -419,7 +419,11 @@ export async function skvRequestWithAuth(
|
||||
// System (CCG) auth is deliberately NOT brokered: background ombud reads
|
||||
// are a hosted-only feature and stay on the direct path, where a
|
||||
// credential-less self-host fails with SYSTEM_AUTH_FAILED.
|
||||
const connector = auth.mode === 'user' ? skatteverketConnectorMode() : null
|
||||
// The company id lets CONNECT_SKV_CANARY_COMPANIES route a few companies
|
||||
// through the connector while this installation still has own credentials
|
||||
// (hosted moving to Connect upstream by upstream); token refresh stays on
|
||||
// whichever path the installation as a whole is on.
|
||||
const connector = auth.mode === 'user' ? skatteverketConnectorMode(auth.companyId) : null
|
||||
const effectiveBase = options?.baseUrl || getApiBaseUrl()
|
||||
let url: string
|
||||
const headers: Record<string, string> = {}
|
||||
|
||||
Reference in New Issue
Block a user