Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,5 @@ apps/dashboard/common_types.tsbuildinfo
*.log
junit.xml
.env.test.github-action

*.swp
15 changes: 15 additions & 0 deletions src/audit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Edlink } from '..';
import { AuditEvents, AuditSchemas } from '../common';
import { BearerTokenAPI, TokenSet } from '../types';

export class Audit extends BearerTokenAPI {
public events: AuditEvents;
public schemas: AuditSchemas;

constructor(edlink: Edlink, token_set: TokenSet) {
super(edlink, token_set);

this.events = new AuditEvents(this);
this.schemas = new AuditSchemas(this);
}
}
45 changes: 45 additions & 0 deletions src/common/audit_event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Actor, AuditEvent, BearerTokenAPI, Context, RequestOptionsGet, RequestOptionsPaging, RequestOptionsPost, Scope, Target } from '../types';

export class AuditEvents {
constructor(private api: BearerTokenAPI) {
if (api.api !== 'audit') {
throw new Error('The Audit Logs API only works with the Application TokenSetType');
}
}

/**
* Paginates through all Events within a given scope.
* @param scope_type The type of scope, either `integration` or `institution`
* @param scope_id The UUID of the scope
* @param options Provide a `limit` for the max number of results
*/
async *list(scope_type: string, scope_id: string, options: RequestOptionsPaging = {}): AsyncGenerator<AuditEvent> {
yield* this.api.paginate<AuditEvent>(`/events/${scope_type}/${scope_id}`, options);
}

/**
* Fetches a single Event by ID.
* @param event_id The UUID of the Event
* @returns The requested Event
*/
fetch(event_id: string, options: RequestOptionsGet = {}): Promise<AuditEvent> {
return this.api.request(`/events/${event_id}`, options);
}

/**
* Records a new Event.
* @param event The Event body to record
* @param options Optional request options
* @returns The recorded Event's assigned ID
*/
create(
event: { actor: Actor; action: string; targets: Target[]; scope: Scope; context?: Context; data?: any },
options: RequestOptionsPost = {},
): Promise<Pick<AuditEvent, 'id'>> {
return this.api.request({
url: '/events',
method: 'POST',
data: event,
}, options);
}
}
80 changes: 80 additions & 0 deletions src/common/audit_schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
AuditSchema,
AuditSchemaAction,
BearerTokenAPI,
CRUDXType,
RequestOptionsBase,
RequestOptionsGet,
RequestOptionsPost,
} from "../types";

export class AuditSchemas {
constructor(private api: BearerTokenAPI) {
if (api.api !== "audit") {
throw new Error(
"The Audit Logs API only works with the Application TokenSetType",
);
}
}

/**
* Fetches a single AuditSchema by ID.
* @param schema_id_or_action The UUID of the AuditSchema, or the 'action' string
* @returns The requested AuditSchema
*/
fetch(
schema_id_or_action: string,
options: RequestOptionsGet = {},
): Promise<AuditSchema> {
return this.api.request(`/schemas/${schema_id_or_action}`, options);
}

/**
* Update the existing Schema for a given 'action'
* @param action The 'action' string the Schema operates on
* @returns The new AuditSchema
*/
update(
action: string,
params: {
action_type?: CRUDXType;
validation_level?: "strict" | "lax";
data?: object;
},
options: RequestOptionsBase = {},
): Promise<AuditSchema> {
return this.api.request(
{
url: `/schemas`,
method: "PATCH",
data: {
action,
...params,
},
},
options,
);
}

/**
* Create the Schema for a given 'action'.
* @param class_id The UUID of the class
* @returns The requested class
*/
create(
action: AuditSchemaAction,
data: any = {},
validation_level: "strict" | "lax" = "lax",
options: RequestOptionsPost = {},
): Promise<AuditSchema> {
return this.api.request({
url: `/schemas`,
method: "POST",
data: {
action,
validation_level,
data,
},
}, options);
}
}
2 changes: 2 additions & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { AuditEvents } from './audit_event';
export { AuditSchemas } from './audit_schema';
export { Agents } from './agents';
export { Classes } from './classes';
export { Courses } from './courses';
Expand Down
13 changes: 11 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { serialize } from './common';
import { Audit } from './audit';
import { Graph } from './graph';
import { IntegrationTokenSet, PersonTokenSet, TokenSetType } from './types';
import { ApplicationTokenSet, IntegrationTokenSet, PersonTokenSet, TokenSetType } from './types';
import { User } from './user';
import { Auth } from './user/auth';

