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

Fix/unblock graphql #183

Merged
merged 6 commits into from
Sep 8, 2023
Merged
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
6 changes: 2 additions & 4 deletions docs/developer-guide/tutorials/authentication-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,7 @@ A session can expire:

## Accessing the GraphQL API

All requests to the GraphQL api should be [authenticated requests](#sessions-and-authenticated-requests), regardless of whether they are queries or mutations.

Of course different requests may still require different privileges, ie. some mutations like `setSupportedCategories` will be only accessible for _root user_ etc., while other mutations may only be accessible for _Gateway account owners_.
Different requests may still require different privileges, ie. some mutations like `setSupportedCategories` will be only accessible for _root user_ etc., while other mutations may only be accessible for _Gateway account owners_.

## Authentication API interactions

Expand Down Expand Up @@ -421,4 +419,4 @@ export DEV_DISABLE_SAME_SITE=true

In order to be able to pass the cookie to Orion's Auth API when making requests from Atlas deployed under different domain, you should specify `credentials: 'include'` option in ApolloClient's `HttpLink` (see: https://www.apollographql.com/docs/react/networking/authentication/).

Similarly, to include the cookie when making requests to the GraphQL API, you should provide `credentials: 'include'` to `fetch`: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included
Similarly, to include the cookie when making requests to the GraphQL API, you should provide `credentials: 'include'` to `fetch`: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included
2 changes: 1 addition & 1 deletion src/auth-server/handlers/createAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const createAccount: (
isEmailConfirmed: false,
registeredAt: new Date(),
isBlocked: false,
userId: authContext.user.id,
userId: authContext?.user.id,
joystreamAccount: joystreamAccountId,
membershipId: memberId.toString(),
})
Expand Down
2 changes: 1 addition & 1 deletion src/auth-server/handlers/getSessionArtifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const getSessionArtifacts: (
try {
const em = await globalEm
const { authContext: session } = res.locals
if (!session.account) {
if (!session?.account) {
throw new UnauthorizedError('Cannot get session artifacts for anonymous session')
}
const artifacts = await em
Expand Down
4 changes: 4 additions & 0 deletions src/auth-server/handlers/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express from 'express'
import { AuthContext } from '../../utils/auth'
import { globalEm } from '../../utils/globalEm'
import { components } from '../generated/api-types'
import { BadRequestError } from '../errors'

type ReqParams = Record<string, string>
type ResBody =
Expand All @@ -16,6 +17,9 @@ export const logout: (
) => Promise<void> = async (req, res, next) => {
try {
const { authContext: session } = res.locals
if (!session) {
throw new BadRequestError('No session to logout found.')
}
const em = await globalEm
session.expiry = new Date()
await em.save(session)
Expand Down
5 changes: 4 additions & 1 deletion src/auth-server/handlers/postSessionArtifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ export const postSessionArtifacts: (
try {
const { authContext: session } = res.locals
const em = await globalEm
if (!session?.id) {
throw new UnauthorizedError('Cannot save session artifacts for empty session')
}
const existingArtifacts = await em
.getRepository(SessionEncryptionArtifacts)
.findOneBy({ sessionId: session.id })
if (!session.account) {
if (!session?.account) {
throw new UnauthorizedError('Cannot save session artifacts for anonymous session')
}
if (existingArtifacts) {
Expand Down
5 changes: 1 addition & 4 deletions src/auth-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import cors from 'cors'
import * as OpenApiValidator from 'express-openapi-validator'
import { HttpError } from 'express-openapi-validator/dist/framework/types'
import path from 'path'
import { AuthApiError, UnauthorizedError } from './errors'
import { AuthApiError } from './errors'
import { createLogger } from '@subsquid/logger'
import { authenticate, getCorsOrigin } from '../utils/auth'
import cookieParser from 'cookie-parser'
Expand All @@ -19,9 +19,6 @@ export const app = express()
function authHandler(type: 'header' | 'cookie') {
return async (req: express.Request) => {
const authContext = await authenticate(req, type)
if (!authContext) {
throw new UnauthorizedError()
}
if (req.res) {
req.res.locals.authContext = authContext
}
Expand Down
7 changes: 2 additions & 5 deletions src/server-extension/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,11 @@ export const requestCheck: RequestCheckFunction = async (ctx) => {
const context = ctx.context as Context

const authContext = await authenticate(context.req, 'cookie')
if (!authContext) {
return 'Unauthorized'
}

Object.assign(context, authContext)

if (
!authContext.user.isRoot &&
(!authContext || !authContext.user.isRoot) &&
ctx.operation.selectionSet.selections.some(
(s) => s.kind === 'Field' && autogeneratedOperatorQueries.includes(s.name.value)
)
Expand All @@ -44,7 +41,7 @@ export const requestCheck: RequestCheckFunction = async (ctx) => {
}

// Set search_path accordingly if it's an operator request
if (authContext.user.isRoot) {
if (authContext?.user.isRoot) {
const em = await (context.openreader as unknown as TypeormOpenreaderContext).getEntityManager()
await em.query('SET LOCAL search_path TO admin,public')
}
Expand Down
3 changes: 2 additions & 1 deletion src/server-extension/resolvers/ChannelsResolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { ListQuery } from '@subsquid/openreader/lib/sql/query'
import { model } from '../model'
import { Context } from '../../check'
import { uniqueId } from '../../../utils/crypto'
import { AccountOnly } from '../middleware'
import { AccountOnly, UserOnly } from '../middleware'

@Resolver()
export class ChannelsResolver {
Expand Down Expand Up @@ -242,6 +242,7 @@ export class ChannelsResolver {
})
}

@UseMiddleware(UserOnly)
@Mutation(() => ChannelReportInfo)
async reportChannel(
@Args() { channelId, rationale }: ReportChannelArgs,
Expand Down
5 changes: 4 additions & 1 deletion src/server-extension/resolvers/VideosResolver/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'reflect-metadata'
import { Arg, Args, Ctx, Info, Mutation, Query, Resolver } from 'type-graphql'
import { Arg, Args, Ctx, Info, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql'
import { EntityManager, MoreThan } from 'typeorm'
import {
AddVideoViewResult,
Expand Down Expand Up @@ -36,6 +36,7 @@ import { isObject } from 'lodash'
import { has } from '../../../utils/misc'
import { videoRelevanceManager } from '../../../mappings/utils'
import { uniqueId } from '../../../utils/crypto'
import { UserOnly } from '../middleware'

@Resolver()
export class VideosResolver {
Expand Down Expand Up @@ -184,6 +185,7 @@ export class VideosResolver {
return result as VideosConnection
}

@UseMiddleware(UserOnly)
@Mutation(() => AddVideoViewResult)
async addVideoView(
@Arg('videoId', () => String, { nullable: false }) videoId: string,
Expand Down Expand Up @@ -250,6 +252,7 @@ export class VideosResolver {
})
}

@UseMiddleware(UserOnly)
@Mutation(() => VideoReportInfo)
async reportVideo(
@Args() { videoId, rationale }: ReportVideoArgs,
Expand Down
12 changes: 10 additions & 2 deletions src/server-extension/resolvers/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ import { MiddlewareFn } from 'type-graphql'
import { Context } from '../check'

export const OperatorOnly: MiddlewareFn<Context> = async ({ context }, next) => {
if (!context.user.isRoot) {
if (!context?.user.isRoot) {
throw new Error('Unauthorized: Root access required')
}
return next()
}

export const AccountOnly: MiddlewareFn<Context> = async ({ context }, next) => {
if (context.account === null) {
if (!context?.account) {
throw new Error('Unauthorized: Account required')
}

return next()
}

export const UserOnly: MiddlewareFn<Context> = async ({ context }, next) => {
if (!context?.user) {
throw new Error('Unauthorized: User required')
}

return next()
}
42 changes: 20 additions & 22 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ sessionCache.on('expired', (sessionId: string, cachedData: CachedSessionData) =>
})
})

export type AuthContext = Session
export type AuthContext = Session | null

export async function getSessionIdFromHeader(req: Request): Promise<string | undefined> {
authLogger.trace(`Authorization header: ${JSON.stringify(req.headers.authorization, null, 2)}`)
Expand All @@ -121,32 +121,30 @@ export async function getSessionIdFromCookie(req: Request): Promise<string | und
export async function authenticate(
req: Request,
authType: 'cookie' | 'header'
): Promise<AuthContext | false> {
): Promise<AuthContext> {
const em = await globalEm
const sessionId =
authType === 'cookie' ? await getSessionIdFromCookie(req) : await getSessionIdFromHeader(req)
if (!sessionId) {
authLogger.debug(`Recieved a request w/ no sessionId provided. AuthType: ${authType}.`)
return false
}
authLogger.trace(`Authenticating... SessionId: ${sessionId}`)

const session = await findActiveSession(req, em, { id: sessionId })
if (session) {
const cachedSessionData = sessionCache.get<CachedSessionData>(sessionId)
if (cachedSessionData) {
cachedSessionData.lastActivity = new Date()
authLogger.trace(
`Updated last activity of session ${sessionId} to ${cachedSessionData.lastActivity.toISOString()}`
)
} else {
await tryToProlongSession(session.id, new Date())

if (sessionId) {
authLogger.trace(`Authenticating... SessionId: ${sessionId}`)

const session = await findActiveSession(req, em, { id: sessionId })
if (session) {
const cachedSessionData = sessionCache.get<CachedSessionData>(sessionId)
if (cachedSessionData) {
cachedSessionData.lastActivity = new Date()
authLogger.trace(
`Updated last activity of session ${sessionId} to ${cachedSessionData.lastActivity.toISOString()}`
)
} else {
await tryToProlongSession(session.id, new Date())
}
return session
}
return session
}
authLogger.warn(`Cannot authenticate user. Session not found or expired: ${sessionId}`)

return false
authLogger.debug(`Recieved a request w/ no sessionId provided. AuthType: ${authType}.`)
return null
}

export async function getOrCreateSession(
Expand Down
Loading