Skip to content

Commit

Permalink
N21-1479 System provisioning options for schools (#4631)
Browse files Browse the repository at this point in the history
* add FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED
* migration script and provisioning impl
* add remove cascade
*add seed data
---------

Co-authored-by: Igor Richter <[email protected]>
Co-authored-by: Arne Gnisa <[email protected]>
Co-authored-by: Igor Richter <[email protected]>
  • Loading branch information
4 people authored Dec 15, 2023
1 parent 00f8125 commit 071a0ef
Show file tree
Hide file tree
Showing 98 changed files with 2,842 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,14 @@ data:
SANIS_CLIENT_SECRET=$(node scripts/secret.js -s $AES_KEY -e $SANIS_CLIENT_SECRET)
SANIS_SYSTEM_ID=0000d186816abba584714c93
if [[ $SC_THEME == "n21" ]]; then
mongosh $DATABASE__URL --quiet --eval 'db.schools.updateOne(
mongosh $DATABASE__URL --quiet --eval 'db.schools.updateMany(
{
"_id": ObjectId("5f2987e020834114b8efd6f8")
"_id": {
$in: [
ObjectId("5f2987e020834114b8efd6f8"),
ObjectId("5fa2c5ccb229544f2c69666c")
]
}
},
{
$set: { "systems" : [ ObjectId("'$SANIS_SYSTEM_ID'") ] }
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/modules/account/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './account.module';
export * from './account-config';
export { AccountService, AccountDto } from './services';
export { AccountService, AccountDto, AccountSaveDto } from './services';
2 changes: 1 addition & 1 deletion apps/server/src/modules/account/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './account.service';
export { AccountDto } from './dto';
export { AccountDto, AccountSaveDto } from './dto';
2 changes: 2 additions & 0 deletions apps/server/src/modules/authorization/authorization.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
LegacySchoolRule,
LessonRule,
SchoolExternalToolRule,
SchoolSystemOptionsRule,
SubmissionRule,
SystemRule,
TaskRule,
Expand Down Expand Up @@ -45,6 +46,7 @@ import { FeathersAuthorizationService, FeathersAuthProvider } from './feathers';
UserLoginMigrationRule,
LegacySchoolRule,
SystemRule,
SchoolSystemOptionsRule,
],
exports: [FeathersAuthorizationService, AuthorizationService, SystemRule],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './user-login-migration.rule';
export * from './user.rule';
export * from './group.rule';
export { SystemRule } from './system.rule';
export { SchoolSystemOptionsRule } from './school-system-options.rule';
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { ObjectId } from '@mikro-orm/mongodb';
import { SchoolSystemOptions } from '@modules/legacy-school';
import { Test, TestingModule } from '@nestjs/testing';
import { SchoolEntity, SystemEntity, User } from '@shared/domain/entity';
import { Permission } from '@shared/domain/interface';
import {
schoolFactory,
schoolSystemOptionsFactory,
setupEntities,
systemEntityFactory,
userFactory,
} from '@shared/testing';
import { AuthorizationContextBuilder } from '../mapper';
import { AuthorizationHelper } from '../service/authorization.helper';
import { SchoolSystemOptionsRule } from './school-system-options.rule';

describe(SchoolSystemOptionsRule.name, () => {
let module: TestingModule;
let rule: SchoolSystemOptionsRule;

let authorizationHelper: DeepMocked<AuthorizationHelper>;

beforeAll(async () => {
await setupEntities();

module = await Test.createTestingModule({
providers: [
SchoolSystemOptionsRule,
{
provide: AuthorizationHelper,
useValue: createMock<AuthorizationHelper>(),
},
],
}).compile();

rule = module.get(SchoolSystemOptionsRule);
authorizationHelper = module.get(AuthorizationHelper);
});

afterAll(async () => {
await module.close();
});

afterEach(() => {
jest.resetAllMocks();
});

describe('isApplicable', () => {
describe('when the entity is applicable', () => {
const setup = () => {
const user: User = userFactory.buildWithId();
const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build();

return {
user,
schoolSystemOptions,
};
};

it('should return true', () => {
const { user, schoolSystemOptions } = setup();

const result = rule.isApplicable(user, schoolSystemOptions);

expect(result).toEqual(true);
});
});

describe('when the entity is not applicable', () => {
const setup = () => {
const user: User = userFactory.buildWithId();

return {
user,
};
};

it('should return false', () => {
const { user } = setup();

const result = rule.isApplicable(user, {} as unknown as SchoolSystemOptions);

expect(result).toEqual(false);
});
});
});

describe('hasPermission', () => {
describe('when the user accesses a system at his school with the required permissions', () => {
const setup = () => {
const systemEntity: SystemEntity = systemEntityFactory.buildWithId();
const school: SchoolEntity = schoolFactory.buildWithId({
systems: [systemEntity],
});
const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build({
systemId: systemEntity.id,
schoolId: school.id,
});
const user: User = userFactory.buildWithId({ school });
const authorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_SYSTEM_VIEW]);

authorizationHelper.hasAllPermissions.mockReturnValueOnce(true);

return {
user,
schoolSystemOptions,
authorizationContext,
};
};

it('should check the permission', () => {
const { user, schoolSystemOptions, authorizationContext } = setup();

rule.hasPermission(user, schoolSystemOptions, authorizationContext);

expect(authorizationHelper.hasAllPermissions).toHaveBeenCalledWith(
user,
authorizationContext.requiredPermissions
);
});

it('should return true', () => {
const { user, schoolSystemOptions, authorizationContext } = setup();

const result = rule.hasPermission(user, schoolSystemOptions, authorizationContext);

expect(result).toEqual(true);
});
});

describe('when the user accesses a system at his school, but does not have the required permissions', () => {
const setup = () => {
const systemEntity: SystemEntity = systemEntityFactory.buildWithId();
const school: SchoolEntity = schoolFactory.buildWithId({
systems: [systemEntity],
});
const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build({
systemId: systemEntity.id,
schoolId: school.id,
});
const user: User = userFactory.buildWithId({ school });
const authorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_SYSTEM_VIEW]);

authorizationHelper.hasAllPermissions.mockReturnValueOnce(false);

return {
user,
schoolSystemOptions,
authorizationContext,
};
};

it('should return false', () => {
const { user, schoolSystemOptions, authorizationContext } = setup();

const result = rule.hasPermission(user, schoolSystemOptions, authorizationContext);

expect(result).toEqual(false);
});
});

describe('when the system is not part of the users school', () => {
const setup = () => {
const systemEntity: SystemEntity = systemEntityFactory.buildWithId();
const school: SchoolEntity = schoolFactory.buildWithId({
systems: [systemEntity],
});
const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build({
systemId: new ObjectId().toHexString(),
schoolId: school.id,
});
const user: User = userFactory.buildWithId({ school });
const authorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_SYSTEM_VIEW]);

authorizationHelper.hasAllPermissions.mockReturnValueOnce(true);

return {
user,
schoolSystemOptions,
authorizationContext,
};
};

it('should return false', () => {
const { user, schoolSystemOptions, authorizationContext } = setup();

const result = rule.hasPermission(user, schoolSystemOptions, authorizationContext);

expect(result).toEqual(false);
});
});

