mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-19 21:57:51 +00:00
Merge pull request #2665 from balena-io/accept-device-application-keys-as-experimental-feature
Accept device & application keys on login as experimental feature
This commit is contained in:
commit
8646be7979
@ -621,6 +621,10 @@ password
|
|||||||
|
|
||||||
TCP port number of local HTTP login server (--web auth only)
|
TCP port number of local HTTP login server (--web auth only)
|
||||||
|
|
||||||
|
#### -H, --hideExperimentalWarning
|
||||||
|
|
||||||
|
Hides warning for experimental features
|
||||||
|
|
||||||
## logout
|
## logout
|
||||||
|
|
||||||
Logout from your balena account.
|
Logout from your balena account.
|
||||||
|
@ -20,6 +20,7 @@ import Command from '../command';
|
|||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../utils/lazy';
|
||||||
import { ExpectedError } from '../errors';
|
import { ExpectedError } from '../errors';
|
||||||
|
import type { WhoamiResult } from 'balena-sdk';
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
token: boolean;
|
token: boolean;
|
||||||
@ -30,6 +31,7 @@ interface FlagsDef {
|
|||||||
password?: string;
|
password?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
help: void;
|
help: void;
|
||||||
|
hideExperimentalWarning: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArgsDef {
|
interface ArgsDef {
|
||||||
@ -114,6 +116,11 @@ export default class LoginCmd extends Command {
|
|||||||
'TCP port number of local HTTP login server (--web auth only)',
|
'TCP port number of local HTTP login server (--web auth only)',
|
||||||
dependsOn: ['web'],
|
dependsOn: ['web'],
|
||||||
}),
|
}),
|
||||||
|
hideExperimentalWarning: flags.boolean({
|
||||||
|
char: 'H',
|
||||||
|
default: false,
|
||||||
|
description: 'Hides warning for experimental features',
|
||||||
|
}),
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,9 +144,24 @@ export default class LoginCmd extends Command {
|
|||||||
console.log(`\nLogging in to ${balenaUrl}`);
|
console.log(`\nLogging in to ${balenaUrl}`);
|
||||||
await this.doLogin(options, balenaUrl, params.token);
|
await this.doLogin(options, balenaUrl, params.token);
|
||||||
|
|
||||||
const { username } = await balena.auth.getUserInfo();
|
// We can safely assume this won't be undefined as doLogin will throw if this call fails
|
||||||
|
// We also don't need to worry too much about the amount of calls to whoami
|
||||||
|
// as these are cached by the SDK
|
||||||
|
const whoamiResult = (await balena.auth.whoami()) as WhoamiResult;
|
||||||
|
|
||||||
console.info(`Successfully logged in as: ${username}`);
|
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
|
||||||
|
console.info(stripIndent`
|
||||||
|
----------------------------------------------------------------------------------------
|
||||||
|
You are logging in with a ${whoamiResult.actorType} key.
|
||||||
|
This is an experimental feature and many features of the CLI might not work as expected.
|
||||||
|
We sure hope you know what you are doing.
|
||||||
|
----------------------------------------------------------------------------------------
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
`Successfully logged in as: ${this.getLoggedInMessage(whoamiResult)}`,
|
||||||
|
);
|
||||||
console.info(`\
|
console.info(`\
|
||||||
|
|
||||||
Find out about the available commands by running:
|
Find out about the available commands by running:
|
||||||
@ -149,6 +171,16 @@ Find out about the available commands by running:
|
|||||||
${messages.reachingOut}`);
|
${messages.reachingOut}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getLoggedInMessage(whoami: WhoamiResult): string {
|
||||||
|
if (whoami.actorType === 'user') {
|
||||||
|
return whoami.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier =
|
||||||
|
whoami.actorType === 'device' ? whoami.uuid : whoami.slug;
|
||||||
|
return `${whoami.actorType} ${identifier}`;
|
||||||
|
}
|
||||||
|
|
||||||
async doLogin(
|
async doLogin(
|
||||||
loginOptions: FlagsDef,
|
loginOptions: FlagsDef,
|
||||||
balenaUrl: string = 'balena-cloud.com',
|
balenaUrl: string = 'balena-cloud.com',
|
||||||
@ -166,7 +198,9 @@ ${messages.reachingOut}`);
|
|||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
await balena.auth.loginWithToken(token!);
|
await balena.auth.loginWithToken(token!);
|
||||||
try {
|
try {
|
||||||
await balena.auth.getUserInfo();
|
if (!(await balena.auth.whoami())) {
|
||||||
|
throw new ExpectedError('Token authentication failed');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (process.env.DEBUG) {
|
if (process.env.DEBUG) {
|
||||||
console.error(`Get user info failed with: ${err.message}`);
|
console.error(`Get user info failed with: ${err.message}`);
|
||||||
|
@ -36,18 +36,37 @@ export default class WhoamiCmd extends Command {
|
|||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const [{ username, email }, url] = await Promise.all([
|
const [whoamiResult, url] = await Promise.all([
|
||||||
balena.auth.getUserInfo(),
|
balena.auth.whoami(),
|
||||||
balena.settings.get('balenaUrl'),
|
balena.settings.get('balenaUrl'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(
|
if (whoamiResult?.actorType === 'user') {
|
||||||
getVisuals().table.vertical({ username, email, url }, [
|
const { username, email } = whoamiResult;
|
||||||
'$account information$',
|
console.log(
|
||||||
'username',
|
getVisuals().table.vertical({ username, email, url }, [
|
||||||
'email',
|
'$account information$',
|
||||||
'url',
|
'username',
|
||||||
]),
|
'email',
|
||||||
);
|
'url',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} else if (whoamiResult?.actorType === 'device') {
|
||||||
|
console.log(
|
||||||
|
getVisuals().table.vertical({ device: whoamiResult.uuid, url }, [
|
||||||
|
'$account information$',
|
||||||
|
'device',
|
||||||
|
'url',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
} else if (whoamiResult?.actorType === 'application') {
|
||||||
|
console.log(
|
||||||
|
getVisuals().table.vertical({ application: whoamiResult.slug, url }, [
|
||||||
|
'$account information$',
|
||||||
|
'application',
|
||||||
|
'url',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,29 @@ export async function getApplication(
|
|||||||
options?: PineOptions<Application>,
|
options?: PineOptions<Application>,
|
||||||
): Promise<Application> {
|
): Promise<Application> {
|
||||||
const { looksLikeFleetSlug } = await import('./validation');
|
const { looksLikeFleetSlug } = await import('./validation');
|
||||||
|
const whoamiResult = await sdk.auth.whoami();
|
||||||
|
const isDeviceActor = whoamiResult?.actorType === 'device';
|
||||||
|
|
||||||
|
if (isDeviceActor) {
|
||||||
|
const $filterByActor = {
|
||||||
|
$filter: {
|
||||||
|
owns__device: {
|
||||||
|
$any: {
|
||||||
|
$alias: 'd',
|
||||||
|
$expr: {
|
||||||
|
d: {
|
||||||
|
actor: whoamiResult.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
options = options
|
||||||
|
? sdk.utils.mergePineOptions(options, $filterByActor)
|
||||||
|
: $filterByActor;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof nameOrSlugOrId === 'string' &&
|
typeof nameOrSlugOrId === 'string' &&
|
||||||
!looksLikeFleetSlug(nameOrSlugOrId)
|
!looksLikeFleetSlug(nameOrSlugOrId)
|
||||||
@ -52,13 +75,15 @@ export async function getApplication(
|
|||||||
return await sdk.models.application.getAppByName(
|
return await sdk.models.application.getAppByName(
|
||||||
nameOrSlugOrId,
|
nameOrSlugOrId,
|
||||||
options,
|
options,
|
||||||
'directly_accessible',
|
isDeviceActor ? undefined : 'directly_accessible',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return await sdk.models.application.getDirectlyAccessible(
|
|
||||||
nameOrSlugOrId,
|
const getFunction = isDeviceActor
|
||||||
options,
|
? sdk.models.application.get
|
||||||
);
|
: sdk.models.application.getDirectlyAccessible;
|
||||||
|
|
||||||
|
return getFunction(nameOrSlugOrId, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
12
npm-shrinkwrap.json
generated
12
npm-shrinkwrap.json
generated
@ -3082,9 +3082,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "16.18.42",
|
"version": "16.18.43",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.42.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.43.tgz",
|
||||||
"integrity": "sha512-IrFfX/1oxDFQNpQzgt/BoP/hbMuQT68DPsNwzJmw8y3K8lfnPp0XymVN9GLFz+LobFmJGZ/peRzq+9wXYfCCtw=="
|
"integrity": "sha512-YFpgPKPRcwYbeNOimfu70B+TVJe6tr88WiW/TzEldkwGxQXrmabpU+lDjrFlNqdqIi3ON0o69EQBW62VH4MIxw=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-cleanup": {
|
"node_modules/@types/node-cleanup": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@ -24782,9 +24782,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "16.18.42",
|
"version": "16.18.43",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.42.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.43.tgz",
|
||||||
"integrity": "sha512-IrFfX/1oxDFQNpQzgt/BoP/hbMuQT68DPsNwzJmw8y3K8lfnPp0XymVN9GLFz+LobFmJGZ/peRzq+9wXYfCCtw=="
|
"integrity": "sha512-YFpgPKPRcwYbeNOimfu70B+TVJe6tr88WiW/TzEldkwGxQXrmabpU+lDjrFlNqdqIi3ON0o69EQBW62VH4MIxw=="
|
||||||
},
|
},
|
||||||
"@types/node-cleanup": {
|
"@types/node-cleanup": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
75
tests/commands/whoami.spec.ts
Normal file
75
tests/commands/whoami.spec.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2019-2023 Balena Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
import { BalenaAPIMock } from '../nock/balena-api-mock';
|
||||||
|
import { cleanOutput, runCommand } from '../helpers';
|
||||||
|
|
||||||
|
describe('balena whoami', function () {
|
||||||
|
let api: BalenaAPIMock;
|
||||||
|
|
||||||
|
this.beforeEach(() => {
|
||||||
|
api = new BalenaAPIMock();
|
||||||
|
api.expectGetMixpanel({ optional: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.afterEach(async () => {
|
||||||
|
// Check all expected api calls have been made and clean up.
|
||||||
|
api.done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should output login required message if haven't logged in`, async () => {
|
||||||
|
api.expectWhoAmIFail();
|
||||||
|
const { err, out } = await runCommand('whoami');
|
||||||
|
expect(out).to.be.empty;
|
||||||
|
expect(err[0]).to.include('Login required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display device with device response', async () => {
|
||||||
|
api.expectDeviceWhoAmI();
|
||||||
|
const { err, out } = await runCommand('whoami');
|
||||||
|
|
||||||
|
const lines = cleanOutput(out);
|
||||||
|
expect(lines[0]).to.contain('== ACCOUNT INFORMATION');
|
||||||
|
expect(lines[1]).to.contain('DEVICE: a11dc1acd31b623a0e4e084a6cf13aaa');
|
||||||
|
expect(lines[2]).to.contain('URL: balena-cloud.com');
|
||||||
|
expect(err).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display application with application response', async () => {
|
||||||
|
api.expectApplicationWhoAmI();
|
||||||
|
const { err, out } = await runCommand('whoami');
|
||||||
|
|
||||||
|
const lines = cleanOutput(out);
|
||||||
|
expect(lines[0]).to.contain('== ACCOUNT INFORMATION');
|
||||||
|
expect(lines[1]).to.contain('APPLICATION: mytestorf/mytestfleet');
|
||||||
|
expect(lines[2]).to.contain('URL: balena-cloud.com');
|
||||||
|
expect(err).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display user with user response', async () => {
|
||||||
|
api.expectGetWhoAmI();
|
||||||
|
const { err, out } = await runCommand('whoami');
|
||||||
|
|
||||||
|
const lines = cleanOutput(out);
|
||||||
|
expect(lines[0]).to.contain('== ACCOUNT INFORMATION');
|
||||||
|
expect(lines[1]).to.contain('USERNAME: gh_user');
|
||||||
|
expect(lines[2]).to.contain('EMAIL: testuser@test.com');
|
||||||
|
expect(lines[3]).to.contain('URL: balena-cloud.com');
|
||||||
|
expect(err).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
@ -439,6 +439,28 @@ export class BalenaAPIMock extends NockMock {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public expectDeviceWhoAmI(opts: ScopeOpts = { optional: true }) {
|
||||||
|
this.optGet('/actor/v1/whoami', opts).reply(200, {
|
||||||
|
id: 1235,
|
||||||
|
actorType: 'device',
|
||||||
|
actorTypeId: 88888,
|
||||||
|
uuid: 'a11dc1acd31b623a0e4e084a6cf13aaa',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectApplicationWhoAmI(opts: ScopeOpts = { optional: true }) {
|
||||||
|
this.optGet('/actor/v1/whoami', opts).reply(200, {
|
||||||
|
id: 1236,
|
||||||
|
actorType: 'application',
|
||||||
|
actorTypeId: 77777,
|
||||||
|
slug: 'mytestorf/mytestfleet',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectWhoAmIFail(opts: ScopeOpts = { optional: true }) {
|
||||||
|
this.optGet('/actor/v1/whoami', opts).reply(401);
|
||||||
|
}
|
||||||
|
|
||||||
public expectGetMixpanel(opts: ScopeOpts = {}) {
|
public expectGetMixpanel(opts: ScopeOpts = {}) {
|
||||||
this.optGet(/^\/mixpanel\/track/, opts).reply(200, {});
|
this.optGet(/^\/mixpanel\/track/, opts).reply(200, {});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user