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: support queries to generate self-service api-keys #428

Merged
merged 28 commits into from
Nov 8, 2024

Conversation

swetabar
Copy link
Contributor

@swetabar swetabar commented Nov 7, 2024

Related Issues

SITES-25915

Supporting the following queries with this PR:

  • getApiKeysByImsUserIdAndImsOrgId
  • getApiKeyById
  • updateApiKey

Sample apiKey entry in the DB:

{
  id: '321456987efgh',
  hashedApiKey: 'axspbgh5649075',
  name: 'test-name',
  imsUserId: 'test-ims-user-id',
  imsOrgId: 'test-ims-org-id',
  createdAt: '2024-10-29T14:26:00.000Z'
  expiresAt: '2025-05-29T14:26:00.000Z',
  scopes: [{
        name: 'imports.write',
        domains: ['https://www.test.com'],
      }],
}

readonly ACTIVE: 'ACTIVE';
readonly INACTIVE: 'INACTIVE';
};

// TODO: introduce AuditType interface or Scores interface
Copy link
Contributor

Choose a reason for hiding this comment

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

Intentional TODO?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was already present in the codebase.

packages/spacecat-shared-data-access/src/dto/api-key.js Outdated Show resolved Hide resolved
@@ -29,6 +29,8 @@ export const ApiKeyDto = {
createdAt: apiKey.getCreatedAt(),
expiresAt: apiKey.getExpiresAt(),
revokedAt: apiKey.getRevokedAt(),
deletedAt: apiKey.getDeletedAt(),
status: apiKey.getStatus(),
Copy link
Contributor

Choose a reason for hiding this comment

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

What does status represent? A sample API Key object in the PR description with details on each field would be helpful.

readonly ACTIVE: 'ACTIVE';
readonly INACTIVE: 'INACTIVE';
};

// TODO: introduce AuditType interface or Scores interface
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was already present in the codebase.

getApiKeyByImsUserIdAndImsOrgId: (
imsUserId: string,
imsOrgId: string,
) => Promise<ApiKey | null>;
Copy link
Contributor

Choose a reason for hiding this comment

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

What if a user has more than 1 API key? IIRC, we were going to allow 3 per IMS User ID, so this query could return up to 3 results.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. Typo for the interface. I'll fix it.

* Api Key Status Types
* Any changes to this object needs to be reflected in the index.d.ts file as well.
*/
export const ApiKeyStatus = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we depend on this status property? For instance, if an API key "expires" by the passing of time but its entity in the DB is not updated to reflect that it is now INACTIVE, is the key still considered ACTIVE?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The key is still considered ACTIVE in that case. It is updated to INACTIVE when keys are deleted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just thinking out loud: would we be any worse off if we didn't have the status column? We'll have to check the deletedAt, revokedAt, and expiresAt anyways to determine if a key is still valid, so status seems potentially unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I could remove status for the api-key.

*/
self.updateDeletedAt = (deletedAt) => updateState('deletedAt', deletedAt, (value) => {
if (!isIsoDate(value)) {
throw new Error(`Invalid deletedAt during update: ${value}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Worth mentioning the expected format in this error?

*/
export const createNewApiKey = async (apiKey, dynamoClient, config, log) => {
const item = ApiKeyDto.toDynamoItem(apiKey);
await dynamoClient.putItem(config.tableNameApiKeys, item, log);
const item = createApiKey(apiKey);
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this function was previously invoked with an API key object. Has it changed to accept a POJO instead? If so, the JSDoc should be updated to reflect that.

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 think we were not using this method before. I refactored it to work similar to how import-job works now for consistency.

Copy link

github-actions bot commented Nov 8, 2024

This PR will trigger a minor release when merged.

"AttributeType": "S"
},
{
"AttributeName": "status",
Copy link
Contributor

Choose a reason for hiding this comment

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

Status is still present here.

* imsOrgId: *, expiresAt: *}}
* @returns {{createdAt: string, name: string, imsUserId: string,
* scopes: array<object>, revokedAt: string, deletedAt: string,
* hashedApiKey: *, imsOrgId: *, expiresAt: *}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add types.

self.getScopes = () => self.state.scopes;

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this model be the right place for an isValid() method, which could check all the internal properties to ensure that a key is still valid?

Copy link
Contributor

Choose a reason for hiding this comment

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

Or isActive(), alternatively?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could do that in the service. Do we need it in spacecat-shared?

Copy link
Contributor

Choose a reason for hiding this comment

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

If we do so in the service than anywhere that re-uses this model will need to implement something similar. I see it as a direct concern of the model itself, "am I still valid", and think that it makes sense to have that logic live alongside the model.

I also like the pattern of "asking" the entity itself, are you still valid: if (apiKeyInstance.isValid()) { — nice and clean.

self.isValid = () => {
const now = new Date();

if (self.state.deletedAt && new Date(self.state.deletedAt) > now) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this comparison correct? If deletedAt is > now, that would mean it was deleted in the future — right? And therefore still valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. Correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will be deleted in future. So, the key is still valid.

Comment on lines 57 to 63
if (self.state.revokedAt && new Date(self.state.revokedAt) > now) {
return false;
}

if (self.state.expiresAt && new Date(self.state.expiresAt) > now) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same issue with these two, I think.

If an expiry is set when the key is created (as it must) than it will always be in the future, which would mean that this check fails and the key is not considered valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

});

it('returns false if deletedAt is after the current time', () => {
apiKey.updateDeletedAt(new Date(Date.now() + 10000).toISOString());
Copy link
Contributor

Choose a reason for hiding this comment

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

Can concrete values please be used for these test cases so they are easier for a human to reason about?

ie. if a key expires in 2025 and it is 2024, it should still be valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

expect(apiKey.isValid()).to.be.false;
});

it('returns true if deletedAt, revokedAt, and expiresAt are before the current time', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the core of the issue, I think.

  • if expiresAt is in the future: still valid
  • if deletedAt or revokedAt are in the past: no longer valid

Copy link
Contributor

Choose a reason for hiding this comment

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

In other words: expiresAt is the only property we should expect to be in the future. If ANY of these properties are set to a date before now, the key should not be considered valid.

Copy link
Contributor

@blefebvre blefebvre left a comment

Choose a reason for hiding this comment

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

Minor item about dates in test cases.

Comment on lines 184 to 186
apiKey.updateDeletedAt('2025-05-27T14:26:00.000Z');
apiKey.updateRevokedAt('2025-05-27T14:26:00.000Z');
apiKey.updateExpiresAt('2025-05-27T14:26:00.000Z');
Copy link
Contributor

Choose a reason for hiding this comment

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

This test will start failing next May :)

Can we bump the year to 3025 please?

@swetabar swetabar self-assigned this Nov 8, 2024
@swetabar swetabar added the enhancement New feature or request label Nov 8, 2024
@swetabar swetabar merged commit 90a46c2 into main Nov 8, 2024
9 checks passed
@swetabar swetabar deleted the SITES-25915-generate-api-keys-for-importer branch November 8, 2024 19:28
adobe-bot pushed a commit that referenced this pull request Nov 8, 2024
# [@adobe/spacecat-shared-data-access-v1.50.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.49.8...@adobe/spacecat-shared-data-access-v1.50.0) (2024-11-08)

### Features

* support queries to generate self-service api-keys ([#428](#428)) ([90a46c2](90a46c2))
@adobe-bot
Copy link

🎉 This PR is included in version @adobe/spacecat-shared-data-access-v1.50.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request released
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants