mirror of
https://github.com/kemko/nomad.git
synced 2026-01-03 17:05:43 +03:00
* ui: Handle new token self response object when ACLs are disabled. The ACL self lookup now returns a spoof token when ACLs are disbaled, rather than an error. The UI needs to be updated to handle this change, so permissions checks are not performed which grey out buttons such as client drain incorrectly. * changelog: add entry for #25881
217 lines
6.6 KiB
JavaScript
217 lines
6.6 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import Service, { inject as service } from '@ember/service';
|
|
import { computed } from '@ember/object';
|
|
import { alias, reads } from '@ember/object/computed';
|
|
import { getOwner } from '@ember/application';
|
|
import { assign } from '@ember/polyfills';
|
|
import { task, timeout } from 'ember-concurrency';
|
|
import queryString from 'query-string';
|
|
import fetch from 'nomad-ui/utils/fetch';
|
|
import classic from 'ember-classic-decorator';
|
|
import moment from 'moment';
|
|
|
|
const MINUTES_LEFT_AT_WARNING = 10;
|
|
const EXPIRY_NOTIFICATION_TITLE = 'Your access is about to expire';
|
|
@classic
|
|
export default class TokenService extends Service {
|
|
@service store;
|
|
@service system;
|
|
@service router;
|
|
@service notifications;
|
|
|
|
aclEnabled = true;
|
|
|
|
tokenNotFound = false;
|
|
|
|
@computed
|
|
get secret() {
|
|
return window.localStorage.nomadTokenSecret;
|
|
}
|
|
|
|
set secret(value) {
|
|
if (value == null) {
|
|
window.localStorage.removeItem('nomadTokenSecret');
|
|
} else {
|
|
window.localStorage.nomadTokenSecret = value;
|
|
}
|
|
}
|
|
|
|
@task(function* () {
|
|
const TokenAdapter = getOwner(this).lookup('adapter:token');
|
|
try {
|
|
var token = yield TokenAdapter.findSelf();
|
|
if (token.accessor === 'acls-disabled') {
|
|
this.set('aclEnabled', false);
|
|
return null;
|
|
}
|
|
this.secret = token.secret;
|
|
return token;
|
|
} catch (e) {
|
|
const errors = e.errors ? e.errors.mapBy('detail') : [];
|
|
if (errors.find((error) => error === 'ACL token not found')) {
|
|
this.set('tokenNotFound', true);
|
|
}
|
|
return null;
|
|
}
|
|
})
|
|
fetchSelfToken;
|
|
|
|
@reads('fetchSelfToken.lastSuccessful.value') selfToken;
|
|
|
|
async exchangeOneTimeToken(oneTimeToken) {
|
|
const TokenAdapter = getOwner(this).lookup('adapter:token');
|
|
|
|
const token = await TokenAdapter.exchangeOneTimeToken(oneTimeToken);
|
|
this.secret = token.secret;
|
|
}
|
|
|
|
@task(function* () {
|
|
try {
|
|
if (this.selfToken) {
|
|
// return yield this.selfToken.get('policies');
|
|
let tokenPolicies = yield this.selfToken.get('policies');
|
|
let rolePolicies = [];
|
|
const roles = yield this.selfToken.get('roles');
|
|
if (roles.length) {
|
|
yield Promise.all(
|
|
roles.map((role) => {
|
|
return role.policies;
|
|
})
|
|
);
|
|
rolePolicies = roles
|
|
.map((role) => {
|
|
return role.policies;
|
|
})
|
|
.map((policies) => policies.toArray())
|
|
.flat();
|
|
}
|
|
return [...tokenPolicies.toArray(), ...rolePolicies];
|
|
} else {
|
|
let policy = yield this.store.findRecord('policy', 'anonymous');
|
|
return [policy];
|
|
}
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
})
|
|
fetchSelfTokenPolicies;
|
|
|
|
@alias('fetchSelfTokenPolicies.lastSuccessful.value') selfTokenPolicies;
|
|
|
|
@task(function* () {
|
|
yield this.fetchSelfToken.perform();
|
|
this.kickoffTokenTTLMonitoring();
|
|
if (this.aclEnabled) {
|
|
yield this.fetchSelfTokenPolicies.perform();
|
|
}
|
|
})
|
|
fetchSelfTokenAndPolicies;
|
|
|
|
// All non Ember Data requests should go through authorizedRequest.
|
|
// However, the request that gets regions falls into that category.
|
|
// This authorizedRawRequest is necessary in order to fetch data
|
|
// with the guarantee of a token but without the automatic region
|
|
// param since the region cannot be known at this point.
|
|
authorizedRawRequest(url, options = {}) {
|
|
const credentials = 'include';
|
|
const headers = {};
|
|
const token = this.secret;
|
|
|
|
if (token) {
|
|
headers['X-Nomad-Token'] = token;
|
|
}
|
|
|
|
return fetch(url, assign(options, { headers, credentials }));
|
|
}
|
|
|
|
authorizedRequest(url, options) {
|
|
if (this.get('system.shouldIncludeRegion')) {
|
|
const region = this.get('system.activeRegion');
|
|
if (region && url.indexOf('region=') === -1) {
|
|
url = addParams(url, { region });
|
|
}
|
|
}
|
|
|
|
return this.authorizedRawRequest(url, options);
|
|
}
|
|
|
|
reset() {
|
|
this.fetchSelfToken.cancelAll({ resetState: true });
|
|
this.fetchSelfTokenPolicies.cancelAll({ resetState: true });
|
|
this.fetchSelfTokenAndPolicies.cancelAll({ resetState: true });
|
|
this.monitorTokenTime.cancelAll({ resetState: true });
|
|
window.localStorage.removeItem('nomadOIDCNonce');
|
|
window.localStorage.removeItem('nomadOIDCAuthMethod');
|
|
}
|
|
|
|
kickoffTokenTTLMonitoring() {
|
|
this.monitorTokenTime.perform();
|
|
}
|
|
|
|
@task(function* () {
|
|
while (this.selfToken?.expirationTime) {
|
|
const diff = new Date(this.selfToken.expirationTime) - new Date();
|
|
// Let the user know at the 10 minute mark,
|
|
// or any time they refresh with under 10 minutes left
|
|
if (diff < 1000 * 60 * MINUTES_LEFT_AT_WARNING) {
|
|
const existingNotification = this.notifications.queue?.find(
|
|
(m) => m.title === EXPIRY_NOTIFICATION_TITLE
|
|
);
|
|
// For the sake of updating the "time left" message, we keep running the task down to the moment of expiration
|
|
if (diff > 0) {
|
|
if (existingNotification) {
|
|
existingNotification.set(
|
|
'message',
|
|
`Your token access expires ${moment(
|
|
this.selfToken.expirationTime
|
|
).fromNow()}`
|
|
);
|
|
} else {
|
|
if (!this.expirationNotificationDismissed) {
|
|
this.notifications.add({
|
|
title: EXPIRY_NOTIFICATION_TITLE,
|
|
message: `Your token access expires ${moment(
|
|
this.selfToken.expirationTime
|
|
).fromNow()}`,
|
|
color: 'warning',
|
|
sticky: true,
|
|
customCloseAction: () => {
|
|
this.set('expirationNotificationDismissed', true);
|
|
},
|
|
customAction: {
|
|
label: 'Re-authenticate',
|
|
action: () => {
|
|
this.router.transitionTo('settings.tokens');
|
|
},
|
|
},
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
if (existingNotification) {
|
|
existingNotification.setProperties({
|
|
title: 'Your access has expired',
|
|
message: `Your token will need to be re-authenticated`,
|
|
});
|
|
const currentPath = this.router.currentURL;
|
|
this.postExpiryPath = currentPath;
|
|
}
|
|
this.monitorTokenTime.cancelAll(); // Stop updating time left after expiration
|
|
}
|
|
}
|
|
yield timeout(1000);
|
|
}
|
|
})
|
|
monitorTokenTime;
|
|
}
|
|
|
|
function addParams(url, params) {
|
|
const paramsStr = queryString.stringify(params);
|
|
const delimiter = url.includes('?') ? '&' : '?';
|
|
return `${url}${delimiter}${paramsStr}`;
|
|
}
|