Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #4 from JupiterOne/sq-user-model-step
Browse files Browse the repository at this point in the history
User entity and user group user relationship
  • Loading branch information
Kenan Warren authored Apr 29, 2021
2 parents d901909 + 0832ca3 commit 5b7b60c
Showing 29 changed files with 3,103 additions and 150 deletions.
15 changes: 14 additions & 1 deletion docs/jupiterone.md
Original file line number Diff line number Diff line change
@@ -18,7 +18,11 @@
## Requirements

- JupiterOne requires an API token. You need permission to create a user in
Sonarqube that will be used to obtain the API token.
Sonarqube that will be used to obtain the API token. The token should have the
`Administer System` permission to allow the ability to pull extra user
metadata. More information on this can be found in the sonarqube api
documentation of your instance
(`<your-instance-url>/web_api/api/users/search`).
- You must have permission in JupiterOne to install new integrations.

## Support
@@ -82,8 +86,17 @@ The following entities are created:
| Resources | Entity `_type` | Entity `_class` |
| --------- | ---------------------- | --------------- |
| Project | `sonarqube_project` | `Project` |
| User | `sonarqube_user` | `User` |
| UserGroup | `sonarqube_user_group` | `UserGroup` |

### Relationships

The following relationships are created/mapped:

| Source Entity `_type` | Relationship `_class` | Target Entity `_type` |
| ---------------------- | --------------------- | --------------------- |
| `sonarqube_user_group` | **HAS** | `sonarqube_user` |

<!--
********************************************************************************
END OF GENERATED DOCUMENTATION AFTER BELOW MARKER
153 changes: 150 additions & 3 deletions src/provider/SonarqubeClient.test.ts
Original file line number Diff line number Diff line change
@@ -7,9 +7,9 @@ import {
} from '@jupiterone/integration-sdk-testing';

import { createSonarqubeClient } from '.';
import { SonarqubeProject, SonarqubeUserGroup } from './types';
import { SonarqubeProject, SonarqubeUserGroup, SonarqubeUser } from './types';

