fix(apps): initialize launch permissions from license policy
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
import { AppsService } from './apps.service';
|
||||||
|
|
||||||
|
describe('initial launcher permissions', () => {
|
||||||
|
const setup = () => {
|
||||||
|
const prisma = {
|
||||||
|
launcherApp: { findMany: jest.fn().mockResolvedValue([{ id: 'lms', appKey: 'LMS' }, { id: 'other', appKey: 'OTHER' }]) },
|
||||||
|
launcherUserApp: {
|
||||||
|
findMany: jest.fn().mockResolvedValue([]),
|
||||||
|
createMany: jest.fn().mockResolvedValue({ count: 2 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const auth = {
|
||||||
|
getLauncherAppPolicy: jest.fn().mockResolvedValue({
|
||||||
|
defaultCanLaunch: true,
|
||||||
|
permissions: [{ appKey: 'LMS', canLaunch: true }, { appKey: 'OTHER', canLaunch: false }],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const service = new AppsService(prisma as never, auth as never);
|
||||||
|
return { service, prisma, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
it('honors both allowed and denied apps in the license policy', async () => {
|
||||||
|
const { service, prisma, auth } = setup();
|
||||||
|
await service['ensureDefaultAppViewPermissions']('student', 'token');
|
||||||
|
expect(auth.getLauncherAppPolicy).toHaveBeenCalledWith('token');
|
||||||
|
expect(prisma.launcherUserApp.createMany).toHaveBeenCalledWith({
|
||||||
|
data: [
|
||||||
|
{ authUserId: 'student', appId: 'lms', canView: true, canLaunch: true },
|
||||||
|
{ authUserId: 'student', appId: 'other', canView: true, canLaunch: false },
|
||||||
|
], skipDuplicates: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves existing user permissions unchanged and only initializes missing apps', async () => {
|
||||||
|
const { service, prisma } = setup();
|
||||||
|
prisma.launcherUserApp.findMany.mockResolvedValue([{ appId: 'lms' }]);
|
||||||
|
await service['ensureDefaultAppViewPermissions']('student', 'token');
|
||||||
|
expect(prisma.launcherUserApp.createMany.mock.calls[0][0].data).toEqual([
|
||||||
|
{ authUserId: 'student', appId: 'other', canView: true, canLaunch: false },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not write permanent defaults when the auth server fails', async () => {
|
||||||
|
const { service, prisma, auth } = setup();
|
||||||
|
auth.getLauncherAppPolicy.mockRejectedValue(new Error('unavailable'));
|
||||||
|
await expect(service['ensureDefaultAppViewPermissions']('student', 'token')).rejects.toThrow('unavailable');
|
||||||
|
expect(prisma.launcherUserApp.createMany).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies apps when there is no active license', async () => {
|
||||||
|
const { service, prisma, auth } = setup();
|
||||||
|
auth.getLauncherAppPolicy.mockResolvedValue({ defaultCanLaunch: false, permissions: [] });
|
||||||
|
await service['ensureDefaultAppViewPermissions']('student', 'token');
|
||||||
|
expect(prisma.launcherUserApp.createMany.mock.calls[0][0].data.every((row: { canLaunch: boolean }) => !row.canLaunch)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips policy lookup once all user permissions exist', async () => {
|
||||||
|
const { service, prisma, auth } = setup();
|
||||||
|
prisma.launcherUserApp.findMany.mockResolvedValue([{ appId: 'lms' }, { appId: 'other' }]);
|
||||||
|
await service['ensureDefaultAppViewPermissions']('student', 'token');
|
||||||
|
expect(auth.getLauncherAppPolicy).not.toHaveBeenCalled();
|
||||||
|
expect(prisma.launcherUserApp.createMany).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -62,7 +62,7 @@ export class AppsService {
|
|||||||
async getApps(accessToken?: string) {
|
async getApps(accessToken?: string) {
|
||||||
const authUserId = await this.getAuthUserId(accessToken);
|
const authUserId = await this.getAuthUserId(accessToken);
|
||||||
if (authUserId) {
|
if (authUserId) {
|
||||||
await this.ensureDefaultAppViewPermissions(authUserId);
|
await this.ensureDefaultAppViewPermissions(authUserId, accessToken!);
|
||||||
}
|
}
|
||||||
|
|
||||||
const apps = await this.prisma.launcherApp.findMany({
|
const apps = await this.prisma.launcherApp.findMany({
|
||||||
@@ -119,6 +119,9 @@ export class AppsService {
|
|||||||
async getUpdateStatus(appKey: string, input: UpdateStatusInput) {
|
async getUpdateStatus(appKey: string, input: UpdateStatusInput) {
|
||||||
const isLauncherUpdate = appKey.toLowerCase() === 'launcher';
|
const isLauncherUpdate = appKey.toLowerCase() === 'launcher';
|
||||||
const authUserId = isLauncherUpdate ? null : await this.getAuthUserId(input.accessToken);
|
const authUserId = isLauncherUpdate ? null : await this.getAuthUserId(input.accessToken);
|
||||||
|
if (authUserId) {
|
||||||
|
await this.ensureDefaultAppViewPermissions(authUserId, input.accessToken!);
|
||||||
|
}
|
||||||
const canLaunch = isLauncherUpdate
|
const canLaunch = isLauncherUpdate
|
||||||
? true
|
? true
|
||||||
: authUserId
|
: authUserId
|
||||||
@@ -301,19 +304,31 @@ export class AppsService {
|
|||||||
return Boolean(permission);
|
return Boolean(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureDefaultAppViewPermissions(authUserId: string) {
|
private async ensureDefaultAppViewPermissions(authUserId: string, accessToken: string) {
|
||||||
const apps = await this.prisma.launcherApp.findMany({
|
const apps = await this.prisma.launcherApp.findMany({
|
||||||
where: { isEnabled: true },
|
where: { isEnabled: true },
|
||||||
select: { id: true },
|
select: { id: true, appKey: true },
|
||||||
});
|
});
|
||||||
if (!apps.length) return;
|
if (!apps.length) return;
|
||||||
|
|
||||||
|
const existing = await this.prisma.launcherUserApp.findMany({
|
||||||
|
where: { authUserId },
|
||||||
|
select: { appId: true },
|
||||||
|
});
|
||||||
|
const existingIds = new Set(existing.map((permission) => permission.appId));
|
||||||
|
const missing = apps.filter((app) => !existingIds.has(app.id));
|
||||||
|
if (!missing.length) return;
|
||||||
|
|
||||||
|
// Do not persist deny defaults when policy lookup fails; retry on the next request.
|
||||||
|
const policy = await this.authService.getLauncherAppPolicy(accessToken);
|
||||||
|
const byAppKey = new Map(policy.permissions.map((permission) => [permission.appKey, permission.canLaunch]));
|
||||||
|
|
||||||
await this.prisma.launcherUserApp.createMany({
|
await this.prisma.launcherUserApp.createMany({
|
||||||
data: apps.map((app) => ({
|
data: missing.map((app) => ({
|
||||||
authUserId,
|
authUserId,
|
||||||
appId: app.id,
|
appId: app.id,
|
||||||
canView: true,
|
canView: true,
|
||||||
canLaunch: false,
|
canLaunch: byAppKey.get(app.appKey) ?? policy.defaultCanLaunch,
|
||||||
})),
|
})),
|
||||||
skipDuplicates: true,
|
skipDuplicates: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,6 +106,17 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getLauncherAppPolicy(accessToken: string) {
|
||||||
|
return this.request<{
|
||||||
|
defaultCanLaunch: boolean;
|
||||||
|
permissions: Array<{ appKey: string; canLaunch: boolean }>;
|
||||||
|
}>('/auth/launcher-app-policy', {
|
||||||
|
method: 'GET',
|
||||||
|
accessToken,
|
||||||
|
unauthorizedMessage: 'Invalid bearer token',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async handoff(accessToken: string | undefined, audienceApp: string) {
|
async handoff(accessToken: string | undefined, audienceApp: string) {
|
||||||
return this.request('/auth/handoff', {
|
return this.request('/auth/handoff', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
Reference in New Issue
Block a user