Skip to content

Commit

Permalink
Implement #16: Implement API Key system
Browse files Browse the repository at this point in the history
  • Loading branch information
big213 committed Apr 14, 2021
1 parent 46ea458 commit 3653336
Show file tree
Hide file tree
Showing 19 changed files with 678 additions and 8 deletions.
18 changes: 18 additions & 0 deletions backend/functions/db/migrations/20210414095931_add_apiKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as Knex from "knex";

export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable("apiKey", function (table) {
table.increments();
table.string("name").notNullable();
table.string("code").notNullable().unique();
table.integer("user").notNullable();
table.json("permissions").nullable();
table.dateTime("created_at").notNullable().defaultTo(knex.fn.now());
table.dateTime("updated_at").nullable();
table.integer("created_by").notNullable();
});
}

export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable("apiKey");
}
12 changes: 11 additions & 1 deletion backend/functions/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,21 @@ export async function up(knex: Knex): Promise<void[]> {
table.integer("time_elapsed").nullable();
table.decimal("moves_count").nullable();
table.boolean("is_current").notNullable();
table.text("private_comments").nullable();
table.text("public_comments").nullable();
table.dateTime("created_at").notNullable().defaultTo(knex.fn.now());
table.dateTime("updated_at").nullable();
table.integer("created_by").notNullable();
}),
knex.schema.createTable("apiKey", function (table) {
table.increments();
table.string("name").notNullable();
table.string("code").notNullable().unique();
table.integer("user").notNullable();
table.json("permissions").nullable();
table.dateTime("created_at").notNullable().defaultTo(knex.fn.now());
table.dateTime("updated_at").nullable();
table.integer("created_by").notNullable();
}),
]);
}

Expand All @@ -85,5 +94,6 @@ export async function down(knex: Knex): Promise<void[]> {
knex.schema.dropTable("product"),
knex.schema.dropTable("personalBestClass"),
knex.schema.dropTable("personalBest"),
knex.schema.dropTable("apiKey"),
]);
}
100 changes: 99 additions & 1 deletion backend/functions/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Query builder (Typescript version >= 4.1.3 required)
/* const queryResult = executeGiraffeql({
// Start typing here to get hints
}); */

