Skip to content

Commit c76bcca

Browse files
authored
AXON-1516: Call API to check Rovo Dev Entitlement (#1288)
* AXON-1516: Call API to check Rovo Dev Entitlement * AXON-1516: fix boolean checking logic * AXON-1516: simplify entitlement logic
1 parent e91b151 commit c76bcca

File tree

6 files changed

+304
-62
lines changed

6 files changed

+304
-62
lines changed

src/analytics.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ export function performanceEvent(tag: string, measure: number, params?: Record<s
252252

253253
export type RovoDevEnv = 'IDE' | 'Boysenberry';
254254

255+
export function rovoDevEntitlementCheckEvent(isEntitled: boolean, type: string, source?: string) {
256+
return trackEvent('checked', 'rovoDevEntitlement', {
257+
attributes: { source, isEntitled, type },
258+
});
259+
}
255260
export function rovoDevNewSessionActionEvent(
256261
rovoDevEnv: RovoDevEnv,
257262
appInstanceId: string,

src/atlclients/clientManager.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Time } from '../util/time';
2828
import {
2929
AuthInfo,
3030
AuthInfoState,
31+
BasicAuthInfo,
3132
DetailedSiteInfo,
3233
isBasicAuthInfo,
3334
isOAuthInfo,
@@ -39,6 +40,14 @@ import { BasicInterceptor } from './basicInterceptor';
3940
const oauthTTL: number = 45 * Time.MINUTES;
4041
const serverTTL: number = Time.FOREVER;
4142

43+
export type ValidBasicAuthSiteData = {
44+
authInfo: BasicAuthInfo;
45+
host: string;
46+
siteCloudId: string;
47+
isValid: boolean;
48+
isStaging: boolean;
49+
};
50+
4251
export class ClientManager implements Disposable {
4352
private _clients: CacheMap = new CacheMap();
4453
private _queue = new PQueue({ concurrency: 1 });
@@ -201,6 +210,59 @@ export class ClientManager implements Disposable {
201210
return newClient;
202211
}
203212

213+
public async getValidBasicAuthCloudSites(): Promise<ValidBasicAuthSiteData[]> {
214+
try {
215+
const allJiraSites = Container.siteManager.getSitesAvailable(ProductJira);
216+
217+
const promises = allJiraSites.map(async (site) => {
218+
if (!site.isCloud && !site.host.endsWith('.jira-dev.com')) {
219+
return;
220+
}
221+
222+
const authInfo = await Container.credentialManager.getApiTokenIfExists(site);
223+
224+
if (!authInfo) {
225+
return;
226+
}
227+
228+
// verify the credentials work
229+
let isValid: boolean;
230+
try {
231+
await this.jiraClient(site);
232+
isValid = true;
233+
} catch {
234+
isValid = false;
235+
}
236+
237+
return {
238+
authInfo,
239+
host: site.host,
240+
siteCloudId: site.id,
241+
isValid,
242+
isStaging: site.host.endsWith('.jira-dev.com'),
243+
};
244+
});
245+
246+
const results = (await Promise.all(promises)).filter((res) => res !== undefined);
247+
248+
return results.sort((a, b) => {
249+
return a.host.localeCompare(b.host);
250+
});
251+
} catch (error) {
252+
Logger.error(error, 'Error checking for Rovo Dev Entitlement');
253+
return [];
254+
}
255+
}
256+
257+
public async getCloudPrimarySite(): Promise<ValidBasicAuthSiteData | undefined> {
258+
const results = await this.getValidBasicAuthCloudSites();
259+
if (results.length === 0) {
260+
return undefined;
261+
}
262+
263+
return results.filter((s) => s.isStaging)[0] || results[0];
264+
}
265+
204266
private createOAuthHTTPClient(site: DetailedSiteInfo, token: string): HTTPClient {
205267
return new HTTPClient(
206268
site.baseApiUrl,

src/container.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { RovoDevLogger } from './rovo-dev/util/rovoDevLogger';
4141
import { SiteManager } from './siteManager';
4242
import { AtlascodeUriHandler, SETTINGS_URL } from './uriHandler';
4343
import { Experiments, FeatureFlagClient, FeatureFlagClientInitError, Features } from './util/featureFlags';
44+
import { RovoDevEntitlementChecker } from './util/rovo-dev-entitlement/rovoDevEntitlementChecker';
4445
import { AuthStatusBar } from './views/authStatusBar';
4546
import { HelpExplorer } from './views/HelpExplorer';
4647
import { JiraActiveIssueStatusBar } from './views/jira/activeIssueStatusBar';
@@ -223,6 +224,13 @@ export class Container {
223224
setCommandContext(CommandContext.UseNewAuthFlow, false);
224225
}
225226

227+
context.subscriptions.push(
228+
(this._rovoDevEntitlementChecker = new RovoDevEntitlementChecker(this._analyticsClient)),
229+
);
230+
231+
// Check Rovo Dev entitlement on startup
232+
await this._rovoDevEntitlementChecker.checkEntitlement();
233+
226234
// in Boysenberry we don't need to listen to Jira auth updates
227235
if (!process.env.ROVODEV_BBY) {
228236
// refresh Rovo Dev when auth sites change
@@ -666,6 +674,11 @@ export class Container {
666674
return this._rovodevWebviewProvider;
667675
}
668676

677+
private static _rovoDevEntitlementChecker: RovoDevEntitlementChecker;
678+
public static get rovoDevEntitlementChecker() {
679+
return this._rovoDevEntitlementChecker;
680+
}
681+
669682
private static _createWorkItemWebviewProvider: CreateWorkItemWebviewProvider;
670683
public static get createWorkItemWebviewProvider() {
671684
return this._createWorkItemWebviewProvider;

src/rovo-dev/rovoDevProcessManager.ts

Lines changed: 16 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import fs from 'fs';
44
import net from 'net';
55
import packageJson from 'package.json';
66
import path from 'path';
7+
import { ValidBasicAuthSiteData } from 'src/atlclients/clientManager';
78
import { downloadAndUnzip } from 'src/rovo-dev/util/downloadFile';
89
import { getFsPromise } from 'src/rovo-dev/util/fsPromises';
910
import { waitFor } from 'src/rovo-dev/util/waitFor';
1011
import { Disposable, Event, EventEmitter, ExtensionContext, Terminal, Uri, window, workspace } from 'vscode';
1112

12-
import { DetailedSiteInfo, isBasicAuthInfo, ProductJira } from '../atlclients/authInfo';
13+
import { DetailedSiteInfo } from '../atlclients/authInfo';
1314
import { Container } from '../container';
1415
import { RovoDevApiClient } from './client';
1516
import { RovoDevDisabledReason, RovoDevEntitlementCheckFailedDetail } from './rovoDevWebviewProviderMessages';
@@ -107,58 +108,7 @@ async function getOrAssignPortForWorkspace(): Promise<number> {
107108
throw new Error('unable to find an available port.');
108109
}
109110

110-
/**
111-
* Placeholder implementation for Rovo Dev CLI credential storage
112-
*/
113-
async function getCloudCredentials(): Promise<
114-
{ username: string; key: string; host: string; isValid: boolean; isStaging: boolean } | undefined
115-
> {
116-
try {
117-
const sites = Container.siteManager.getSitesAvailable(ProductJira);
118-
119-
const promises = sites.map(async (site) => {
120-
// *.atlassian.net are PROD cloud sites
121-
// *.jira-dev.com are Staging cloud sites
122-
if (!site.host.endsWith('.atlassian.net') && !site.host.endsWith('.jira-dev.com')) {
123-
return undefined;
124-
}
125-
126-
const authInfo = await Container.credentialManager.getAuthInfo(site);
127-
if (!isBasicAuthInfo(authInfo)) {
128-
return undefined;
129-
}
130-
131-
// verify the credentials work
132-
let isValid: boolean;
133-
try {
134-
await Container.clientManager.jiraClient(site);
135-
isValid = true;
136-
} catch {
137-
isValid = false;
138-
}
139-
140-
return {
141-
username: authInfo.username,
142-
key: authInfo.password,
143-
host: site.host,
144-
isValid,
145-
isStaging: site.host.endsWith('.jira-dev.com'),
146-
};
147-
});
148-
149-
const results = (await Promise.all(promises)).filter((result) => result !== undefined);
150-
151-
// give priority to staging sites
152-
return results.filter((x) => x.isStaging)[0] || results[0];
153-
} catch (error) {
154-
RovoDevLogger.error(error, 'Error fetching cloud credentials for Rovo Dev');
155-
return undefined;
156-
}
157-
}
158-
159-
type CloudCredentials = NonNullable<Awaited<ReturnType<typeof getCloudCredentials>>>;
160-
161-
function areCredentialsEqual(cred1?: CloudCredentials, cred2?: CloudCredentials) {
111+
function areCredentialsEqual(cred1?: ValidBasicAuthSiteData, cred2?: ValidBasicAuthSiteData) {
162112
if (cred1 === cred2) {
163113
return true;
164114
}
@@ -167,7 +117,11 @@ function areCredentialsEqual(cred1?: CloudCredentials, cred2?: CloudCredentials)
167117
return false;
168118
}
169119

170-
return cred1.host === cred2.host && cred1.key === cred2.key && cred1.username === cred2.username;
120+
return (
121+
cred1.host === cred2.host &&
122+
cred1.authInfo.password === cred2.authInfo.password &&
123+
cred1.authInfo.username === cred2.authInfo.username
124+
);
171125
}
172126

173127
export interface RovoDevProcessNotStartedState {
@@ -239,7 +193,7 @@ export abstract class RovoDevProcessManager {
239193
return this._onStateChanged.event;
240194
}
241195

242-
private static currentCredentials: CloudCredentials | undefined;
196+
private static currentCredentials: ValidBasicAuthSiteData | undefined;
243197

244198
/** This lock ensures this class is async-safe, preventing repeated invocations
245199
* of `initializeRovoDev` or `refreshRovoDevCredentials` to launch multiple processes
@@ -324,7 +278,7 @@ export abstract class RovoDevProcessManager {
324278

325279
private static async internalInitializeRovoDev(
326280
context: ExtensionContext,
327-
credentials: CloudCredentials | undefined,
281+
credentials: ValidBasicAuthSiteData | undefined,
328282
forceNewInstance?: boolean,
329283
) {
330284
if (!workspace.workspaceFolders?.length) {
@@ -403,7 +357,7 @@ export abstract class RovoDevProcessManager {
403357
this.failIfRovoDevInstanceIsRunning();
404358
}
405359

406-
const credentials = await getCloudCredentials();
360+
const credentials = await Container.clientManager.getCloudPrimarySite();
407361
await this.internalInitializeRovoDev(context, credentials, forceNewInstance);
408362
} finally {
409363
this.asyncLocked = false;
@@ -419,7 +373,7 @@ export abstract class RovoDevProcessManager {
419373
this.asyncLocked = true;
420374

421375
try {
422-
const credentials = await getCloudCredentials();
376+
const credentials = await Container.clientManager.getCloudPrimarySite();
423377
if (areCredentialsEqual(credentials, this.currentCredentials)) {
424378
return;
425379
}
@@ -451,7 +405,7 @@ export abstract class RovoDevProcessManager {
451405

452406
private static async startRovoDev(
453407
context: ExtensionContext,
454-
credentials: CloudCredentials,
408+
credentials: ValidBasicAuthSiteData,
455409
rovoDevURIs: RovoDevURIs,
456410
) {
457411
// skip if there is no workspace folder open
@@ -495,7 +449,7 @@ class RovoDevTerminalInstance extends Disposable {
495449
}
496450

497451
public async start(
498-
credentials: CloudCredentials,
452+
credentials: ValidBasicAuthSiteData,
499453
setState: (newState: RovoDevProcessState) => void,
500454
): Promise<void> {
501455
if (this.started) {
@@ -538,9 +492,9 @@ class RovoDevTerminalInstance extends Disposable {
538492
iconPath: this.rovoDevIconUri,
539493
env: {
540494
USER: process.env.USER || process.env.USERNAME,
541-
USER_EMAIL: credentials.username,
495+
USER_EMAIL: credentials.authInfo.username,
542496
ROVODEV_SANDBOX_ID: Container.appInstanceId,
543-
...(credentials.key ? { USER_API_TOKEN: credentials.key } : {}),
497+
...(credentials.authInfo.password ? { USER_API_TOKEN: credentials.authInfo.password } : {}),
544498
},
545499
});
546500

0 commit comments

Comments
 (0)