Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle updating operator api secret #3328

Open
wants to merge 26 commits into
base: 2893/multi-tenancy-v1
Choose a base branch
from

Conversation

BlairCurrey
Copy link
Contributor

@BlairCurrey BlairCurrey commented Mar 4, 2025

Changes proposed in this pull request

  • Disallows operator from updating their api secret via admin api (implemented at resolver level, as opposed to service)
  • Adds method to update operator api secret from config (if it has changed)
  • Changes tenant create form to submit sections independently, similar to our other create forms
  • Changes truncateTables interface to remove possibility of truncating wrong schema

Context

fixes #3276

Checklist

  • Related issues linked using fixes #number
  • Tests added/updated
  • Make sure that all checks pass
  • Bruno collection updated (if necessary)
  • Documentation issue created with user-docs label (if necessary)
  • OpenAPI specs updated (if necessary)

@github-actions github-actions bot added type: tests Testing related pkg: backend Changes in the backend package. pkg: frontend Changes in the frontend package. type: source Changes business logic labels Mar 4, 2025
@BlairCurrey
Copy link
Contributor Author

BlairCurrey commented Mar 4, 2025

Intended to add some docs explaining how to update operator api secret and how it would require coordination with the integration server owner (would need to update in tandem). However it seems like there is nothing for multi-tenancy so I wasnt sure where to put it and figure we need to think about docs for mt in general. Brought this up on this in our slack workspace here: https://interledgerfoundation.slack.com/archives/C06KZCKEEKW/p1741114127552369

@@ -447,3 +446,89 @@ describe('Tenant Service', (): void => {
})
})
})

describe('Tenant Service (no tenant truncate)', (): void => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds an entirely new top-level describe block for the service test which re-inits the dependencies and app and everything.

I did this strictly to manage the truncating of the tenants table differently. I do not want to truncate for these tests, but the other tests do. I was unable to unify them. I think it's OK but just noting because it might not be obvious why this was necessary.

Comment on lines 195 to 213
async function updateOperatorApiSecretFromConfig(
deps: ServiceDependencies
): Promise<undefined | TenantError> {
const { adminApiSecret, operatorTenantId } = deps.config

const tenant = await Tenant.query(deps.knex)
.findById(operatorTenantId)
.whereNull('deletedAt')
.first()

if (!tenant) {
return TenantError.TenantNotFound
}
if (tenant.apiSecret !== adminApiSecret) {
await Tenant.query(deps.knex)
.patch({ apiSecret: adminApiSecret })
.where('id', operatorTenantId)
}
}
Copy link
Contributor Author

@BlairCurrey BlairCurrey Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New method to update the secret from config. I made a new method rather than using the update because:

  • I wanted to be opinionated about where the secret and operator id are coming from. This is coming from the config, not just some value being passed in which is intended to be from the config.
  • I didnt want to update unless its changed and I didnt want to add that logic to the update just for this. Perhaps we could just always write it but figured this was safer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just noticing that this exemplifies the value of the repo layer... notice we use the cache in the service get method but we cant use that here (could if we were getting from the repo). I suppose if thats important I can use/update the cache here as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to have another method. As far as caching goes, I think we should update it if we end up updating the secret.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, of course. updated.

Comment on lines +772 to +776
// Update Operator Tenant from config
const tenantService = await container.use('tenantService')
const error = await tenantService.updateOperatorApiSecretFromConfig()
if (error) throw error

Copy link
Contributor Author

@BlairCurrey BlairCurrey Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is happening in the start method after we run migrations and start the admin server, etc.

Is this the correct behavior? It will error if the operator id from the env var is not found (tenant service returns an error), or some unknown error throws.

This will show an error log and exit the process. I debated catching and logging at warning level because strictly speaking the app will still work. Operator tenant id isnt (at this point) used for anything else in the app (just used in the initial migration and here). So it can work without this but it could be in an unexpected state (still with the old api secret). You wouldnt know unless dutifully watching the logs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it more, we throw an error when parsing the config if OPERATOR_TENANT_ID is not set. So I think its probably correct to throw here if we know its bad.

@@ -85,7 +86,7 @@ describe('Tenant Resolvers', (): void => {
})

afterEach(async (): Promise<void> => {
await truncateTables(appContainer.knex, true)
Copy link
Contributor Author

@BlairCurrey BlairCurrey Mar 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example of the problem that lead to changing the truncateTables interface. #3328 (comment)

We used a non-default db schema but then didnt pass it into truncateTables. This test ended up having a side effect of deleting the tenants from other schemas and causing unexpected test failures because now the app looks up the operator tenant on start (to maybe update api secret from config).

Comment on lines +14 to +15
deps: IocContract<AppServices>,
options?: { truncateTenants?: boolean }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tightens up the truncateTables interface. Most of the changed files in this PR are just changing how truncateTables is called.

When we added the options to truncate tenants and provide a schema we created an opportunity to truncate tenants from the wrong schema.

For example, here, where we truncated with no dbSchema provided: https://github.com/interledger/rafiki/pull/3328/files#r1982206044

This coincidentally wasnt causing problems as most tests just werent interacting with the tenants. However, with these changes we attempt to fetch the tenant every time the app starts (so every test via createTestApp) which yielded many TenantNotFound errors because another test had dropped them.

This removes the (not completely obvious) responsibility from the caller to pass in dbSchema when truncating tenants. It just uses the config directly to truncate from the same dbSchema that the app is using.

@BlairCurrey BlairCurrey marked this pull request as ready for review March 5, 2025 21:41
Comment on lines 195 to 213
async function updateOperatorApiSecretFromConfig(
deps: ServiceDependencies
): Promise<undefined | TenantError> {
const { adminApiSecret, operatorTenantId } = deps.config

const tenant = await Tenant.query(deps.knex)
.findById(operatorTenantId)
.whereNull('deletedAt')
.first()

if (!tenant) {
return TenantError.TenantNotFound
}
if (tenant.apiSecret !== adminApiSecret) {
await Tenant.query(deps.knex)
.patch({ apiSecret: adminApiSecret })
.where('id', operatorTenantId)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to have another method. As far as caching goes, I think we should update it if we end up updating the secret.

@BlairCurrey BlairCurrey requested a review from mkurapov March 7, 2025 15:42
await Tenant.query(deps.knex)
.patch({ apiSecret: adminApiSecret })
.where('id', operatorTenantId)
await tenant.$query(deps.knex).patch({ apiSecret: adminApiSecret })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that .$query().patch updates tenant in place (ie, cache still ends up saving the old tenant). Based on the test you added I think await tenantCache.get(tenant.id) is actually hitting the DB directly (since it's the first time it's called)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: backend Changes in the backend package. pkg: frontend Changes in the frontend package. type: source Changes business logic type: tests Testing related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants