Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add connection_id to machine user API creation #438

Merged
merged 13 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion api/lib/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ export const Agency = Type.Object({
description: Type.Any()
});

export const Integration = Type.Object({
id: Type.Number(),
name: Type.String(),
});

export const MachineUser = Type.Object({
id: Type.Number(),
email: Type.String(),
integrations: Type.Array(Integration),
});

export const Channel = Type.Object({
Expand Down Expand Up @@ -79,6 +85,7 @@ export default class ExternalProvider {
name: string;
description: string;
management_url: string;
active: boolean;
}
}): Promise<Static<typeof MachineUser>> {
const creds = await this.auth();
Expand All @@ -95,7 +102,6 @@ export default class ExternalProvider {
active: true,
integration: {
...body.integration,
active: true
}
}

Expand Down Expand Up @@ -144,6 +150,57 @@ export default class ExternalProvider {
return;
}

async updateIntegrationConnectionId(uid: number, body: {
integration_id: number;
connection_id: number;
}): Promise<void> {
const creds = await this.auth();

const url = new URL(`api/v1/proxy/integrations/etl/${body.integration_id}`, this.config.server.provider_url);
url.searchParams.append('proxy_user_id', String(uid));

const req = {
management_url: this.config.API_URL + `/connection/${body.connection_id}`,
external_identifier: body.connection_id,
active: true,
}

const userres = await fetch(url, {
method: 'PATCH',
headers: {
Accept: 'application/json',
"Content-Type": "application/json",
"Authorization": `Bearer ${creds.token}`
},
body: JSON.stringify(req)
});

if (!userres.ok) throw new Err(500, new Error(await userres.text()), 'External Integration Update Error');

return;
}

async deleteIntegrationByConnectionId(uid: number, body: {
connection_id: number;
}): Promise<void> {
const creds = await this.auth();

// there is a ?delete_machine_user query param you can add, if you want to delete any MU's associated with the integration
const url = new URL(`api/v1/proxy/integrations/etl/identifier/${body.connection_id}`, this.config.server.provider_url);
url.searchParams.append('proxy_user_id', String(uid));

await fetch(url, {
method: 'DELETE',
headers: {
Accept: 'application/json',
"Content-Type": "application/json",
"Authorization": `Bearer ${creds.token}`
}
});

return;
}

async agency(uid: number, agency_id: number): Promise<Static<typeof Agency>> {
const creds = await this.auth();

Expand Down
52 changes: 41 additions & 11 deletions api/routes/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export default async function router(schema: Schema, config: Config) {
total: Type.Integer(),
status: Type.Object({
dead: Type.Integer({ description: 'The connection is not currently connected to a TAK server' }),
live: Type.Integer({ description: 'The connection is currently connected to a TAK server'}),
unknown: Type.Integer({ description: 'The status of the connection could not be determined'}),
live: Type.Integer({ description: 'The connection is currently connected to a TAK server' }),
unknown: Type.Integer({ description: 'The status of the connection could not be determined' }),
}),
items: Type.Array(ConnectionResponse)
})
Expand All @@ -40,10 +40,14 @@ export default async function router(schema: Schema, config: Config) {

let where;
if (profile.system_admin) {
where = sql`name ~* ${req.query.filter}`
where = sql`name
~*
${req.query.filter}`
} else if (profile.agency_admin.length) {
where = and(
sql`name ~* ${req.query.filter}`,
sql`name
cheesegrits marked this conversation as resolved.
Show resolved Hide resolved
~*
${req.query.filter}`,
inArray(Connection.agency, profile.agency_admin)
);
} else {
Expand Down Expand Up @@ -92,6 +96,7 @@ export default async function router(schema: Schema, config: Config) {
description: Default.DescriptionField,
enabled: Type.Optional(Type.Boolean({ default: true })),
agency: Type.Union([Type.Null(), Type.Optional(Type.Integer({ minimum: 1 }))]),
integrationId: Type.Union([Type.Integer(), Type.Null()]),
cheesegrits marked this conversation as resolved.
Show resolved Hide resolved
auth: Type.Object({
key: Type.String({ minLength: 1, maxLength: 4096 }),
cert: Type.String({ minLength: 1, maxLength: 4096 })
Expand All @@ -101,12 +106,11 @@ export default async function router(schema: Schema, config: Config) {
}, async (req, res) => {
try {
const user = await Auth.as_user(config, req);
const profile = await config.models.Profile.from(user.email);

if (!req.body.agency && user.access !== 'admin') {
throw new Err(400, null, 'Only System Admins can create a server without an Agency Configured');
} else if (req.body.agency && user.access !== 'admin') {
const profile = await config.models.Profile.from(user.email);

if (!profile.agency_admin || !profile.agency_admin.includes(req.body.agency)) {
throw new Err(400, null, 'Cannot create a connection for an Agency you are not an admin of');
}
Expand All @@ -120,6 +124,15 @@ export default async function router(schema: Schema, config: Config) {

const { validFrom, validTo, subject } = new X509Certificate(conn.auth.cert);

if (req.body.integrationId) {
if (!profile.id) throw new Err(400, null, 'External ID must be set on profile');

await config.external.updateIntegrationConnectionId(profile.id, {
connection_id: conn.id,
integration_id: req.body.integrationId
})
}

res.json({
status: config.conns.status(conn.id),
certificate: { validFrom, validTo, subject },
Expand Down Expand Up @@ -160,7 +173,8 @@ export default async function router(schema: Schema, config: Config) {
}

const conn = await config.models.Connection.commit(req.params.connectionid, {
updated: sql`Now()`,
updated: sql`Now
()`,
...req.body
});

Expand Down Expand Up @@ -260,25 +274,41 @@ export default async function router(schema: Schema, config: Config) {
await Auth.is_connection(config, req, {}, req.params.connectionid);

if (await config.models.Layer.count({
where: sql`connection = ${req.params.connectionid}`
where: sql`connection =
${req.params.connectionid}`
}) > 0) throw new Err(400, null, 'Connection has active Layers - Delete layers before deleting Connection');

if (await config.models.ConnectionSink.count({
where: sql`connection = ${req.params.connectionid}`
where: sql`connection =
${req.params.connectionid}`
}) > 0) throw new Err(400, null, 'Connection has active Sinks - Delete Sinks before deleting Connection');

if (await config.models.Data.count({
where: sql`connection = ${req.params.connectionid}`
where: sql`connection =
${req.params.connectionid}`
}) > 0) throw new Err(400, null, 'Connection has active Data Syncs - Delete Syncs before deleting Connection');

await config.models.Connection.delete(req.params.connectionid);

await config.models.ConnectionToken.delete(sql`
connection = ${req.params.connectionid}
connection =
${req.params.connectionid}
`);

config.conns.delete(req.params.connectionid);

const user = await Auth.as_user(config, req);
const profile = await config.models.Profile.from(user.email);

if (profile.id) {
cheesegrits marked this conversation as resolved.
Show resolved Hide resolved
// I don't know how to figure out if the connection was created with a machine user and hence registered
// with COTAK, so just firing off the delete, which won't error out if no integration found.
await config.external.deleteIntegrationByConnectionId(profile.id, {
connection_id: req.params.connectionid,
})
}


res.json({
status: 200,
message: 'Connection Deleted'
Expand Down
21 changes: 14 additions & 7 deletions api/routes/ldap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Config from '../lib/config.js';
import Schema from '@openaddresses/batch-schema';
import Err from '@openaddresses/batch-error';
import Auth from '../lib/auth.js';
import { Channel } from '../lib/external.js';
import { Channel } from '../lib/external.js';
import TAKAPI, {
APIAuthPassword,
} from '../lib/tak-api.js';
Expand Down Expand Up @@ -36,7 +36,7 @@ export default async function router(schema: Schema, config: Config) {

res.json(list);
} catch (err) {
Err.respond(err, res);
Err.respond(err, res);
}
});

Expand All @@ -53,8 +53,11 @@ export default async function router(schema: Schema, config: Config) {
})
}),
res: Type.Object({
cert: Type.String(),
key: Type.String()
integrationId: Type.Union([Type.Integer(), Type.Null()]),
certificate: Type.Object({
cheesegrits marked this conversation as resolved.
Show resolved Hide resolved
cert: Type.String(),
key: Type.String()
})
})
}, async (req, res) => {
try {
Expand All @@ -74,7 +77,8 @@ export default async function router(schema: Schema, config: Config) {
integration: {
name: req.body.name,
description: req.body.description,
management_url: config.API_URL
management_url: config.API_URL,
active: false,
}
});

Expand All @@ -92,9 +96,12 @@ export default async function router(schema: Schema, config: Config) {

const certs = await api.Credentials.generate();

res.json(certs)
res.json({
integrationId: user.integrations.find(Boolean)?.id ?? null,
certificate: certs
})
} catch (err) {
Err.respond(err, res);
Err.respond(err, res);
}
});
}
5 changes: 3 additions & 2 deletions api/web/src/components/Connection/CertificateMachineUser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default {
connection: Object
},
emits: [
'certs',
'certs', 'integration',
],
data: function() {
return {
Expand Down Expand Up @@ -173,7 +173,8 @@ export default {
})

this.loading.gen = true;
this.$emit('certs', res);
this.$emit('certs', res.certificate);
this.$emit('integration', res.integrationId)
cheesegrits marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
15 changes: 13 additions & 2 deletions api/web/src/components/ConnectionEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
<template v-else-if='type === "creation"'>
<CertificateMachineUser
:connection='connection'
@certs='marti($event)'
@certs='creation($event)'
@integration='integration($event)'
@err='err = $event'
/>
</template>
Expand Down Expand Up @@ -314,6 +315,7 @@ export default {
agency: undefined,
description: '',
enabled: true,
integrationId: undefined,
auth: { cert: '', key: '' }
}
}
Expand Down Expand Up @@ -342,17 +344,26 @@ export default {
this.connection.auth = { cert: '', key: '' }
this.loading = false;
},
creation: function(certs) {
this.connection.auth.cert = certs.cert;
this.connection.auth.key = certs.key;
},
integration: function(integrationId) {
this.connection.integrationId = integrationId;
},
marti: function(certs) {
this.connection.integrationId = null;
this.connection.auth.cert = certs.cert;
this.connection.auth.key = certs.key;
},
p12upload: function(certs) {
this.modal.upload = false;
this.connection.integrationId = null;
this.connection.auth.cert = certs.cert;
this.connection.auth.key = certs.key;
},
create: async function() {
for (const field of ['name', 'description' ]) {
for (const field of ['name', 'description']) {
if (!this.connection[field]) this.errors[field] = 'Cannot be empty';
else this.errors[field] = '';
}
Expand Down
Loading