mirror of https://github.com/portainer/portainer
				
				
				
			feat(ui): restrict views by role [EE-6595] (#11010)
							parent
							
								
									437831fa80
								
							
						
					
					
						commit
						f5f84c5fa4
					
				| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import angular from 'angular';
 | 
					import angular from 'angular';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AccessHeaders } from '@/portainer/authorization-guard';
 | 
				
			||||||
import edgeStackModule from './views/edge-stacks';
 | 
					import edgeStackModule from './views/edge-stacks';
 | 
				
			||||||
import { reactModule } from './react';
 | 
					import { reactModule } from './react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +13,9 @@ angular
 | 
				
			||||||
      url: '/edge',
 | 
					      url: '/edge',
 | 
				
			||||||
      parent: 'root',
 | 
					      parent: 'root',
 | 
				
			||||||
      abstract: true,
 | 
					      abstract: true,
 | 
				
			||||||
 | 
					      data: {
 | 
				
			||||||
 | 
					        access: AccessHeaders.EdgeAdmin,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const groups = {
 | 
					    const groups = {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ import { reactModule } from './react';
 | 
				
			||||||
import { sidebarModule } from './react/views/sidebar';
 | 
					import { sidebarModule } from './react/views/sidebar';
 | 
				
			||||||
import environmentsModule from './environments';
 | 
					import environmentsModule from './environments';
 | 
				
			||||||
import { helpersModule } from './helpers';
 | 
					import { helpersModule } from './helpers';
 | 
				
			||||||
 | 
					import { AccessHeaders, requiresAuthHook } from './authorization-guard';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function initAuthentication(Authentication) {
 | 
					async function initAuthentication(Authentication) {
 | 
				
			||||||
  return await Authentication.init();
 | 
					  return await Authentication.init();
 | 
				
			||||||
| 
						 | 
					@ -60,6 +61,9 @@ angular
 | 
				
			||||||
            component: 'sidebar',
 | 
					            component: 'sidebar',
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          access: AccessHeaders.Restricted,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var endpointRoot = {
 | 
					      var endpointRoot = {
 | 
				
			||||||
| 
						 | 
					@ -122,6 +126,16 @@ angular
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const createHelmRepository = {
 | 
				
			||||||
 | 
					        name: 'portainer.account.createHelmRepository',
 | 
				
			||||||
 | 
					        url: '/helm-repository/new',
 | 
				
			||||||
 | 
					        views: {
 | 
				
			||||||
 | 
					          'content@': {
 | 
				
			||||||
 | 
					            component: 'createHelmRepositoryView',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var authentication = {
 | 
					      var authentication = {
 | 
				
			||||||
        name: 'portainer.auth',
 | 
					        name: 'portainer.auth',
 | 
				
			||||||
        url: '/auth',
 | 
					        url: '/auth',
 | 
				
			||||||
| 
						 | 
					@ -136,6 +150,9 @@ angular
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          'sidebar@': {},
 | 
					          'sidebar@': {},
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          access: undefined,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const logout = {
 | 
					      const logout = {
 | 
				
			||||||
| 
						 | 
					@ -152,6 +169,9 @@ angular
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          'sidebar@': {},
 | 
					          'sidebar@': {},
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          access: undefined,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var endpoints = {
 | 
					      var endpoints = {
 | 
				
			||||||
| 
						 | 
					@ -256,6 +276,7 @@ angular
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          docs: '/admin/environments/groups',
 | 
					          docs: '/admin/environments/groups',
 | 
				
			||||||
 | 
					          access: AccessHeaders.Admin,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -312,6 +333,9 @@ angular
 | 
				
			||||||
        views: {
 | 
					        views: {
 | 
				
			||||||
          'sidebar@': {},
 | 
					          'sidebar@': {},
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          access: undefined,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var initAdmin = {
 | 
					      var initAdmin = {
 | 
				
			||||||
| 
						 | 
					@ -336,6 +360,7 @@ angular
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          docs: '/admin/registries',
 | 
					          docs: '/admin/registries',
 | 
				
			||||||
 | 
					          access: AccessHeaders.Admin,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -369,6 +394,7 @@ angular
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          docs: '/admin/settings',
 | 
					          docs: '/admin/settings',
 | 
				
			||||||
 | 
					          access: AccessHeaders.Admin,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -410,6 +436,7 @@ angular
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          docs: '/admin/environments/tags',
 | 
					          docs: '/admin/environments/tags',
 | 
				
			||||||
 | 
					          access: AccessHeaders.Admin,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -424,6 +451,7 @@ angular
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          docs: '/admin/users',
 | 
					          docs: '/admin/users',
 | 
				
			||||||
 | 
					          access: AccessHeaders.Restricted, // allow for team leaders
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -438,16 +466,6 @@ angular
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const createHelmRepository = {
 | 
					 | 
				
			||||||
        name: 'portainer.account.createHelmRepository',
 | 
					 | 
				
			||||||
        url: '/helm-repository/new',
 | 
					 | 
				
			||||||
        views: {
 | 
					 | 
				
			||||||
          'content@': {
 | 
					 | 
				
			||||||
            component: 'createHelmRepositoryView',
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      $stateRegistryProvider.register(root);
 | 
					      $stateRegistryProvider.register(root);
 | 
				
			||||||
      $stateRegistryProvider.register(endpointRoot);
 | 
					      $stateRegistryProvider.register(endpointRoot);
 | 
				
			||||||
      $stateRegistryProvider.register(portainer);
 | 
					      $stateRegistryProvider.register(portainer);
 | 
				
			||||||
| 
						 | 
					@ -481,7 +499,8 @@ angular
 | 
				
			||||||
      $stateRegistryProvider.register(user);
 | 
					      $stateRegistryProvider.register(user);
 | 
				
			||||||
      $stateRegistryProvider.register(createHelmRepository);
 | 
					      $stateRegistryProvider.register(createHelmRepository);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  ]);
 | 
					  ])
 | 
				
			||||||
 | 
					  .run(run);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function isTransitionRequiresAuthentication(transition) {
 | 
					function isTransitionRequiresAuthentication(transition) {
 | 
				
			||||||
  const UNAUTHENTICATED_ROUTES = ['portainer.logout', 'portainer.auth'];
 | 
					  const UNAUTHENTICATED_ROUTES = ['portainer.logout', 'portainer.auth'];
 | 
				
			||||||
| 
						 | 
					@ -492,3 +511,8 @@ function isTransitionRequiresAuthentication(transition) {
 | 
				
			||||||
  const nextTransitionName = nextTransition ? nextTransition.name : '';
 | 
					  const nextTransitionName = nextTransition ? nextTransition.name : '';
 | 
				
			||||||
  return !UNAUTHENTICATED_ROUTES.some((route) => nextTransitionName.startsWith(route));
 | 
					  return !UNAUTHENTICATED_ROUTES.some((route) => nextTransitionName.startsWith(route));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* @ngInject */
 | 
				
			||||||
 | 
					function run($transitions) {
 | 
				
			||||||
 | 
					  requiresAuthHook($transitions);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,118 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  StateDeclaration,
 | 
				
			||||||
 | 
					  StateService,
 | 
				
			||||||
 | 
					  Transition,
 | 
				
			||||||
 | 
					} from '@uirouter/angularjs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { checkAuthorizations } from './authorization-guard';
 | 
				
			||||||
 | 
					import { IAuthenticationService } from './services/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('checkAuthorizations', () => {
 | 
				
			||||||
 | 
					  let authService = {
 | 
				
			||||||
 | 
					    init: vi.fn(),
 | 
				
			||||||
 | 
					    isPureAdmin: vi.fn(),
 | 
				
			||||||
 | 
					    isAdmin: vi.fn(),
 | 
				
			||||||
 | 
					    hasAuthorizations: vi.fn(),
 | 
				
			||||||
 | 
					    getUserDetails: vi.fn(),
 | 
				
			||||||
 | 
					    isAuthenticated: vi.fn(),
 | 
				
			||||||
 | 
					  } satisfies IAuthenticationService;
 | 
				
			||||||
 | 
					  let transition: Transition;
 | 
				
			||||||
 | 
					  const stateTo: StateDeclaration = {
 | 
				
			||||||
 | 
					    data: {
 | 
				
			||||||
 | 
					      access: 'restricted',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const $state = {
 | 
				
			||||||
 | 
					    target: vi.fn((t) => t),
 | 
				
			||||||
 | 
					  } as unknown as StateService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    authService = {
 | 
				
			||||||
 | 
					      init: vi.fn(),
 | 
				
			||||||
 | 
					      isPureAdmin: vi.fn(),
 | 
				
			||||||
 | 
					      isAdmin: vi.fn(),
 | 
				
			||||||
 | 
					      hasAuthorizations: vi.fn(),
 | 
				
			||||||
 | 
					      getUserDetails: vi.fn(),
 | 
				
			||||||
 | 
					      isAuthenticated: vi.fn(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    transition = {
 | 
				
			||||||
 | 
					      injector: vi.fn().mockReturnValue({
 | 
				
			||||||
 | 
					        get: vi.fn().mockReturnValue(authService),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      to: vi.fn().mockReturnValue(stateTo),
 | 
				
			||||||
 | 
					      router: {
 | 
				
			||||||
 | 
					        stateService: $state,
 | 
				
			||||||
 | 
					      } as Transition['router'],
 | 
				
			||||||
 | 
					    } as unknown as Transition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    stateTo.data.access = 'restricted';
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  afterEach(() => {
 | 
				
			||||||
 | 
					    vi.clearAllMocks();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should return undefined if access is not defined', async () => {
 | 
				
			||||||
 | 
					    stateTo.data.access = undefined;
 | 
				
			||||||
 | 
					    const result = await checkAuthorizations(transition);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result).toBeUndefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should return undefined if user is not authenticated and route access is defined', async () => {
 | 
				
			||||||
 | 
					    stateTo.data.access = 'something';
 | 
				
			||||||
 | 
					    authService.init.mockResolvedValue(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = await checkAuthorizations(transition);
 | 
				
			||||||
 | 
					    expect(result).toBeUndefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should return logout if access is "restricted"', async () => {
 | 
				
			||||||
 | 
					    const result = await checkAuthorizations(transition);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result).toBeDefined();
 | 
				
			||||||
 | 
					    expect($state.target).toHaveBeenCalledWith('portainer.logout');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should return undefined if user is an admin and access is "admin"', async () => {
 | 
				
			||||||
 | 
					    authService.init.mockResolvedValue(true);
 | 
				
			||||||
 | 
					    authService.isPureAdmin.mockReturnValue(true);
 | 
				
			||||||
 | 
					    stateTo.data.access = 'admin';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = await checkAuthorizations(transition);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result).toBeUndefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should return undefined if user is an admin and access is "edge-admin"', async () => {
 | 
				
			||||||
 | 
					    authService.init.mockResolvedValue(true);
 | 
				
			||||||
 | 
					    authService.isAdmin.mockReturnValue(true);
 | 
				
			||||||
 | 
					    stateTo.data.access = 'edge-admin';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = await checkAuthorizations(transition);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result).toBeUndefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should return undefined if user has the required authorizations', async () => {
 | 
				
			||||||
 | 
					    authService.init.mockResolvedValue(true);
 | 
				
			||||||
 | 
					    authService.hasAuthorizations.mockReturnValue(true);
 | 
				
			||||||
 | 
					    stateTo.data.access = ['permission1', 'permission2'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = await checkAuthorizations(transition);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result).toBeUndefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should redirect to home if user does not have the required authorizations', async () => {
 | 
				
			||||||
 | 
					    authService.init.mockResolvedValue(true);
 | 
				
			||||||
 | 
					    authService.hasAuthorizations.mockReturnValue(false);
 | 
				
			||||||
 | 
					    stateTo.data.access = ['permission1', 'permission2'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const result = await checkAuthorizations(transition);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(result).toBeDefined();
 | 
				
			||||||
 | 
					    expect($state.target).toHaveBeenCalledWith('portainer.home');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,99 @@
 | 
				
			||||||
 | 
					import { Transition, TransitionService } from '@uirouter/angularjs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { IAuthenticationService } from './services/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum AccessHeaders {
 | 
				
			||||||
 | 
					  Restricted = 'restricted',
 | 
				
			||||||
 | 
					  Admin = 'admin',
 | 
				
			||||||
 | 
					  EdgeAdmin = 'edge-admin',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Authorizations = string[];
 | 
				
			||||||
 | 
					type Access =
 | 
				
			||||||
 | 
					  | AccessHeaders.Restricted
 | 
				
			||||||
 | 
					  | AccessHeaders.Admin
 | 
				
			||||||
 | 
					  | AccessHeaders.EdgeAdmin
 | 
				
			||||||
 | 
					  | Authorizations;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function requiresAuthHook(transitionService: TransitionService) {
 | 
				
			||||||
 | 
					  transitionService.onBefore({}, checkAuthorizations);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// exported for tests
 | 
				
			||||||
 | 
					export async function checkAuthorizations(transition: Transition) {
 | 
				
			||||||
 | 
					  const authService: IAuthenticationService = transition
 | 
				
			||||||
 | 
					    .injector()
 | 
				
			||||||
 | 
					    .get('Authentication');
 | 
				
			||||||
 | 
					  const stateTo = transition.to();
 | 
				
			||||||
 | 
					  const $state = transition.router.stateService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { access } = stateTo.data || {};
 | 
				
			||||||
 | 
					  if (!isAccess(access)) {
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isLoggedIn = await authService.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!isLoggedIn) {
 | 
				
			||||||
 | 
					    // eslint-disable-next-line no-console
 | 
				
			||||||
 | 
					    console.info(
 | 
				
			||||||
 | 
					      'User is not authenticated, redirecting to login, access:',
 | 
				
			||||||
 | 
					      access
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return $state.target('portainer.logout');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (typeof access === 'string') {
 | 
				
			||||||
 | 
					    if (access === 'restricted') {
 | 
				
			||||||
 | 
					      return undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (access === 'admin') {
 | 
				
			||||||
 | 
					      if (authService.isPureAdmin()) {
 | 
				
			||||||
 | 
					        return undefined;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // eslint-disable-next-line no-console
 | 
				
			||||||
 | 
					      console.info(
 | 
				
			||||||
 | 
					        'User is not an admin, redirecting to home, access:',
 | 
				
			||||||
 | 
					        access
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return $state.target('portainer.home');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (access === 'edge-admin') {
 | 
				
			||||||
 | 
					      if (authService.isAdmin()) {
 | 
				
			||||||
 | 
					        return undefined;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // eslint-disable-next-line no-console
 | 
				
			||||||
 | 
					      console.info(
 | 
				
			||||||
 | 
					        'User is not an edge admin, redirecting to home, access:',
 | 
				
			||||||
 | 
					        access
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return $state.target('portainer.home');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (access.length > 0 && !authService.hasAuthorizations(access)) {
 | 
				
			||||||
 | 
					    // eslint-disable-next-line no-console
 | 
				
			||||||
 | 
					    console.info(
 | 
				
			||||||
 | 
					      'User does not have the required authorizations, redirecting to home'
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return $state.target('portainer.home');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isAccess(access: unknown): access is Access {
 | 
				
			||||||
 | 
					  if (!access || (typeof access !== 'string' && !Array.isArray(access))) {
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (Array.isArray(access)) {
 | 
				
			||||||
 | 
					    return access.every((a) => typeof a === 'string');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return ['restricted', 'admin', 'edge-admin'].includes(access);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					import { AccessHeaders } from '../authorization-guard';
 | 
				
			||||||
import { rolesView } from './views/roles';
 | 
					import { rolesView } from './views/roles';
 | 
				
			||||||
import { accessViewer } from './components/access-viewer';
 | 
					import { accessViewer } from './components/access-viewer';
 | 
				
			||||||
import { accessViewerDatatable } from './components/access-viewer/access-viewer-datatable';
 | 
					import { accessViewerDatatable } from './components/access-viewer/access-viewer-datatable';
 | 
				
			||||||
| 
						 | 
					@ -29,6 +30,7 @@ function config($stateRegistryProvider) {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data: {
 | 
					    data: {
 | 
				
			||||||
      docs: '/admin/users/roles',
 | 
					      docs: '/admin/users/roles',
 | 
				
			||||||
 | 
					      access: AccessHeaders.Admin,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ import { r2a } from '@/react-tools/react2angular';
 | 
				
			||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
 | 
					import { withCurrentUser } from '@/react-tools/withCurrentUser';
 | 
				
			||||||
import { withReactQuery } from '@/react-tools/withReactQuery';
 | 
					import { withReactQuery } from '@/react-tools/withReactQuery';
 | 
				
			||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
 | 
					import { withUIRouter } from '@/react-tools/withUIRouter';
 | 
				
			||||||
 | 
					import { AccessHeaders } from '@/portainer/authorization-guard';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const teamsModule = angular
 | 
					export const teamsModule = angular
 | 
				
			||||||
  .module('portainer.app.teams', [])
 | 
					  .module('portainer.app.teams', [])
 | 
				
			||||||
| 
						 | 
					@ -31,6 +32,7 @@ function config($stateRegistryProvider: StateRegistry) {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data: {
 | 
					    data: {
 | 
				
			||||||
      docs: '/admin/users/teams',
 | 
					      docs: '/admin/users/teams',
 | 
				
			||||||
 | 
					      access: AccessHeaders.Restricted, // allow for team leaders
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ import {
 | 
				
			||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
 | 
					import { withCurrentUser } from '@/react-tools/withCurrentUser';
 | 
				
			||||||
import { withReactQuery } from '@/react-tools/withReactQuery';
 | 
					import { withReactQuery } from '@/react-tools/withReactQuery';
 | 
				
			||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
 | 
					import { withUIRouter } from '@/react-tools/withUIRouter';
 | 
				
			||||||
 | 
					import { AccessHeaders } from '@/portainer/authorization-guard';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const wizardModule = angular
 | 
					export const wizardModule = angular
 | 
				
			||||||
  .module('portainer.app.react.views.wizard', [])
 | 
					  .module('portainer.app.react.views.wizard', [])
 | 
				
			||||||
| 
						 | 
					@ -42,6 +43,9 @@ function config($stateRegistryProvider: StateRegistry) {
 | 
				
			||||||
        component: 'wizardMainView',
 | 
					        component: 'wizardMainView',
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    data: {
 | 
				
			||||||
 | 
					      access: AccessHeaders.Admin,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $stateRegistryProvider.register({
 | 
					  $stateRegistryProvider.register({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,34 +1,40 @@
 | 
				
			||||||
 | 
					import { hasAuthorizations as useUserHasAuthorization } from '@/react/hooks/useUser';
 | 
				
			||||||
import { getCurrentUser } from '../users/queries/useLoadCurrentUser';
 | 
					import { getCurrentUser } from '../users/queries/useLoadCurrentUser';
 | 
				
			||||||
import * as userHelpers from '../users/user.helpers';
 | 
					import * as userHelpers from '../users/user.helpers';
 | 
				
			||||||
import { clear as clearSessionStorage } from './session-storage';
 | 
					import { clear as clearSessionStorage } from './session-storage';
 | 
				
			||||||
 | 
					 | 
				
			||||||
const DEFAULT_USER = 'admin';
 | 
					const DEFAULT_USER = 'admin';
 | 
				
			||||||
const DEFAULT_PASSWORD = 'K7yJPP5qNK4hf1QsRnfV';
 | 
					const DEFAULT_PASSWORD = 'K7yJPP5qNK4hf1QsRnfV';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
angular.module('portainer.app').factory('Authentication', [
 | 
					angular.module('portainer.app').factory('Authentication', [
 | 
				
			||||||
  '$async',
 | 
					  '$async',
 | 
				
			||||||
 | 
					  '$state',
 | 
				
			||||||
  'Auth',
 | 
					  'Auth',
 | 
				
			||||||
  'OAuth',
 | 
					  'OAuth',
 | 
				
			||||||
  'LocalStorage',
 | 
					  'LocalStorage',
 | 
				
			||||||
  'StateManager',
 | 
					  'StateManager',
 | 
				
			||||||
  'EndpointProvider',
 | 
					  'EndpointProvider',
 | 
				
			||||||
  'ThemeManager',
 | 
					  'ThemeManager',
 | 
				
			||||||
  function AuthenticationFactory($async, Auth, OAuth, LocalStorage, StateManager, EndpointProvider, ThemeManager) {
 | 
					  function AuthenticationFactory($async, $state, Auth, OAuth, LocalStorage, StateManager, EndpointProvider, ThemeManager) {
 | 
				
			||||||
    'use strict';
 | 
					    'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var service = {};
 | 
					 | 
				
			||||||
    var user = {};
 | 
					    var user = {};
 | 
				
			||||||
 | 
					    if (process.env.NODE_ENV === 'development') {
 | 
				
			||||||
 | 
					      window.login = loginAsync;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    service.init = init;
 | 
					    return {
 | 
				
			||||||
    service.OAuthLogin = OAuthLogin;
 | 
					      init,
 | 
				
			||||||
    service.login = login;
 | 
					      OAuthLogin,
 | 
				
			||||||
    service.logout = logout;
 | 
					      login,
 | 
				
			||||||
    service.isAuthenticated = isAuthenticated;
 | 
					      logout,
 | 
				
			||||||
    service.getUserDetails = getUserDetails;
 | 
					      isAuthenticated,
 | 
				
			||||||
    service.isAdmin = isAdmin;
 | 
					      getUserDetails,
 | 
				
			||||||
    service.isEdgeAdmin = isEdgeAdmin;
 | 
					      isAdmin,
 | 
				
			||||||
    service.isPureAdmin = isPureAdmin;
 | 
					      isEdgeAdmin,
 | 
				
			||||||
    service.hasAuthorizations = hasAuthorizations;
 | 
					      isPureAdmin,
 | 
				
			||||||
 | 
					      hasAuthorizations,
 | 
				
			||||||
 | 
					      redirectIfUnauthorized,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function initAsync() {
 | 
					    async function initAsync() {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
| 
						 | 
					@ -126,6 +132,7 @@ angular.module('portainer.app').factory('Authentication', [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // To avoid creating divergence between CE and EE
 | 
					    // To avoid creating divergence between CE and EE
 | 
				
			||||||
    // isAdmin checks if the user is a portainer admin or edge admin
 | 
					    // isAdmin checks if the user is a portainer admin or edge admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function isEdgeAdmin() {
 | 
					    function isEdgeAdmin() {
 | 
				
			||||||
      const environment = EndpointProvider.currentEndpoint();
 | 
					      const environment = EndpointProvider.currentEndpoint();
 | 
				
			||||||
      return userHelpers.isEdgeAdmin({ Role: user.role }, environment);
 | 
					      return userHelpers.isEdgeAdmin({ Role: user.role }, environment);
 | 
				
			||||||
| 
						 | 
					@ -146,20 +153,25 @@ angular.module('portainer.app').factory('Authentication', [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function hasAuthorizations(authorizations) {
 | 
					    function hasAuthorizations(authorizations) {
 | 
				
			||||||
      const endpointId = EndpointProvider.endpointID();
 | 
					      const endpointId = EndpointProvider.endpointID();
 | 
				
			||||||
      if (isAdmin()) {
 | 
					
 | 
				
			||||||
 | 
					      if (isEdgeAdmin()) {
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (!user.endpointAuthorizations || !user.endpointAuthorizations[endpointId]) {
 | 
					
 | 
				
			||||||
        return false;
 | 
					      return useUserHasAuthorization(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          EndpointAuthorizations: user.endpointAuthorizations,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        authorizations,
 | 
				
			||||||
 | 
					        endpointId
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function redirectIfUnauthorized(authorizations) {
 | 
				
			||||||
 | 
					      const authorized = hasAuthorizations(authorizations);
 | 
				
			||||||
 | 
					      if (!authorized) {
 | 
				
			||||||
 | 
					        $state.go('portainer.home');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const userEndpointAuthorizations = user.endpointAuthorizations[endpointId];
 | 
					 | 
				
			||||||
      return authorizations.some((authorization) => userEndpointAuthorizations[authorization]);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (process.env.NODE_ENV === 'development') {
 | 
					 | 
				
			||||||
      window.login = loginAsync;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return service;
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,17 @@ export interface StateManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IAuthenticationService {
 | 
					export interface IAuthenticationService {
 | 
				
			||||||
  getUserDetails(): { ID: number };
 | 
					  getUserDetails(): { ID: number };
 | 
				
			||||||
 | 
					  isAuthenticated(): boolean;
 | 
				
			||||||
 | 
					  isAdmin(): boolean;
 | 
				
			||||||
 | 
					  isPureAdmin(): boolean;
 | 
				
			||||||
 | 
					  hasAuthorizations(authorizations: string[]): boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  init(): Promise<boolean>;
 | 
				
			||||||
 | 
					  //     OAuthLogin,
 | 
				
			||||||
 | 
					  //     login,
 | 
				
			||||||
 | 
					  //     logout,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //     redirectIfUnauthorized,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AsyncService = <T>(fn: () => Promise<T>) => Promise<T>;
 | 
					export type AsyncService = <T>(fn: () => Promise<T>) => Promise<T>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import angular from 'angular';
 | 
					import angular from 'angular';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { NotificationsViewAngular } from '@/react/portainer/notifications/NotificationsView';
 | 
					import { NotificationsViewAngular } from '@/react/portainer/notifications/NotificationsView';
 | 
				
			||||||
 | 
					import { AccessHeaders } from '../authorization-guard';
 | 
				
			||||||
import authLogsViewModule from './auth-logs-view';
 | 
					import authLogsViewModule from './auth-logs-view';
 | 
				
			||||||
import activityLogsViewModule from './activity-logs-view';
 | 
					import activityLogsViewModule from './activity-logs-view';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +19,7 @@ function config($stateRegistryProvider) {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data: {
 | 
					    data: {
 | 
				
			||||||
      docs: '/admin/logs',
 | 
					      docs: '/admin/logs',
 | 
				
			||||||
 | 
					      access: AccessHeaders.Admin,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,6 +33,7 @@ function config($stateRegistryProvider) {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data: {
 | 
					    data: {
 | 
				
			||||||
      docs: '/admin/logs/activity',
 | 
					      docs: '/admin/logs/activity',
 | 
				
			||||||
 | 
					      access: AccessHeaders.Admin,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import { isEdgeEnvironment } from '@/react/portainer/environments/utils';
 | 
				
			||||||
import { Role, User } from './types';
 | 
					import { Role, User } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function filterNonAdministratorUsers(users: User[]) {
 | 
					export function filterNonAdministratorUsers(users: User[]) {
 | 
				
			||||||
  return users.filter((user) => user.Role !== Role.Admin);
 | 
					  return users.filter((user) => !isPureAdmin(user));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UserLike = Pick<User, 'Role'>;
 | 
					type UserLike = Pick<User, 'Role'>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -108,6 +108,8 @@ async function renderComponent(
 | 
				
			||||||
  const state = { user };
 | 
					  const state = { user };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  server.use(
 | 
					  server.use(
 | 
				
			||||||
 | 
					    http.get('/api/endpoints/1', () => HttpResponse.json({})),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    http.get('/api/endpoints/:endpointId/azure/subscriptions', () =>
 | 
					    http.get('/api/endpoints/:endpointId/azure/subscriptions', () =>
 | 
				
			||||||
      HttpResponse.json(createMockSubscriptions(subscriptionsCount), {
 | 
					      HttpResponse.json(createMockSubscriptions(subscriptionsCount), {
 | 
				
			||||||
        status: subscriptionsStatus,
 | 
					        status: subscriptionsStatus,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,10 @@
 | 
				
			||||||
import userEvent from '@testing-library/user-event';
 | 
					import userEvent from '@testing-library/user-event';
 | 
				
			||||||
 | 
					import { HttpResponse } from 'msw';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { UserContext } from '@/react/hooks/useUser';
 | 
					import { UserContext } from '@/react/hooks/useUser';
 | 
				
			||||||
import { UserViewModel } from '@/portainer/models/user';
 | 
					import { UserViewModel } from '@/portainer/models/user';
 | 
				
			||||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
 | 
					import { renderWithQueryClient } from '@/react-tools/test-utils';
 | 
				
			||||||
 | 
					import { http, server } from '@/setup-tests/server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CreateContainerInstanceForm } from './CreateContainerInstanceForm';
 | 
					import { CreateContainerInstanceForm } from './CreateContainerInstanceForm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +16,8 @@ vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('submit button should be disabled when name or image is missing', async () => {
 | 
					test('submit button should be disabled when name or image is missing', async () => {
 | 
				
			||||||
 | 
					  server.use(http.get('/api/endpoints/5', () => HttpResponse.json({})));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const user = new UserViewModel({ Username: 'user' });
 | 
					  const user = new UserViewModel({ Username: 'user' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { findByText, getByText, getByLabelText } = renderWithQueryClient(
 | 
					  const { findByText, getByText, getByLabelText } = renderWithQueryClient(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,9 @@
 | 
				
			||||||
 | 
					import { HttpResponse } from 'msw';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
 | 
					import { renderWithQueryClient } from '@/react-tools/test-utils';
 | 
				
			||||||
import { UserContext } from '@/react/hooks/useUser';
 | 
					import { UserContext } from '@/react/hooks/useUser';
 | 
				
			||||||
import { UserViewModel } from '@/portainer/models/user';
 | 
					import { UserViewModel } from '@/portainer/models/user';
 | 
				
			||||||
 | 
					import { http, server } from '@/setup-tests/server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { NetworkContainer } from '../types';
 | 
					import { NetworkContainer } from '../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +29,8 @@ vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('Network container values should be visible and the link should be valid', async () => {
 | 
					test('Network container values should be visible and the link should be valid', async () => {
 | 
				
			||||||
 | 
					  server.use(http.get('/api/endpoints/1', () => HttpResponse.json({})));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const user = new UserViewModel({ Username: 'test', Role: 1 });
 | 
					  const user = new UserViewModel({ Username: 'test', Role: 1 });
 | 
				
			||||||
  const { findByText } = renderWithQueryClient(
 | 
					  const { findByText } = renderWithQueryClient(
 | 
				
			||||||
    <UserContext.Provider value={{ user }}>
 | 
					    <UserContext.Provider value={{ user }}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,9 @@
 | 
				
			||||||
 | 
					import { HttpResponse, http } from 'msw';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
 | 
					import { renderWithQueryClient } from '@/react-tools/test-utils';
 | 
				
			||||||
import { UserContext } from '@/react/hooks/useUser';
 | 
					import { UserContext } from '@/react/hooks/useUser';
 | 
				
			||||||
import { UserViewModel } from '@/portainer/models/user';
 | 
					import { UserViewModel } from '@/portainer/models/user';
 | 
				
			||||||
 | 
					import { server } from '@/setup-tests/server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { DockerNetwork } from '../types';
 | 
					import { DockerNetwork } from '../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,6 +51,8 @@ test('Non system networks should have a delete button', async () => {
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
 | 
					async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
 | 
				
			||||||
 | 
					  server.use(http.get('/api/endpoints/1', () => HttpResponse.json({})));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
 | 
					  const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const queries = renderWithQueryClient(
 | 
					  const queries = renderWithQueryClient(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,8 +17,10 @@ test('renders TemplateSelector component', async () => {
 | 
				
			||||||
  expect(templateSelectorElement).toBeInTheDocument();
 | 
					  expect(templateSelectorElement).toBeInTheDocument();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO skipped select tests because the tests take too long to run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line vitest/expect-expect
 | 
					// eslint-disable-next-line vitest/expect-expect
 | 
				
			||||||
test('selects an edge app template', async () => {
 | 
					test.skip('selects an edge app template', async () => {
 | 
				
			||||||
  const onChange = vi.fn();
 | 
					  const onChange = vi.fn();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const selectedTemplate = {
 | 
					  const selectedTemplate = {
 | 
				
			||||||
| 
						 | 
					@ -48,7 +50,7 @@ test('selects an edge app template', async () => {
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line vitest/expect-expect
 | 
					// eslint-disable-next-line vitest/expect-expect
 | 
				
			||||||
test('selects an edge custom template', async () => {
 | 
					test.skip('selects an edge custom template', async () => {
 | 
				
			||||||
  const onChange = vi.fn();
 | 
					  const onChange = vi.fn();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const selectedTemplate = {
 | 
					  const selectedTemplate = {
 | 
				
			||||||
| 
						 | 
					@ -84,7 +86,7 @@ test('renders with error', async () => {
 | 
				
			||||||
  expect(errorElement).toBeInTheDocument();
 | 
					  expect(errorElement).toBeInTheDocument();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('renders TemplateSelector component with no custom templates available', async () => {
 | 
					test.skip('renders TemplateSelector component with no custom templates available', async () => {
 | 
				
			||||||
  render({
 | 
					  render({
 | 
				
			||||||
    customTemplates: [],
 | 
					    customTemplates: [],
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,28 +0,0 @@
 | 
				
			||||||
import { RawParams, useRouter } from '@uirouter/react';
 | 
					 | 
				
			||||||
import { useEffect } from 'react';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { useCurrentUser } from './useUser';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type RedirectOptions = {
 | 
					 | 
				
			||||||
  to: string;
 | 
					 | 
				
			||||||
  params?: RawParams;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Redirects to the given route if the user is not a Portainer admin.
 | 
					 | 
				
			||||||
 * @param to The route to redirect to (default is `'portainer.home'`).
 | 
					 | 
				
			||||||
 * @param params The params to pass to the route.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export function useAdminOnlyRedirect(
 | 
					 | 
				
			||||||
  { to, params }: RedirectOptions = { to: 'portainer.home' }
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
  const router = useRouter();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const { isAdmin } = useCurrentUser();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    if (!isAdmin) {
 | 
					 | 
				
			||||||
      router.stateService.go(to, params);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [isAdmin, to, params, router.stateService]);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -138,11 +138,6 @@ export function useIsEnvironmentAdmin({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * will return true if the user has the authorizations. assumes the user is authenticated and not an admin
 | 
					 * will return true if the user has the authorizations. assumes the user is authenticated and not an admin
 | 
				
			||||||
 * @param user
 | 
					 | 
				
			||||||
 * @param authorizations
 | 
					 | 
				
			||||||
 * @param environmentId
 | 
					 | 
				
			||||||
 * @param adminOnlyCE
 | 
					 | 
				
			||||||
 * @returns
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function hasAuthorizations(
 | 
					function hasAuthorizations(
 | 
				
			||||||
  user: User,
 | 
					  user: User,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,12 +4,12 @@ import { InsightsBox } from '@@/InsightsBox';
 | 
				
			||||||
import { Link } from '@@/Link';
 | 
					import { Link } from '@@/Link';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function StackNameLabelInsight() {
 | 
					export function StackNameLabelInsight() {
 | 
				
			||||||
  const { isAdmin } = useCurrentUser();
 | 
					  const { isPureAdmin } = useCurrentUser();
 | 
				
			||||||
  const insightsBoxContent = (
 | 
					  const insightsBoxContent = (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      The stack field below was previously labelled 'Name' but, in
 | 
					      The stack field below was previously labelled 'Name' but, in
 | 
				
			||||||
      fact, it's always been the stack name (hence the relabelling).
 | 
					      fact, it's always been the stack name (hence the relabelling).
 | 
				
			||||||
      {isAdmin && (
 | 
					      {isPureAdmin && (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          <br />
 | 
					          <br />
 | 
				
			||||||
          Kubernetes Stacks functionality can be turned off entirely via{' '}
 | 
					          Kubernetes Stacks functionality can be turned off entirely via{' '}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,6 @@ import _ from 'lodash';
 | 
				
			||||||
import { Wand2 } from 'lucide-react';
 | 
					import { Wand2 } from 'lucide-react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
 | 
					import { useAnalytics } from '@/react/hooks/useAnalytics';
 | 
				
			||||||
import { useAdminOnlyRedirect } from '@/react/hooks/useAdminOnlyRedirect';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Button } from '@@/buttons';
 | 
					import { Button } from '@@/buttons';
 | 
				
			||||||
import { PageHeader } from '@@/PageHeader';
 | 
					import { PageHeader } from '@@/PageHeader';
 | 
				
			||||||
| 
						 | 
					@ -19,8 +18,6 @@ import {
 | 
				
			||||||
} from './environment-types';
 | 
					} from './environment-types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function EnvironmentTypeSelectView() {
 | 
					export function EnvironmentTypeSelectView() {
 | 
				
			||||||
  // TODO: move this redirect logic to the router when migrating the router to react
 | 
					 | 
				
			||||||
  useAdminOnlyRedirect();
 | 
					 | 
				
			||||||
  const [types, setTypes] = useState<EnvironmentOptionValue[]>([]);
 | 
					  const [types, setTypes] = useState<EnvironmentOptionValue[]>([]);
 | 
				
			||||||
  const { trackEvent } = useAnalytics();
 | 
					  const { trackEvent } = useAnalytics();
 | 
				
			||||||
  const router = useRouter();
 | 
					  const router = useRouter();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,6 @@ import {
 | 
				
			||||||
  EnvironmentId,
 | 
					  EnvironmentId,
 | 
				
			||||||
} from '@/react/portainer/environments/types';
 | 
					} from '@/react/portainer/environments/types';
 | 
				
			||||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
 | 
					import { useAnalytics } from '@/react/hooks/useAnalytics';
 | 
				
			||||||
import { useAdminOnlyRedirect } from '@/react/hooks/useAdminOnlyRedirect';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Stepper } from '@@/Stepper';
 | 
					import { Stepper } from '@@/Stepper';
 | 
				
			||||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
 | 
					import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
 | 
				
			||||||
| 
						 | 
					@ -33,8 +32,6 @@ import styles from './EnvironmentsCreationView.module.css';
 | 
				
			||||||
import { WizardEndpointsList } from './WizardEndpointsList';
 | 
					import { WizardEndpointsList } from './WizardEndpointsList';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function EnvironmentCreationView() {
 | 
					export function EnvironmentCreationView() {
 | 
				
			||||||
  // TODO: move this redirect logic to the router when migrating the router to react
 | 
					 | 
				
			||||||
  useAdminOnlyRedirect();
 | 
					 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    params: { localEndpointId: localEndpointIdParam },
 | 
					    params: { localEndpointId: localEndpointIdParam },
 | 
				
			||||||
  } = useCurrentStateAndParams();
 | 
					  } = useCurrentStateAndParams();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,6 @@ import { EnvironmentType } from '@/react/portainer/environments/types';
 | 
				
			||||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
 | 
					import { useAnalytics } from '@/react/hooks/useAnalytics';
 | 
				
			||||||
import DockerIcon from '@/assets/ico/vendor/docker-icon.svg?c';
 | 
					import DockerIcon from '@/assets/ico/vendor/docker-icon.svg?c';
 | 
				
			||||||
import Kube from '@/assets/ico/kube.svg?c';
 | 
					import Kube from '@/assets/ico/kube.svg?c';
 | 
				
			||||||
import { useAdminOnlyRedirect } from '@/react/hooks/useAdminOnlyRedirect';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { PageHeader } from '@@/PageHeader';
 | 
					import { PageHeader } from '@@/PageHeader';
 | 
				
			||||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
 | 
					import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
 | 
				
			||||||
| 
						 | 
					@ -16,8 +15,6 @@ import { useConnectLocalEnvironment } from './useFetchOrCreateLocalEnvironment';
 | 
				
			||||||
import styles from './HomeView.module.css';
 | 
					import styles from './HomeView.module.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function HomeView() {
 | 
					export function HomeView() {
 | 
				
			||||||
  // TODO: move this redirect logic to the router when migrating the router to react
 | 
					 | 
				
			||||||
  useAdminOnlyRedirect();
 | 
					 | 
				
			||||||
  const localEnvironmentAdded = useConnectLocalEnvironment();
 | 
					  const localEnvironmentAdded = useConnectLocalEnvironment();
 | 
				
			||||||
  const { trackEvent } = useAnalytics();
 | 
					  const { trackEvent } = useAnalytics();
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue