Skip to content
Open
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
9 changes: 8 additions & 1 deletion __mocks__/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const mockSession = {
image: null,
},
scopes: ['dataset:update', 'stac:collection:update'],
tenants: ['tenant1', 'tenant2', 'tenant3'],
allowedTenants: ['tenant1', 'tenant2', 'tenant3'],
accessToken: 'mock-access-token',
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
};

Expand Down Expand Up @@ -178,6 +179,12 @@ export const handlers = [
return HttpResponse.json(mockSession);
}),

http.get('/api/allowed-tenants', ({ request }) => {
return HttpResponse.json({
tenants: ['tenant1', 'tenant2', 'tenant3'],
});
}),

http.get(
'https://example.com/api/raster/cog/tiles/WebMercatorQuad/:z/:x/:y.png',
({ request, params }) => {
Expand Down
12 changes: 4 additions & 8 deletions __tests__/pages/CreateIngestPageClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,17 @@ vi.mock('@/components/layout/AppLayout', () => ({
}));

const AllProviders = ({ children }: { children: ReactNode }) => {
const mockSession = {
expires: '1',
};
const mockSession = null;

const mockTenantContext = {
allowedTenants: ['test-tenant-1', 'test-tenant-2'],
isLoading: false,
};

return (
<SessionProvider session={mockSession}>
<TenantContext.Provider value={mockTenantContext}>
{children}
</TenantContext.Provider>
</SessionProvider>
<TenantContext.Provider value={mockTenantContext}>
{children}
</TenantContext.Provider>
);
};

Expand Down
30 changes: 27 additions & 3 deletions app/contexts/TenantContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
'use client';

import React, { createContext, useContext, ReactNode } from 'react';
import React, {
createContext,
useContext,
ReactNode,
useEffect,
useState,
} from 'react';
import { Spin } from 'antd';
import { useSession } from 'next-auth/react';

Expand All @@ -15,10 +21,28 @@ export const TenantContext = createContext<TenantContextType | undefined>(

export const TenantProvider = ({ children }: { children: ReactNode }) => {
const { data: session, status } = useSession();
const [allowedTenants, setAllowedTenants] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);

const isLoading = status === 'loading';
useEffect(() => {
if (status === 'loading') {
setIsLoading(true);
return;
}

const allowedTenants = session?.tenants || [];
setIsLoading(false);

if (!session) {
setAllowedTenants([]);
return;
}

const sessionAllowedTenants = (session as any)?.allowedTenants;
const tenants = Array.isArray(sessionAllowedTenants)
? sessionAllowedTenants
: [];
setAllowedTenants(tenants);
}, [session, status]);

if (isLoading) {
return <Spin fullscreen />;
Expand Down
92 changes: 79 additions & 13 deletions auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import NextAuth, { type NextAuthConfig, Session } from 'next-auth';
import KeycloakProvider from 'next-auth/providers/keycloak';
import { JWT } from 'next-auth/jwt';
import { NextResponse } from 'next/server';
import { VEDA_BACKEND_URL } from '@/config/env';

const authDisabled = process.env.NEXT_PUBLIC_DISABLE_AUTH === 'true';

Expand All @@ -14,10 +15,21 @@ const getMockTenants = (): string[] => {
.map((tenant) => tenant.trim())
.filter(Boolean);
}
// Default fallback tenants if none specified
return [''];
};

const getMockScopes = (): string[] => {
const mockScopes = process.env.NEXT_PUBLIC_MOCK_SCOPES;
if (mockScopes && mockScopes.trim() !== '') {
// Handle both comma and space separated scopes
return mockScopes
.split(/[,\s]+/)
.map((scope) => scope.trim())
.filter(Boolean);
}
return [];
};

let auth: any, handlers: any, signIn: any, signOut: any;

if (authDisabled) {
Expand All @@ -27,23 +39,21 @@ if (authDisabled) {
const mockTenants = getMockTenants();
console.log('🎭 Mock tenants:', mockTenants);

// Inject mock scopes from env if present
let mockScopes: string[] = [];
if (
process.env.NEXT_PUBLIC_MOCK_SCOPES &&
process.env.NEXT_PUBLIC_MOCK_SCOPES.trim() !== ''
) {
mockScopes =
process.env.NEXT_PUBLIC_MOCK_SCOPES.split(/[ ,]+/).filter(Boolean);
console.log('🎭 Mock scopes:', mockScopes);
}
const mockSession: Session & { scopes?: string[] } = {
const mockScopes = getMockScopes();
console.log('Mock scopes:', mockScopes);

const mockSession: Session & {
scopes?: string[];
accessToken?: string;
allowedTenants?: string[];
} = {
user: {
name: 'Mock User',
email: '[email protected]',
},
expires: '2099-12-31T23:59:59.999Z',
tenants: mockTenants,
allowedTenants: mockTenants,
accessToken: 'mock-access-token-for-development',
...(mockScopes.length > 0 ? { scopes: mockScopes } : {}),
};

Expand Down Expand Up @@ -93,6 +103,52 @@ if (authDisabled) {
} else if (typeof rawScopes === 'string') {
(token as JWT).scopes = rawScopes.split(' ');
}

try {
if (process.env.NEXT_PUBLIC_DISABLE_AUTH === 'true') {
console.log(
'Skipping external tenants fetch in test environment'
);
const mockTenants = process.env.NEXT_PUBLIC_MOCK_TENANTS;
if (mockTenants && mockTenants.trim() !== '') {
(token as JWT).allowedTenants = mockTenants
.split(',')
.map((tenant) => tenant.trim())
.filter(Boolean);
}
} else {
const allowedTenantsResponse = await fetch(
`${VEDA_BACKEND_URL}/ingest/auth/tenants/writable`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${account.access_token}`,
Accept: 'application/json',
},
}
);
console.log({ allowedTenantsResponse });

if (allowedTenantsResponse.ok) {
const allowedTenantsData = await allowedTenantsResponse.json();
console.log(
'Fetched allowed tenants during auth:',
allowedTenantsData.tenants
);
(token as JWT).allowedTenants =
allowedTenantsData.tenants || [];
} else {
console.warn(
'Failed to fetch allowed tenants during auth:',
allowedTenantsResponse.status
);
(token as JWT).allowedTenants = [];
}
}
} catch (error) {
console.error('Error fetching allowed tenants during auth:', error);
(token as JWT).allowedTenants = [];
}
}
return token;
},
Expand All @@ -101,8 +157,18 @@ if (authDisabled) {
const customSession = session as Session & {
tenants?: string[];
scopes?: string[];
accessToken?: string;
allowedTenants?: string[];
};

if (customToken.accessToken) {
(customSession as any).accessToken = customToken.accessToken;
}

if (customToken.allowedTenants) {
customSession.allowedTenants = customToken.allowedTenants as string[];
}

// Check if we should use mock tenants instead of real ones
const mockTenants = process.env.NEXT_PUBLIC_MOCK_TENANTS;
if (mockTenants && mockTenants.trim() !== '') {
Expand Down
32 changes: 19 additions & 13 deletions middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ const DISABLE_AUTH = process.env.NEXT_PUBLIC_DISABLE_AUTH === 'true';
// Define route permissions in a declarative way
const routeConfig = {
// Routes that require authentication but no special permissions
limited: ['/collections', '/datasets', '/cog-viewer'],
limited: ['/collections', '/datasets', '/cog-viewer', '/upload-url'],

// Routes that require create permissions (blocked for limited access)
createAccess: ['/create-collection', '/create-dataset', '/upload'],
createAccess: [
'/create-collection',
'/create-dataset',
'/upload',
'/create-ingest',
],

// Routes that require edit permissions (blocked for limited access + need dataset:update)
editAccess: ['/edit-collection', '/edit-dataset'],

editStacCollectionAccess: ['/edit-existing-collection'],

// API routes that require authentication
apiRoutes: [
editAccess: [
'/edit-collection',
'/edit-dataset',
'/list-ingests',
'/retrieve-ingest',
'/create-ingest',
'/upload-url',
],

editStacCollectionAccess: [
'/edit-existing-collection',
'/existing-collection',
],
};
Expand Down Expand Up @@ -60,8 +64,8 @@ function isRouteAllowed(pathname: string, permissionLevel: string) {
return false;

case 'limited':
// Limited users can access authenticated routes, but not create/edit
return matchesRoute(routeConfig.limited);
// Limited users can access authenticated routes and upload-url, but not create/edit
return matchesRoute([...routeConfig.limited]);

case 'create':
return matchesRoute([
Expand Down Expand Up @@ -106,7 +110,9 @@ export async function middleware(request: NextRequest) {
}

if (DISABLE_AUTH) {
console.warn('WARNING: Authentication is disabled for development');
console.warn(
'WARNING: Authentication is disabled for development - middleware skipping auth checks'
);
return NextResponse.next();
}

Expand Down