Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Monitor user Groups and Templates scripts #52

Merged
merged 21 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 100 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,6 @@ yarn start users migrate \
#### Execution:

```
yarn install

yarn build

yarn start usermonitoring run-permissions-fixer --config-file config.json
or
yarn start usermonitoring run-2fa-reporter --config-file config.json
Expand All @@ -418,10 +414,6 @@ yarn start usermonitoring run-2fa-reporter --config-file config.json
#### Debug:

```
yarn install

yarn build:dev

LOG_LEVEL=debug node --inspect-brk dist/index.js usermonitoring run-users-monitoring --config-file config.json
LOG_LEVEL=debug node --inspect-brk dist/index.js usermonitoring run-2fa-reporter --config-file config.json
```
Expand Down Expand Up @@ -594,10 +586,6 @@ Note: the names are used only to make easy understand and debug the keys.
#### Execution:

```bash
yarn install

yarn build

yarn start usermonitoring run-authorities-monitoring --config-file config.json

# To get the debug logs and store them in a file use:
Expand Down Expand Up @@ -629,7 +617,7 @@ A config file with the access info of the server and the message webhook details
```

This reports stores data into the `d2-tools.authorities-monitor` datastore. This key needs to be setup before the first run to get a correct report.
Its possible to leave `usersByAuthority` empty and use the `-s` flag to populate it.
It's possible to leave `usersByAuthority` empty and use the `-s` flag to populate it.

A sample:

Expand Down Expand Up @@ -674,6 +662,105 @@ A sample:
}
```

### User Groups Monitoring

This script will compare the metadata of the monitored userGroups with the version stored in the datastore and generate a report of the changes. This report will be sent to the MS Teams channel set in the webhook config section. Then the new version of the metadata will be stored in the datastore.

#### Execution:

```bash
yarn start usermonitoring run-user-groups-monitoring --config-file config.json

# To get the debug logs and store them in a file use:
LOG_LEVEL=debug yarn start usermonitoring run-user-groups-monitoring --config-file config.json &> user-groups-monitoring.log
```

#### Parameters:

- `--config-file`: Connection and webhook config file.
- `-s` | `--set-datastore`: Write usergroups data to datastore, use in script setup. It assumes there is a monitoring config in d2-tools/user-groups-monitoring.

#### Requirements:

A config file with the access info of the server and the message webhook details:

```JSON
{
"URL": {
"username": "user",
"password": "passwd",
"server": "https://dhis.url/"
},
"WEBHOOK": {
"ms_url": "http://webhook.url/",
"proxy": "http://proxy.url/",
"server_name": "INSTANCE_NAME"
}
}
```

This reports stores data into the `d2-tools.user-groups-monitoring` datastore. This key needs to be setup before the first run to get a correct report.
Its possible to leave `monitoredUserGroups` empty and use the `-s` flag to populate it.

The report, potentially, has tree sections for each user group:

- New entries: JSON with the properties that were unset or empty and changed.
- Modified fields: This section has two JSONs, one showing the old values and one with the news.
- User assignment changes: This section will show the users lost and added to the group.

If a section is empty it will be omitted.

### User Templates Monitoring

The User Templates Monitoring script is used to compare user templates with the version stored in the datastore and generate a report of the changes. The report includes information on modified fields, and a detailed report on user groups and roles added or lost. This report will be sent to the MS Teams channel set in the webhook config section. The new version of the metadata will be stored in the datastore.

#### Execution:

```bash
yarn start usermonitoring run-user-templates-monitoring --config-file config.json

# To get the debug logs and store them in a file use:
LOG_LEVEL=debug yarn start usermonitoring run-user-templates-monitoring --config-file config.json &> user-templates-monitoring.log
```

#### Parameters:

- `--config-file`: Connection and webhook config file.
- `-s` | `--set-datastore`: Write user templates data to datastore, use in script setup. It assumes there is a monitoring config in d2-tools/user-templates-monitoring.

#### Requirements:

A config file with the access info of the server and the message webhook details:

```JSON
{
"URL": {
"username": "user",
"password": "passwd",
"server": "https://dhis.url/"
},
"WEBHOOK": {
"ms_url": "http://webhook.url/",
"proxy": "http://proxy.url/",
"server_name": "INSTANCE_NAME"
}
}
```

#### Report

