Skip to content

Commit

Permalink
SPSH-624 Implemented itslearning client (#535)
Browse files Browse the repository at this point in the history
* Implemented itslearning client
  • Loading branch information
marode-cap authored Jun 12, 2024
1 parent 51ae7bd commit 1859fbe
Show file tree
Hide file tree
Showing 27 changed files with 782 additions and 6 deletions.
16 changes: 11 additions & 5 deletions charts/dbildungs-iam-server/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
"USERNAME": "default",
"USE_TLS": false
},
"LDAP": {
"URL": "ldap://dbildungs-iam-server-ldap",
"BIND_DN": "cn=admin,dc=schule-sh,dc=de",
"PASSWORD": "admin"
},
"LDAP": {
"URL": "ldap://dbildungs-iam-server-ldap",
"BIND_DN": "cn=admin,dc=schule-sh,dc=de",
"PASSWORD": "admin"
},
"DATA": {
"ROOT_ORGANISATION_ID": "d39cb7cf-2f9b-45f1-849f-973661f2f057"
},
Expand All @@ -45,5 +45,11 @@
"KEYCLOAK_ADMINISTRATION_MODULE_LOG_LEVEL": "debug",
"HEALTH_MODULE_LOG_LEVEL": "debug",
"BACKEND_FOR_FRONTEND_MODULE_LOG_LEVEL": "debug"
},
"ITSLEARNING": {
"ENABLED": false,
"ENDPOINT": "https://itslearning.example.com",
"USERNAME": "username",
"PASSWORD": "password"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,19 @@
secretKeyRef:
name: {{ default .Values.auth.existingSecret .Values.auth.secretName }}
key: frontend-sessionSecret
- name: ITSLEARNING_ENDPOINT
valueFrom:
secretKeyRef:
name: {{ default .Values.auth.existingSecret .Values.auth.secretName }}
key: itslearning-endpoint
- name: ITSLEARNING_USERNAME
valueFrom:
secretKeyRef:
name: {{ default .Values.auth.existingSecret .Values.auth.secretName }}
key: itslearning-username
- name: ITSLEARNING_PASSWORD
valueFrom:
secretKeyRef:
name: {{ default .Values.auth.existingSecret .Values.auth.secretName }}
key: itslearning-password
{{- end}}
5 changes: 4 additions & 1 deletion charts/dbildungs-iam-server/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ data:
db-username: {{ .Values.database.username }}
keycloak-adminSecret: {{ .Values.auth.keycloak_adminSecret }}
keycloak-clientSecret: {{ .Values.auth.keycloak_clientSecret }}
itslearning-endpoint: {{ .Values.auth.itslearning_endpoint }}
itslearning-username: {{ .Values.auth.itslearning_username }}
itslearning-password: {{ .Values.auth.itslearning_password }}
secrets-json: {{ .Values.auth.secrets_json }}
{{- end }}
{{- end }}
3 changes: 3 additions & 0 deletions charts/dbildungs-iam-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ auth:
keycloak_clientSecret: ""
secrets_json: ""
frontend_sessionSecret: ""
itslearning_endpoint: ""
itslearning_username: ""
itslearning_password: ""


backend:
Expand Down
6 changes: 6 additions & 0 deletions config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,11 @@
"KEYCLOAK_ADMINISTRATION_MODULE_LOG_LEVEL": "debug",
"HEALTH_MODULE_LOG_LEVEL": "debug",
"BACKEND_FOR_FRONTEND_MODULE_LOG_LEVEL": "debug"
},
"ITSLEARNING": {
"ENABLED": false,
"ENDPOINT": "https://itslearning-test.example.com",
"USERNAME": "username",
"PASSWORD": "password"
}
}
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"connect-redis": "^7.1.1",
"express": "^4.19.2",
"express-session": "^1.18.0",
"fast-xml-parser": "^4.4.0",
"follow-redirects": "^1.15.6",
"generate-password-ts": "^1.6.5",
"jsonwebtoken": "^9.0.2",
Expand Down
84 changes: 84 additions & 0 deletions src/modules/itslearning/actions/base-action.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { DomainError } from '../../../shared/error/domain.error.js';
import { ItsLearningError } from '../../../shared/error/its-learning.error.js';
import { IMSESAction } from './base-action.js';
import { faker } from '@faker-js/faker';

function buildXMLResponse(codeMajor: 'success' | 'failure', severity: 'status' | 'error', body: string): string {
return `<s:Envelope
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<h:syncResponseHeaderInfo
xmlns:h="http://www.imsglobal.org/services/common/imsMessBindSchema_v1p0"
xmlns="http://www.imsglobal.org/services/common/imsMessBindSchema_v1p0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<messageIdentifier/>
<statusInfo>
<codeMajor>${codeMajor}</codeMajor>
<severity>${severity}c</severity>
<messageIdRef/>
</statusInfo>
</h:syncResponseHeaderInfo>
<o:Security s:mustUnderstand="1"
xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<u:Timestamp u:Id="_0">
<u:Created>${faker.date.recent().toISOString()}</u:Created>
<u:Expires>${faker.date.soon().toISOString()}</u:Expires>
</u:Timestamp>
</o:Security>
</s:Header>
<s:Body
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
${body}
</s:Body>
</s:Envelope>`;
}

type DummyResponse = {
dummyResponse: string;
};

class TestAction extends IMSESAction<DummyResponse, string> {
public action: string = faker.internet.url();

public buildRequest(): object {
return {};
}

public parseBody(body: DummyResponse): Result<string, DomainError> {
return {
ok: true,
value: body.dummyResponse,
};
}
}

