Skip to content
68 changes: 68 additions & 0 deletions packages/atlas-admin-api/src/atlas-admin-api-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,74 @@ describe('AtlasAdminApiService', function () {
});
});

describe('getSystemStatus', function () {
it('should hit the system status endpoint and return the ip address and user', async function () {
atlasServiceStub.authenticatedFetch.resolves({
json: () =>
Promise.resolve({
appName: 'MongoDB Atlas',
ipAddress: '1.2.3.4',
user: { username: 'user@example.com' },
}),
});

const res = await service.getSystemStatus();

expect(res).to.deep.equal({
ipAddress: '1.2.3.4',
user: { username: 'user@example.com' },
});
expect(fetchUrl(0)).to.equal('http://example.com/api/atlas/v2');
expect(
atlasServiceStub.authenticatedFetch.firstCall.args[1].headers.Accept
).to.equal(
`application/vnd.atlas.${ATLAS_ADMIN_API_DEFAULT_VERSION}+json`
);
});

it('should omit the user when it is not returned', async function () {
atlasServiceStub.authenticatedFetch.resolves({
json: () => Promise.resolve({ ipAddress: '1.2.3.4' }),
});

expect(await service.getSystemStatus()).to.deep.equal({
ipAddress: '1.2.3.4',
});
});

it('should throw when the ip address is missing', async function () {
atlasServiceStub.authenticatedFetch.resolves({
json: () => Promise.resolve({ user: { username: 'user@example.com' } }),
});

try {
await service.getSystemStatus();
expect.fail('Expected getSystemStatus to throw');
} catch (err) {
expect(err).to.have.property(
'message',
'Got unexpected backend response for Atlas Admin API system status request, missing or malformed ipAddress'
);
}
});

it('should throw when the returned user is malformed', async function () {
atlasServiceStub.authenticatedFetch.resolves({
json: () => Promise.resolve({ ipAddress: '1.2.3.4', user: {} }),
});

try {
await service.getSystemStatus();
expect.fail('Expected getSystemStatus to throw');
} catch (err) {
expect(err).to.have.property(
'message',
'Got unexpected backend response for Atlas Admin API system status request, missing or malformed username'
);
}
});
});

