@@ -27,9 +27,6 @@ export type HeadscalePolicyStatus = {
|
|||||||
raw: HeadscalePolicy | Record<string, unknown> | null
|
raw: HeadscalePolicy | Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatedPrefix = 'group:lms-platform-'
|
|
||||||
const gatewayGroup = 'group:lms-platform-gateway'
|
|
||||||
|
|
||||||
export class HeadscaleAdapter {
|
export class HeadscaleAdapter {
|
||||||
async applyGrant(_grant: AccessGrant): Promise<{ applied: boolean; mode: 'noop' | 'headscale' }> {
|
async applyGrant(_grant: AccessGrant): Promise<{ applied: boolean; mode: 'noop' | 'headscale' }> {
|
||||||
return { applied: true, mode: 'noop' }
|
return { applied: true, mode: 'noop' }
|
||||||
@@ -76,60 +73,16 @@ export class HeadscaleAdapter {
|
|||||||
unresolved: string[]
|
unresolved: string[]
|
||||||
error?: string
|
error?: string
|
||||||
}> {
|
}> {
|
||||||
const nodes = await this.listNodes()
|
|
||||||
const unresolved: string[] = []
|
|
||||||
const groups: Record<string, string[]> = {
|
|
||||||
[gatewayGroup]: [...new Set(config.headscaleGatewayUsers.map((user) => this.toHeadscaleUserName(user)))],
|
|
||||||
}
|
|
||||||
const acls: HeadscalePolicy['acls'] = [
|
|
||||||
{
|
|
||||||
// The local bridge initiates discovery from the trusted gateway host.
|
|
||||||
action: 'accept',
|
|
||||||
src: [gatewayGroup],
|
|
||||||
dst: ['tag:teaching-device:*'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// DDS discovery and topic payloads use independent UDP flows, so the
|
|
||||||
// teaching device must also be able to reach the bridge on the gateway.
|
|
||||||
action: 'accept',
|
|
||||||
src: ['tag:teaching-device'],
|
|
||||||
dst: [`${gatewayGroup}:*`],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
assignments.forEach((assignment, index) => {
|
|
||||||
const users = [...new Set(assignment.studentEmails.map((email) => this.toHeadscaleUserName(email)).filter(Boolean))]
|
|
||||||
if (!users.length) return
|
|
||||||
|
|
||||||
const node = this.findPlatformNode(nodes, assignment)
|
|
||||||
const ip = node ? this.extractIpAddress(node) : null
|
|
||||||
if (!ip) {
|
|
||||||
unresolved.push(assignment.platformName || assignment.agentHostname || assignment.platformId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupName = `${generatedPrefix}${this.toPolicyToken(assignment.platformId || assignment.agentHostname || String(index))}`
|
|
||||||
groups[groupName] = users
|
|
||||||
acls.push({
|
|
||||||
action: 'accept',
|
|
||||||
src: [groupName],
|
|
||||||
dst: [`${ip}:*`],
|
|
||||||
})
|
|
||||||
acls.push({
|
|
||||||
// ROS 2 DDS discovery and topic payloads also originate from the
|
|
||||||
// assigned platform, so keep the student workspace link bidirectional.
|
|
||||||
action: 'accept',
|
|
||||||
src: [ip],
|
|
||||||
dst: [`${groupName}:*`],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const policy: HeadscalePolicy = {
|
const policy: HeadscalePolicy = {
|
||||||
groups,
|
groups: {},
|
||||||
tagOwners: {
|
tagOwners: {
|
||||||
'tag:teaching-device': [`${config.headscaleTeachingDeviceOwner}@`],
|
'tag:teaching-device': [`${config.headscaleTeachingDeviceOwner}@`],
|
||||||
},
|
},
|
||||||
acls,
|
acls: [{
|
||||||
|
action: 'accept',
|
||||||
|
src: ['*'],
|
||||||
|
dst: ['*:*'],
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.applyPolicy(policy)
|
const result = await this.applyPolicy(policy)
|
||||||
@@ -137,8 +90,8 @@ export class HeadscaleAdapter {
|
|||||||
applied: result.applied,
|
applied: result.applied,
|
||||||
mode: 'headscale',
|
mode: 'headscale',
|
||||||
assignmentCount: assignments.length,
|
assignmentCount: assignments.length,
|
||||||
aclCount: acls.length,
|
aclCount: policy.acls.length,
|
||||||
unresolved,
|
unresolved: [],
|
||||||
...(result.error ? { error: result.error } : {}),
|
...(result.error ? { error: result.error } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user