export function executeGiraffeql<Key extends keyof Root>(
Expand Down Expand Up @@ -99,6 +98,7 @@ export type FilterByField<T> = {
| "user_delete"
| "personalBest_create"
| "product_create"
| "apiKey_create"
| "userUserFollowLink_get";
/**Enum stored as is*/ scoreMethod: "STANDARD" | "FMC" | "MBLD";
userSortByKey: "id" | "createdAt" | "updatedAt";
Expand All @@ -120,6 +120,8 @@ export type FilterByField<T> = {
| "happenedOn"
| "isCurrent";
personalBestGroupByKey: undefined;
apiKeySortByKey: "id" | "createdAt";
apiKeyGroupByKey: undefined;
userUserFollowLinkSortByKey: "createdAt";
userUserFollowLinkGroupByKey: undefined;
};
Expand Down Expand Up @@ -331,6 +333,50 @@ export type FilterByField<T> = {
happenedOn: Scalars["unixTimestamp"];
timeElapsed?: Scalars["number"] | null;
movesCount?: Scalars["number"] | null;
publicComments?: Scalars["string"] | null;
};
updatePersonalBestFields: {
pbClass?: InputTypes["personalBestClass"];
event?: InputTypes["event"];
setSize?: Scalars["number"];
attemptsSucceeded?: Scalars["number"] | null;
attemptsTotal?: Scalars["number"] | null;
product?: InputTypes["product"] | null;
happenedOn?: Scalars["unixTimestamp"];
timeElapsed?: Scalars["number"] | null;
movesCount?: Scalars["number"] | null;
publicComments?: Scalars["string"] | null;
};
updatePersonalBest: {
item: InputTypes["personalBest"];
fields: InputTypes["updatePersonalBestFields"];
};
apiKey: { id?: Scalars["id"] };
"apiKeyFilterByField/id": FilterByField<Scalars["id"]>;
apiKeyFilterByObject: { id?: InputTypes["apiKeyFilterByField/id"] };
apiKeyPaginator: {
first?: Scalars["number"];
last?: Scalars["number"];
after?: Scalars["string"];
before?: Scalars["string"];
sortBy?: Scalars["apiKeySortByKey"][];
sortDesc?: Scalars["boolean"][];
filterBy?: InputTypes["apiKeyFilterByObject"][];
groupBy?: Scalars["apiKeyGroupByKey"][];
};
createApiKey: {
name: Scalars["string"];
user: InputTypes["user"];
permissions?: Scalars["userPermission"][] | null;
};
updateApiKeyFields: {
name?: Scalars["string"];
user?: InputTypes["user"];
permissions?: Scalars["userPermission"][] | null;
};
updateApiKey: {
item: InputTypes["apiKey"];
fields: InputTypes["updateApiKeyFields"];
};
/**Input object for getRepositoryReleases*/ getRepositoryReleases: {
first: Scalars["number"];
Expand Down Expand Up @@ -384,6 +430,11 @@ export type FilterByField<T> = {
Typename: "personalBestPaginator";
Type: GetType<PersonalBestPaginator>;
};
apiKeyEdge: { Typename: "apiKeyEdge"; Type: GetType<ApiKeyEdge> };
apiKeyPaginator: {
Typename: "apiKeyPaginator";
Type: GetType<ApiKeyPaginator>;
};
userUserFollowLinkEdge: {
Typename: "userUserFollowLinkEdge";
Type: GetType<UserUserFollowLinkEdge>;
Expand Down Expand Up @@ -413,6 +464,7 @@ export type FilterByField<T> = {
Type: GetType<PersonalBestClass>;
};
personalBest: { Typename: "personalBest"; Type: GetType<PersonalBest> };
apiKey: { Typename: "apiKey"; Type: GetType<ApiKey> };
};
/**PaginatorInfo Type*/ export type PaginatorInfo = {
/**The typename of the record*/ __typename: {
Expand Down Expand Up @@ -469,6 +521,15 @@ export type PersonalBestEdge = Edge<PersonalBest>;
paginatorInfo: { Type: PaginatorInfo; Args: undefined };
edges: { Type: PersonalBestEdge[]; Args: undefined };
};
export type ApiKeyEdge = Edge<ApiKey>;
/**Paginator*/ export type ApiKeyPaginator = {
/**The typename of the record*/ __typename: {
Type: Scalars["string"];
Args: [Scalars["number"]];
};
paginatorInfo: { Type: PaginatorInfo; Args: undefined };
edges: { Type: ApiKeyEdge[]; Args: undefined };
};
export type UserUserFollowLinkEdge = Edge<UserUserFollowLink>;
/**Paginator*/ export type UserUserFollowLinkPaginator = {
/**The typename of the record*/ __typename: {
Expand Down Expand Up @@ -635,6 +696,31 @@ export type UserUserFollowLinkEdge = Edge<UserUserFollowLink>;
Args: undefined;
};
isCurrent: { Type: Scalars["boolean"]; Args: undefined };
publicComments: { Type: Scalars["string"] | null; Args: undefined };
/**The numerical score rank of this PB given its event, pbClass, and setSize, among public PBs only*/ ranking: {
Type: Scalars["number"] | null;
Args: undefined;
};
/**When the record was created*/ createdAt: {
Type: Scalars["unixTimestamp"];
Args: undefined;
};
/**When the record was last updated*/ updatedAt: {
Type: Scalars["unixTimestamp"] | null;
Args: undefined;
};
createdBy: { Type: User; Args: undefined };
};
/**API Key Type*/ export type ApiKey = {
/**The unique ID of the field*/ id: { Type: Scalars["id"]; Args: undefined };
/**The typename of the record*/ __typename: {
Type: Scalars["string"];
Args: [Scalars["number"]];
};
name: { Type: Scalars["string"]; Args: undefined };
code: { Type: Scalars["string"]; Args: undefined };
user: { Type: User; Args: undefined };
permissions: { Type: Scalars["userPermission"][] | null; Args: undefined };
/**When the record was created*/ createdAt: {
Type: Scalars["unixTimestamp"];
Args: undefined;
Expand Down Expand Up @@ -704,6 +790,18 @@ export type UserUserFollowLinkEdge = Edge<UserUserFollowLink>;
Type: PersonalBest;
Args: InputTypes["createPersonalBest"];
};
updatePersonalBest: {
Type: PersonalBest;
Args: InputTypes["updatePersonalBest"];
};
getApiKey: { Type: ApiKey; Args: InputTypes["apiKey"] };
getApiKeyPaginator: {
Type: ApiKeyPaginator;
Args: InputTypes["apiKeyPaginator"];
};
deleteApiKey: { Type: ApiKey; Args: InputTypes["apiKey"] };
createApiKey: { Type: ApiKey; Args: InputTypes["createApiKey"] };
updateApiKey: { Type: ApiKey; Args: InputTypes["updateApiKey"] };
getRepositoryReleases: {
Type: Scalars["unknown"][];
Args: InputTypes["getRepositoryReleases"];
Expand Down
81 changes: 80 additions & 1 deletion backend/functions/src/helpers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { env } from "../config";
import * as jwt from "jsonwebtoken";
import { User } from "../schema/services";
import { User, ApiKey } from "../schema/services";
import { userRoleKenum, userPermissionEnum } from "../schema/enums";
import { userRoleToPermissionsMap } from "../schema/helpers/permissions";
import type { ContextUser } from "../types";
Expand Down Expand Up @@ -61,3 +61,82 @@ export async function validateToken(auth: string): Promise<ContextUser> {
throw new Error(message);
}
}

export async function validateApiKey(auth: string): Promise<ContextUser> {
if (!auth) {
throw new Error("Invalid Api Key");
}

try {
// lookup user by API key
const apiKeyResults = await sqlHelper.fetchTableRows({
select: [
{ field: "permissions" },
{ field: "user.id" },
{ field: "user.role" },
{ field: "user.permissions" },
],
from: ApiKey.typename,
where: {
fields: [{ field: "code", value: auth }],
},
});

if (apiKeyResults.length < 1) {
throw new Error("Invalid Api Key");
}

const role = userRoleKenum.fromIndex(apiKeyResults[0]["user.role"]);

const originalUserPermissions: userPermissionEnum[] = (
apiKeyResults[0]["user.permissions"] ?? []
)
.map((ele) => userPermissionEnum.fromName(ele))
.concat(userRoleToPermissionsMap[role.name] ?? []);

let finalPermissions = originalUserPermissions;

if (
apiKeyResults[0]["permissions"] &&
apiKeyResults[0]["permissions"].length > 0
) {
const requestedPermissionsSet: Set<userPermissionEnum> = new Set(
apiKeyResults[0]["permissions"]
? apiKeyResults[0]["permissions"].map((ele) =>
userPermissionEnum.fromName(ele)
)
: []
);

// check if all requestedPermissions are indeed allowed

// if user has A_A, skip this
if (!originalUserPermissions.includes(userPermissionEnum.A_A)) {
requestedPermissionsSet.forEach((ele) => {
// if user does not have the specific requested permission, see if they have the wildcard for that permission
if (!originalUserPermissions.includes(ele)) {
const wildcardKey = ele.name.split("_")[0] + "_x";
if (
!originalUserPermissions.includes(userPermissionEnum[wildcardKey])
) {
requestedPermissionsSet.delete(ele);
// failing that, remove the permission from the set
}
}
});
}

finalPermissions = [...requestedPermissionsSet];
}

return {
id: apiKeyResults[0]["user.id"],
role,
permissions: finalPermissions,
};
} catch (err) {
console.log(err);
const message = "Token error: " + (err.message || err.name);
throw new Error(message);
}
}
10 changes: 8 additions & 2 deletions backend/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { env, giraffeqlOptions } from "./config";

import { initializePusher } from "./utils/pusher";
import { handlePusherAuth } from "./helpers/pusher";
import { validateToken } from "./helpers/auth";
import { validateToken, validateApiKey } from "./helpers/auth";
import { CustomSchemaGenerator } from "./helpers/schema";

const app = express();
Expand All @@ -21,10 +21,16 @@ const allowedOrigins = [
// extract the user ID from all requests.
app.use(async function (req, res, next) {
try {
if (req.headers.authorization) {
// if api key provided, attempt to validate using that
const apiKey = req.get("x-api-key");
if (apiKey) {
req.user = await validateApiKey(apiKey);
} else if (req.headers.authorization) {
req.user = await validateToken(req.headers.authorization);
}

console.log(req.user);

// handle origins -- only accepting string type origins.
const origin =
Array.isArray(allowedOrigins) && allowedOrigins.length
Expand Down
2 changes: 2 additions & 0 deletions backend/functions/src/schema/enums/userPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export class userPermissionEnum extends Enum {

static readonly product_create = new userPermissionEnum("product_create");

static readonly apiKey_create = new userPermissionEnum("apiKey_create");

static readonly userUserFollowLink_get = new userPermissionEnum(
"userUserFollowLink_get"
);
Expand Down
4 changes: 4 additions & 0 deletions backend/functions/src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import event from "./models/event/typeDef";
import product from "./models/product/typeDef";
import personalBestClass from "./models/personalBestClass/typeDef";
import personalBest from "./models/personalBest/typeDef";
import apiKey from "./models/apiKey/typeDef";

// add the typeDefs for the services with typeDefs
allServices.User.setTypeDef(user);
Expand All @@ -16,13 +17,15 @@ allServices.Event.setTypeDef(event);
allServices.Product.setTypeDef(product);
allServices.PersonalBestClass.setTypeDef(personalBestClass);
allServices.PersonalBest.setTypeDef(personalBest);
allServices.ApiKey.setTypeDef(apiKey);

import User from "./models/user/rootResolver";
import Auth from "./models/auth/rootResolver";
import Event from "./models/event/rootResolver";
import Product from "./models/product/rootResolver";
import PersonalBestClass from "./models/personalBestClass/rootResolver";
import PersonalBest from "./models/personalBest/rootResolver";
import ApiKey from "./models/apiKey/rootResolver";
import Github from "./models/github/rootResolver";
import UserUserFollowLink from "./links/userUserFollowLink/rootResolver";

Expand All @@ -32,6 +35,7 @@ allServices.Event.setRootResolvers(Event);
allServices.Product.setRootResolvers(Product);
allServices.PersonalBestClass.setRootResolvers(PersonalBestClass);
allServices.PersonalBest.setRootResolvers(PersonalBest);
allServices.ApiKey.setRootResolvers(ApiKey);
allServices.Github.setRootResolvers(Github);

allServices.UserUserFollowLink.setRootResolvers(UserUserFollowLink);
12 changes: 12 additions & 0 deletions backend/functions/src/schema/models/apiKey/rootResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiKey } from "../../services";
import { generateBaseRootResolvers } from "../../core/helpers/rootResolver";

export default {
...generateBaseRootResolvers(ApiKey, [
"get",
"getMultiple",
"delete",
"create",
"update",
]),
};
Loading

0 comments on commit 3653336

Please sign in to comment.