Skip to content

Commit 4bb41a6

Browse files
committed
remove gcloud dependencies
1 parent bf53824 commit 4bb41a6

File tree

11 files changed

+76
-147
lines changed

11 files changed

+76
-147
lines changed

.github/workflows/integration-test.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,6 @@ jobs:
9898
run: nix develop -c buck2 build ${{ matrix.build_args }}
9999
- name: Start deps and run tests via tilt
100100
run: nix develop -c xvfb-run ./dev/bin/tilt-ci.sh ${{ matrix.component }}
101-
env:
102-
NODE_ENV: test
103-
BYPASS_ROLE_CHECK: true
104101
- name: Prepare Tilt log
105102
id: prepare_tilt_log
106103
if: always()

apps/admin-panel/app/api/auth/[...nextauth]/options.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ import { CallbacksOptions } from "next-auth"
66

77
import { env } from "../../../env"
88

9+
declare module "next-auth" {
10+
interface Session {
11+
sub: string | null
12+
accessToken: string
13+
role: string
14+
scope: string[]
15+
}
16+
}
17+
918
const providers: Provider[] = [
1019
GoogleProvider({
1120
clientId: env.GOOGLE_CLIENT_ID ?? "",
@@ -30,7 +39,13 @@ if (env.NODE_ENV === "development") {
3039
},
3140
authorize: async (credentials) => {
3241
if (credentials?.username === "admin" && credentials?.password === "admin") {
33-
return { id: "1", name: "admin", email: "[email protected]" }
42+
return { id: "1", name: "admin", email: "[email protected]" }
43+
}
44+
if (credentials?.username === "alice" && credentials?.password === "alice") {
45+
return { id: "2", name: "bob", email: "[email protected]" }
46+
}
47+
if (credentials?.username === "bob" && credentials?.password === "bob") {
48+
return { id: "2", name: "bob", email: "[email protected]" }
3449
}
3550
return null
3651
},
@@ -60,32 +75,19 @@ const callbacks: Partial<CallbacksOptions> = {
6075
return verified && env.AUTHORIZED_EMAILS.includes(email)
6176
},
6277
async jwt({ token, account, profile, user }) {
63-
// dummy values ToDo grab from config
64-
// ToDo is it really necessary or is the session callback enough?!
65-
token.scopes = ["muh","meh"]
66-
console.log("jwt", token)
67-
console.log("-------------------------")
78+
const role_mapping = env.ROLE_MAPPING
79+
if (user) {
80+
// get this from config depending if you prefere scope or role
81+
token.scope = ["READ", "WRITE"]
82+
token.role = role_mapping[user.email as keyof typeof role_mapping] || "VIEWER"
83+
} else {
84+
console.log("no user")
85+
}
6886
return token
6987
},
7088
async session({ session, token }) {
71-
// Create custom JWT for admin-api
72-
const adminJwtPayload = {
73-
sub: token.email,
74-
email: token.email,
75-
iat: Math.floor(Date.now() / 1000),
76-
exp: Math.floor(Date.now() / 1000) + (60 * 60), // 1 hour
77-
iss: "admin-panel",
78-
aud: "admin-api",
79-
scopes: ["muh","meh"]
80-
}
81-
82-
const jwt = require('jsonwebtoken')
83-
const adminToken = jwt.sign(adminJwtPayload, env.NEXTAUTH_SECRET)
84-
85-
session.adminToken = adminToken
86-
session.user.email = token.email
87-
// need scopes in session as well in order to retrieve it in oathkeeper's mutator
88-
session.scopes = ["muh","meh"]
89+
session.scope = token.scope as string[]
90+
session.role = token.role as string
8991
return session
9092
},
9193
}

apps/admin-panel/app/env.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ export const env = createEnv({
1616
.string()
1717
.transform((x) => x.split(",").map((email) => email.trim()))
1818
.default("[email protected]"),
19+
ROLE_MAPPING: z
20+
.string()
21+
.transform((str) => {
22+
try {
23+
return JSON.parse(str)
24+
} catch {
25+
return {}
26+
}
27+
})
28+
.default("{}"),
1929
},
2030
/*
2131
* Environment variables available on the client (and server).
@@ -40,5 +50,6 @@ export const env = createEnv({
4050
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
4151
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
4252
AUTHORIZED_EMAILS: process.env.AUTHORIZED_EMAILS,
53+
ROLE_MAPPING: process.env.ROLE_MAPPING || "{}",
4354
},
4455
})

core/api/BUCK

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ sdl(
140140
generator = ":write-sdl",
141141
args = ["admin"],
142142
deps_srcs = [":src"],
143-
env = {"NODE_ENV": "development"},
143+
144144
visibility = ["PUBLIC"],
145145
)
146146

core/api/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@
7878
"express": "^4.21.2",
7979
"express-jwt": "^8.5.1",
8080
"firebase-admin": "^12.6.0",
81-
"google-auth-library": "^10.2.0",
8281
"google-protobuf": "^3.21.4",
8382
"graphql": "^16.11.0",
8483
"graphql-middleware": "^6.1.33",

core/api/src/config/env.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ export const env = createEnv({
2424
.pipe(z.coerce.boolean())
2525
.default(false),
2626

27-
BYPASS_ROLE_CHECK: z.boolean().or(z.string()).pipe(z.coerce.boolean()).default(false),
28-
2927
TELEGRAM_BOT_API_TOKEN: z.string().optional(),
3028
TELEGRAM_PASSPORT_PRIVATE_KEY: z.string().optional(),
3129

@@ -125,10 +123,6 @@ export const env = createEnv({
125123
.pipe(z.coerce.number())
126124
.default(50052),
127125

128-
GCP_PROJECT_ID: z.string().min(1).optional(),
129-
130-
GCP_IAM_SERVICE_ACCOUNT_PATH: z.string().min(1).optional(),
131-
GCS_APPLICATION_CREDENTIALS_PATH: z.string().min(1).optional(),
132126
NEXTCLOUD_URL: z.string().min(1).optional(),
133127
NEXTCLOUD_USER: z.string().min(1).optional(),
134128
NEXTCLOUD_PASSWORD: z.string().min(1).optional(),
@@ -152,8 +146,6 @@ export const env = createEnv({
152146
.or(z.string())
153147
.pipe(z.coerce.number().min(0))
154148
.default(60),
155-
156-
NODE_ENV: z.string(),
157149
},
158150

159151
runtimeEnvStrict: {
@@ -165,8 +157,6 @@ export const env = createEnv({
165157
UNSECURE_DEFAULT_LOGIN_CODE: process.env.UNSECURE_DEFAULT_LOGIN_CODE,
166158
UNSECURE_IP_FROM_REQUEST_OBJECT: process.env.UNSECURE_IP_FROM_REQUEST_OBJECT,
167159

168-
BYPASS_ROLE_CHECK: process.env.BYPASS_ROLE_CHECK,
169-
170160
TELEGRAM_BOT_API_TOKEN: process.env.TELEGRAM_BOT_API_TOKEN,
171161
TELEGRAM_PASSPORT_PRIVATE_KEY: process.env.TELEGRAM_PASSPORT_PRIVATE_KEY,
172162

@@ -243,9 +233,6 @@ export const env = createEnv({
243233
PRICE_HISTORY_HOST: process.env.PRICE_HISTORY_HOST,
244234
PRICE_HISTORY_PORT: process.env.PRICE_HISTORY_PORT,
245235

246-
GCP_PROJECT_ID: process.env.GCP_PROJECT_ID,
247-
GCP_IAM_SERVICE_ACCOUNT_PATH: process.env.GCP_IAM_SERVICE_ACCOUNT_PATH,
248-
GCS_APPLICATION_CREDENTIALS_PATH: process.env.GCS_APPLICATION_CREDENTIALS_PATH,
249236
NEXTCLOUD_URL: process.env.NEXTCLOUD_URL,
250237
NEXTCLOUD_USER: process.env.NEXTCLOUD_USER,
251238
NEXTCLOUD_PASSWORD: process.env.NEXTCLOUD_PASSWORD,
@@ -265,7 +252,5 @@ export const env = createEnv({
265252

266253
EXPORTER_ASSETS_LIABILITIES_DELAY_SECS:
267254
process.env.EXPORTER_ASSETS_LIABILITIES_DELAY_SECS,
268-
269-
NODE_ENV: process.env.NODE_ENV,
270255
},
271256
})

core/api/src/servers/graphql-admin-api-server.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import { rule, shield } from "graphql-shield"
33
import { Rule } from "graphql-shield/typings/rules"
44

55
import { NextFunction, Request, Response } from "express"
6-
import { expressjwt } from "express-jwt"
7-
import { getJwksArgs, jwtAlgorithms } from "./graphql-server"
8-
import jwksRsa from "jwks-rsa"
96

107
import DataLoader from "dataloader"
118

@@ -31,7 +28,12 @@ import { Transactions } from "@/app"
3128

3229
import { AuthorizationError } from "@/graphql/error"
3330

34-
import { AdminFeature, hasFeature } from "@/services/auth/role-checker"
31+
import { AdminAccessRight, AdminRoleString, hasAccessRight } from "@/services/auth/role-checker"
32+
33+
// Helper function to validate role
34+
const isValidAdminRole = (role: string): role is AdminRoleString => {
35+
return role === "VIEWER" || role === "SUPPORT" || role === "ADMIN"
36+
}
3537

3638
// TODO: loaders probably not needed for the admin panel
3739
const loaders = {
@@ -62,12 +64,14 @@ const setGqlAdminContext = async (
6264
console.log("JWT Token payload:", tokenPayload)
6365

6466
const userEmail = tokenPayload.sub as string // This should be the email from OAuth
67+
const role = tokenPayload.role as string
6568
const privilegedClientId = tokenPayload.sub as PrivilegedClientId
6669

6770
req.gqlContext = {
6871
loaders,
6972
privilegedClientId,
7073
userEmail, // Add email to context
74+
role,
7175
logger,
7276
}
7377

@@ -86,17 +90,18 @@ const requiresViewAccess = rule({ cache: "contextual" })(async (
8690
args,
8791
ctx: GraphQLAdminContext,
8892
) => {
89-
if (!ctx.userEmail) return false
90-
return hasFeature(ctx.userEmail, AdminFeature.VIEW_ACCOUNTS)
93+
if (!ctx.userEmail || !ctx.role || !isValidAdminRole(ctx.role)) return false
94+
console.log("ctx",ctx)
95+
return hasAccessRight(ctx.role, AdminAccessRight.VIEW_ACCOUNTS)
9196
})
9297

9398
const requiresModifyAccess = rule({ cache: "contextual" })(async (
9499
parent,
95100
args,
96101
ctx: GraphQLAdminContext,
97102
) => {
98-
if (!ctx.userEmail) return false
99-
return hasFeature(ctx.userEmail, AdminFeature.MODIFY_ACCOUNTS)
103+
if (!ctx.userEmail || !ctx.role || !isValidAdminRole(ctx.role)) return false
104+
return hasAccessRight(ctx.role, AdminAccessRight.MODIFY_ACCOUNTS)
100105
})
101106

102107
export async function startApolloServerForAdminSchema() {

core/api/src/servers/index.files.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type GraphQLAdminContext = {
2525
loaders: Loaders
2626
privilegedClientId: PrivilegedClientId
2727
userEmail: string
28+
role: string
2829
}
2930

3031
type GraphQLContext =
Lines changed: 23 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,14 @@
1-
import { GoogleAuth } from "google-auth-library"
21

3-
import { env } from "../../config/env"
42

5-
import { baseLogger } from "@/services/logger"
63

7-
import { recordExceptionInCurrentSpan } from "@/services/tracing"
8-
9-
import { ErrorLevel } from "@/domain/shared"
10-
11-
// Based on https://cloud.google.com/resource-manager/reference/rest/v1/Policy
12-
interface Policy {
13-
bindings?: Binding[]
14-
etag?: string
15-
version?: number
16-
}
17-
18-
interface Binding {
19-
role?: string
20-
members?: string[]
21-
condition?: {
22-
title?: string
23-
description?: string
24-
expression?: string
25-
}
26-
}
274

285
export enum AdminRole {
296
VIEWER = "roles/adminPanelViewer",
307
SUPPORT = "roles/adminPanelSupport",
318
ADMIN = "roles/adminPanelAdmin",
329
}
3310

34-
export enum AdminFeature {
11+
export enum AdminAccessRight {
3512
VIEW_ACCOUNTS = "VIEW_ACCOUNTS",
3613
MODIFY_ACCOUNTS = "MODIFY_ACCOUNTS",
3714
DELETE_ACCOUNTS = "DELETE_ACCOUNTS",
@@ -40,76 +17,31 @@ export enum AdminFeature {
4017
SYSTEM_CONFIG = "SYSTEM_CONFIG",
4118
}
4219

43-
const ROLE_FEATURES = {
44-
[AdminRole.VIEWER]: [AdminFeature.VIEW_ACCOUNTS, AdminFeature.VIEW_TRANSACTIONS],
45-
[AdminRole.SUPPORT]: [
46-
AdminFeature.VIEW_ACCOUNTS,
47-
AdminFeature.MODIFY_ACCOUNTS,
48-
AdminFeature.VIEW_TRANSACTIONS,
49-
AdminFeature.SEND_NOTIFICATIONS,
20+
// String-based role values from options.ts
21+
export type AdminRoleString = "VIEWER" | "SUPPORT" | "ADMIN"
22+
23+
// String-based role access rights mapping
24+
const STRING_ROLE_ACCESS_RIGHTS = {
25+
VIEWER: [AdminAccessRight.VIEW_ACCOUNTS, AdminAccessRight.VIEW_TRANSACTIONS],
26+
SUPPORT: [
27+
AdminAccessRight.VIEW_ACCOUNTS,
28+
AdminAccessRight.MODIFY_ACCOUNTS,
29+
AdminAccessRight.VIEW_TRANSACTIONS,
30+
AdminAccessRight.SEND_NOTIFICATIONS,
5031
],
51-
[AdminRole.ADMIN]: Object.values(AdminFeature),
32+
ADMIN: Object.values(AdminAccessRight),
5233
}
5334

54-
const auth = new GoogleAuth({
55-
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
56-
keyFilename: env.GCP_IAM_SERVICE_ACCOUNT_PATH,
57-
})
58-
59-
export const getUserRoles = async (userEmail: string): Promise<AdminRole[]> => {
60-
// Only bypass in development/test environments
61-
if (
62-
(env.NODE_ENV === "development" || env.NODE_ENV === "test") &&
63-
env.BYPASS_ROLE_CHECK
64-
) {
65-
return Object.values(AdminRole)
66-
}
67-
68-
// Production uses real role checking
69-
try {
70-
const client = await auth.getClient()
71-
const projectId = process.env.GCP_PROJECT_ID
72-
if (!projectId) {
73-
throw new Error("GCP_PROJECT_ID environment variable is required for role checking")
74-
}
7535

76-
const response = await client.request({
77-
url: `https://cloudresourcemanager.googleapis.com/v1/projects/${projectId}:getIamPolicy`,
78-
method: "POST",
79-
})
80-
81-
const policy = response.data as Policy
82-
const bindings = policy.bindings || []
83-
const userRoles = bindings.reduce<AdminRole[]>((roles, binding) => {
84-
if (binding.members?.includes(`user:${userEmail}`)) {
85-
if (
86-
binding.role &&
87-
Object.values(AdminRole).includes(binding.role as AdminRole)
88-
) {
89-
roles.push(binding.role as AdminRole)
90-
}
91-
}
92-
return roles
93-
}, [])
94-
95-
return userRoles
96-
} catch (error) {
97-
baseLogger.error("Failed to get user roles:", error)
98-
recordExceptionInCurrentSpan({
99-
error,
100-
level: ErrorLevel.Critical,
101-
attributes: {
102-
"getUserRoles.error.userEmail": userEmail,
103-
},
104-
})
105-
return []
106-
}
107-
}
108-
109-
export const hasFeature = async (
110-
userEmail: string,
111-
feature: AdminFeature,
36+
export const hasAccessRight = async (
37+
role: AdminRoleString,
38+
accessRight: AdminAccessRight,
11239
): Promise<boolean> => {
113-
const roles = await getUserRoles(userEmail)
114-
return roles.some((role) => ROLE_FEATURES[role]?.includes(feature))
40+
return STRING_ROLE_ACCESS_RIGHTS[role]?.includes(accessRight) || false
11541
}
42+
43+
// Backward compatibility - deprecated, use AdminAccessRight instead
44+
export const AdminFeature = AdminAccessRight
45+
46+
// Backward compatibility - deprecated, use hasAccessRight instead
47+
export const hasFeature = hasAccessRight

dev/config/ory/oathkeeper_rules.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,4 @@
106106
mutators:
107107
- handler: id_token
108108
config: #! TODO: add aud: {"aud": ["https://api/admin/graphql"] }
109-
# This is currently failing with invalid character '\"' after array element"
110-
claims: "{\"sub\": \"{{ print .Subject }}\", \"scopes\": {{ printf \"%+q\" .Extra.scopes }}, \"email\": \"{{ print .Extra.user.email }}\" }"
109+
claims: "{\"sub\": \"{{ print .Subject }}\", \"role\": \"{{ print .Extra.role }}\", \"email\": \"{{ print .Extra.user.emailr }}\" }"

0 commit comments

Comments
 (0)