This reports stores data into the `d2-tools.user-templates-monitoring` datastore. This key needs to be setup before the first run to get a correct report.

Its possible to leave `monitoredUserTemplates` empty and use the `-s` flag to populate it.
nshandra marked this conversation as resolved.
Show resolved Hide resolved

The report includes the following sections:

- New entries: This section lists new properties that didn't exist in the old version.
- Modified fields: This section shows the fields that have been modified in the user templates and shows the before/after values.
- User Membership changes: This section displays the changes in the template userGroups and userRoles membership.

If a section is empty, it will be omitted from the report.

## Move Attributes from a Program

Get all the TEIS in the program and move the value from the attribute in the argument `--from-attribute-id` to the attribute `--to-attribute-id`. Then delete the value in `--from-attribute-id`.
Expand Down
6 changes: 5 additions & 1 deletion src/data/externalConfig/Namespaces.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
export const d2ToolsNamespace = "d2-tools";

export type Namespace = typeof Namespace[keyof typeof Namespace];
export type Namespace = (typeof Namespace)[keyof typeof Namespace];

export const Namespace = {
PERMISSION_FIXER: "permission-fixer",
TWO_FACTOR_MONITORING: "two-factor-monitoring",
AUTHORITIES_MONITOR: "authorities-monitor",
USER_GROUPS_MONITORING: "user-groups-monitoring",
USER_TEMPLATE_MONITORING: "user-templates-monitoring",
} as const;

