@@ -11,48 +11,97 @@ import {
1111} from './config/suseai' ;
1212import type { RancherStore } from './types/rancher-types' ;
1313import { checkOperatorConnection } from './utils/operator-config' ;
14+ import { canAccessExtension , invalidateAccessCache , CRTB_TYPE , LOCAL_CLUSTER } from './utils/access' ;
15+ import { logger } from './utils/logger' ;
1416
1517export { PRODUCT } from './config/suseai' ;
1618
17- export function init ( $plugin : IPlugin , store : RancherStore ) {
18- const { product, virtualType, basicType, weightType } = $plugin . DSL ( store , PRODUCT ) ;
19+ const AIFACTORY_API_GROUP = 'ai-platform.suse.com' ;
1920
20- // Register store modules following standard patterns
21+ let removeNavGuard : ( ( ) => void ) | null = null ;
22+ let removeCrtbWatch : ( ( ) => void ) | null = null ;
23+
24+ export function init ( $plugin : IPlugin , store : RancherStore ) {
2125 store . registerModule ?.( PRODUCT , suseaiStore ) ;
2226
23- // Configure product following standard patterns
27+ const { product, virtualType, basicType, weightType } = $plugin . DSL ( store , PRODUCT ) ;
28+
2429 product ( {
25- icon : 'suseai' ,
26- iconHeader : require ( './assets/SUSE-AI-Factory-Logo_pos-green-horizontal.svg' ) ,
27- inStore : SUSEAI_PRODUCT . inStore ,
30+ icon : 'suseai' ,
31+ iconHeader : require ( './assets/SUSE-AI-Factory-Logo_pos-green-horizontal.svg' ) ,
32+ inStore : SUSEAI_PRODUCT . inStore ,
2833 isMultiClusterApp : true ,
2934 showClusterSwitcher : false ,
30- weight : SUSEAI_PRODUCT . weight ,
35+ weight : SUSEAI_PRODUCT . weight ,
36+ // Both conditions are AND-checked by Rancher's activeProducts getter.
37+ // ifHaveType: users without CRTB schema access (standard users, cluster members)
38+ // never see the icon.
39+ // ifHaveGroup: the ai-platform.suse.com CRDs are management-cluster-scoped;
40+ // downstream cluster owners do not have access to them in the management store,
41+ // so they are excluded despite having CRTB access.
42+ // Navigation is further restricted by the nav guard below.
43+ ifHaveType : CRTB_TYPE ,
44+ ifHaveGroup : AIFACTORY_API_GROUP ,
3145 to : {
3246 name : `c-cluster-${ PRODUCT } -${ PAGE_TYPES . OVERVIEW } ` ,
3347 params : { product : PRODUCT , cluster : MANAGEMENT_CLUSTER } ,
3448 meta : { product : PRODUCT , cluster : MANAGEMENT_CLUSTER }
3549 }
36- } as any ) ;
50+ // ifHaveType and ifHaveGroup are valid at runtime but absent from the
51+ // published @rancher /shell DSL TypeScript types.
52+ } as Record < string , unknown > ) ;
3753
38- // Register virtual types following standard patterns
39- VIRTUAL_TYPES . forEach ( vType => {
40- virtualType ( {
41- name : vType . name ,
42- label : vType . label ,
43- route : vType . route
54+ const router = store . state . $router ;
55+
56+ if ( router && typeof router . beforeEach === 'function' ) {
57+ removeNavGuard ?.( ) ;
58+ removeNavGuard = router . beforeEach ( async ( to , _from , next ) => {
59+ if ( ! to . name ?. toString ( ) . startsWith ( `c-cluster-${ PRODUCT } -` ) ) return next ( ) ;
60+
61+ try {
62+ const canAccess = await canAccessExtension ( store ) ;
63+
64+ canAccess ? next ( ) : next ( { name : 'home' } ) ;
65+ } catch ( err ) {
66+ // canAccessExtension can throw if the management store is reset at
67+ // runtime (logout, session expiry, network failure). Fail closed to
68+ // avoid leaving the router in a hung state with next() never called.
69+ logger . warn ( 'canAccessExtension threw unexpectedly; failing closed' , { action : 'nav-guard' , data : err } ) ;
70+ store . dispatch ( 'growl/error' , {
71+ title : 'Access check failed' ,
72+ message : 'Unable to verify extension access. Please reload the page.' ,
73+ timeout : 8000 ,
74+ } ) ;
75+ next ( { name : 'home' } ) ;
76+ }
4477 } ) ;
78+
79+ // Invalidate the cached access decision when CRTBs change so a mid-session
80+ // role revocation is caught on the next navigation into the extension.
81+ removeCrtbWatch ?.( ) ;
82+ removeCrtbWatch = store . watch (
83+ ( _ , getters ) => getters [ 'management/all' ] ( CRTB_TYPE ) ,
84+ ( ) => invalidateAccessCache ( )
85+ ) ;
86+ }
87+
88+ VIRTUAL_TYPES . forEach ( vType => {
89+ virtualType ( { name : vType . name , label : vType . label , route : vType . route } ) ;
4590 } ) ;
4691
47- // Apply explicit sidebar ordering (higher weight = higher in list).
4892 Object . entries ( NAV_WEIGHTS ) . forEach ( ( [ type , weight ] ) => {
4993 weightType ( type , weight , true ) ;
5094 } ) ;
5195
52- // Register basic types
5396 basicType ( BASIC_TYPES ) ;
5497
55- // Warm config cache + connection check in background.
56- // Fire-and-forget here; page fetch() hooks await the shared promise.
98+ // Prefetch local-namespace CRTBs for users who have CRTB schema access, so
99+ // the first navigation hits the Vuex cache rather than blocking on a network
100+ // request inside the nav guard. Skipped for users without schema access to
101+ // avoid a guaranteed 403 on every login.
102+ if ( store . getters ?. [ 'management/schemaFor' ] ?.( CRTB_TYPE ) ) {
103+ void store . dispatch ( 'management/findAll' , { type : CRTB_TYPE , opt : { namespaced : LOCAL_CLUSTER } } ) ;
104+ }
105+
57106 void checkOperatorConnection ( ) ;
58107}
0 commit comments