describe('when the user is not at the school', () => {
const setup = () => {
const schoolSystemOptions: SchoolSystemOptions = schoolSystemOptionsFactory.build();
const systemEntity: SystemEntity = systemEntityFactory.buildWithId();
const school: SchoolEntity = schoolFactory.buildWithId({
systems: [systemEntity],
});
const user: User = userFactory.buildWithId({ school });
const authorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_SYSTEM_VIEW]);

authorizationHelper.hasAllPermissions.mockReturnValueOnce(true);

return {
user,
schoolSystemOptions,
authorizationContext,
};
};

it('should return false', () => {
const { user, schoolSystemOptions, authorizationContext } = setup();

const result = rule.hasPermission(user, schoolSystemOptions, authorizationContext);

expect(result).toEqual(false);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AnyProvisioningOptions, SchoolSystemOptions } from '@modules/legacy-school';
import { Injectable } from '@nestjs/common';
import { User } from '@shared/domain/entity';
import { AuthorizationHelper } from '../service/authorization.helper';
import { AuthorizationContext, Rule } from '../type';

@Injectable()
export class SchoolSystemOptionsRule implements Rule<SchoolSystemOptions> {
constructor(private readonly authorizationHelper: AuthorizationHelper) {}

public isApplicable(user: User, domainObject: SchoolSystemOptions): boolean {
const isMatched: boolean = domainObject instanceof SchoolSystemOptions<AnyProvisioningOptions>;

return isMatched;
}

public hasPermission(user: User, domainObject: SchoolSystemOptions, context: AuthorizationContext): boolean {
const hasPermissions: boolean = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions);

const isAtSchool: boolean = user.school.id === domainObject.schoolId;

const hasSystem: boolean = user.school.systems.getIdentifiers().includes(domainObject.systemId);

const isAuthorized: boolean = hasPermissions && isAtSchool && hasSystem;

return isAuthorized;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
LegacySchoolRule,
LessonRule,
SchoolExternalToolRule,
SchoolSystemOptionsRule,
SubmissionRule,
SystemRule,
TaskRule,
Expand All @@ -37,6 +38,7 @@ describe('RuleManager', () => {
let userLoginMigrationRule: DeepMocked<UserLoginMigrationRule>;
let groupRule: DeepMocked<GroupRule>;
let systemRule: DeepMocked<SystemRule>;
let schoolSystemOptionsRule: DeepMocked<SchoolSystemOptionsRule>;

beforeAll(async () => {
await setupEntities();
Expand All @@ -58,6 +60,7 @@ describe('RuleManager', () => {
{ provide: ContextExternalToolRule, useValue: createMock<ContextExternalToolRule>() },
{ provide: UserLoginMigrationRule, useValue: createMock<UserLoginMigrationRule>() },
{ provide: SystemRule, useValue: createMock<SystemRule>() },
{ provide: SchoolSystemOptionsRule, useValue: createMock<SchoolSystemOptionsRule>() },
],
}).compile();

Expand All @@ -76,6 +79,7 @@ describe('RuleManager', () => {
userLoginMigrationRule = await module.get(UserLoginMigrationRule);
groupRule = await module.get(GroupRule);
systemRule = await module.get(SystemRule);
schoolSystemOptionsRule = await module.get(SchoolSystemOptionsRule);
});

afterEach(() => {
Expand Down Expand Up @@ -108,6 +112,7 @@ describe('RuleManager', () => {
userLoginMigrationRule.isApplicable.mockReturnValueOnce(false);
groupRule.isApplicable.mockReturnValueOnce(false);
systemRule.isApplicable.mockReturnValueOnce(false);
schoolSystemOptionsRule.isApplicable.mockReturnValueOnce(false);

return { user, object, context };
};
Expand All @@ -131,6 +136,7 @@ describe('RuleManager', () => {
expect(userLoginMigrationRule.isApplicable).toBeCalled();
expect(groupRule.isApplicable).toBeCalled();
expect(systemRule.isApplicable).toBeCalled();
expect(schoolSystemOptionsRule.isApplicable).toBeCalled();
});

it('should return CourseRule', () => {
Expand Down Expand Up @@ -162,6 +168,7 @@ describe('RuleManager', () => {
userLoginMigrationRule.isApplicable.mockReturnValueOnce(false);
groupRule.isApplicable.mockReturnValueOnce(false);
systemRule.isApplicable.mockReturnValueOnce(false);
schoolSystemOptionsRule.isApplicable.mockReturnValueOnce(false);

return { user, object, context };
};
Expand Down Expand Up @@ -193,6 +200,7 @@ describe('RuleManager', () => {
userLoginMigrationRule.isApplicable.mockReturnValueOnce(false);
groupRule.isApplicable.mockReturnValueOnce(false);
systemRule.isApplicable.mockReturnValueOnce(false);
schoolSystemOptionsRule.isApplicable.mockReturnValueOnce(false);

return { user, object, context };
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
LegacySchoolRule,
LessonRule,
SchoolExternalToolRule,
SchoolSystemOptionsRule,
SubmissionRule,
SystemRule,
TaskRule,
Expand Down Expand Up @@ -38,7 +39,8 @@ export class RuleManager {
private readonly contextExternalToolRule: ContextExternalToolRule,
private readonly userLoginMigrationRule: UserLoginMigrationRule,
private readonly groupRule: GroupRule,
private readonly systemRule: SystemRule
private readonly systemRule: SystemRule,
private readonly schoolSystemOptionsRule: SchoolSystemOptionsRule
) {
this.rules = [
this.courseRule,
Expand All @@ -55,6 +57,7 @@ export class RuleManager {
this.userLoginMigrationRule,
this.groupRule,
this.systemRule,
this.schoolSystemOptionsRule,
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class ColumnBoardService {
return rootBoardDo;
}

throw new NotFoundLoggableException(ColumnBoard.name, 'id', rootId);
throw new NotFoundLoggableException(ColumnBoard.name, { id: rootId });
}

async getBoardObjectTitlesById(boardIds: EntityId[]): Promise<Record<EntityId, string>> {
Expand Down
Loading

0 comments on commit 071a0ef

Please sign in to comment.