export const NamespaceProperties: Record<string, string[]> = {
[Namespace.PERMISSION_FIXER]: [],
[Namespace.TWO_FACTOR_MONITORING]: [],
[Namespace.AUTHORITIES_MONITOR]: [],
[Namespace.USER_GROUPS_MONITORING]: [],
[Namespace.USER_TEMPLATE_MONITORING]: [],
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import _, { isEmpty } from "lodash";
import log from "utils/log";
import { Async } from "domain/entities/Async";
import { MSTeamsWebhookOptions } from "data/user-monitoring/entities/MSTeamsWebhookOptions";
import { MessageRepository } from "domain/repositories/user-monitoring/authorities-monitoring/MessageRepository";
import { MessageRepository } from "domain/repositories/user-monitoring/common/MessageRepository";

export class MessageMSTeamsRepository implements MessageRepository {
constructor(private webhook: MSTeamsWebhookOptions) {}

async sendMessage(message: string): Async<boolean> {
async sendMessage(messageType: string, message: string): Async<boolean> {
const httpProxy = this.webhook.proxy;
const url = this.webhook.ms_url;
const server_name = this.webhook.server_name;
Expand All @@ -18,7 +18,7 @@ export class MessageMSTeamsRepository implements MessageRepository {
}

const postData = JSON.stringify({
text: `[*AUTHORITIES-MONITORING* - ${server_name}] - ${message}`,
text: `[*${messageType}* - ${server_name}] - ${message}`,
});

const requestOptions = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { D2Api } from "@eyeseetea/d2-api/2.36";

import { Id } from "domain/entities/Base";
import { Async } from "domain/entities/Async";
import { UserGroup } from "domain/entities/user-monitoring/user-groups-monitoring/UserGroups";
import { UserGroupRepository } from "domain/repositories/user-monitoring/user-groups-monitoring/UserGroupRepository";

export class UserGroupD2Repository implements UserGroupRepository {
constructor(private api: D2Api) {}

async get(ids: Id[]): Async<UserGroup[]> {
const { userGroups } = await this.api.metadata
.get({
userGroups: {
fields: userFields,
filter: { id: { in: ids } },
},
})
.getData();

return userGroups.map((userGroup): UserGroup => {
return {
...userGroup,
attributeValues: userGroup.attributeValues.map(attributeValue => ({
attribute: attributeValue.attribute.name,
value: attributeValue.value,
})),
};
});
}
}

const userFields = {
id: true,
code: true,
name: true,
access: true,
created: true,
favorite: true,
favorites: true,
lastUpdated: true,
displayName: true,
translations: true,
publicAccess: true,
externalAccess: true,
managedGroups: { id: true, name: true },
managedByGroups: { id: true, name: true },
sharing: { userGroups: true, external: true, users: true, owner: true, public: true },
attributeValues: { attribute: { name: true }, value: true },
user: { id: true, name: true, username: true, displayName: true },
createdBy: { id: true, name: true, username: true, displayName: true },
lastUpdatedBy: { id: true, name: true, username: true, displayName: true },
userAccesses: { id: true, access: true, displayName: true, userUid: true },
userGroupAccesses: { id: true, access: true, displayName: true, userUid: true },
users: { id: true, name: true },
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import _ from "lodash";

import log from "utils/log";
import { D2Api } from "@eyeseetea/d2-api/2.36";
import { Async } from "domain/entities/Async";
import { getObject } from "../common/GetDataStoreObjectByKey";
import { d2ToolsNamespace, Namespace } from "data/externalConfig/Namespaces";
import { UserGroupsMonitoringOptions } from "domain/entities/user-monitoring/user-groups-monitoring/UserGroupsMonitoringOptions";
import { UserGroupsMonitoringConfigRepository } from "domain/repositories/user-monitoring/user-groups-monitoring/UserGroupsMonitoringConfigRepository";

export class UserGroupsMonitoringConfigD2Repository implements UserGroupsMonitoringConfigRepository {
constructor(private api: D2Api) {}

public async get(): Async<UserGroupsMonitoringOptions> {
const config = await getObject<UserGroupsMonitoringOptions>(
this.api,
d2ToolsNamespace,
Namespace.USER_GROUPS_MONITORING
);

if (!config) {
log.warn("Error loading config from datastore");
throw new Error("Error loading config from datastore");
}

return config;
}

public async save(config: UserGroupsMonitoringOptions): Promise<void> {
await this.api.dataStore(d2ToolsNamespace).save(Namespace.USER_GROUPS_MONITORING, config).getData();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Username } from "domain/entities/Base";
import { Async } from "domain/entities/Async";
import { User } from "domain/entities/user-monitoring/user-templates-monitoring/Users";
import { UserRepository } from "domain/repositories/user-monitoring/user-templates-monitoring/UserRepository";
import { D2Api } from "@eyeseetea/d2-api/2.36";

export class UserD2Repository implements UserRepository {
constructor(private api: D2Api) {}

async getByUsername(usernames: Username[]): Async<User[]> {
const users = await this.api.models.users
.get({
fields: userFields,
filter: { username: { in: usernames } },
paging: false,
})
.getData();

return users.objects.map((user): User => {
return {
...user,
username: user.userCredentials.username,
disabled: user.userCredentials.disabled,
twoFA: user.userCredentials.twoFA,
userRoles: user.userCredentials.userRoles,
invitation: user.userCredentials.invitation,
externalAuth: user.userCredentials.externalAuth,
selfRegistered: user.userCredentials.selfRegistered,
catDimensionConstraints: user.userCredentials.catDimensionConstraints,
cogsDimensionConstraints: user.userCredentials.cogsDimensionConstraints,
attributeValues: user.attributeValues.map(attributeValue => ({
attribute: attributeValue.attribute.name,
value: attributeValue.value,
})),
};
});
}
}

const userFields = {
id: true,
code: true,
lastUpdated: true,
created: true,
invitation: true,
firstName: true,
name: true,
favorite: true,
displayName: true,
externalAccess: true,
surname: true,
sharing: true,
access: true,
translations: true,
favorites: true,
organisationUnits: { id: true },
userGroups: { id: true, name: true },
dataViewOrganisationUnits: { id: true },
teiSearchOrganisationUnits: { id: true },
attributeValues: { attribute: { name: true }, value: true },
user: { id: true, name: true, username: true, displayName: true },
createdBy: { id: true, name: true, username: true, displayName: true },
lastUpdatedBy: { id: true, name: true, username: true, displayName: true },
userAccesses: { id: true, access: true, displayName: true, userUid: true },
userGroupAccesses: { id: true, access: true, displayName: true, userUid: true },
userCredentials: {
id: true,
code: true,
twoFA: true,
// NOTE: twoFA changes to twoFactorEnabled in 2.4x
// twoFactorEnabled: true,
access: true,
ldapId: true,
openId: true,
sharing: true,
username: true,
disabled: true,
invitation: true,
externalAuth: true,
selfRegistered: true,
catDimensionConstraints: true,
cogsDimensionConstraints: true,
userRoles: { id: true, name: true },
},
} as const;
Loading