From 561cde6e7e5e519e716fb09ef3c8f305083db4d3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 27 Dec 2022 15:18:58 -0800 Subject: [PATCH] Add admin check. (#1716) * Add admin check. * Fix teamId check. --- lib/auth.ts | 109 +++++++++++++++++++++------ lib/constants.ts | 6 +- pages/api/teams/[id]/index.ts | 9 +-- pages/api/teams/[id]/users.ts | 9 +-- pages/api/teams/[id]/websites.ts | 5 +- pages/api/teams/index.ts | 2 +- pages/api/users/[id]/index.ts | 8 +- pages/api/users/[id]/password.ts | 5 +- pages/api/users/[id]/websites.ts | 9 ++- pages/api/users/index.ts | 13 ++-- pages/api/websites/[id]/active.ts | 5 +- pages/api/websites/[id]/eventdata.ts | 5 +- pages/api/websites/[id]/events.ts | 5 +- pages/api/websites/[id]/index.ts | 9 +-- pages/api/websites/[id]/metrics.ts | 5 +- pages/api/websites/[id]/pageviews.ts | 5 +- pages/api/websites/[id]/reset.ts | 5 +- pages/api/websites/[id]/stats.ts | 5 +- pages/api/websites/index.ts | 9 ++- pages/console/[[...id]].js | 3 +- 20 files changed, 133 insertions(+), 98 deletions(-) diff --git a/lib/auth.ts b/lib/auth.ts index 7ba778c7..1113ebf5 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,9 +1,10 @@ -import { parseSecureToken, parseToken, ensureArray } from 'next-basics'; import debug from 'debug'; import cache from 'lib/cache'; -import { SHARE_TOKEN_HEADER, PERMISSIONS, ROLE_PERMISSIONS } from 'lib/constants'; +import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; +import { ensureArray, parseSecureToken, parseToken } from 'next-basics'; import { getTeamUser } from 'queries'; +import { Auth } from './types'; const log = debug('umami:auth'); @@ -48,29 +49,51 @@ export function isValidToken(token, validation) { return false; } -export async function canViewWebsite(userId: string, websiteId: string) { +export async function canViewWebsite({ user }: Auth, websiteId: string) { + if (user.isAdmin) { + return true; + } + const website = await cache.fetchWebsite(websiteId); if (website.userId) { - return userId === website.userId; + return user.id === website.userId; } if (website.teamId) { - return getTeamUser(website.teamId, userId); + return getTeamUser(website.teamId, user.id); } return false; } -export async function canUpdateWebsite(userId: string, websiteId: string) { +export async function canCreateWebsite({ user }: Auth, teamId?: string) { + if (user.isAdmin) { + return true; + } + + if (teamId) { + const teamUser = await getTeamUser(teamId, user.id); + + return hasPermission(teamUser.role, PERMISSIONS.websiteCreate); + } + + return hasPermission(user.role, PERMISSIONS.websiteCreate); +} + +export async function canUpdateWebsite({ user }: Auth, websiteId: string) { + if (user.isAdmin) { + return true; + } + const website = await cache.fetchWebsite(websiteId); if (website.userId) { - return userId === website.userId; + return user.id === website.userId; } if (website.teamId) { - const teamUser = await getTeamUser(website.teamId, userId); + const teamUser = await getTeamUser(website.teamId, user.id); return hasPermission(teamUser.role, PERMISSIONS.websiteUpdate); } @@ -78,15 +101,19 @@ export async function canUpdateWebsite(userId: string, websiteId: string) { return false; } -export async function canDeleteWebsite(userId: string, websiteId: string) { +export async function canDeleteWebsite({ user }: Auth, websiteId: string) { + if (user.isAdmin) { + return true; + } + const website = await cache.fetchWebsite(websiteId); if (website.userId) { - return userId === website.userId; + return user.id === website.userId; } if (website.teamId) { - const teamUser = await getTeamUser(website.teamId, userId); + const teamUser = await getTeamUser(website.teamId, user.id); return hasPermission(teamUser.role, PERMISSIONS.websiteDelete); } @@ -95,33 +122,69 @@ export async function canDeleteWebsite(userId: string, websiteId: string) { } // To-do: Implement when payments are setup. -export async function canCreateTeam(userId: string) { - return !!userId; +export async function canCreateTeam({ user }: Auth) { + if (user.isAdmin) { + return true; + } + + return !!user; } // To-do: Implement when payments are setup. -export async function canViewTeam(userId: string, teamId) { - return getTeamUser(teamId, userId); +export async function canViewTeam({ user }: Auth, teamId: string) { + if (user.isAdmin) { + return true; + } + + return getTeamUser(teamId, user.id); } -export async function canUpdateTeam(userId: string, teamId: string) { - const teamUser = await getTeamUser(teamId, userId); +export async function canUpdateTeam({ user }: Auth, teamId: string) { + if (user.isAdmin) { + return true; + } + + const teamUser = await getTeamUser(teamId, user.id); return hasPermission(teamUser.role, PERMISSIONS.teamUpdate); } -export async function canDeleteTeam(userId: string, teamId: string) { - const teamUser = await getTeamUser(teamId, userId); +export async function canDeleteTeam({ user }: Auth, teamId: string) { + if (user.isAdmin) { + return true; + } + + const teamUser = await getTeamUser(teamId, user.id); return hasPermission(teamUser.role, PERMISSIONS.teamDelete); } -export async function canViewUser(userId: string, viewedUserId: string) { - return userId === viewedUserId; +export async function canCreateUser({ user }: Auth) { + return user.isAdmin; } -export async function canUpdateUser(userId: string, viewedUserId: string) { - return userId === viewedUserId; +export async function canViewUser({ user }: Auth, viewedUserId: string) { + if (user.isAdmin) { + return true; + } + + return user.id === viewedUserId; +} + +export async function canViewUsers({ user }: Auth) { + return user.isAdmin; +} + +export async function canUpdateUser({ user }: Auth, viewedUserId: string) { + if (user.isAdmin) { + return true; + } + + return user.id === viewedUserId; +} + +export async function canDeleteUser({ user }: Auth) { + return user.isAdmin; } export async function hasPermission(role: string, permission: string | string[]) { diff --git a/lib/constants.ts b/lib/constants.ts index 5407131c..c2be8f51 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -25,7 +25,7 @@ export const REALTIME_INTERVAL = 3000; export const EVENT_TYPE = { pageView: 1, customEvent: 2, -}; +} as const; export const ROLES = { admin: 'admin', @@ -43,7 +43,7 @@ export const PERMISSIONS = { teamCreate: 'team:create', teamUpdate: 'team:update', teamDelete: 'team:delete', -}; +} as const; export const ROLE_PERMISSIONS = { [ROLES.admin]: [PERMISSIONS.all], @@ -66,7 +66,7 @@ export const ROLE_PERMISSIONS = { PERMISSIONS.websiteDelete, ], [ROLES.teamGuest]: [], -}; +} as const; export const THEME_COLORS = { light: { diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index a3ccd0b6..4612f4ed 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -20,13 +20,10 @@ export default async ( ) => { await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: teamId } = req.query; if (req.method === 'GET') { - if (!(await canViewTeam(userId, teamId))) { + if (!(await canViewTeam(req.auth, teamId))) { return unauthorized(res); } @@ -38,7 +35,7 @@ export default async ( if (req.method === 'POST') { const { name } = req.body; - if (!(await canUpdateTeam(userId, teamId))) { + if (!(await canUpdateTeam(req.auth, teamId))) { return unauthorized(res, 'You must be the owner of this team.'); } @@ -48,7 +45,7 @@ export default async ( } if (req.method === 'DELETE') { - if (!(await canDeleteTeam(userId, teamId))) { + if (!(await canDeleteTeam(req.auth, teamId))) { return unauthorized(res, 'You must be the owner of this team.'); } diff --git a/pages/api/teams/[id]/users.ts b/pages/api/teams/[id]/users.ts index e90dc648..a5da215a 100644 --- a/pages/api/teams/[id]/users.ts +++ b/pages/api/teams/[id]/users.ts @@ -21,13 +21,10 @@ export default async ( ) => { await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: teamId } = req.query; if (req.method === 'GET') { - if (!(await canViewTeam(userId, teamId))) { + if (!(await canViewTeam(req.auth, teamId))) { return unauthorized(res); } @@ -37,7 +34,7 @@ export default async ( } if (req.method === 'POST') { - if (!(await canUpdateTeam(userId, teamId))) { + if (!(await canUpdateTeam(req.auth, teamId))) { return unauthorized(res, 'You must be the owner of this team.'); } @@ -56,7 +53,7 @@ export default async ( } if (req.method === 'DELETE') { - if (await canUpdateTeam(userId, teamId)) { + if (await canUpdateTeam(req.auth, teamId)) { return unauthorized(res, 'You must be the owner of this team.'); } const { teamUserId } = req.body; diff --git a/pages/api/teams/[id]/websites.ts b/pages/api/teams/[id]/websites.ts index d1f22da8..0e610649 100644 --- a/pages/api/teams/[id]/websites.ts +++ b/pages/api/teams/[id]/websites.ts @@ -20,13 +20,10 @@ export default async ( ) => { await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: teamId } = req.query; if (req.method === 'GET') { - if (await canViewTeam(userId, teamId)) { + if (await canViewTeam(req.auth, teamId)) { return unauthorized(res); } diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 46cf65a9..14527898 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -28,7 +28,7 @@ export default async ( } if (req.method === 'POST') { - if (!(await canCreateTeam(userId))) { + if (!(await canCreateTeam(req.auth))) { return unauthorized(res); } diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index d31039f1..25fa337b 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -1,5 +1,5 @@ import { NextApiRequestQueryBody } from 'lib/types'; -import { canUpdateUser, canViewUser } from 'lib/auth'; +import { canDeleteUser, canUpdateUser, canViewUser } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -26,7 +26,7 @@ export default async ( const { id } = req.query; if (req.method === 'GET') { - if (!isAdmin && !(await canViewUser(userId, id))) { + if (!(await canViewUser(req.auth, id))) { return unauthorized(res); } @@ -36,7 +36,7 @@ export default async ( } if (req.method === 'POST') { - if (!isAdmin && !(await canUpdateUser(userId, id))) { + if (!(await canUpdateUser(req.auth, id))) { return unauthorized(res); } @@ -71,7 +71,7 @@ export default async ( } if (req.method === 'DELETE') { - if (!isAdmin) { + if (!(await canDeleteUser(req.auth))) { return unauthorized(res); } diff --git a/pages/api/users/[id]/password.ts b/pages/api/users/[id]/password.ts index 8fabb355..862609e1 100644 --- a/pages/api/users/[id]/password.ts +++ b/pages/api/users/[id]/password.ts @@ -29,12 +29,9 @@ export default async ( const { currentPassword, newPassword } = req.body; const { id } = req.query; - const { - user: { id: userId, isAdmin }, - } = req.auth; if (req.method === 'POST') { - if (!isAdmin && !(await canUpdateUser(userId, id))) { + if (!(await canUpdateUser(req.auth, id))) { return unauthorized(res); } diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index bc79f91a..ec151c95 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -1,9 +1,10 @@ import { Prisma } from '@prisma/client'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { canCreateWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite, getUserWebsites } from 'queries'; export interface WebsitesRequestQuery {} @@ -35,6 +36,10 @@ export default async ( if (req.method === 'POST') { const { name, domain, shareId, teamId } = req.body; + if (!(await canCreateWebsite(req.auth, teamId))) { + return unauthorized(res); + } + const data: Prisma.WebsiteUncheckedCreateInput = { id: uuid(), name, diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 4abe4353..89d4d564 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,7 +1,8 @@ -import { NextApiRequestQueryBody } from 'lib/types'; +import { canCreateUser, canViewUsers } from 'lib/auth'; +import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; -import { ROLES } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUser, getUsers, User } from 'queries'; @@ -18,12 +19,8 @@ export default async ( ) => { await useAuth(req, res); - const { - user: { isAdmin }, - } = req.auth; - if (req.method === 'GET') { - if (!isAdmin) { + if (!(await canViewUsers(req.auth))) { return unauthorized(res); } @@ -33,7 +30,7 @@ export default async ( } if (req.method === 'POST') { - if (!isAdmin) { + if (!(await canCreateUser(req.auth))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/active.ts b/pages/api/websites/[id]/active.ts index 1d007f7f..1cfcd54c 100644 --- a/pages/api/websites/[id]/active.ts +++ b/pages/api/websites/[id]/active.ts @@ -17,13 +17,10 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: websiteId } = req.query; if (req.method === 'GET') { - if (await canViewWebsite(userId, websiteId)) { + if (await canViewWebsite(req.auth, websiteId)) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/eventdata.ts b/pages/api/websites/[id]/eventdata.ts index 9e21eb13..023a6878 100644 --- a/pages/api/websites/[id]/eventdata.ts +++ b/pages/api/websites/[id]/eventdata.ts @@ -24,13 +24,10 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: websiteId } = req.query; if (req.method === 'POST') { - if (canViewWebsite(userId, websiteId)) { + if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/events.ts b/pages/api/websites/[id]/events.ts index 088f2ff4..918a9542 100644 --- a/pages/api/websites/[id]/events.ts +++ b/pages/api/websites/[id]/events.ts @@ -25,13 +25,10 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: websiteId, startAt, endAt, unit, tz, url, eventName } = req.query; if (req.method === 'GET') { - if (canViewWebsite(userId, websiteId)) { + if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 2f11ec70..671f6274 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -22,13 +22,10 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: websiteId } = req.query; if (req.method === 'GET') { - if (!(await canViewWebsite(userId, websiteId))) { + if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } @@ -38,7 +35,7 @@ export default async ( } if (req.method === 'POST') { - if (!(await canUpdateWebsite(userId, websiteId))) { + if (!(await canUpdateWebsite(req.auth, websiteId))) { return unauthorized(res); } @@ -56,7 +53,7 @@ export default async ( } if (req.method === 'DELETE') { - if (!(await canDeleteWebsite(userId, websiteId))) { + if (!(await canDeleteWebsite(req.auth, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index 62497dbd..25759531 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -55,9 +55,6 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: websiteId, type, @@ -72,7 +69,7 @@ export default async ( } = req.query; if (req.method === 'GET') { - if (!(await canViewWebsite(userId, websiteId))) { + if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 098a3192..19303aa3 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -30,9 +30,6 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: websiteId, startAt, @@ -48,7 +45,7 @@ export default async ( } = req.query; if (req.method === 'GET') { - if (!(await canViewWebsite(userId, websiteId))) { + if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index 292672ae..dc98c591 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -16,13 +16,10 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: websiteId } = req.query; if (req.method === 'POST') { - if (!(await canViewWebsite(userId, websiteId))) { + if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index f0ce4b6b..27262615 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -26,13 +26,10 @@ export default async ( await useCors(req, res); await useAuth(req, res); - const { - user: { id: userId }, - } = req.auth; const { id: websiteId, startAt, endAt, url, referrer, os, browser, device, country } = req.query; if (req.method === 'GET') { - if (!(await canViewWebsite(userId, websiteId))) { + if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index bc79f91a..ec151c95 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,9 +1,10 @@ import { Prisma } from '@prisma/client'; -import { NextApiRequestQueryBody } from 'lib/types'; +import { canCreateWebsite } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createWebsite, getUserWebsites } from 'queries'; export interface WebsitesRequestQuery {} @@ -35,6 +36,10 @@ export default async ( if (req.method === 'POST') { const { name, domain, shareId, teamId } = req.body; + if (!(await canCreateWebsite(req.auth, teamId))) { + return unauthorized(res); + } + const data: Prisma.WebsiteUncheckedCreateInput = { id: uuid(), name, diff --git a/pages/console/[[...id]].js b/pages/console/[[...id]].js index 270a81df..fa73c3a9 100644 --- a/pages/console/[[...id]].js +++ b/pages/console/[[...id]].js @@ -3,12 +3,13 @@ import Layout from 'components/layout/Layout'; import TestConsole from 'components/pages/TestConsole'; import useRequireLogin from 'hooks/useRequireLogin'; import useUser from 'hooks/useUser'; +import { ROLES } from 'lib/constants'; export default function ConsolePage({ pageDisabled }) { const { loading } = useRequireLogin(); const { user } = useUser(); - if (pageDisabled || loading || !user?.isAdmin) { + if (pageDisabled || loading || user?.role !== ROLES.admin) { return null; }