describe('#SonarqubeClient', () => {
describe('#iterateResources', () => {
let recording: Recording;

afterEach(async () => {
@@ -19,7 +19,7 @@ describe('#SonarqubeClient', () => {
test('should fail with invalid token', async () => {
recording = setupRecording({
directory: __dirname,
name: 'SonarqubeClientShouldFailWithInvalidToken',
name: 'iterateResourcesShouldFailWithInvalidToken',
options: {
matchRequestsBy: {
url: {
@@ -44,6 +44,38 @@ describe('#SonarqubeClient', () => {
}),
).rejects.toThrowError(IntegrationProviderAuthenticationError);
});

test('should paginate correctly', async () => {
recording = setupRecording({
directory: __dirname,
name: 'iterateResourcesShouldPaginateCorrectly',
options: {
matchRequestsBy: {
url: {
hostname: false,
},
},
recordFailedRequests: true,
},
mutateEntry: mutations.unzipGzippedRecordingEntry,
});

const context = createMockStepExecutionContext({
instanceConfig: {
baseUrl: process.env.BASE_URL || 'http://localhost:9000',
apiToken: process.env.API_TOKEN || 'string-value',
},
});
const provider = createSonarqubeClient(context.instance.config);
const results: SonarqubeProject[] = [];
await provider.iterateProjects(
(project) => {
results.push(project);
},
{ ps: '1' },
);
expect(results).toHaveLength(2);
});
});

describe('#iterateProjects', () => {
@@ -144,3 +176,118 @@ describe('#iterateUserGroups', () => {
);
});
});

describe('#iterateUsers', () => {
let recording: Recording;

afterEach(async () => {
await recording.stop();
});

test('should fetch users with valid config', async () => {
recording = setupRecording({
directory: __dirname,
name: 'iterateUsersShouldFetchUsersWithValidConfig',
options: {
matchRequestsBy: {
url: {
hostname: false,
},
},
recordFailedRequests: true,
},
mutateEntry: mutations.unzipGzippedRecordingEntry,
});

const context = createMockStepExecutionContext({
instanceConfig: {
baseUrl: process.env.BASE_URL || 'http://localhost:9000',
apiToken: process.env.API_TOKEN || 'string-value',
},
});
const provider = createSonarqubeClient(context.instance.config);

const results: SonarqubeUser[] = [];
await provider.iterateUsers((user) => {
results.push(user);
});

expect(results).toEqual(
expect.arrayContaining([
expect.objectContaining({
login: expect.any(String),
name: expect.any(String),
active: expect.any(Boolean),
email: expect.any(String),
groups: expect.any(Array),
tokensCount: expect.any(Number),
local: expect.any(Boolean),
externalIdentity: expect.any(String),
externalProvider: expect.any(String),
avatar: expect.any(String),
}),
expect.objectContaining({
login: expect.any(String),
name: expect.any(String),
active: expect.any(Boolean),
email: expect.any(String),
groups: expect.any(Array),
tokensCount: expect.any(Number),
local: expect.any(Boolean),
externalIdentity: expect.any(String),
externalProvider: expect.any(String),
avatar: expect.any(String),
}),
]),
);
});
});

describe('#iterateGroupsAssignedToUser', () => {
let recording: Recording;

afterEach(async () => {
await recording.stop();
});

test('should fetch users user groups with valid config', async () => {
recording = setupRecording({
directory: __dirname,
name: 'iterateGroupsAssignedToUserShouldFetchUserGroupsWithValidConfig',
options: {
matchRequestsBy: {
url: {
hostname: false,
},
},
recordFailedRequests: true,
},
mutateEntry: mutations.unzipGzippedRecordingEntry,
});

const context = createMockStepExecutionContext({
instanceConfig: {
baseUrl: process.env.BASE_URL || 'http://localhost:9000',
apiToken: process.env.API_TOKEN || 'string-value',
},
});
const provider = createSonarqubeClient(context.instance.config);

const results: SonarqubeUserGroup[] = [];
await provider.iterateGroupsAssignedToUser('testUser', (userGroup) => {
results.push(userGroup);
});

expect(results).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
description: expect.any(String),
selected: expect.any(Boolean),
default: expect.any(Boolean),
}),
]),
);
});
});
40 changes: 36 additions & 4 deletions src/provider/SonarqubeClient.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import {
PaginatedResponse,
ValidationResponse,
SonarqubeUserGroup,
SonarqubeUser,
} from './types';

/**
@@ -42,21 +43,50 @@ export class SonarqubeClient {

async iterateProjects(
iteratee: ResourceIteratee<SonarqubeProject>,
params?: NodeJS.Dict<string | string[]>,
): Promise<void> {
return this.iterateResources<'components', SonarqubeProject>(
'/projects/search',
'components',
iteratee,
params,
);
}

async iterateUserGroups(
iteratee: ResourceIteratee<SonarqubeUserGroup>,
params?: NodeJS.Dict<string | string[]>,
): Promise<void> {
return this.iterateResources<'groups', SonarqubeUserGroup>(
'/user_groups/search',
'groups',
iteratee,
params,
);
}

async iterateUsers(
iteratee: ResourceIteratee<SonarqubeUser>,
params?: NodeJS.Dict<string | string[]>,
): Promise<void> {
return this.iterateResources<'users', SonarqubeUser>(
'/users/search',
'users',
iteratee,
params,
);
}

async iterateGroupsAssignedToUser(
login: string,
iteratee: ResourceIteratee<SonarqubeUserGroup>,
params?: NodeJS.Dict<string | string[]>,
): Promise<void> {
return this.iterateResources<'groups', SonarqubeUserGroup>(
'/users/groups',
'groups',
iteratee,
{ login, ...params },
);
}

@@ -109,13 +139,15 @@ export class SonarqubeClient {
endpoint: string,
iterableObjectKey: T,
iteratee: ResourceIteratee<U>,
params?: NodeJS.Dict<string | string[]>,
): Promise<void> {
let page = 1;

do {
const searchParams = new URLSearchParams({
page: String(page),
per_page: String(ITEMS_PER_PAGE),
p: String(page),
ps: String(ITEMS_PER_PAGE),
...params,
});

const parametizedEndpoint = `${endpoint}?${searchParams.toString()}`;
@@ -134,9 +166,9 @@ export class SonarqubeClient {
}

if (result[iterableObjectKey].length) {
page = 0; // stop pagination, we've reached the end of the line
} else {
page += 1;
} else {
page = 0; // stop pagination, we've reached the end of the line
}
} while (page);
}
Loading

0 comments on commit 5b7b60c

Please sign in to comment.