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: fix token expiry banner for batch tokens #27479

Merged
merged 11 commits into from
Jun 28, 2024
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));
Comment on lines -299 to -300
Copy link
Contributor Author

@andaley andaley Jun 13, 2024

Choose a reason for hiding this comment

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

the this.calculateExpiration function is already set up to handle batch tokens returned by /auth/[backend]/login methods since those APIs return a lease_duration, which is accounted for on line 220 above.

as a result, i didn't see a reason why we wouldn't couldn't call calculateExpiration regardless of whether or not resp.renewable or not.

so you'll notice i modified this.calculateExpiration to handle the case where resp.type === 'batch' (which happens when logging in via a token directly). this way calculateExpiration is used universally, regardless of the authentication method (token/userpass/ldap, batch token, service token etc).

} 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);
andaley marked this conversation as resolved.
Show resolved Hide resolved
}
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');
});
});
});
Loading