Skip to content

Commit

Permalink
feat: header based user permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Nov 6, 2024
1 parent 00ec691 commit b154931
Show file tree
Hide file tree
Showing 128 changed files with 1,776 additions and 4,664 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ meteor/.coverage/
node_modules
**/yarn-error.log
scratch/
meteor-settings.json

# Exclude JetBrains IDE specific files
.idea
Expand Down
2 changes: 0 additions & 2 deletions meteor/.meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,4 @@ [email protected] # Enable TypeScript syntax in .ts and .tsx modules

[email protected] # Meteor's client-side reactive programming library

[email protected]

zodern:types
1 change: 0 additions & 1 deletion meteor/__mocks__/_setupMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ jest.mock('meteor/meteor', (...args) => require('./meteor').setup(args), { virtu
jest.mock('meteor/random', (...args) => require('./random').setup(args), { virtual: true })
jest.mock('meteor/check', (...args) => require('./check').setup(args), { virtual: true })
jest.mock('meteor/tracker', (...args) => require('./tracker').setup(args), { virtual: true })
jest.mock('meteor/accounts-base', (...args) => require('./accounts-base').setup(args), { virtual: true })
jest.mock('meteor/ejson', (...args) => require('./ejson').setup(args), { virtual: true })

jest.mock('meteor/mdg:validated-method', (...args) => require('./validated-method').setup(args), { virtual: true })
Expand Down
81 changes: 0 additions & 81 deletions meteor/__mocks__/accounts-base.ts

This file was deleted.

39 changes: 6 additions & 33 deletions meteor/__mocks__/meteor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MongoMock } from './mongo'
import { USER_PERMISSIONS_HEADER } from '@sofie-automation/meteor-lib/dist/userPermissions'

let controllableDefer = false

Expand All @@ -9,27 +9,14 @@ export function useNextTickDefer(): void {
controllableDefer = false
}

namespace Meteor {
export namespace Meteor {
export interface Settings {
public: {
[id: string]: any
}
[id: string]: any
}

export interface UserEmail {
address: string
verified: boolean
}
export interface User {
_id?: string
username?: string
emails?: UserEmail[]
createdAt?: number
profile?: any
services?: any
}

export interface ErrorStatic {
new (error: string | number, reason?: string, details?: string): Error
}
Expand Down Expand Up @@ -103,22 +90,18 @@ export namespace MeteorMock {
export const settings: any = {}

export const mockMethods: { [name: string]: Function } = {}
export let mockUser: Meteor.User | undefined = undefined
export const mockStartupFunctions: Function[] = []

export const absolutePath = process.cwd()

export function user(): Meteor.User | undefined {
return mockUser
}
export function userId(): string | undefined {
return mockUser ? mockUser._id : undefined
}
function getMethodContext() {
return {
userId: mockUser ? mockUser._id : undefined,
connection: {
clientAddress: '1.1.1.1',
httpHeaders: {
// Default to full permissions for tests
[USER_PERMISSIONS_HEADER]: 'admin',
},
},
unblock: () => {
// noop
Expand Down Expand Up @@ -212,9 +195,6 @@ export namespace MeteorMock {
// but it'll do for now:
return callAsync(methodName, ...args)
}
export function absoluteUrl(path?: string): string {
return path + '' // todo
}
export function setTimeout(fcn: () => void | Promise<void>, time: number): number {
return $.setTimeout(() => {
Promise.resolve()
Expand Down Expand Up @@ -256,7 +236,6 @@ export namespace MeteorMock {
return fcn(...args)
}
}
export let users: MongoMock.Collection<any> | undefined = undefined

// -- Mock functions: --------------------------
/**
Expand All @@ -269,12 +248,6 @@ export namespace MeteorMock {

await waitTimeNoFakeTimers(10) // So that any observers or defers has had time to run.
}
export function mockLoginUser(newUser: Meteor.User): void {
mockUser = newUser
}
export function mockSetUsersCollection(usersCollection: MongoMock.Collection<any>): void {
users = usersCollection
}
export function mockSetClientEnvironment(): void {
mockIsClient = true
}
Expand Down
2 changes: 0 additions & 2 deletions meteor/__mocks__/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,5 +453,3 @@ export function setup(): any {
Mongo: MongoMock,
}
}

MeteorMock.mockSetUsersCollection(new MongoMock.Collection('Meteor.users'))
20 changes: 20 additions & 0 deletions meteor/server/Connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { logger } from './logging'
import { sendTrace } from './api/integration/influx'
import { PeripheralDevices } from './collections'
import { MetricsGauge } from '@sofie-automation/corelib/dist/prometheus'
import { parseUserPermissions, USER_PERMISSIONS_HEADER } from '@sofie-automation/meteor-lib/dist/userPermissions'
import { Settings } from './Settings'

const connections = new Set<string>()
const connectionsGauge = new MetricsGauge({
Expand All @@ -14,6 +16,24 @@ const connectionsGauge = new MetricsGauge({
Meteor.onConnection((conn: Meteor.Connection) => {
// This is called whenever a new ddp-connection is opened (ie a web-client or a peripheral-device)

if (Settings.enableHeaderAuth) {
const userLevel = parseUserPermissions(conn.httpHeaders[USER_PERMISSIONS_HEADER])

// HACK: force the userId of the connection before it can be used.
// This ensures we know the permissions of the connection before it can try to do anything
// This could probably be safely done inside a meteor method, as we only need it when directly modifying a collection in the client,
// but that will cause all the publications to restart when changing the userId.
const connSession = (Meteor as any).server.sessions.get(conn.id)
if (!connSession) {
logger.error(`Failed to find session for ddp connection! "${conn.id}"`)
// Close the connection, it won't be secure
conn.close()
return
} else {
connSession.userId = JSON.stringify(userLevel)
}
}

const connectionId: string = conn.id
// var clientAddress = conn.clientAddress; // ip-adress

Expand Down
13 changes: 10 additions & 3 deletions meteor/server/__tests__/cronjobs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,8 @@ describe('cronjobs', () => {
expect(await Snapshots.findOneAsync(snapshot1)).toBeUndefined()
})
async function insertPlayoutDevice(
props: Pick<PeripheralDevice, 'subType' | 'deviceName' | 'lastSeen' | 'parentDeviceId'>
props: Pick<PeripheralDevice, 'subType' | 'deviceName' | 'lastSeen' | 'parentDeviceId'> &
Partial<Pick<PeripheralDevice, 'token'>>
): Promise<PeripheralDeviceId> {
const deviceId = protectString<PeripheralDeviceId>(getRandomString())
await PeripheralDevices.insertAsync({
Expand Down Expand Up @@ -495,37 +496,43 @@ describe('cronjobs', () => {
}

async function createMockPlayoutGatewayAndDevices(lastSeen: number): Promise<{
deviceToken: string
mockPlayoutGw: PeripheralDeviceId
mockCasparCg: PeripheralDeviceId
mockAtem: PeripheralDeviceId
}> {
const deviceToken = 'token1'
const mockPlayoutGw = await insertPlayoutDevice({
deviceName: 'Playout Gateway',
lastSeen: lastSeen,
subType: PERIPHERAL_SUBTYPE_PROCESS,
token: deviceToken,
})
const mockCasparCg = await insertPlayoutDevice({
deviceName: 'CasparCG',
lastSeen: lastSeen,
subType: TSR.DeviceType.CASPARCG,
parentDeviceId: mockPlayoutGw,
token: deviceToken,
})
const mockAtem = await insertPlayoutDevice({
deviceName: 'ATEM',
lastSeen: lastSeen,
subType: TSR.DeviceType.ATEM,
parentDeviceId: mockPlayoutGw,
token: deviceToken,
})

return {
deviceToken,
mockPlayoutGw,
mockCasparCg,
mockAtem,
}
}

test('Attempts to restart CasparCG when job is enabled', async () => {
const { mockCasparCg } = await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold
const { mockCasparCg, deviceToken } = await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold

;(logger.info as jest.Mock).mockClear()
// set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC
Expand All @@ -548,7 +555,7 @@ describe('cronjobs', () => {
Meteor.callAsync(
'peripheralDevice.functionReply',
cmd.deviceId, // deviceId
'', // deviceToken
deviceToken, // deviceToken
cmd._id, // commandId
null, // err
null // result
Expand Down
Loading

0 comments on commit b154931

Please sign in to comment.