Expand Down Expand Up @@ -44,10 +45,18 @@ export class Edlink {
* @returns {User} An instance of the Edlink User API interface using the provided TokenSet
*/
public use(token_set: PersonTokenSet): User;
/**
* Initialize an instance of the Edlink Audit API with an application token set.
* @param token_set The ApplicationTokenSet used to authenticate the request
* @returns {Audit} An instance of the Edlink Audit API interface using the provided TokenSet
*/
public use(token_set: ApplicationTokenSet): Audit;

public use(token_set: PersonTokenSet | IntegrationTokenSet): User | Graph {
public use(token_set: PersonTokenSet | IntegrationTokenSet | ApplicationTokenSet): User | Graph | Audit {
if (token_set.type === TokenSetType.Person) {
return new User(this, token_set);
} else if (token_set.type === TokenSetType.Application) {
return new Audit(this, token_set);
} else {
return new Graph(this, token_set);
}
Expand Down
15 changes: 11 additions & 4 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Edlink } from '..';
import { deepDefaults } from '../utils';
import { RequestOptions, TokenSet, TokenSetType } from './common';

const EDLINK_URL = process.env.ALTERNATE_EDLINK_URL ?? 'https://ed.link';

export type RequestConfig = {
url: string;
method: string;
Expand All @@ -26,14 +28,19 @@ class EdlinkError extends Error {
export class BearerTokenAPI {
private token_set: TokenSet;
private version: number;
private api: 'graph' | 'my';
public api: 'graph' | 'my' | 'audit';
public edlink: Edlink;

constructor(edlink: Edlink, token_set: TokenSet) {
// Assign config
this.token_set = token_set;
this.version = edlink.version;
this.api = token_set.type === TokenSetType.Integration ? 'graph' : 'my';
this.api = 'graph';// default for TokenSetType.Integration
if (token_set.type === TokenSetType.Application) {
this.api = 'audit';
} else if (token_set.type === TokenSetType.Person) {
this.api = 'my';
}
this.edlink = edlink;
}

Expand Down Expand Up @@ -63,7 +70,7 @@ export class BearerTokenAPI {
// Set url
const formattedUrl = new URL(url.startsWith('http')
? url
: `https://ed.link/api/v${this.version}/${this.api}${url}`);
: `${EDLINK_URL}/api/v${this.version}/${this.api}${url}`);

// Add formatted options to the URL
for (const [key, value] of Object.entries(formatted_options)) {
Expand Down Expand Up @@ -176,7 +183,7 @@ export class BearerTokenAPI {
}

private requiresTokenRefresh(tokens: TokenSet = this.token_set): boolean {
if (tokens.type === TokenSetType.Integration) {
if (tokens.type === TokenSetType.Integration || tokens.type === TokenSetType.Application) {
return false;
}

Expand Down
116 changes: 116 additions & 0 deletions src/types/audit_event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { CRUDXType } from ".";
type UUID = `${string}-${string}-${string}-${string}-${string}`;

export enum ScopeType {
Institution = "institution",
Integration = "integration",
}

export interface AuditIdentifier {
value: string;
issuer: string;
}

export interface Actor {
type: "person" | "system" | "external";
identifiers: AuditIdentifier[];
details?: object;
}

export interface Target {
type: string;
identifiers: AuditIdentifier[];
details?: object;
}

export interface Scope {
id: UUID;
type: ScopeType;
}

export interface SchemaRef {
id: string;
version: string;
}

export interface Context {
source?: "client" | "server";
http_method?: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH";
http_status?: number;
path?: string;
ip?: string;
query?: string;
user_agent?: string;
hostname?: string;
os?: string;
environment?: string;
trigger?: "person" | "system" | "external";
deployment_id?: string;
}

/**
* @export
* @interface AuditEvent
*/
export interface AuditEvent {
/**
* Read-only. Assigned by Edlink when the Event is recorded.
* @type {string}
* @memberof AuditEvent
*/
id?: string;
/**
* Read-only. The timestamp at which Edlink received the Event.
* @type {string}
* @memberof AuditEvent
*/
created_date?: string;
/**
* Read-only. The Schema that validated this Event, if any.
* @type {SchemaRef}
* @memberof AuditEvent
*/
schema?: SchemaRef;
/**
* The entity that performed the action.
* @type {Actor}
* @memberof AuditEvent
*/
actor: Actor;
/**
* The customer-defined identifier for the action, e.g. `assignment.submit`.
* @type {string}
* @memberof AuditEvent
*/
action: string;
/**
* Read-only. The CRUD classification of the action, derived from the matching Schema.
* @type {CRUDXType}
* @memberof AuditEvent
*/
action_type?: CRUDXType;
/**
* The entities affected by the action.
* @type {Array<Target>}
* @memberof AuditEvent
*/
targets: Target[];
/**
* The scope (integration or institution) the Event belongs to.
* @type {Scope}
* @memberof AuditEvent
*/
scope: Scope;
/**
* Information about the underlying network request. Strongly suggested.
* @type {Context}
* @memberof AuditEvent
*/
context?: Context;
/**
* Arbitrary data relevant to the Event. Validated only if a Schema defines a data shape.
* @type {any}
* @memberof AuditEvent
*/
data?: any;
}
Loading