icynet management

This commit is contained in:
Evert Prants 2022-11-06 16:19:19 +02:00
parent fa93a47603
commit 26401c130a
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
10 changed files with 162 additions and 6 deletions

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
/dist
/node_modules
*.zone
*.env
*.db
# Logs
logs

BIN
data.db

Binary file not shown.

View File

@ -6,6 +6,7 @@ import { AppService } from './app.service';
import { ObjectsModule } from './modules/objects/objects.module';
import { ZoneModule } from './modules/zone/zone.module';
import configuration from './config/configuration';
import { IcynetModule } from './modules/icynet/icynet.module';
@Module({
imports: [
@ -17,6 +18,7 @@ import configuration from './config/configuration';
}),
ObjectsModule,
ZoneModule,
IcynetModule,
],
controllers: [AppController],
providers: [AppService],

View File

@ -11,6 +11,7 @@ export default () => ({
},
cacheTTL: parseInt(process.env.ZONE_CACHE_TTL, 10) || 1600,
zoneDir: '.',
icynetKey: process.env.ICYNET_KEY || 'ch4ng3 m3!',
rndc: {
host: process.env.RNDC_SERVER || '127.0.0.1',
port: parseInt(process.env.RNDC_PORT, 10) || 953,

View File

@ -0,0 +1,23 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import { timingSafeEqual } from 'crypto';
@Injectable()
export class IcynetGuard implements CanActivate {
constructor(private config: ConfigService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const authHeader = request.headers.authorization;
if (!authHeader) return false;
const [base, token] = authHeader.split(' ');
if (!base || base.toLowerCase() !== 'bearer' || !token) return false;
const configured = this.config.get<string>('icynetKey');
return timingSafeEqual(Buffer.from(token), Buffer.from(configured));
}
}

View File

@ -0,0 +1,82 @@
import {
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import { IcynetGuard } from 'src/guards/icynet.guard';
import { ManagerService } from '../objects/manager/manager.service';
import { ZoneEntity } from '../objects/manager/zone.entity';
@ApiExcludeController()
@UseGuards(IcynetGuard)
@Controller({
path: 'api/v1/icynet',
})
export class IcynetController {
constructor(private service: ManagerService) {}
@Get('zones')
async getZoneList(@Query('uuid') uuid?: string) {
let list: ZoneEntity[] = [];
if (uuid) {
list = await this.service.getZonesByIcynetUUID(uuid);
} else {
list = await this.service.getAllZones();
}
return list.map(({ zone }) => zone);
}
@Get('access')
async getAccessList() {
return this.service.getZonesWithIcynet();
}
@Put('zone/:domain/:uuid')
async addZoneAccess(
@Param('uuid') uuid: string,
@Param('domain') domain: string,
) {
const success = await this.service.authorizeIcynetUser(uuid, domain);
return { success };
}
@Delete('zone/:domain/:uuid')
async removeZoneAccess(
@Param('uuid') uuid: string,
@Param('domain') domain: string,
) {
const success = await this.service.revokeIcynetUser(uuid, domain);
return { success };
}
@Post('zone/:domain/:uuid')
async getAccess(
@Param('uuid') uuid: string,
@Param('domain') domain: string,
) {
const added = await this.service.createIcynetAccessKey(uuid, domain);
if (!added) throw new HttpException('Invalid request', 400);
return { token: added.key };
}
@Put('zone/:domain')
async addZone(@Param('domain') domain: string) {
const added = await this.service.addZone(domain);
return { success: !!added };
}
@Delete('zone/:domain')
async removeZone(@Param('domain') domain: string) {
const success = await this.service.removeZone(domain);
return { success };
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ObjectsModule } from '../objects/objects.module';
import { IcynetController } from './icynet.controller';
@Module({
imports: [ObjectsModule],
controllers: [IcynetController],
})
export class IcynetModule {}

View File

@ -25,6 +25,6 @@ export class AccessEntity {
@UpdateDateColumn()
public updated_at: Date;
@Column({ type: 'varchar', nullable: true })
@Column({ type: 'datetime', nullable: true })
public expires_at?: Date;
}

View File

@ -42,6 +42,10 @@ export class ManagerService {
});
}
public async getAllZones(): Promise<ZoneEntity[]> {
return this.zone.find();
}
public async getAllKeys(zone: string): Promise<AccessEntity[]> {
const obj = await this.getZone(zone);
if (!obj) return [];
@ -54,6 +58,7 @@ export class ManagerService {
public async getZonesByIcynetUUID(uuid: string): Promise<ZoneEntity[]> {
const actor = await this.actors.findOne({
where: { icynetUUID: uuid },
relations: ['zones'],
});
if (!actor) return [];
@ -61,6 +66,20 @@ export class ManagerService {
return actor.zones;
}
public async getZonesWithIcynet(): Promise<Record<string, string[]>> {
const actors = await this.actors.find({
relations: ['zones'],
});
return actors.reduce<Record<string, string[]>>((obj, current) => {
for (const item of current.zones) {
if (!obj[item.zone]) obj[item.zone] = [];
obj[item.zone].push(current.icynetUUID);
}
return obj;
}, {});
}
/**
* Create a new temporary API key for Icy Network user for their domain.
* @param icynetUUID Icy Network user UUID
@ -80,7 +99,7 @@ export class ManagerService {
const newObject = {
key: getToken(),
zone: entry,
expires_at: Date.now() + 24 * 60 * 60 * 1000,
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000),
};
return this.access.save(newObject);
@ -103,10 +122,14 @@ export class ManagerService {
return false;
}
const actor = await this.actors.findOne({
let actor = await this.actors.findOne({
where: { icynetUUID },
});
if (!actor) {
actor = await this.actors.save({ icynetUUID });
}
actor.zones = [...zones, zone];
await this.actors.save(actor);
return true;
@ -125,12 +148,13 @@ export class ManagerService {
if (!zone) return false;
const zones = await this.getZonesByIcynetUUID(icynetUUID);
if (zones.length || !zones.some((item) => item.zone === zone.zone)) {
if (!zones.length || !zones.some((item) => item.zone === zone.zone)) {
return false;
}
const actor = await this.actors.findOne({
where: { icynetUUID },
relations: ['zones'],
});
actor.zones = actor.zones.filter((item) => item.zone !== zone.zone);
@ -168,4 +192,17 @@ export class ManagerService {
zone: domain,
});
}
/**
* Removes a managed zone.
* @param domain Zone to remove
* @returns boolean
*/
public async removeZone(domain: string): Promise<boolean> {
const zone = await this.getZone(domain);
if (!zone) return false;
await this.zone.remove(zone);
return true;
}
}

View File

@ -1,5 +1,5 @@
import crypto from 'crypto';
import { randomBytes } from 'crypto';
export function getToken() {
return crypto.randomBytes(256 / 8).toString('hex');
return randomBytes(256 / 8).toString('hex');
}