зеркало из https://github.com/microsoft/CCF.git
handle auth errors in error handler (#1827)
This commit is contained in:
Родитель
03028afe48
Коммит
c81b7d4277
|
@ -3,28 +3,35 @@
|
|||
|
||||
import jwt_decode from 'jwt-decode'
|
||||
import * as ccf from './types/ccf'
|
||||
import { UnauthorizedError } from './error_handler'
|
||||
|
||||
export function authentication(request: ccf.Request, securityName: string, scopes?: string[]): any {
|
||||
export interface User {
|
||||
claims: { [name: string]: any }
|
||||
userId: string
|
||||
}
|
||||
|
||||
export function authentication(request: ccf.Request, securityName: string, scopes?: string[]): void {
|
||||
if (securityName === "jwt") {
|
||||
const authHeader = request.headers['authorization']
|
||||
if (!authHeader) {
|
||||
throw new Error('authorization header missing')
|
||||
throw new UnauthorizedError('authorization header missing')
|
||||
}
|
||||
const parts = authHeader.split(' ', 2)
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
throw new Error('unexpected authentication type')
|
||||
throw new UnauthorizedError('unexpected authentication type')
|
||||
}
|
||||
const token = parts[1]
|
||||
let claims: any
|
||||
try {
|
||||
claims = jwt_decode(token)
|
||||
} catch (e) {
|
||||
throw new Error(`malformed jwt: ${e.message}`)
|
||||
throw new UnauthorizedError(`malformed jwt: ${e.message}`)
|
||||
}
|
||||
return {
|
||||
request.user = {
|
||||
claims: claims,
|
||||
userId: claims.sub
|
||||
}
|
||||
} as User
|
||||
} else {
|
||||
throw new Error(`BUG: unknown securityName: ${securityName}`)
|
||||
}
|
||||
throw new Error(`BUG: unknown securityName: ${securityName}`)
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ import * as _ from 'lodash-es'
|
|||
import * as math from 'mathjs'
|
||||
|
||||
import {
|
||||
ErrorResponse, ValidateErrorResponse, ValidateErrorStatus,
|
||||
BadRequestError, ForbiddenError, NotFoundError
|
||||
ErrorResponse, ValidateErrorResponse, ValidateError,
|
||||
BadRequestError, ForbiddenError, NotFoundError, UnauthorizedError
|
||||
} from "../error_handler"
|
||||
import { parseAuthToken } from "../util"
|
||||
import { User } from "../authentication"
|
||||
import * as ccf from "../types/ccf"
|
||||
|
||||
export const MINIMUM_OPINION_THRESHOLD = 10
|
||||
|
@ -106,6 +106,8 @@ namespace kv {
|
|||
|
||||
@Route("polls")
|
||||
@Security("jwt")
|
||||
@Response<ErrorResponse>(UnauthorizedError.Status, "Unauthorized")
|
||||
@Response<ValidateErrorResponse>(ValidateError.Status, "Schema validation error")
|
||||
export class PollController extends Controller {
|
||||
|
||||
private kvPolls = new ccf.TypedKVMap(ccf.kv.polls, ccf.string, ccf.json<kv.Poll>())
|
||||
|
@ -114,20 +116,19 @@ export class PollController extends Controller {
|
|||
|
||||
@SuccessResponse(201, "Poll has been successfully created")
|
||||
@Response<ErrorResponse>(ForbiddenError.Status, "Poll has not been created because a poll with the same topic exists already")
|
||||
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
|
||||
@Post('{topic}')
|
||||
public createPoll(
|
||||
@Path() topic: string,
|
||||
@Body() body: CreatePollRequest,
|
||||
@Request() request: ccf.Request
|
||||
): void {
|
||||
const user = request.user.userId
|
||||
const user: User = request.user
|
||||
|
||||
if (this.kvPolls.has(topic)) {
|
||||
throw new ForbiddenError("Poll with given topic exists already")
|
||||
}
|
||||
this.kvPolls.set(topic, {
|
||||
creator: user,
|
||||
creator: user.userId,
|
||||
type: body.type,
|
||||
opinions: {}
|
||||
})
|
||||
|
@ -139,20 +140,19 @@ export class PollController extends Controller {
|
|||
|
||||
@SuccessResponse(201, "Polls have been successfully created")
|
||||
@Response<ErrorResponse>(ForbiddenError.Status, "Polls were not created because a poll with the same topic exists already")
|
||||
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
|
||||
@Post()
|
||||
public createPolls(
|
||||
@Body() body: CreatePollsRequest,
|
||||
@Request() request: ccf.Request
|
||||
): void {
|
||||
const user = request.user.userId
|
||||
const user: User = request.user
|
||||
|
||||
for (let [topic, poll] of Object.entries(body.polls)) {
|
||||
if (this.kvPolls.has(topic)) {
|
||||
throw new ForbiddenError(`Poll with topic '${topic}' exists already`)
|
||||
}
|
||||
this.kvPolls.set(topic, {
|
||||
creator: user,
|
||||
creator: user.userId,
|
||||
type: poll.type,
|
||||
opinions: {}
|
||||
})
|
||||
|
@ -166,14 +166,13 @@ export class PollController extends Controller {
|
|||
@SuccessResponse(204, "Opinion has been successfully recorded")
|
||||
@Response<ErrorResponse>(BadRequestError.Status, "Opinion was not recorded because the opinion data type does not match the poll type")
|
||||
@Response<ErrorResponse>(NotFoundError.Status, "Opinion was not recorded because no poll with the given topic exists")
|
||||
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
|
||||
@Put('{topic}')
|
||||
public submitOpinion(
|
||||
@Path() topic: string,
|
||||
@Body() body: SubmitOpinionRequest,
|
||||
@Request() request: ccf.Request
|
||||
): void {
|
||||
const user = request.user.userId
|
||||
const user: User = request.user
|
||||
|
||||
const poll = this.kvPolls.get(topic)
|
||||
if (poll === undefined) {
|
||||
|
@ -182,20 +181,19 @@ export class PollController extends Controller {
|
|||
if (typeof body.opinion !== poll.type) {
|
||||
throw new BadRequestError("Poll has a different opinion type")
|
||||
}
|
||||
poll.opinions[user] = body.opinion
|
||||
poll.opinions[user.userId] = body.opinion
|
||||
this.kvPolls.set(topic, poll)
|
||||
this.setStatus(204)
|
||||
}
|
||||
|
||||
@SuccessResponse(204, "Opinions have been successfully recorded")
|
||||
@Response<ErrorResponse>(BadRequestError.Status, "Opinions were not recorded because either an opinion data type did not match the poll type or a poll with the given topic was not found")
|
||||
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
|
||||
@Put()
|
||||
public submitOpinions(
|
||||
@Body() body: SubmitOpinionsRequest,
|
||||
@Request() request: ccf.Request
|
||||
): void {
|
||||
const user = request.user.userId
|
||||
const user: User = request.user
|
||||
|
||||
for (const [topic, opinion] of Object.entries(body.opinions)) {
|
||||
const poll = this.kvPolls.get(topic)
|
||||
|
@ -205,7 +203,7 @@ export class PollController extends Controller {
|
|||
if (typeof opinion.opinion !== poll.type) {
|
||||
throw new BadRequestError(`Poll with topic '${topic}' has a different opinion type`)
|
||||
}
|
||||
poll.opinions[user] = opinion.opinion
|
||||
poll.opinions[user.userId] = opinion.opinion
|
||||
this.kvPolls.set(topic, poll)
|
||||
}
|
||||
|
||||
|
@ -214,35 +212,32 @@ export class PollController extends Controller {
|
|||
|
||||
@SuccessResponse(200, "Poll data")
|
||||
@Response<ErrorResponse>(NotFoundError.Status, "Poll data could not be returned because no poll with the given topic exists")
|
||||
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
|
||||
@Get('{topic}')
|
||||
public getPoll(
|
||||
@Path() topic: string,
|
||||
@Request() request: ccf.Request
|
||||
): GetPollResponse {
|
||||
const user = request.user.userId
|
||||
const user: User = request.user
|
||||
|
||||
if (!this.kvPolls.has(topic)){
|
||||
throw new NotFoundError("Poll does not exist")
|
||||
}
|
||||
|
||||
this.setStatus(200)
|
||||
return this._getPoll(user, topic)
|
||||
return this._getPoll(user.userId, topic)
|
||||
}
|
||||
|
||||
@SuccessResponse(200, "Poll data")
|
||||
@Response<ValidateErrorResponse>(ValidateErrorStatus, "Schema validation error")
|
||||
@Get()
|
||||
public getPolls(
|
||||
@Header() authorization: string,
|
||||
@Request() request: ccf.Request
|
||||
): GetPollsResponse {
|
||||
const user = request.user.userId
|
||||
const user: User = request.user
|
||||
|
||||
let response: GetPollsResponse = { polls: {} }
|
||||
|
||||
for (const topic of this._getTopics()) {
|
||||
response.polls[topic] = this._getPoll(user, topic)
|
||||
response.polls[topic] = this._getPoll(user.userId, topic)
|
||||
}
|
||||
|
||||
this.setStatus(200)
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
|
||||
import { ValidateError, FieldErrors } from "@tsoa/runtime";
|
||||
import { ValidateError as TsoaValidateError, FieldErrors } from "@tsoa/runtime";
|
||||
import * as ccf from './types/ccf'
|
||||
|
||||
// The global error handler. Gets called for:
|
||||
// - Request schema validation errors
|
||||
// - Uncaught exceptions in controller actions
|
||||
|
||||
// See https://tsoa-community.github.io/docs/error-handling.html#setting-up-error-handling
|
||||
// The code that imports and calls this handler is in tsoa-support/routes.ts.tmpl.
|
||||
|
||||
export interface ErrorResponse {
|
||||
message: string
|
||||
}
|
||||
|
@ -20,7 +13,9 @@ export interface ValidateErrorResponse extends ErrorResponse {
|
|||
details: FieldErrors
|
||||
}
|
||||
|
||||
export const ValidateErrorStatus = 422
|
||||
export abstract class ValidateError {
|
||||
static Status = 422
|
||||
}
|
||||
|
||||
class HttpError extends Error {
|
||||
constructor(public statusCode: number, message: string) {
|
||||
|
@ -36,6 +31,14 @@ export class BadRequestError extends HttpError {
|
|||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends HttpError {
|
||||
static Status = 401
|
||||
|
||||
constructor(message: string) {
|
||||
super(UnauthorizedError.Status, message)
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends HttpError {
|
||||
static Status = 403
|
||||
|
||||
|
@ -52,14 +55,24 @@ export class NotFoundError extends HttpError {
|
|||
}
|
||||
}
|
||||
|
||||
/** The global error handler.
|
||||
*
|
||||
* This handler is called for:
|
||||
* - Request schema validation errors
|
||||
* - Exceptions thrown by the authentication module
|
||||
* - Uncaught exceptions in controller actions
|
||||
*
|
||||
* See https://tsoa-community.github.io/docs/error-handling.html#setting-up-error-handling
|
||||
* The code that imports and calls this handler is in tsoa-support/routes.ts.tmpl.
|
||||
*/
|
||||
export function errorHandler(err: unknown, req: ccf.Request): ccf.Response<ErrorResponse | ValidateErrorResponse> {
|
||||
if (err instanceof ValidateError) {
|
||||
if (err instanceof TsoaValidateError) {
|
||||
return {
|
||||
body: {
|
||||
message: "Validation failed",
|
||||
details: err.fields
|
||||
},
|
||||
statusCode: ValidateErrorStatus
|
||||
statusCode: ValidateError.Status
|
||||
}
|
||||
} else if (err instanceof HttpError) {
|
||||
return {
|
||||
|
|
|
@ -113,14 +113,13 @@ export function getValidatedArgs(args: any, request: any, response: any): any[]
|
|||
}
|
||||
|
||||
{{#if useSecurity}}
|
||||
export function authenticateMiddleware(security: TsoaRoute.Security[], request: any, response: any) {
|
||||
let user = undefined
|
||||
export function authenticateMiddleware(security: TsoaRoute.Security[], request: any) {
|
||||
let success = false
|
||||
let error = undefined
|
||||
for (const secMethod of security) {
|
||||
for (const name in secMethod) {
|
||||
try {
|
||||
user = authentication(request, name, secMethod[name])
|
||||
authentication(request, name, secMethod[name])
|
||||
success = true
|
||||
break
|
||||
} catch (e) {
|
||||
|
@ -131,17 +130,8 @@ export function authenticateMiddleware(security: TsoaRoute.Security[], request:
|
|||
break
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
request['user'] = user
|
||||
return true
|
||||
} else {
|
||||
response.statusCode = 401
|
||||
if (error.message) {
|
||||
response.body = {
|
||||
message: error.message
|
||||
}
|
||||
}
|
||||
return false
|
||||
if (!success) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
{{/if}}
|
||||
|
@ -187,34 +177,32 @@ import { {{name}} } from '{{modulePath}}';
|
|||
{{#each actions}}
|
||||
// {{method}} {{fullPath}}
|
||||
export function {{name}}(request) {
|
||||
const response = {
|
||||
body: undefined,
|
||||
statusCode: undefined,
|
||||
headers: {}
|
||||
}
|
||||
|
||||
{{#if security.length}}
|
||||
if (!authenticateMiddleware({{json security}}, request, response)) {
|
||||
return response;
|
||||
}
|
||||
{{/if}}
|
||||
|
||||
const args = {
|
||||
{{#each parameters}}
|
||||
{{@key}}: {{{json this}}},
|
||||
{{/each}}
|
||||
};
|
||||
|
||||
const request_ = {
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
query: parseQuery(request.query),
|
||||
params: request.params,
|
||||
user: request.user
|
||||
}
|
||||
|
||||
try {
|
||||
let validatedArgs = getValidatedArgs(args, request_, response);
|
||||
{{#if security.length}}
|
||||
authenticateMiddleware({{json security}}, request)
|
||||
{{/if}}
|
||||
|
||||
const args = {
|
||||
{{#each parameters}}
|
||||
{{@key}}: {{{json this}}},
|
||||
{{/each}}
|
||||
};
|
||||
|
||||
const request_ = {
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
query: parseQuery(request.query),
|
||||
params: request.params,
|
||||
user: request.user
|
||||
}
|
||||
|
||||
const response = {
|
||||
body: undefined,
|
||||
statusCode: undefined,
|
||||
headers: {}
|
||||
}
|
||||
|
||||
const validatedArgs = getValidatedArgs(args, request_, response);
|
||||
|
||||
const controller = new {{../name}}();
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче