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

UI: Add capabilities service #28168

Merged
merged 13 commits into from
Aug 23, 2024
49 changes: 49 additions & 0 deletions ui/app/services/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Service, { service } from '@ember/service';

import type StoreService from 'vault/services/store';

export default class CapabilitiesService extends Service {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

could also call this @permissions if we're worried about confusing it with the @lazyCapabilities method

@service declare readonly store: StoreService;

request = (apiPath: string) => {
return this.store.findRecord('capabilities', apiPath);
};

async fetchAll(apiPath: string) {
try {
return await this.request(apiPath);
} catch (e) {
return e;
}
}

async fetchSpecific(apiPath: string, capability: string) {
try {
const capabilities = await this.request(apiPath);
return capabilities[capability];
} catch (e) {
return e;
}
}
hellobontempo marked this conversation as resolved.
Show resolved Hide resolved

async canRead(apiPath: string) {
try {
return await this.fetchSpecific(apiPath, 'canRead');
} catch (e) {
return e;
}
}

async canUpdate(apiPath: string) {
try {
return await this.fetchSpecific(apiPath, 'canUpdate');
} catch (e) {
return e;
}
}
}
2 changes: 1 addition & 1 deletion ui/app/services/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/

import Service, { inject as service } from '@ember/service';
import Service, { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { keepLatestTask } from 'ember-concurrency';
import { DEBUG } from '@glimmer/env';
Expand Down
Copy link
Contributor Author

@hellobontempo hellobontempo Aug 22, 2024

Choose a reason for hiding this comment

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

move this to addon so in templates we can do things like

{{#if (await (this.capabilities.canRead "secret/data/my-secret"))}}
   Show something...
{{/if}}

File renamed without changes.
6 changes: 6 additions & 0 deletions ui/lib/core/app/helpers/await.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

export { default } from 'core/helpers/await';
79 changes: 79 additions & 0 deletions ui/tests/unit/services/capabilities-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';

module('Unit | Service | capabilities', function (hooks) {
setupTest(hooks);
setupMirage(hooks);

hooks.beforeEach(function () {
this.capabilities = this.owner.lookup('service:capabilities');
this.store = this.owner.lookup('service:store');
this.generateResponse = (apiPath, perms) => {
return {
[apiPath]: perms,
capabilities: perms,
request_id: '6cc7a484-921a-a730-179c-eaf6c6fbe97e',
data: {
capabilities: perms,
[apiPath]: perms,
},
};
};
});

test('it makes request to capabilities-self', function (assert) {
const apiPath = '/my/api/path';
const expectedPayload = {
paths: [apiPath],
};
this.server.post('/sys/capabilities-self', (schema, req) => {
const actual = JSON.parse(req.requestBody);
assert.true(true, 'request made to capabilities-self');
assert.propEqual(actual, expectedPayload, `request made with path: ${JSON.stringify(actual)}`);
return this.generateResponse(apiPath, ['read']);
});
this.capabilities.request(apiPath);
});

const TEST_CASES = [
{
capabilities: ['read'],
canRead: true,
canUpdate: false,
},
{
capabilities: ['update'],
canRead: false,
canUpdate: true,
},
{
capabilities: ['deny'],
canRead: false,
canUpdate: false,
},
{
capabilities: ['read', 'update'],
canRead: true,
canUpdate: true,
},
];
TEST_CASES.forEach(({ capabilities, canRead, canUpdate }) => {
test(`it returns expected boolean for "${capabilities.join(', ')}"`, async function (assert) {
const apiPath = '/my/api/path';
this.server.post('/sys/capabilities-self', () => {
return this.generateResponse(apiPath, capabilities);
});

const canReadResponse = await this.capabilities.canRead(apiPath);
const canUpdateResponse = await this.capabilities.canUpdate(apiPath);
assert[canRead](canReadResponse, `canRead returns ${canRead}`);
assert[canUpdate](canUpdateResponse, `canUpdate returns ${canRead}`);
});
});
});
1 change: 1 addition & 0 deletions ui/types/vault/services/store.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export default class StoreService extends Store {
): Promise<RecordArray>;

clearDataset(modelName: string);
findRecord(modelName: string, path: string);
}
Loading