-
Notifications
You must be signed in to change notification settings - Fork 90
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
base: 2893/multi-tenancy-v1
Are you sure you want to change the base?
feat: handle updating operator api secret #3328
Conversation
…submit sections independently
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 => { |
There was a problem hiding this comment.
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.
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) | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, of course. updated.
// Update Operator Tenant from config | ||
const tenantService = await container.use('tenantService') | ||
const error = await tenantService.updateOperatorApiSecretFromConfig() | ||
if (error) throw error | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…or as needed - previous way of controlling if the request was from an operator or not no longer worked after changing app start to sync apiSecret
@@ -85,7 +86,7 @@ describe('Tenant Resolvers', (): void => { | |||
}) | |||
|
|||
afterEach(async (): Promise<void> => { | |||
await truncateTables(appContainer.knex, true) |
There was a problem hiding this comment.
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).
deps: IocContract<AppServices>, | ||
options?: { truncateTenants?: boolean } |
There was a problem hiding this comment.
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.
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) | ||
} | ||
} |
There was a problem hiding this comment.
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.
Co-authored-by: Max Kurapov <max@interledger.org>
await Tenant.query(deps.knex) | ||
.patch({ apiSecret: adminApiSecret }) | ||
.where('id', operatorTenantId) | ||
await tenant.$query(deps.knex).patch({ apiSecret: adminApiSecret }) |
There was a problem hiding this comment.
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)
Changes proposed in this pull request
truncateTables
interface to remove possibility of truncating wrong schemaContext
fixes #3276
Checklist
fixes #number
user-docs
label (if necessary)