describe('IMSESAction', () => {
describe('parseResponse', () => {
it('should parse XML', () => {
const xmlTest: string = buildXMLResponse('success', 'status', '<dummyResponse>test</dummyResponse>');
const testAction: TestAction = new TestAction();

const result: Result<string, DomainError> = testAction.parseResponse(xmlTest);

expect(result).toEqual({
ok: true,
value: 'test',
});
});

it('should return ItsLearningError if response is an error', () => {
const xmlTest: string = buildXMLResponse('failure', 'error', '<dummyResponse/>');
const testAction: TestAction = new TestAction();

const result: Result<string, DomainError> = testAction.parseResponse(xmlTest);

expect(result).toEqual({
ok: false,
error: new ItsLearningError('Request failed', expect.anything() as Record<string, unknown>),
});
});
});
});
64 changes: 64 additions & 0 deletions src/modules/itslearning/actions/base-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { XMLBuilder, XMLParser } from 'fast-xml-parser';

import { DomainError, ItsLearningError } from '../../../shared/error/index.js';

export type StatusInfo =
| {
codeMajor: 'failure';
severity: 'error';
}
| {
codeMajor: 'success';
severity: 'status';
};

export type BaseResponse<BodyResponse> = {
Envelope: {
Header: {
syncResponseHeaderInfo: {
statusInfo: StatusInfo;
};
};

Body: BodyResponse;
};
};

export abstract class IMSESAction<ResponseBodyType, ResultType> {
protected readonly xmlBuilder: XMLBuilder = new XMLBuilder({ ignoreAttributes: false });

protected readonly xmlParser: XMLParser = new XMLParser({
ignoreAttributes: false,
removeNSPrefix: true,
isArray: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) =>
this.isArrayOverride(tagName, jPath, isLeafNode, isAttribute),
});

public abstract action: string;

public abstract buildRequest(): object;

// Customize parsing behaviour, see X2jOptions.isArray
public isArrayOverride(_tagName: string, _jPath: string, _isLeafNode: boolean, _isAttribute: boolean): boolean {
return false;
}

/**
* Will be called if the response was successful
* @param body The contents of the response body
*/
public abstract parseBody(body: ResponseBodyType): Result<ResultType, DomainError>;

public parseResponse(input: string): Result<ResultType, DomainError> {
const result: BaseResponse<ResponseBodyType> = this.xmlParser.parse(input) as BaseResponse<ResponseBodyType>;

if (result.Envelope.Header.syncResponseHeaderInfo.statusInfo.codeMajor === 'failure') {
return {
ok: false,
error: new ItsLearningError('Request failed', result),
};
} else {
return this.parseBody(result.Envelope.Body);
}
}
}
33 changes: 33 additions & 0 deletions src/modules/itslearning/actions/create-group.action.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { faker } from '@faker-js/faker';
import { CreateGroupAction } from './create-group.action.js';

describe('CreateGroupAction', () => {
describe('buildRequest', () => {
it('should return object', () => {
const action: CreateGroupAction = new CreateGroupAction({
id: faker.string.uuid(),
name: `${faker.word.adjective()} school`,
parentId: faker.string.uuid(),
type: 'School',
});

expect(action.buildRequest()).toBeDefined();
});
});

describe('parseBody', () => {
it('should void result', () => {
const action: CreateGroupAction = new CreateGroupAction({
id: faker.string.uuid(),
name: `${faker.word.adjective()} school`,
parentId: faker.string.uuid(),
type: 'School',
});

expect(action.parseBody()).toEqual({
ok: true,
value: undefined,
});
});
});
});
72 changes: 72 additions & 0 deletions src/modules/itslearning/actions/create-group.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { DomainError } from '../../../shared/error/domain.error.js';
import { IMS_COMMON_SCHEMA, IMS_GROUP_MAN_DATA_SCHEMA, IMS_GROUP_MAN_MESS_SCHEMA } from '../schemas.js';
import { IMSESAction } from './base-action.js';

// Incomplete
export type CreateGroupParams = {
id: string;

name: string;
type: 'School' | 'Course' | 'CourseGroup';

parentId: string;
relationLabel?: string;

longDescription?: string;
fullDescription?: string;
};

type CreateGroupResponseBody = {
createGroupResponse: undefined;
};

export class CreateGroupAction extends IMSESAction<CreateGroupResponseBody, void> {
public override action: string = 'http://www.imsglobal.org/soap/gms/createGroup';

public constructor(private readonly params: CreateGroupParams) {
super();
}

public override buildRequest(): object {
return {
'ims:createGroupRequest': {
'@_xmlns:ims': IMS_GROUP_MAN_MESS_SCHEMA,
'@_xmlns:ims1': IMS_COMMON_SCHEMA,
'@_xmlns:ims2': IMS_GROUP_MAN_DATA_SCHEMA,

'ims:sourcedId': {
'ims1:identifier': this.params.id,
},

'ims:group': {
'ims2:groupType': {
'ims2:scheme': 'ItslearningOrganisationTypes',
'ims2:typeValue': {
'ims2:type': this.params.type,
},
},
'ims2:relationship': {
'ims2:relation': 'Parent',
'ims2:sourceId': {
'ims1:identifier': this.params.parentId,
},
'ims2:label': this.params.relationLabel,
},
'ims2:description': {
'ims2:descShort': this.params.name,
'ims2:descLong': this.params.longDescription,
'ims2:descFull': this.params.fullDescription,
},
},
},
};
}

public override parseBody(): Result<void, DomainError> {
// Response does not contain data
return {
ok: true,
value: undefined,
};
}
}
Loading

0 comments on commit 1859fbe

Please sign in to comment.