describe('getProjectIPAccessList', function () {
it('should hit the access list endpoint and return the entries', async function () {
stubSequentialJsonResponses([page([{ cidrBlock: '0.0.0.0/0' }])]);
Expand Down
21 changes: 21 additions & 0 deletions packages/atlas-admin-api/src/atlas-admin-api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
type AtlasGroupCluster,
type AtlasGroupClusterResponse,
} from './cluster-types';
import {
assertSystemStatus,
type AtlasSystemStatus,
} from './system-status-types';
import { connectionStringMatches, extractConnectionStrings } from './util';
import { getAtlasAdminApiAcceptHeader } from './version';

Expand Down Expand Up @@ -100,6 +104,23 @@ export class AtlasAdminApiService {
return results;
}

/**
* Fetches the Atlas Admin API system status, which reports the public IP
* address the request came from and, when authenticated as a user, that user
* (the username is the email the user is logged in with).
*/
async getSystemStatus(
options?: AtlasAdminApiRequestOptions
): Promise<AtlasSystemStatus> {
const requestUrl = this.atlasService.adminApiEndpoint('/v2');
const json = await this.fetchJson(requestUrl, options);
assertSystemStatus(json);
return {
ipAddress: json.ipAddress,
...(json.user && { user: { username: json.user.username } }),
};
}

async listGroupIds(): Promise<string[]> {
const clusters = await this.fetchAllPages<{ groupId: string }>(
(pagination) =>
Expand Down
1 change: 1 addition & 0 deletions packages/atlas-admin-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export {
type AtlasClusterState,
type AtlasGroupCluster,
} from './cluster-types';
export { type AtlasSystemStatus } from './system-status-types';
export { ATLAS_ADMIN_API_DEFAULT_VERSION } from './version';
34 changes: 34 additions & 0 deletions packages/atlas-admin-api/src/system-status-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Subset of the Atlas Admin API system status object (`GET /api/atlas/v2`) that
* we consume: the public IP address the request originated from (always
* returned) and, when the request is authenticated as a user rather than an API
* key, the user making it.
*/
export type AtlasSystemStatus = {
ipAddress: string;
user?: { username: string };
};

export function assertSystemStatus(
json: unknown
): asserts json is AtlasSystemStatus {
const status = json as { ipAddress?: unknown; user?: { username?: unknown } };
if (
!json ||
typeof json !== 'object' ||
typeof status.ipAddress !== 'string'
) {
throw new Error(
'Got unexpected backend response for Atlas Admin API system status request, missing or malformed ipAddress'
);
}
if (
status.user !== undefined &&
(typeof status.user !== 'object' ||
typeof status.user.username !== 'string')
Comment thread
esvm marked this conversation as resolved.
) {
throw new Error(
'Got unexpected backend response for Atlas Admin API system status request, missing or malformed username'
);
Comment thread
paula-stacho marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const groupId = /([a-f0-9]{24})/;
const clusterName = /[a-zA-Z0-9][a-zA-Z0-9-]*/;

export const ATLAS_ADMIN_API_AUTH_ENDPOINTS = [
'/api/atlas/v2',
'/api/atlas/v2/clusters',
new RegExp(`^/api/atlas/v2/groups/${groupId.source}/clusters$`),
new RegExp(
Expand Down
11 changes: 11 additions & 0 deletions packages/atlas-service/src/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,17 @@ describe('CompassAuthServiceMain', function () {
expect(authHeaders).to.not.have.property('X-Compass-Auth');
});

it('should add auth headers for the system status request', async function () {
const authHeaders = await CompassAuthService.handleAuthHeaders({
requestHeaders: { 'X-Compass-Auth': 'true' },
url: `${defaultConfig.atlasAdminApiBaseUrl}/v2`,
});
expect(authHeaders).to.have.property(
'Authorization',
`Bearer ${accessToken}`
);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think we need to test every endpoint that is on the list, but it won't hurt I suppose

it('should not add auth headers if they werent asked for', async function () {
const url = 'http://example.com/api/private/some-endpoint';
const oldHeaders = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,24 @@ describe('debugConnection', function () {
getProjectIdAndClusterName: Sinon.SinonStub;
getClusterState: Sinon.SinonStub;
getProjectIPAccessList: Sinon.SinonStub;
getSystemStatus: Sinon.SinonStub;
};

function mockAtlasAdminApi(
opts: {
state?: AtlasClusterState;
paused?: boolean;
ipAccessList?: AtlasAccessListEntry[];
userIp?: string;
projectIdAndClusterName?: { projectId: string; clusterName: string };
} = {}
) {
const { state = 'IDLE', paused = false, ipAccessList = [] } = opts;
const {
state = 'IDLE',
paused = false,
ipAccessList = [],
userIp = USER_IP,
} = opts;
// The cluster lookup resolves to undefined when the cluster is not among
// the ones the user can see, so an explicit undefined has to be
// distinguishable from an omitted option here.
Expand All @@ -94,6 +101,7 @@ describe('debugConnection', function () {
.resolves(projectIdAndClusterName),
getClusterState: sandbox.stub().resolves({ state, paused }),
getProjectIPAccessList: sandbox.stub().resolves(ipAccessList),
getSystemStatus: sandbox.stub().resolves({ ipAddress: userIp }),
};
return atlasAdminApi as unknown as AtlasAdminApiService;
}
Expand All @@ -119,6 +127,7 @@ describe('debugConnection', function () {
});
expect(atlasAdminApi.getClusterState).to.not.have.been.called;
expect(atlasAdminApi.getProjectIPAccessList).to.not.have.been.called;
expect(atlasAdminApi.getSystemStatus).to.not.have.been.called;
});

it('looks up the cluster and the access list with the resolved project id and cluster name', async function () {
Expand Down Expand Up @@ -184,6 +193,31 @@ describe('debugConnection', function () {
expect(result.ipAccessStatus).to.equal('Could not confirm');
});

it('matches the access list against the ip reported by the system status endpoint', async function () {
const api = mockAtlasAdminApi({
userIp: '9.9.9.9',
ipAccessList: [{ ipAddress: '9.9.9.9' }],
});

const result = await debugConnection(CONNECTION_STRING, api);

expect(result.ipAccessStatus).to.equal('Client IP Allowed');
});

it('fails when the user ip cannot be resolved', async function () {
const api = mockAtlasAdminApi({
ipAccessList: [{ ipAddress: USER_IP }],
});
atlasAdminApi.getSystemStatus.rejects(new Error('nope'));

try {
await debugConnection(CONNECTION_STRING, api);
expect.fail('expected debugConnection to reject');
} catch (err) {
expect((err as Error).message).to.equal('nope');
}
});

it('cannot confirm when the access list is empty', async function () {
const api = mockAtlasAdminApi({ ipAccessList: [] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,18 @@ async function getNetworkAccessInfo({
ipAccessStatus: IpAccessStatus;
networkAccessDetails: NetworkAccessDetails;
}> {
const ipAccessList = await atlasAdminApi.getProjectIPAccessList(projectId);
// TODO(COMPASS-10981): replace with Atlas Admin API once it's ready
const userIp = '1.2.3.4';
const [ipAccessList, { ipAddress: userIp }] = await Promise.all([
atlasAdminApi.getProjectIPAccessList(projectId),
atlasAdminApi.getSystemStatus(),
]);
return {
ipAccessStatus:
ipAccessList && userIp && isUserIpIncluded(ipAccessList, userIp)
? 'Client IP Allowed'
: 'Could not confirm',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Surprisingly, many customers have problems with ip whitelisting. To give more information, I would separate:

  • Atlas is unavailable and we can't confirm
  • Atlas is available and my ip is not in the range.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@paula-stacho paula-stacho Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We left the "can't confirm" intentionally vague for now as we realised we can't always tell (for example we don't know how to check awsSecurityGroup), but distinguishing that the request failed would make sense if we allow partial results. For now the plan is to not go with partial results though, if some of the requests fail we just let the user to retry the tool.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We might want to add a clear 'not match' for when we can tell for sure though, will probably be a follow up improvement

networkAccessDetails: {
networkAccessList: ipAccessList,
userIp,
...(userIp && { userIp }),
},
};
}
Expand Down
Loading