Skip to content

Commit

Permalink
UI: fix token expiry banner for batch tokens (#27479)
Browse files Browse the repository at this point in the history
* fix: calculate expiration of all batch tokens to ensure expire warning banner is shown

* fix: ensure allowExpiration doesn't get overridden

* fix: set expirationCalcTS outside of calculateExpression

* tests: verify expirationEpoch is calculated when only expiry_time is passed in

* fix: calculate expireTime using expire_time if its passed in

* tests: clean up auth tests

* tests: organize batch token vs. service token tests into separate module

* chore: update changelog

* Update changelog/27479.txt

Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>

* fix: ensure tokens in test envs do not expire

* cleanup: pull setExpiration settings into own method & add tests

---------

Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
  • Loading branch information
Noelle Daley and hashishaw authored Jun 28, 2024
1 parent 93bda36 commit 61a37f2
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 34 deletions.
3 changes: 3 additions & 0 deletions changelog/27479.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Ensure token expired banner displays when batch token expires
```
1 change: 1 addition & 0 deletions ui/app/components/token-expire-warning.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default class TokenExpireWarning extends Component {
if ('vault.cluster.oidc-provider' === currentRoute) {
return false;
}

return !!this.args.expirationDate;
}
}
48 changes: 28 additions & 20 deletions ui/app/services/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ export default Service.extend({
if (!tokenName) {
return;
}

const { tokenExpirationEpoch } = this.getTokenData(tokenName);
const expirationDate = new Date(0);

return tokenExpirationEpoch ? expirationDate.setUTCMilliseconds(tokenExpirationEpoch) : null;
}),

Expand Down Expand Up @@ -215,15 +217,20 @@ export default Service.extend({
return this.ajax(url, 'POST', { namespace });
},

calculateExpiration(resp) {
const now = this.now();
calculateExpiration(resp, now) {
const ttl = resp.ttl || resp.lease_duration;
const tokenExpirationEpoch = now + ttl * 1e3;
this.set('expirationCalcTS', now);
return {
ttl,
tokenExpirationEpoch,
};
const tokenExpirationEpoch = resp.expire_time ? new Date(resp.expire_time).getTime() : now + ttl * 1e3;

return { ttl, tokenExpirationEpoch };
},

setExpirationSettings(resp, now) {
if (resp.renewable) {
this.set('expirationCalcTS', now);
this.set('allowExpiration', false);
} else {
this.set('allowExpiration', true);
}
},

calculateRootNamespace(currentNamespace, namespace_path, backend) {
Expand Down Expand Up @@ -296,21 +303,22 @@ export default Service.extend({
resp.policies
);

if (resp.renewable) {
Object.assign(data, this.calculateExpiration(resp));
} else if (resp.type === 'batch') {
// if it's a batch token, it's not renewable but has an expire time
// so manually set tokenExpirationEpoch and allow expiration
data.tokenExpirationEpoch = new Date(resp.expire_time).getTime();
this.set('allowExpiration', true);
}
const now = this.now();

Object.assign(data, this.calculateExpiration(resp, now));
this.setExpirationSettings(resp, now);

// ensure we don't call renew-self within tests
// this is intentionally not included in setExpirationSettings so we can unit test that method
if (Ember.testing) this.set('allowExpiration', false);

if (!data.displayName) {
data.displayName = (this.getTokenData(tokenName) || {}).displayName;
}

this.set('tokens', addToArray(this.tokens, tokenName));
this.set('allowExpiration', false);
this.setTokenData(tokenName, data);

return resolve({
namespace: currentNamespace || data.userRootNamespace,
token: tokenName,
Expand All @@ -333,9 +341,9 @@ export default Service.extend({
renew() {
const tokenName = this.currentTokenName;
const currentlyRenewing = this.isRenewing;
if (currentlyRenewing) {
return;
}

if (currentlyRenewing) return;

this.isRenewing = true;
return this.renewCurrentToken().then(
(resp) => {
Expand Down
152 changes: 152 additions & 0 deletions ui/tests/integration/services/auth-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,90 @@ const GITHUB_RESPONSE = {
},
};

const BATCH_TOKEN_RESPONSE = {
request_id: '60bcef62-cc20-facf-8c0d-1418d05e9a42',
lease_id: '',
renewable: false,
lease_duration: 0,
data: {
accessor: '',
creation_time: 1718672331,
creation_ttl: 60,
display_name: 'token',
entity_id: '',
expire_time: '2024-06-17T17:59:51-07:00',
explicit_max_ttl: 0,
id: 'hvb.AAAAAQIUMVkhx9rnA',
issue_time: '2024-06-17T17:58:51-07:00',
meta: null,
num_uses: 0,
orphan: false,
path: 'auth/token/create',
policies: ['default'],
renewable: false,
ttl: 45,
type: 'batch',
},
wrap_info: null,
warnings: null,
auth: null,
mount_type: 'token',
};

const USERPASS_BATCH_TOKEN_RESPONSE = {
request_id: 'eb4c31a0-1745-5701-cce7-1668f5839dbf',
lease_id: '',
renewable: false,
lease_duration: 0,
data: null,
wrap_info: null,
warnings: null,
auth: {
client_token: 'hvb.AAAAAQJ0eGwP5e48S61kBRYmR',
accessor: '',
policies: ['default'],
token_policies: ['default'],
metadata: {
username: 'bob',
},
lease_duration: 360,
renewable: false,
entity_id: 'b52f8591-02b6-828b-7f36-620afa539126',
token_type: 'batch',
orphan: true,
mfa_requirement: null,
num_uses: 0,
},
mount_type: '',
};

const USERPASS_SERVICE_TOKEN_RESPONSE = {
request_id: 'e735ffad-f2fe-5d1b-14b8-90aeb9d05976',
lease_id: '',
renewable: false,
lease_duration: 0,
data: null,
wrap_info: null,
warnings: null,
auth: {
client_token: 'hvs.CAESINY6Qbs8rm',
accessor: '9bDizzlcIHiXwEOK5mZ6gjHI',
policies: ['default'],
token_policies: ['default'],
metadata: {
username: 'bob',
},
lease_duration: 360,
renewable: true,
entity_id: 'd9a0cac8-779c-e766-716a-6f80552f0e81',
token_type: 'service',
orphan: true,
mfa_requirement: null,
num_uses: 0,
},
mount_type: '',
};

module('Integration | Service | auth', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
Expand Down Expand Up @@ -334,4 +418,72 @@ module('Integration | Service | auth', function (hooks) {
});
});
});

module('token types', function (hooks) {
hooks.beforeEach(function () {
this.server.post('/auth/userpass/login/:username', (_, request) => {
const { username } = request.params;
const resp =
username === 'batch'
? { ...USERPASS_BATCH_TOKEN_RESPONSE }
: { ...USERPASS_SERVICE_TOKEN_RESPONSE };
resp.auth.metadata.username = username;
return resp;
});

this.service = this.owner.factoryFor('service:auth').create({ storage: () => this.store });
});

module('batch tokens', function () {
test('batch tokens generated by token auth method', async function (assert) {
this.server.get('/auth/token/lookup-self', () => {
return { ...BATCH_TOKEN_RESPONSE };
});

await this.service.authenticate({
clusterId: '1',
backend: 'token',
data: { token: 'test' },
});

// exact expiration time is calculated in unit tests
assert.notEqual(
this.service.tokenExpirationDate,
undefined,
'expiration is calculated for batch tokens'
);
});

test('batch tokens generated by auth methods', async function (assert) {
await this.service.authenticate({
clusterId: '1',
backend: 'userpass',
data: { username: 'batch', password: 'password' },
});

// exact expiration time is calculated in unit tests
assert.notEqual(
this.service.tokenExpirationDate,
undefined,
'expiration is calculated for batch tokens'
);
});
});

test('service token authentication', async function (assert) {
await this.service.authenticate({
clusterId: '1',
backend: 'userpass',
data: { username: 'service', password: 'password' },
});

// exact expiration time is calculated in unit tests
assert.notEqual(
this.service.tokenExpirationDate,
undefined,
'expiration is calculated for service tokens'
);
assert.false(this.service.allowExpiration, 'allowExpiration is false for service tokens');
});
});
});
71 changes: 57 additions & 14 deletions ui/tests/unit/services/auth-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,69 @@ import { setupTest } from 'ember-qunit';
module('Unit | Service | auth', function (hooks) {
setupTest(hooks);

[
['#calculateExpiration w/ttl', { ttl: 30 }, 30],
['#calculateExpiration w/lease_duration', { ttl: 15 }, 15],
].forEach(([testName, response, ttlValue]) => {
test(testName, function (assert) {
const now = Date.now();
const service = this.owner.factoryFor('service:auth').create({
now() {
return now;
},
hooks.beforeEach(function () {
this.service = this.owner.lookup('service:auth');
});

module('#calculateExpiration', function () {
[
['#calculateExpiration w/ttl', { ttl: 30 }, 30],
['#calculateExpiration w/lease_duration', { lease_duration: 15 }, 15],
].forEach(([testName, response, ttlValue]) => {
test(testName, function (assert) {
const now = Date.now();

const resp = this.service.calculateExpiration(response, now);

assert.strictEqual(resp.ttl, ttlValue, 'returns the ttl');
assert.strictEqual(
resp.tokenExpirationEpoch,
now + ttlValue * 1e3,
'calculates expiration from ttl as epoch timestamp'
);
});
});

const resp = service.calculateExpiration(response);
test('#calculateExpiration w/ expire_time', function (assert) {
const now = Date.now();
const expirationString = '2024-06-13T09:10:21-07:00';
const expectedExpirationEpoch = new Date(expirationString).getTime();

assert.strictEqual(resp.ttl, ttlValue, 'returns the ttl');
const resp = this.service.calculateExpiration(
{ ttl: 30, expire_time: '2024-06-13T09:10:21-07:00' },
now
);

assert.strictEqual(resp.ttl, 30, 'returns ttl');
assert.strictEqual(
resp.tokenExpirationEpoch,
now + ttlValue * 1e3,
'calculates expiration from ttl as epoch timestamp'
expectedExpirationEpoch,
'calculates expiration from expire_time'
);
});
});

module('#setExpirationSettings', function () {
test('#setExpirationSettings for a renewable token', function (assert) {
const now = Date.now();
const ttl = 30;
const response = { ttl, renewable: true };

this.service.setExpirationSettings(response, now);

assert.false(this.service.allowExpiration, 'sets allowExpiration to false');
assert.strictEqual(this.service.expirationCalcTS, now, 'sets expirationCalcTS to now');
});

test('#setExpirationSettings for a non-renewable token', function (assert) {
const now = Date.now();
const ttl = 30;
const response = { ttl, renewable: false };

this.service.setExpirationSettings(response, now);

assert.true(this.service.allowExpiration, 'sets allowExpiration to true');
assert.strictEqual(this.service.expirationCalcTS, null, 'keeps expirationCalcTS as null');
});
});
});

0 comments on commit 61a37f2

Please sign in to comment.