mirror of https://github.com/apache/nifi.git
NIFI-12968: Simplify login sequence (#8843)
* NIFI-12968: - Remove usage of Access-Token-Expiration - No longer attempt SPNEGO auth - Leverage authentication configuration to drive log in/out URIs - Remove Login/Logout servlet filters - Remove usage of access configuration and access status - Fixing broken unit tests * NIFI-12968: - Only rendering the user identity when the user is not anonymous. - Fixing an issue where the fallback route would render when redirecting the user to an external SSO log in. - Using the login supported flag to render the log in link. * NIFI-12968: - Addressing review feedback. This closes #8843
This commit is contained in:
parent
3f9ef07e3c
commit
3a78575b9a
|
@ -1,87 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.apache.nifi.web.filter;
|
|
||||||
|
|
||||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.FilterConfig;
|
|
||||||
import jakarta.servlet.ServletContext;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter for determining appropriate login location.
|
|
||||||
*/
|
|
||||||
public class LoginFilter implements Filter {
|
|
||||||
private static final String OAUTH2_AUTHORIZATION_PATH = "/nifi-api/oauth2/authorization/consumer";
|
|
||||||
|
|
||||||
private static final String SAML2_AUTHENTICATE_FILTER_PATH = "/nifi-api/saml2/authenticate/consumer";
|
|
||||||
|
|
||||||
private static final String KNOX_REQUEST_PATH = "/nifi-api/access/knox/request";
|
|
||||||
|
|
||||||
private static final String NIFI_LOGIN_PATH = "/nf/";
|
|
||||||
private static final String NIFI_LOGIN_FRAGMENT = "/login";
|
|
||||||
|
|
||||||
private ServletContext servletContext;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig filterConfig) throws ServletException {
|
|
||||||
servletContext = filterConfig.getServletContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
|
|
||||||
final boolean supportsOidc = Boolean.parseBoolean(servletContext.getInitParameter("oidc-supported"));
|
|
||||||
final boolean supportsKnoxSso = Boolean.parseBoolean(servletContext.getInitParameter("knox-supported"));
|
|
||||||
final boolean supportsSAML = Boolean.parseBoolean(servletContext.getInitParameter("saml-supported"));
|
|
||||||
|
|
||||||
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
final RequestUriBuilder requestUriBuilder = RequestUriBuilder.fromHttpServletRequest(httpServletRequest);
|
|
||||||
|
|
||||||
if (supportsKnoxSso) {
|
|
||||||
final URI redirectUri = requestUriBuilder.path(KNOX_REQUEST_PATH).build();
|
|
||||||
sendRedirect(response, redirectUri);
|
|
||||||
} else if (supportsOidc) {
|
|
||||||
final URI redirectUri = requestUriBuilder.path(OAUTH2_AUTHORIZATION_PATH).build();
|
|
||||||
// Redirect to authorization URL defined in Spring Security OAuth2AuthorizationRequestRedirectFilter
|
|
||||||
sendRedirect(response, redirectUri);
|
|
||||||
} else if (supportsSAML) {
|
|
||||||
final URI redirectUri = requestUriBuilder.path(SAML2_AUTHENTICATE_FILTER_PATH).build();
|
|
||||||
// Redirect to request consumer URL defined in Spring Security OpenSamlAuthenticationRequestResolver.requestMatcher
|
|
||||||
sendRedirect(response, redirectUri);
|
|
||||||
} else {
|
|
||||||
final URI redirectUri = requestUriBuilder.path(NIFI_LOGIN_PATH).fragment(NIFI_LOGIN_FRAGMENT).build();
|
|
||||||
sendRedirect(response, redirectUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() {
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendRedirect(final ServletResponse response, final URI redirectUri) throws IOException {
|
|
||||||
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
|
||||||
httpServletResponse.sendRedirect(redirectUri.toString());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package org.apache.nifi.web.filter;
|
|
||||||
|
|
||||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.FilterConfig;
|
|
||||||
import jakarta.servlet.ServletContext;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter for determining appropriate logout location.
|
|
||||||
*/
|
|
||||||
public class LogoutFilter implements Filter {
|
|
||||||
|
|
||||||
private static final String OIDC_LOGOUT_URL = "/nifi-api/access/oidc/logout";
|
|
||||||
|
|
||||||
private static final String SAML_LOCAL_LOGOUT_URL = "/nifi-api/access/saml/local-logout/request";
|
|
||||||
|
|
||||||
private static final String SAML_SINGLE_LOGOUT_URL = "/nifi-api/access/saml/single-logout/request";
|
|
||||||
|
|
||||||
private static final String KNOX_LOGOUT_URL = "/nifi-api/access/knox/logout";
|
|
||||||
|
|
||||||
private static final String LOGOUT_COMPLETE_URL = "/nifi-api/access/logout/complete";
|
|
||||||
|
|
||||||
private ServletContext servletContext;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig filterConfig) throws ServletException {
|
|
||||||
servletContext = filterConfig.getServletContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
|
|
||||||
final boolean supportsOidc = Boolean.parseBoolean(servletContext.getInitParameter("oidc-supported"));
|
|
||||||
final boolean supportsKnoxSso = Boolean.parseBoolean(servletContext.getInitParameter("knox-supported"));
|
|
||||||
final boolean supportsSaml = Boolean.parseBoolean(servletContext.getInitParameter("saml-supported"));
|
|
||||||
final boolean supportsSamlSingleLogout = Boolean.parseBoolean(servletContext.getInitParameter("saml-single-logout-supported"));
|
|
||||||
|
|
||||||
// NOTE: This filter runs in the web-ui module and is bound to /nifi/logout. Currently the front-end first makes an ajax call
|
|
||||||
// to issue a DELETE to /nifi-api/access/logout. After successful completion it sets the browser location to /nifi/logout
|
|
||||||
// which triggers this filter. Since this request was made from setting window.location, the JWT will never be sent which
|
|
||||||
// means there will be no logged in user or Authorization header when forwarding to any of the URLs below. Instead the
|
|
||||||
// /access/logout end-point sets a Cookie with a logout request identifier which can be used by the end-points below
|
|
||||||
// to retrieve information about the user logging out.
|
|
||||||
|
|
||||||
if (supportsOidc) {
|
|
||||||
sendRedirect(OIDC_LOGOUT_URL, request, response);
|
|
||||||
} else if (supportsKnoxSso) {
|
|
||||||
sendRedirect(KNOX_LOGOUT_URL, request, response);
|
|
||||||
} else if (supportsSaml) {
|
|
||||||
final String logoutUrl = supportsSamlSingleLogout ? SAML_SINGLE_LOGOUT_URL : SAML_LOCAL_LOGOUT_URL;
|
|
||||||
sendRedirect(logoutUrl, request, response);
|
|
||||||
} else {
|
|
||||||
sendRedirect(LOGOUT_COMPLETE_URL, request, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() {
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendRedirect(final String logoutUrl, final ServletRequest request, final ServletResponse response) throws IOException {
|
|
||||||
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
|
||||||
final URI targetUri = RequestUriBuilder.fromHttpServletRequest(httpServletRequest).path(logoutUrl).build();
|
|
||||||
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
|
||||||
httpServletResponse.sendRedirect(targetUri.toString());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,10 +19,15 @@ import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { authenticationGuard } from './service/guard/authentication.guard';
|
import { authenticationGuard } from './service/guard/authentication.guard';
|
||||||
import { RouteNotFound } from './pages/route-not-found/feature/route-not-found.component';
|
import { RouteNotFound } from './pages/route-not-found/feature/route-not-found.component';
|
||||||
|
import { checkLoginConfiguration } from './service/guard/login-configuration.guard';
|
||||||
|
import { LoginConfiguration } from './state/login-configuration';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
|
canMatch: [
|
||||||
|
checkLoginConfiguration((loginConfiguration: LoginConfiguration) => loginConfiguration.loginSupported)
|
||||||
|
],
|
||||||
loadChildren: () => import('./pages/login/feature/login.module').then((m) => m.LoginModule)
|
loadChildren: () => import('./pages/login/feature/login.module').then((m) => m.LoginModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,15 +24,14 @@ import { StoreModule } from '@ngrx/store';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||||
import { environment } from './environments/environment';
|
import { environment } from './environments/environment';
|
||||||
import { HTTP_INTERCEPTORS, HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
|
import { HttpClientModule, HttpClientXsrfModule, provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { NavigationActionTiming, RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { NavigationActionTiming, RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { rootReducers } from './state';
|
import { rootReducers } from './state';
|
||||||
import { CurrentUserEffects } from './state/current-user/current-user.effects';
|
import { CurrentUserEffects } from './state/current-user/current-user.effects';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { LoadingInterceptor } from './service/interceptors/loading.interceptor';
|
import { authInterceptor } from './service/interceptors/auth.interceptor';
|
||||||
import { AuthInterceptor } from './service/interceptors/auth.interceptor';
|
|
||||||
import { ExtensionTypesEffects } from './state/extension-types/extension-types.effects';
|
import { ExtensionTypesEffects } from './state/extension-types/extension-types.effects';
|
||||||
import { PollingInterceptor } from './service/interceptors/polling.interceptor';
|
import { pollingInterceptor } from './service/interceptors/polling.interceptor';
|
||||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||||
import { MatNativeDateModule } from '@angular/material/core';
|
import { MatNativeDateModule } from '@angular/material/core';
|
||||||
import { AboutEffects } from './state/about/about.effects';
|
import { AboutEffects } from './state/about/about.effects';
|
||||||
|
@ -47,6 +46,8 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { PipesModule } from './pipes/pipes.module';
|
import { PipesModule } from './pipes/pipes.module';
|
||||||
import { DocumentationEffects } from './state/documentation/documentation.effects';
|
import { DocumentationEffects } from './state/documentation/documentation.effects';
|
||||||
import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.effects';
|
import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.effects';
|
||||||
|
import { loadingInterceptor } from './service/interceptors/loading.interceptor';
|
||||||
|
import { LoginConfigurationEffects } from './state/login-configuration/login-configuration.effects';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
|
@ -70,6 +71,7 @@ import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.e
|
||||||
ExtensionTypesEffects,
|
ExtensionTypesEffects,
|
||||||
AboutEffects,
|
AboutEffects,
|
||||||
FlowConfigurationEffects,
|
FlowConfigurationEffects,
|
||||||
|
LoginConfigurationEffects,
|
||||||
StatusHistoryEffects,
|
StatusHistoryEffects,
|
||||||
ControllerServiceStateEffects,
|
ControllerServiceStateEffects,
|
||||||
SystemDiagnosticsEffects,
|
SystemDiagnosticsEffects,
|
||||||
|
@ -89,22 +91,8 @@ import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.e
|
||||||
PipesModule
|
PipesModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } },
|
||||||
provide: HTTP_INTERCEPTORS,
|
provideHttpClient(withInterceptors([authInterceptor, loadingInterceptor, pollingInterceptor]))
|
||||||
useClass: LoadingInterceptor,
|
|
||||||
multi: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: AuthInterceptor,
|
|
||||||
multi: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: PollingInterceptor,
|
|
||||||
multi: true
|
|
||||||
},
|
|
||||||
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } }
|
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,18 +15,23 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { selectFullScreenError } from '../../../state/error/error.selectors';
|
import { selectFullScreenError } from '../../../state/error/error.selectors';
|
||||||
import { NiFiState } from '../../../state';
|
import { NiFiState } from '../../../state';
|
||||||
|
import { resetErrorState } from '../../../state/error/error.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'error',
|
selector: 'error',
|
||||||
templateUrl: './error.component.html',
|
templateUrl: './error.component.html',
|
||||||
styleUrls: ['./error.component.scss']
|
styleUrls: ['./error.component.scss']
|
||||||
})
|
})
|
||||||
export class Error {
|
export class Error implements OnDestroy {
|
||||||
errorDetail$ = this.store.select(selectFullScreenError);
|
errorDetail$ = this.store.select(selectFullScreenError);
|
||||||
|
|
||||||
constructor(private store: Store<NiFiState>) {}
|
constructor(private store: Store<NiFiState>) {}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.store.dispatch(resetErrorState());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,7 @@
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex gap-x-2 items-center">
|
<div class="flex gap-x-2 items-center">
|
||||||
<div class="logo flex flex-col items-center" [style.background-color]="color">
|
<div class="logo flex flex-col items-center" [style.background-color]="color">
|
||||||
<i
|
<i class="icon accent-color icon-processor p-2" [style.color]="contrastColor"></i>
|
||||||
class="icon accent-color icon-processor p-2"
|
|
||||||
[style.color]="contrastColor"></i>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<div class="context-name w-full">Processor Name</div>
|
<div class="context-name w-full">Processor Name</div>
|
||||||
|
|
|
@ -36,6 +36,8 @@ import { selectCurrentUser } from '../../../../../state/current-user/current-use
|
||||||
import * as fromUser from '../../../../../state/current-user/current-user.reducer';
|
import * as fromUser from '../../../../../state/current-user/current-user.reducer';
|
||||||
import { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
|
import { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
|
||||||
import * as fromFlowConfiguration from '../../../../../state/flow-configuration/flow-configuration.reducer';
|
import * as fromFlowConfiguration from '../../../../../state/flow-configuration/flow-configuration.reducer';
|
||||||
|
import { selectLoginConfiguration } from '../../../../../state/login-configuration/login-configuration.selectors';
|
||||||
|
import * as fromLoginConfiguration from '../../../../../state/login-configuration/login-configuration.reducer';
|
||||||
|
|
||||||
describe('HeaderComponent', () => {
|
describe('HeaderComponent', () => {
|
||||||
let component: HeaderComponent;
|
let component: HeaderComponent;
|
||||||
|
@ -120,6 +122,10 @@ describe('HeaderComponent', () => {
|
||||||
{
|
{
|
||||||
selector: selectFlowConfiguration,
|
selector: selectFlowConfiguration,
|
||||||
value: fromFlowConfiguration.initialState.flowConfiguration
|
value: fromFlowConfiguration.initialState.flowConfiguration
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: selectLoginConfiguration,
|
||||||
|
value: fromLoginConfiguration.initialState.loginConfiguration
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { EditLabel } from './edit-label.component';
|
import { EditLabel } from './edit-label.component';
|
||||||
import { EditComponentDialogRequest } from '../../../../../state/flow';
|
import { EditComponentDialogRequest } from '../../../../../state/flow';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { ComponentType } from '../../../../../../../state/shared';
|
import { ComponentType } from '../../../../../../../state/shared';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||||
|
@ -79,6 +79,10 @@ describe('EditLabel', () => {
|
||||||
useValue: {
|
useValue: {
|
||||||
isDisconnectionAcknowledged: jest.fn()
|
isDisconnectionAcknowledged: jest.fn()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { CreatePort } from './create-port.component';
|
import { CreatePort } from './create-port.component';
|
||||||
import { CreateComponentRequest } from '../../../../../state/flow';
|
import { CreateComponentRequest } from '../../../../../state/flow';
|
||||||
import { ComponentType } from '../../../../../../../state/shared';
|
import { ComponentType } from '../../../../../../../state/shared';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
@ -44,7 +44,14 @@ describe('CreatePort', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CreatePort, NoopAnimationsModule],
|
imports: [CreatePort, NoopAnimationsModule],
|
||||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
|
providers: [
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(CreatePort);
|
fixture = TestBed.createComponent(CreatePort);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { EditPort } from './edit-port.component';
|
import { EditPort } from './edit-port.component';
|
||||||
import { EditComponentDialogRequest } from '../../../../../state/flow';
|
import { EditComponentDialogRequest } from '../../../../../state/flow';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { ComponentType } from '../../../../../../../state/shared';
|
import { ComponentType } from '../../../../../../../state/shared';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||||
|
@ -105,6 +105,10 @@ describe('EditPort', () => {
|
||||||
useValue: {
|
useValue: {
|
||||||
isDisconnectionAcknowledged: jest.fn()
|
isDisconnectionAcknowledged: jest.fn()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { CreateProcessGroup } from './create-process-group.component';
|
import { CreateProcessGroup } from './create-process-group.component';
|
||||||
import { CreateProcessGroupDialogRequest } from '../../../../../state/flow';
|
import { CreateProcessGroupDialogRequest } from '../../../../../state/flow';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { ComponentType } from '../../../../../../../state/shared';
|
import { ComponentType } from '../../../../../../../state/shared';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||||
|
@ -156,7 +156,14 @@ describe('CreateProcessGroup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CreateProcessGroup, NoopAnimationsModule],
|
imports: [CreateProcessGroup, NoopAnimationsModule],
|
||||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
|
providers: [
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(CreateProcessGroup);
|
fixture = TestBed.createComponent(CreateProcessGroup);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { CreateProcessor } from './create-processor.component';
|
import { CreateProcessor } from './create-processor.component';
|
||||||
import { CreateProcessorDialogRequest } from '../../../../../state/flow';
|
import { CreateProcessorDialogRequest } from '../../../../../state/flow';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { initialState } from '../../../../../../../state/extension-types/extension-types.reducer';
|
import { initialState } from '../../../../../../../state/extension-types/extension-types.reducer';
|
||||||
import { ComponentType } from '../../../../../../../state/shared';
|
import { ComponentType } from '../../../../../../../state/shared';
|
||||||
|
@ -60,7 +60,14 @@ describe('CreateProcessor', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CreateProcessor, NoopAnimationsModule],
|
imports: [CreateProcessor, NoopAnimationsModule],
|
||||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
|
providers: [
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(CreateProcessor);
|
fixture = TestBed.createComponent(CreateProcessor);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { CreateRemoteProcessGroup } from './create-remote-process-group.component';
|
import { CreateRemoteProcessGroup } from './create-remote-process-group.component';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { ComponentType } from '../../../../../../../state/shared';
|
import { ComponentType } from '../../../../../../../state/shared';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||||
|
@ -44,7 +44,14 @@ describe('CreateRemoteProcessGroup', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CreateRemoteProcessGroup, NoopAnimationsModule],
|
imports: [CreateRemoteProcessGroup, NoopAnimationsModule],
|
||||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }, provideMockStore({ initialState })]
|
providers: [
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(CreateRemoteProcessGroup);
|
fixture = TestBed.createComponent(CreateRemoteProcessGroup);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -15,32 +15,22 @@
|
||||||
~ limitations under the License.
|
~ limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<div class="login-background pt-24 pl-24 h-screen">
|
@if (loading) {
|
||||||
@if (access$ | async; as access) {
|
<div class="splash h-screen p-20">
|
||||||
@if (access.status === 'pending' || access.status === 'loading') {
|
<div class="splash-img h-full flex items-center justify-center">
|
||||||
<div class="w-96">
|
<mat-spinner color="warn"></mat-spinner>
|
||||||
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
@if (access.error) {
|
<div class="login-background pt-24 pl-24 h-screen">
|
||||||
<page-content [title]="access.error.title">
|
@if (currentUserState$ | async; as userState) {
|
||||||
<div class="text-sm">{{ access.error.message }}</div>
|
@if (userState.status === 'success') {
|
||||||
|
<page-content [title]="'Success'">
|
||||||
|
<div class="text-sm">Already logged in. Click home to return to canvas.</div>
|
||||||
</page-content>
|
</page-content>
|
||||||
} @else {
|
} @else {
|
||||||
@if (access.accessStatus.status === 'ACTIVE') {
|
<login-form></login-form>
|
||||||
<page-content [title]="'Success'">
|
|
||||||
<div class="text-sm">{{ access.accessStatus.message }}</div>
|
|
||||||
</page-content>
|
|
||||||
} @else {
|
|
||||||
@if (access.accessConfig.supportsLogin) {
|
|
||||||
<login-form></login-form>
|
|
||||||
} @else {
|
|
||||||
<page-content [title]="'Access Denied'">
|
|
||||||
<div class="text-sm">This NiFi is not configured to support username/password logins.</div>
|
|
||||||
</page-content>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
}
|
||||||
|
|
|
@ -15,23 +15,36 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { LoginState } from '../state';
|
import { LoginState } from '../state';
|
||||||
import { selectAccess } from '../state/access/access.selectors';
|
import { selectCurrentUserState } from '../../../state/current-user/current-user.selectors';
|
||||||
import { loadAccess } from '../state/access/access.actions';
|
import { take } from 'rxjs';
|
||||||
|
import { selectLoginConfiguration } from '../../../state/login-configuration/login-configuration.selectors';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { isDefinedAndNotNull } from '../../../state/shared';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'login',
|
selector: 'login',
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrls: ['./login.component.scss']
|
styleUrls: ['./login.component.scss']
|
||||||
})
|
})
|
||||||
export class Login implements OnInit {
|
export class Login {
|
||||||
access$ = this.store.select(selectAccess);
|
currentUserState$ = this.store.select(selectCurrentUserState).pipe(take(1));
|
||||||
|
loginConfiguration = this.store.selectSignal(selectLoginConfiguration);
|
||||||
|
|
||||||
constructor(private store: Store<LoginState>) {}
|
loading: boolean = true;
|
||||||
|
|
||||||
ngOnInit(): void {
|
constructor(private store: Store<LoginState>) {
|
||||||
this.store.dispatch(loadAccess());
|
this.store
|
||||||
|
.select(selectLoginConfiguration)
|
||||||
|
.pipe(isDefinedAndNotNull(), takeUntilDestroyed())
|
||||||
|
.subscribe((loginConfiguration) => {
|
||||||
|
if (loginConfiguration.externalLoginRequired) {
|
||||||
|
window.location.href = loginConfiguration.loginUri;
|
||||||
|
} else {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { AccessEffects } from '../state/access/access.effects';
|
||||||
import { LoginForm } from '../ui/login-form/login-form.component';
|
import { LoginForm } from '../ui/login-form/login-form.component';
|
||||||
import { PageContent } from '../../../ui/common/page-content/page-content.component';
|
import { PageContent } from '../../../ui/common/page-content/page-content.component';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [Login, LoginForm],
|
declarations: [Login, LoginForm],
|
||||||
|
@ -45,6 +46,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
PageContent
|
PageContent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,21 +16,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createAction, props } from '@ngrx/store';
|
import { createAction, props } from '@ngrx/store';
|
||||||
import { AccessApiError, LoadAccessResponse, LoginRequest } from './index';
|
import { LoginRequest } from './index';
|
||||||
|
|
||||||
export const loadAccess = createAction('[Access] Load Access');
|
|
||||||
|
|
||||||
export const loadAccessSuccess = createAction(
|
|
||||||
'[Access] Load Access Success',
|
|
||||||
props<{ response: LoadAccessResponse }>()
|
|
||||||
);
|
|
||||||
|
|
||||||
export const accessApiError = createAction('[Access] Load Access Error', props<{ error: AccessApiError }>());
|
|
||||||
|
|
||||||
export const login = createAction('[Access] Login', props<{ request: LoginRequest }>());
|
export const login = createAction('[Access] Login', props<{ request: LoginRequest }>());
|
||||||
|
|
||||||
export const loginFailure = createAction('[Access] Login Failure', props<{ failure: string }>());
|
export const loginSuccess = createAction('[Access] Login Success');
|
||||||
|
|
||||||
export const verifyAccess = createAction('[Access] Verify Access');
|
export const loginFailure = createAction('[Access] Login Failure', props<{ loginFailure: string }>());
|
||||||
|
|
||||||
export const verifyAccessSuccess = createAction('[Access] Verify Access Success');
|
export const resetLoginFailure = createAction('[Access] Reset Login Failure');
|
||||||
|
|
|
@ -18,134 +18,75 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||||
import * as AccessActions from './access.actions';
|
import * as AccessActions from './access.actions';
|
||||||
import { catchError, combineLatest, from, map, of, switchMap, tap } from 'rxjs';
|
import { catchError, from, map, of, switchMap, tap } from 'rxjs';
|
||||||
import { AuthService } from '../../../../service/auth.service';
|
import { AuthService } from '../../../../service/auth.service';
|
||||||
import { AuthStorage } from '../../../../service/auth-storage.service';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
|
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
|
||||||
import { MEDIUM_DIALOG } from '../../../../index';
|
import { MEDIUM_DIALOG } from '../../../../index';
|
||||||
import { ErrorHelper } from '../../../../service/error-helper.service';
|
import { ErrorHelper } from '../../../../service/error-helper.service';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NiFiState } from '../../../../state';
|
||||||
|
import { resetLoginFailure } from './access.actions';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccessEffects {
|
export class AccessEffects {
|
||||||
constructor(
|
constructor(
|
||||||
private actions$: Actions,
|
private actions$: Actions,
|
||||||
|
private store: Store<NiFiState>,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private authStorage: AuthStorage,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private errorHelper: ErrorHelper
|
private errorHelper: ErrorHelper
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
loadAccess$ = createEffect(() =>
|
|
||||||
this.actions$.pipe(
|
|
||||||
ofType(AccessActions.loadAccess),
|
|
||||||
switchMap(() =>
|
|
||||||
combineLatest([this.authService.accessConfig(), this.authService.accessStatus()]).pipe(
|
|
||||||
map(([accessConfig, accessStatus]) =>
|
|
||||||
AccessActions.loadAccessSuccess({
|
|
||||||
response: {
|
|
||||||
accessConfig: accessConfig.config,
|
|
||||||
accessStatus: accessStatus.accessStatus
|
|
||||||
}
|
|
||||||
})
|
|
||||||
),
|
|
||||||
catchError((errorResponse: HttpErrorResponse) =>
|
|
||||||
of(
|
|
||||||
AccessActions.accessApiError({
|
|
||||||
error: {
|
|
||||||
title: 'Unable to check Access Status',
|
|
||||||
message: this.errorHelper.getErrorString(errorResponse)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
login$ = createEffect(() =>
|
login$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(AccessActions.login),
|
ofType(AccessActions.login),
|
||||||
map((action) => action.request),
|
map((action) => action.request),
|
||||||
switchMap((request) =>
|
switchMap((request) =>
|
||||||
from(this.authService.login(request.username, request.password)).pipe(
|
from(this.authService.login(request.username, request.password)).pipe(
|
||||||
map((jwt) => {
|
map(() => AccessActions.loginSuccess()),
|
||||||
const sessionExpiration: string | null = this.authService.getSessionExpiration(jwt);
|
|
||||||
if (sessionExpiration) {
|
|
||||||
this.authStorage.setToken(sessionExpiration);
|
|
||||||
}
|
|
||||||
return AccessActions.verifyAccess();
|
|
||||||
}),
|
|
||||||
catchError((errorResponse: HttpErrorResponse) =>
|
catchError((errorResponse: HttpErrorResponse) =>
|
||||||
of(AccessActions.loginFailure({ failure: this.errorHelper.getErrorString(errorResponse) }))
|
of(AccessActions.loginFailure({ loginFailure: this.errorHelper.getErrorString(errorResponse) }))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
loginFailure$ = createEffect(
|
loginSuccess$ = createEffect(
|
||||||
() =>
|
() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(AccessActions.loginFailure),
|
ofType(AccessActions.loginSuccess),
|
||||||
map((action) => action.failure),
|
|
||||||
tap((failure) => {
|
|
||||||
this.dialog.open(OkDialog, {
|
|
||||||
...MEDIUM_DIALOG,
|
|
||||||
data: {
|
|
||||||
title: 'Login',
|
|
||||||
message: failure
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
),
|
|
||||||
{ dispatch: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
verifyAccess$ = createEffect(() =>
|
|
||||||
this.actions$.pipe(
|
|
||||||
ofType(AccessActions.verifyAccess),
|
|
||||||
switchMap(() =>
|
|
||||||
from(this.authService.accessStatus()).pipe(
|
|
||||||
map((response) => {
|
|
||||||
if (response.accessStatus.status === 'ACTIVE') {
|
|
||||||
return AccessActions.verifyAccessSuccess();
|
|
||||||
} else {
|
|
||||||
return AccessActions.accessApiError({
|
|
||||||
error: {
|
|
||||||
title: 'Unable to log in',
|
|
||||||
message: response.accessStatus.message
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
catchError((errorResponse: HttpErrorResponse) =>
|
|
||||||
of(
|
|
||||||
AccessActions.accessApiError({
|
|
||||||
error: {
|
|
||||||
title: 'Unable to log in',
|
|
||||||
message: this.errorHelper.getErrorString(errorResponse)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
verifyAccessSuccess$ = createEffect(
|
|
||||||
() =>
|
|
||||||
this.actions$.pipe(
|
|
||||||
ofType(AccessActions.verifyAccessSuccess),
|
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
{ dispatch: false }
|
{ dispatch: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
loginFailure$ = createEffect(
|
||||||
|
() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(AccessActions.loginFailure),
|
||||||
|
map((action) => action.loginFailure),
|
||||||
|
tap((loginFailure) => {
|
||||||
|
this.dialog
|
||||||
|
.open(OkDialog, {
|
||||||
|
...MEDIUM_DIALOG,
|
||||||
|
data: {
|
||||||
|
title: 'Login',
|
||||||
|
message: loginFailure
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.afterClosed()
|
||||||
|
.subscribe(() => {
|
||||||
|
this.store.dispatch(resetLoginFailure());
|
||||||
|
});
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ dispatch: false }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,42 +16,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createReducer, on } from '@ngrx/store';
|
import { createReducer, on } from '@ngrx/store';
|
||||||
import { Access, AccessConfig, AccessStatus } from './index';
|
import { Access } from './index';
|
||||||
import { accessApiError, loadAccess, loadAccessSuccess } from './access.actions';
|
import { loginFailure, resetLoginFailure } from './access.actions';
|
||||||
|
|
||||||
export const INITIAL_STATUS: AccessStatus = {
|
|
||||||
identity: '',
|
|
||||||
status: 'UNKNOWN',
|
|
||||||
message: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
export const INITIAL_CONFIG: AccessConfig = {
|
|
||||||
supportsLogin: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initialState: Access = {
|
export const initialState: Access = {
|
||||||
accessStatus: INITIAL_STATUS,
|
loginFailure: null
|
||||||
accessConfig: INITIAL_CONFIG,
|
|
||||||
error: null,
|
|
||||||
status: 'pending'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accessReducer = createReducer(
|
export const accessReducer = createReducer(
|
||||||
initialState,
|
initialState,
|
||||||
on(loadAccess, (state) => ({
|
on(loginFailure, (state, { loginFailure }) => ({
|
||||||
...state,
|
...state,
|
||||||
status: 'loading' as const
|
loginFailure
|
||||||
})),
|
})),
|
||||||
on(loadAccessSuccess, (state, { response }) => ({
|
on(resetLoginFailure, (state) => ({
|
||||||
...state,
|
...state,
|
||||||
accessStatus: response.accessStatus,
|
loginFailure: null
|
||||||
accessConfig: response.accessConfig,
|
|
||||||
error: null,
|
|
||||||
status: 'success' as const
|
|
||||||
})),
|
|
||||||
on(accessApiError, (state, { error }) => ({
|
|
||||||
...state,
|
|
||||||
error,
|
|
||||||
status: 'error' as const
|
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
import { createSelector } from '@ngrx/store';
|
import { createSelector } from '@ngrx/store';
|
||||||
import { LoginState, selectLoginState } from '../index';
|
import { LoginState, selectLoginState } from '../index';
|
||||||
import { accessFeatureKey } from './index';
|
import { Access, accessFeatureKey } from './index';
|
||||||
|
|
||||||
export const selectAccess = createSelector(selectLoginState, (state: LoginState) => state[accessFeatureKey]);
|
export const selectAccess = createSelector(selectLoginState, (state: LoginState) => state[accessFeatureKey]);
|
||||||
|
|
||||||
|
export const selectLoginFailure = createSelector(selectAccess, (access: Access) => access.loginFailure);
|
||||||
|
|
|
@ -22,29 +22,6 @@ export interface LoginRequest {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadAccessResponse {
|
|
||||||
accessStatus: AccessStatus;
|
|
||||||
accessConfig: AccessConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessStatus {
|
|
||||||
identity: string;
|
|
||||||
status: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessConfig {
|
|
||||||
supportsLogin: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessApiError {
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Access {
|
export interface Access {
|
||||||
accessStatus: AccessStatus;
|
loginFailure: string | null;
|
||||||
accessConfig: AccessConfig;
|
|
||||||
error: AccessApiError | null;
|
|
||||||
status: 'pending' | 'loading' | 'error' | 'success';
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,10 @@
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h3 class="primary-color">Log In</h3>
|
<h3 class="primary-color">Log In</h3>
|
||||||
<div class="flex gap-x-2">
|
<div class="flex gap-x-2">
|
||||||
@if (hasToken()) {
|
@if (logoutSupported()) {
|
||||||
<a (click)="logout()">log out</a>
|
<a (click)="logout()">log out</a>
|
||||||
}
|
}
|
||||||
<a [routerLink]="['/']">home</a>
|
<a (click)="resetRoutedToFullScreenError()" [routerLink]="['/']">home</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { currentUserFeatureKey } from '../../../../state/current-user';
|
||||||
|
|
||||||
describe('LoginForm', () => {
|
describe('LoginForm', () => {
|
||||||
let component: LoginForm;
|
let component: LoginForm;
|
||||||
|
@ -45,7 +46,13 @@ describe('LoginForm', () => {
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
MatInputModule
|
MatInputModule
|
||||||
],
|
],
|
||||||
providers: [provideMockStore({ initialState })]
|
providers: [
|
||||||
|
provideMockStore({
|
||||||
|
initialState: {
|
||||||
|
[currentUserFeatureKey]: initialState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(LoginForm);
|
fixture = TestBed.createComponent(LoginForm);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -17,11 +17,14 @@
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||||
import { AuthStorage } from '../../../../service/auth-storage.service';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { LoginState } from '../../state';
|
|
||||||
import { login } from '../../state/access/access.actions';
|
import { login } from '../../state/access/access.actions';
|
||||||
import { AuthService } from '../../../../service/auth.service';
|
import { selectLoginFailure } from '../../state/access/access.selectors';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { selectLogoutSupported } from '../../../../state/current-user/current-user.selectors';
|
||||||
|
import { NiFiState } from '../../../../state';
|
||||||
|
import { setRoutedToFullScreenError } from '../../../../state/error/error.actions';
|
||||||
|
import { logout } from '../../../../state/current-user/current-user.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'login-form',
|
selector: 'login-form',
|
||||||
|
@ -29,27 +32,36 @@ import { AuthService } from '../../../../service/auth.service';
|
||||||
styleUrls: ['./login-form.component.scss']
|
styleUrls: ['./login-form.component.scss']
|
||||||
})
|
})
|
||||||
export class LoginForm {
|
export class LoginForm {
|
||||||
|
logoutSupported = this.store.selectSignal(selectLogoutSupported);
|
||||||
|
|
||||||
loginForm: FormGroup;
|
loginForm: FormGroup;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private store: Store<LoginState>,
|
private store: Store<NiFiState>
|
||||||
private authStorage: AuthStorage,
|
|
||||||
private authService: AuthService
|
|
||||||
) {
|
) {
|
||||||
// build the form
|
// build the form
|
||||||
this.loginForm = this.formBuilder.group({
|
this.loginForm = this.formBuilder.group({
|
||||||
username: new FormControl('', Validators.required),
|
username: new FormControl('', Validators.required),
|
||||||
password: new FormControl('', Validators.required)
|
password: new FormControl('', Validators.required)
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
hasToken(): boolean {
|
this.store
|
||||||
return this.authStorage.hasToken();
|
.select(selectLoginFailure)
|
||||||
|
.pipe(takeUntilDestroyed())
|
||||||
|
.subscribe((loginFailure) => {
|
||||||
|
if (loginFailure) {
|
||||||
|
this.loginForm.get('password')?.setValue('');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.authService.logout();
|
this.store.dispatch(logout());
|
||||||
|
}
|
||||||
|
|
||||||
|
resetRoutedToFullScreenError(): void {
|
||||||
|
this.store.dispatch(setRoutedToFullScreenError({ routedToFullScreenError: false }));
|
||||||
}
|
}
|
||||||
|
|
||||||
login() {
|
login() {
|
||||||
|
|
|
@ -21,6 +21,9 @@ import { RouteNotFound } from './route-not-found.component';
|
||||||
import { PageContent } from '../../../ui/common/page-content/page-content.component';
|
import { PageContent } from '../../../ui/common/page-content/page-content.component';
|
||||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { currentUserFeatureKey } from '../../../state/current-user';
|
||||||
|
import { initialState } from '../../../state/current-user/current-user.reducer';
|
||||||
|
|
||||||
describe('RouteNotFound', () => {
|
describe('RouteNotFound', () => {
|
||||||
let component: RouteNotFound;
|
let component: RouteNotFound;
|
||||||
|
@ -29,7 +32,14 @@ describe('RouteNotFound', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [RouteNotFound],
|
declarations: [RouteNotFound],
|
||||||
imports: [PageContent, HttpClientTestingModule, RouterTestingModule]
|
imports: [PageContent, HttpClientTestingModule, RouterTestingModule],
|
||||||
|
providers: [
|
||||||
|
provideMockStore({
|
||||||
|
initialState: {
|
||||||
|
[currentUserFeatureKey]: initialState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(RouteNotFound);
|
fixture = TestBed.createComponent(RouteNotFound);
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { UserAccessPolicies } from './user-access-policies.component';
|
import { UserAccessPolicies } from './user-access-policies.component';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { UserAccessPoliciesDialogRequest } from '../../../state/user-listing';
|
import { UserAccessPoliciesDialogRequest } from '../../../state/user-listing';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
|
||||||
describe('UserAccessPolicies', () => {
|
describe('UserAccessPolicies', () => {
|
||||||
let component: UserAccessPolicies;
|
let component: UserAccessPolicies;
|
||||||
|
@ -53,7 +53,13 @@ describe('UserAccessPolicies', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [UserAccessPolicies, NoopAnimationsModule],
|
imports: [UserAccessPolicies, NoopAnimationsModule],
|
||||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
providers: [
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(UserAccessPolicies);
|
fixture = TestBed.createComponent(UserAccessPolicies);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -167,7 +167,8 @@ describe('UserTable', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
canVersionFlows: false
|
canVersionFlows: false,
|
||||||
|
logoutSupported: true
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
||||||
* (the "License"); you may not use this file except in compliance with
|
|
||||||
* the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AuthStorage {
|
|
||||||
private static readonly TOKEN_ITEM_KEY: string = 'Access-Token-Expiration';
|
|
||||||
|
|
||||||
private static readonly REQUEST_TOKEN_PATTERN: RegExp = new RegExp('Request-Token=([^;]+)');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Request Token from document cookies
|
|
||||||
*
|
|
||||||
* @return Request Token string or null when not found
|
|
||||||
*/
|
|
||||||
public getRequestToken(): string | null {
|
|
||||||
const requestTokenMatcher = AuthStorage.REQUEST_TOKEN_PATTERN.exec(document.cookie);
|
|
||||||
if (requestTokenMatcher) {
|
|
||||||
return requestTokenMatcher[1];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Token from Session Storage
|
|
||||||
*
|
|
||||||
* @return Bearer Token string
|
|
||||||
*/
|
|
||||||
public getToken(): string | null {
|
|
||||||
return sessionStorage.getItem(AuthStorage.TOKEN_ITEM_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Has Token returns the status of whether Session Storage contains the Token
|
|
||||||
*
|
|
||||||
* @return Boolean status of whether Session Storage contains the Token
|
|
||||||
*/
|
|
||||||
public hasToken(): boolean {
|
|
||||||
return typeof this.getToken() === 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove Token from Session Storage
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public removeToken(): void {
|
|
||||||
sessionStorage.removeItem(AuthStorage.TOKEN_ITEM_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set Token in Session Storage
|
|
||||||
*
|
|
||||||
* @param token Token String
|
|
||||||
*/
|
|
||||||
public setToken(token: string): void {
|
|
||||||
sessionStorage.setItem(AuthStorage.TOKEN_ITEM_KEY, token);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,33 +16,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable, take } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { AuthStorage } from './auth-storage.service';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private static readonly API: string = '../nifi-api';
|
private static readonly API: string = '../nifi-api';
|
||||||
|
|
||||||
constructor(
|
constructor(private httpClient: HttpClient) {}
|
||||||
private httpClient: HttpClient,
|
|
||||||
private authStorage: AuthStorage
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public kerberos(): Observable<string> {
|
public getLoginConfiguration(): Observable<any> {
|
||||||
return this.httpClient.post<string>(`${AuthService.API}/access/kerberos`, null);
|
return this.httpClient.get(`${AuthService.API}/authentication/configuration`);
|
||||||
}
|
|
||||||
|
|
||||||
public ticketExpiration(): Observable<any> {
|
|
||||||
return this.httpClient.get(`${AuthService.API}/access/token/expiration`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public accessConfig(): Observable<any> {
|
|
||||||
return this.httpClient.get(`${AuthService.API}/access/config`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public accessStatus(): Observable<any> {
|
|
||||||
return this.httpClient.get(`${AuthService.API}/access`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public login(username: string, password: string): Observable<string> {
|
public login(username: string, password: string): Observable<string> {
|
||||||
|
@ -53,61 +37,7 @@ export class AuthService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout(): void {
|
public logout(): Observable<any> {
|
||||||
this.httpClient
|
return this.httpClient.delete(`${AuthService.API}/access/logout`);
|
||||||
.delete(`${AuthService.API}/access/logout`)
|
|
||||||
.pipe(take(1))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.authStorage.removeToken();
|
|
||||||
window.location.href = './logout';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the subject from the specified jwt. If the jwt is not as expected
|
|
||||||
* an empty string is returned.
|
|
||||||
*
|
|
||||||
* @param {string} jwt
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
public getJwtPayload(jwt: string): any {
|
|
||||||
if (jwt) {
|
|
||||||
const segments: string[] = jwt.split(/\./);
|
|
||||||
if (segments.length !== 3) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawPayload: string = atob(segments[1]);
|
|
||||||
return JSON.parse(rawPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Session Expiration from JSON Web Token Payload exp claim
|
|
||||||
*
|
|
||||||
* @param {string} jwt
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
public getSessionExpiration(jwt: string): string | null {
|
|
||||||
const jwtPayload = this.getJwtPayload(jwt);
|
|
||||||
if (jwtPayload) {
|
|
||||||
return jwtPayload['exp'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Default Session Expiration based on current time plus 12 hours as seconds
|
|
||||||
*
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
public getDefaultExpiration(): string {
|
|
||||||
const now: Date = new Date();
|
|
||||||
const expiration: number = now.getTime() + 43200000;
|
|
||||||
const expirationSeconds: number = Math.round(expiration / 1000);
|
|
||||||
return expirationSeconds.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,9 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import * as ErrorActions from '../state/error/error.actions';
|
import * as ErrorActions from '../state/error/error.actions';
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
import { NiFiCommon } from './nifi-common.service';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ErrorHelper {
|
export class ErrorHelper {
|
||||||
constructor(private nifiCommon: NiFiCommon) {}
|
|
||||||
|
|
||||||
fullScreenError(errorResponse: HttpErrorResponse, skipReplaceUrl?: boolean): Action {
|
fullScreenError(errorResponse: HttpErrorResponse, skipReplaceUrl?: boolean): Action {
|
||||||
let title: string;
|
let title: string;
|
||||||
let message: string;
|
let message: string;
|
||||||
|
@ -51,6 +48,8 @@ export class ErrorHelper {
|
||||||
if (errorResponse.status === 0 || !errorResponse.error) {
|
if (errorResponse.status === 0 || !errorResponse.error) {
|
||||||
message =
|
message =
|
||||||
'An error occurred communicating with NiFi. Please check the logs and fix any configuration issues before restarting.';
|
'An error occurred communicating with NiFi. Please check the logs and fix any configuration issues before restarting.';
|
||||||
|
} else if (errorResponse.status === 401) {
|
||||||
|
message = 'Your session has expired. Please navigate home to log in again.';
|
||||||
} else {
|
} else {
|
||||||
message = this.getErrorString(errorResponse);
|
message = this.getErrorString(errorResponse);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,118 +18,88 @@
|
||||||
import { CanMatchFn } from '@angular/router';
|
import { CanMatchFn } from '@angular/router';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { AuthService } from '../auth.service';
|
import { AuthService } from '../auth.service';
|
||||||
import { AuthStorage } from '../auth-storage.service';
|
import { catchError, from, map, of, switchMap, take, tap } from 'rxjs';
|
||||||
import { take } from 'rxjs';
|
|
||||||
import { CurrentUserService } from '../current-user.service';
|
import { CurrentUserService } from '../current-user.service';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { CurrentUserState } from '../../state/current-user';
|
import { CurrentUserState } from '../../state/current-user';
|
||||||
import { loadCurrentUserSuccess } from '../../state/current-user/current-user.actions';
|
import { loadCurrentUserSuccess } from '../../state/current-user/current-user.actions';
|
||||||
import { selectCurrentUserState } from '../../state/current-user/current-user.selectors';
|
import { selectCurrentUserState } from '../../state/current-user/current-user.selectors';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { fullScreenError } from '../../state/error/error.actions';
|
||||||
|
import { ErrorHelper } from '../error-helper.service';
|
||||||
|
import { selectLoginConfiguration } from '../../state/login-configuration/login-configuration.selectors';
|
||||||
|
import { loadLoginConfigurationSuccess } from '../../state/login-configuration/login-configuration.actions';
|
||||||
|
|
||||||
export const authenticationGuard: CanMatchFn = () => {
|
export const authenticationGuard: CanMatchFn = () => {
|
||||||
const authStorage: AuthStorage = inject(AuthStorage);
|
|
||||||
const authService: AuthService = inject(AuthService);
|
const authService: AuthService = inject(AuthService);
|
||||||
const userService: CurrentUserService = inject(CurrentUserService);
|
const userService: CurrentUserService = inject(CurrentUserService);
|
||||||
|
const errorHelper: ErrorHelper = inject(ErrorHelper);
|
||||||
const store: Store<CurrentUserState> = inject(Store<CurrentUserState>);
|
const store: Store<CurrentUserState> = inject(Store<CurrentUserState>);
|
||||||
|
|
||||||
const handleAuthentication: Promise<boolean> = new Promise((resolve) => {
|
const getAuthenticationConfig = store.select(selectLoginConfiguration).pipe(
|
||||||
if (authStorage.hasToken()) {
|
take(1),
|
||||||
resolve(true);
|
switchMap((loginConfiguration) => {
|
||||||
} else {
|
if (loginConfiguration) {
|
||||||
authService
|
return of(loginConfiguration);
|
||||||
.kerberos()
|
} else {
|
||||||
.pipe(take(1))
|
return from(authService.getLoginConfiguration()).pipe(
|
||||||
.subscribe({
|
tap((response) => {
|
||||||
next: (jwt: string) => {
|
store.dispatch(
|
||||||
// Use Expiration from JWT for tracking authentication status
|
loadLoginConfigurationSuccess({
|
||||||
const sessionExpiration: string | null = authService.getSessionExpiration(jwt);
|
response: {
|
||||||
if (sessionExpiration) {
|
loginConfiguration: response.authenticationConfiguration
|
||||||
authStorage.setToken(sessionExpiration);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
authService
|
|
||||||
.ticketExpiration()
|
|
||||||
.pipe(take(1))
|
|
||||||
.subscribe({
|
|
||||||
next: (accessTokenExpirationEntity: any) => {
|
|
||||||
const accessTokenExpiration: any =
|
|
||||||
accessTokenExpirationEntity.accessTokenExpiration;
|
|
||||||
// Convert ISO 8601 string to session expiration in seconds
|
|
||||||
const expiration: number = Date.parse(accessTokenExpiration.expiration);
|
|
||||||
const expirationSeconds: number = expiration / 1000;
|
|
||||||
const sessionExpiration: number = Math.round(expirationSeconds);
|
|
||||||
authStorage.setToken(String(sessionExpiration));
|
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
resolve(false);
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
);
|
||||||
});
|
})
|
||||||
}
|
);
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return new Promise<boolean>((resolve) => {
|
return getAuthenticationConfig.pipe(
|
||||||
handleAuthentication.finally(() => {
|
switchMap((authConfigResponse) => {
|
||||||
store
|
return store.select(selectCurrentUserState).pipe(
|
||||||
.select(selectCurrentUserState)
|
take(1),
|
||||||
.pipe(take(1))
|
switchMap((userState) => {
|
||||||
.subscribe((userState) => {
|
|
||||||
if (userState.status == 'success') {
|
if (userState.status == 'success') {
|
||||||
resolve(true);
|
return of(true);
|
||||||
} else {
|
} else {
|
||||||
userService
|
return from(userService.getUser()).pipe(
|
||||||
.getUser()
|
tap((response) => {
|
||||||
.pipe(take(1))
|
store.dispatch(
|
||||||
.subscribe({
|
loadCurrentUserSuccess({
|
||||||
next: (response) => {
|
response: {
|
||||||
// store the loaded user
|
user: response
|
||||||
store.dispatch(
|
}
|
||||||
loadCurrentUserSuccess({
|
})
|
||||||
response: {
|
);
|
||||||
user: response
|
}),
|
||||||
}
|
map(() => true),
|
||||||
})
|
catchError((errorResponse: HttpErrorResponse) => {
|
||||||
);
|
if (errorResponse.status !== 401 || authConfigResponse.loginSupported) {
|
||||||
|
store.dispatch(errorHelper.fullScreenError(errorResponse));
|
||||||
if (authStorage.hasToken()) {
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
authService
|
|
||||||
.accessConfig()
|
|
||||||
.pipe(take(1))
|
|
||||||
.subscribe({
|
|
||||||
next: (response) => {
|
|
||||||
if (response.config.supportsLogin) {
|
|
||||||
// Set default expiration when authenticated to enable logout status
|
|
||||||
const expiration: string = authService.getDefaultExpiration();
|
|
||||||
authStorage.setToken(expiration);
|
|
||||||
}
|
|
||||||
resolve(true);
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
window.location.href = './login';
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
// there is no anonymous access and we don't know this user - open the login page which handles login
|
|
||||||
if (error.status === 401) {
|
|
||||||
authStorage.removeToken();
|
|
||||||
window.location.href = './login';
|
|
||||||
}
|
|
||||||
resolve(false);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return of(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
});
|
}),
|
||||||
|
catchError(() => {
|
||||||
|
store.dispatch(
|
||||||
|
fullScreenError({
|
||||||
|
errorDetail: {
|
||||||
|
title: 'Unauthorized',
|
||||||
|
message:
|
||||||
|
'Unable to load authentication configuration. Please contact your system administrator.'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return of(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CanMatchFn } from '@angular/router';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { catchError, map, of, switchMap, tap } from 'rxjs';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { fullScreenError } from '../../state/error/error.actions';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { ErrorHelper } from '../error-helper.service';
|
||||||
|
import { selectLoginConfiguration } from '../../state/login-configuration/login-configuration.selectors';
|
||||||
|
import { AuthService } from '../auth.service';
|
||||||
|
import { loadLoginConfigurationSuccess } from '../../state/login-configuration/login-configuration.actions';
|
||||||
|
import { LoginConfiguration, LoginConfigurationState } from '../../state/login-configuration';
|
||||||
|
|
||||||
|
export const checkLoginConfiguration = (
|
||||||
|
loginConfigurationCheck: (loginConfiguration: LoginConfiguration) => boolean
|
||||||
|
): CanMatchFn => {
|
||||||
|
return () => {
|
||||||
|
const store: Store<LoginConfigurationState> = inject(Store<LoginConfigurationState>);
|
||||||
|
const authService: AuthService = inject(AuthService);
|
||||||
|
const errorHelper: ErrorHelper = inject(ErrorHelper);
|
||||||
|
|
||||||
|
return store.select(selectLoginConfiguration).pipe(
|
||||||
|
switchMap((loginConfiguration) => {
|
||||||
|
if (loginConfiguration) {
|
||||||
|
return of(loginConfiguration);
|
||||||
|
} else {
|
||||||
|
return authService.getLoginConfiguration().pipe(
|
||||||
|
tap((response) =>
|
||||||
|
store.dispatch(
|
||||||
|
loadLoginConfigurationSuccess({
|
||||||
|
response: {
|
||||||
|
loginConfiguration: response.authenticationConfiguration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
map((loginConfiguration) => {
|
||||||
|
if (loginConfigurationCheck(loginConfiguration)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
fullScreenError({
|
||||||
|
skipReplaceUrl: true,
|
||||||
|
errorDetail: {
|
||||||
|
title: 'Unable to load',
|
||||||
|
message: 'Login configuration check failed'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
catchError((errorResponse: HttpErrorResponse) => {
|
||||||
|
store.dispatch(errorHelper.fullScreenError(errorResponse, true));
|
||||||
|
return of(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
|
@ -15,61 +15,58 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
import { HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
|
||||||
import { Observable, tap } from 'rxjs';
|
import { catchError, map, take, combineLatest, tap } from 'rxjs';
|
||||||
import { AuthStorage } from '../auth-storage.service';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { NiFiState } from '../../state';
|
import { NiFiState } from '../../state';
|
||||||
import { fullScreenError } from '../../state/error/error.actions';
|
import { fullScreenError, setRoutedToFullScreenError } from '../../state/error/error.actions';
|
||||||
import { NiFiCommon } from '../nifi-common.service';
|
import { selectCurrentUserState } from '../../state/current-user/current-user.selectors';
|
||||||
|
import { navigateToLogIn, resetCurrentUser } from '../../state/current-user/current-user.actions';
|
||||||
|
import { selectRoutedToFullScreenError } from '../../state/error/error.selectors';
|
||||||
|
import { selectLoginConfiguration } from '../../state/login-configuration/login-configuration.selectors';
|
||||||
|
|
||||||
@Injectable({
|
export const authInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||||
providedIn: 'root'
|
const store: Store<NiFiState> = inject(Store<NiFiState>);
|
||||||
})
|
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
|
||||||
routedToFullScreenError = false;
|
|
||||||
|
|
||||||
constructor(
|
return next(request).pipe(
|
||||||
private authStorage: AuthStorage,
|
catchError((errorResponse) => {
|
||||||
private store: Store<NiFiState>,
|
if (errorResponse instanceof HttpErrorResponse && errorResponse.status === 401) {
|
||||||
private nifiCommon: NiFiCommon
|
return combineLatest([
|
||||||
) {}
|
store.select(selectCurrentUserState).pipe(
|
||||||
|
take(1),
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
tap(() => store.dispatch(resetCurrentUser()))
|
||||||
return next.handle(request).pipe(
|
),
|
||||||
tap({
|
store.select(selectLoginConfiguration).pipe(take(1)),
|
||||||
error: (errorResponse) => {
|
store.select(selectRoutedToFullScreenError).pipe(
|
||||||
if (errorResponse instanceof HttpErrorResponse) {
|
take(1),
|
||||||
if (errorResponse.status === 401) {
|
tap(() => store.dispatch(setRoutedToFullScreenError({ routedToFullScreenError: true })))
|
||||||
if (this.authStorage.hasToken()) {
|
)
|
||||||
this.routedToFullScreenError = true;
|
]).pipe(
|
||||||
|
map(([currentUserState, loginConfiguration, routedToFullScreenError]) => {
|
||||||
this.authStorage.removeToken();
|
if (
|
||||||
|
currentUserState.status === 'pending' &&
|
||||||
let message: string = errorResponse.error;
|
loginConfiguration?.loginSupported &&
|
||||||
if (this.nifiCommon.isBlank(message)) {
|
!routedToFullScreenError
|
||||||
message = 'Your session has expired. Please navigate home to log in again.';
|
) {
|
||||||
} else {
|
store.dispatch(navigateToLogIn());
|
||||||
message += '. Please navigate home to log in again.';
|
} else {
|
||||||
}
|
store.dispatch(
|
||||||
|
fullScreenError({
|
||||||
this.store.dispatch(
|
errorDetail: {
|
||||||
fullScreenError({
|
title: 'Unauthorized',
|
||||||
errorDetail: {
|
message: 'Your session has expired. Please navigate home to log in again.'
|
||||||
title: 'Unauthorized',
|
}
|
||||||
message
|
})
|
||||||
}
|
);
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if (!this.routedToFullScreenError) {
|
|
||||||
// the user has never logged in, redirect them to do so
|
|
||||||
window.location.href = './login';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
throw errorResponse;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
}
|
throw errorResponse;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -15,20 +15,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
|
||||||
import { finalize, Observable } from 'rxjs';
|
import { finalize } from 'rxjs';
|
||||||
import { LoadingService } from '../loading.service';
|
import { LoadingService } from '../loading.service';
|
||||||
|
|
||||||
@Injectable({
|
export const loadingInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||||
providedIn: 'root'
|
const loadingService: LoadingService = inject(LoadingService);
|
||||||
})
|
loadingService.set(true, request.url);
|
||||||
export class LoadingInterceptor implements HttpInterceptor {
|
|
||||||
constructor(private loadingService: LoadingService) {}
|
|
||||||
|
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
return next(request).pipe(finalize(() => loadingService.set(false, request.url)));
|
||||||
this.loadingService.set(true, request.url);
|
};
|
||||||
|
|
||||||
return next.handle(request).pipe(finalize(() => this.loadingService.set(false, request.url)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,32 +15,27 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
import { HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
|
||||||
import { Observable, tap } from 'rxjs';
|
import { tap } from 'rxjs';
|
||||||
import { NiFiState } from '../../state';
|
import { NiFiState } from '../../state';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { stopCurrentUserPolling } from '../../state/current-user/current-user.actions';
|
import { stopCurrentUserPolling } from '../../state/current-user/current-user.actions';
|
||||||
import { stopProcessGroupPolling } from '../../pages/flow-designer/state/flow/flow.actions';
|
import { stopProcessGroupPolling } from '../../pages/flow-designer/state/flow/flow.actions';
|
||||||
import { stopClusterSummaryPolling } from '../../state/cluster-summary/cluster-summary.actions';
|
import { stopClusterSummaryPolling } from '../../state/cluster-summary/cluster-summary.actions';
|
||||||
|
|
||||||
@Injectable({
|
export const pollingInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||||
providedIn: 'root'
|
const store: Store<NiFiState> = inject(Store<NiFiState>);
|
||||||
})
|
|
||||||
export class PollingInterceptor implements HttpInterceptor {
|
|
||||||
constructor(private store: Store<NiFiState>) {}
|
|
||||||
|
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
return next(request).pipe(
|
||||||
return next.handle(request).pipe(
|
tap({
|
||||||
tap({
|
error: (error) => {
|
||||||
error: (error) => {
|
if (error instanceof HttpErrorResponse && error.status === 0) {
|
||||||
if (error instanceof HttpErrorResponse && error.status === 0) {
|
store.dispatch(stopCurrentUserPolling());
|
||||||
this.store.dispatch(stopCurrentUserPolling());
|
store.dispatch(stopProcessGroupPolling());
|
||||||
this.store.dispatch(stopProcessGroupPolling());
|
store.dispatch(stopClusterSummaryPolling());
|
||||||
this.store.dispatch(stopClusterSummaryPolling());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
);
|
})
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -28,3 +28,11 @@ export const loadCurrentUserSuccess = createAction(
|
||||||
export const startCurrentUserPolling = createAction('[Current User] Start Current User Polling');
|
export const startCurrentUserPolling = createAction('[Current User] Start Current User Polling');
|
||||||
|
|
||||||
export const stopCurrentUserPolling = createAction('[Current User] Stop Current User Polling');
|
export const stopCurrentUserPolling = createAction('[Current User] Stop Current User Polling');
|
||||||
|
|
||||||
|
export const resetCurrentUser = createAction('[Current User] Reset Current User');
|
||||||
|
|
||||||
|
export const navigateToLogIn = createAction('[Current User] Navigate To Log In');
|
||||||
|
|
||||||
|
export const logout = createAction('[Current User] Log Out');
|
||||||
|
|
||||||
|
export const navigateToLogOut = createAction('[Current User] Navigate To Log Out');
|
||||||
|
|
|
@ -18,15 +18,25 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||||
import * as UserActions from './current-user.actions';
|
import * as UserActions from './current-user.actions';
|
||||||
import { asyncScheduler, catchError, from, interval, map, of, switchMap, takeUntil } from 'rxjs';
|
import { asyncScheduler, catchError, from, interval, map, of, switchMap, takeUntil, tap } from 'rxjs';
|
||||||
import { CurrentUserService } from '../../service/current-user.service';
|
import { CurrentUserService } from '../../service/current-user.service';
|
||||||
import { ErrorHelper } from '../../service/error-helper.service';
|
import { ErrorHelper } from '../../service/error-helper.service';
|
||||||
|
import { concatLatestFrom } from '@ngrx/operators';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NiFiState } from '../index';
|
||||||
|
import { selectLogoutUri } from '../login-configuration/login-configuration.selectors';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../../service/auth.service';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CurrentUserEffects {
|
export class CurrentUserEffects {
|
||||||
constructor(
|
constructor(
|
||||||
private actions$: Actions,
|
private actions$: Actions,
|
||||||
|
private store: Store<NiFiState>,
|
||||||
|
private router: Router,
|
||||||
private userService: CurrentUserService,
|
private userService: CurrentUserService,
|
||||||
|
private authService: AuthService,
|
||||||
private errorHelper: ErrorHelper
|
private errorHelper: ErrorHelper
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -61,4 +71,43 @@ export class CurrentUserEffects {
|
||||||
switchMap(() => of(UserActions.loadCurrentUser()))
|
switchMap(() => of(UserActions.loadCurrentUser()))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
navigateToLogIn$ = createEffect(
|
||||||
|
() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(UserActions.navigateToLogIn),
|
||||||
|
tap(() => {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ dispatch: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
logout$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(UserActions.logout),
|
||||||
|
switchMap(() =>
|
||||||
|
from(this.authService.logout()).pipe(
|
||||||
|
map(() => UserActions.navigateToLogOut()),
|
||||||
|
catchError((errorResponse: HttpErrorResponse) =>
|
||||||
|
of(this.errorHelper.fullScreenError(errorResponse))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
navigateToLogOut$ = createEffect(
|
||||||
|
() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(UserActions.navigateToLogOut),
|
||||||
|
concatLatestFrom(() => this.store.select(selectLogoutUri)),
|
||||||
|
tap(([, logoutUri]) => {
|
||||||
|
if (logoutUri) {
|
||||||
|
window.location.href = logoutUri;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ dispatch: false }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
import { createReducer, on } from '@ngrx/store';
|
import { createReducer, on } from '@ngrx/store';
|
||||||
import { CurrentUserState } from './index';
|
import { CurrentUserState } from './index';
|
||||||
import { Permissions } from '../shared';
|
import { Permissions } from '../shared';
|
||||||
import { loadCurrentUser, loadCurrentUserSuccess } from './current-user.actions';
|
import { loadCurrentUser, loadCurrentUserSuccess, resetCurrentUser } from './current-user.actions';
|
||||||
|
|
||||||
export const NO_PERMISSIONS: Permissions = {
|
export const NO_PERMISSIONS: Permissions = {
|
||||||
canRead: false,
|
canRead: false,
|
||||||
|
@ -30,6 +30,7 @@ export const initialState: CurrentUserState = {
|
||||||
identity: '',
|
identity: '',
|
||||||
anonymous: true,
|
anonymous: true,
|
||||||
canVersionFlows: false,
|
canVersionFlows: false,
|
||||||
|
logoutSupported: false,
|
||||||
controllerPermissions: NO_PERMISSIONS,
|
controllerPermissions: NO_PERMISSIONS,
|
||||||
countersPermissions: NO_PERMISSIONS,
|
countersPermissions: NO_PERMISSIONS,
|
||||||
parameterContextPermissions: NO_PERMISSIONS,
|
parameterContextPermissions: NO_PERMISSIONS,
|
||||||
|
@ -53,5 +54,8 @@ export const currentUserReducer = createReducer(
|
||||||
...state,
|
...state,
|
||||||
user: response.user,
|
user: response.user,
|
||||||
status: 'success' as const
|
status: 'success' as const
|
||||||
|
})),
|
||||||
|
on(resetCurrentUser, () => ({
|
||||||
|
...initialState
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,8 +16,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||||
import { currentUserFeatureKey, CurrentUserState } from './index';
|
import { CurrentUser, currentUserFeatureKey, CurrentUserState } from './index';
|
||||||
|
|
||||||
export const selectCurrentUserState = createFeatureSelector<CurrentUserState>(currentUserFeatureKey);
|
export const selectCurrentUserState = createFeatureSelector<CurrentUserState>(currentUserFeatureKey);
|
||||||
|
|
||||||
export const selectCurrentUser = createSelector(selectCurrentUserState, (state: CurrentUserState) => state.user);
|
export const selectCurrentUser = createSelector(selectCurrentUserState, (state: CurrentUserState) => state.user);
|
||||||
|
|
||||||
|
export const selectLogoutSupported = createSelector(
|
||||||
|
selectCurrentUser,
|
||||||
|
(currentUser: CurrentUser) => currentUser.logoutSupported
|
||||||
|
);
|
||||||
|
|
|
@ -32,6 +32,7 @@ export interface CurrentUser {
|
||||||
identity: string;
|
identity: string;
|
||||||
anonymous: boolean;
|
anonymous: boolean;
|
||||||
canVersionFlows: boolean;
|
canVersionFlows: boolean;
|
||||||
|
logoutSupported: boolean;
|
||||||
provenancePermissions: Permissions;
|
provenancePermissions: Permissions;
|
||||||
countersPermissions: Permissions;
|
countersPermissions: Permissions;
|
||||||
tenantsPermissions: Permissions;
|
tenantsPermissions: Permissions;
|
||||||
|
|
|
@ -30,3 +30,8 @@ export const addBannerError = createAction('[Error] Add Banner Error', props<{ e
|
||||||
export const clearBannerErrors = createAction('[Error] Clear Banner Errors');
|
export const clearBannerErrors = createAction('[Error] Clear Banner Errors');
|
||||||
|
|
||||||
export const resetErrorState = createAction('[Error] Reset Error State');
|
export const resetErrorState = createAction('[Error] Reset Error State');
|
||||||
|
|
||||||
|
export const setRoutedToFullScreenError = createAction(
|
||||||
|
'[Error] Set Routed To Full Screen Error',
|
||||||
|
props<{ routedToFullScreenError: boolean }>()
|
||||||
|
);
|
||||||
|
|
|
@ -17,12 +17,19 @@
|
||||||
|
|
||||||
import { createReducer, on } from '@ngrx/store';
|
import { createReducer, on } from '@ngrx/store';
|
||||||
import { ErrorState } from './index';
|
import { ErrorState } from './index';
|
||||||
import { resetErrorState, fullScreenError, addBannerError, clearBannerErrors } from './error.actions';
|
import {
|
||||||
|
resetErrorState,
|
||||||
|
fullScreenError,
|
||||||
|
addBannerError,
|
||||||
|
clearBannerErrors,
|
||||||
|
setRoutedToFullScreenError
|
||||||
|
} from './error.actions';
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
|
|
||||||
export const initialState: ErrorState = {
|
export const initialState: ErrorState = {
|
||||||
bannerErrors: null,
|
bannerErrors: null,
|
||||||
fullScreenError: null
|
fullScreenError: null,
|
||||||
|
routedToFullScreenError: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export const errorReducer = createReducer(
|
export const errorReducer = createReducer(
|
||||||
|
@ -44,6 +51,10 @@ export const errorReducer = createReducer(
|
||||||
...state,
|
...state,
|
||||||
bannerErrors: null
|
bannerErrors: null
|
||||||
})),
|
})),
|
||||||
|
on(setRoutedToFullScreenError, (state, { routedToFullScreenError }) => ({
|
||||||
|
...state,
|
||||||
|
routedToFullScreenError
|
||||||
|
})),
|
||||||
on(resetErrorState, () => ({
|
on(resetErrorState, () => ({
|
||||||
...initialState
|
...initialState
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -23,3 +23,8 @@ export const selectErrorState = createFeatureSelector<ErrorState>(errorFeatureKe
|
||||||
export const selectFullScreenError = createSelector(selectErrorState, (state: ErrorState) => state.fullScreenError);
|
export const selectFullScreenError = createSelector(selectErrorState, (state: ErrorState) => state.fullScreenError);
|
||||||
|
|
||||||
export const selectBannerErrors = createSelector(selectErrorState, (state: ErrorState) => state.bannerErrors);
|
export const selectBannerErrors = createSelector(selectErrorState, (state: ErrorState) => state.bannerErrors);
|
||||||
|
|
||||||
|
export const selectRoutedToFullScreenError = createSelector(
|
||||||
|
selectErrorState,
|
||||||
|
(state: ErrorState) => state.routedToFullScreenError
|
||||||
|
);
|
||||||
|
|
|
@ -25,4 +25,5 @@ export interface ErrorDetail {
|
||||||
export interface ErrorState {
|
export interface ErrorState {
|
||||||
bannerErrors: string[] | null;
|
bannerErrors: string[] | null;
|
||||||
fullScreenError: ErrorDetail | null;
|
fullScreenError: ErrorDetail | null;
|
||||||
|
routedToFullScreenError: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ import { documentationFeatureKey, DocumentationState } from './documentation';
|
||||||
import { documentationReducer } from './documentation/documentation.reducer';
|
import { documentationReducer } from './documentation/documentation.reducer';
|
||||||
import { clusterSummaryFeatureKey, ClusterSummaryState } from './cluster-summary';
|
import { clusterSummaryFeatureKey, ClusterSummaryState } from './cluster-summary';
|
||||||
import { clusterSummaryReducer } from './cluster-summary/cluster-summary.reducer';
|
import { clusterSummaryReducer } from './cluster-summary/cluster-summary.reducer';
|
||||||
|
import { loginConfigurationFeatureKey, LoginConfigurationState } from './login-configuration';
|
||||||
|
import { loginConfigurationReducer } from './login-configuration/login-configuration.reducer';
|
||||||
|
|
||||||
export interface NiFiState {
|
export interface NiFiState {
|
||||||
[DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
|
[DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
|
||||||
|
@ -47,6 +49,7 @@ export interface NiFiState {
|
||||||
[extensionTypesFeatureKey]: ExtensionTypesState;
|
[extensionTypesFeatureKey]: ExtensionTypesState;
|
||||||
[aboutFeatureKey]: AboutState;
|
[aboutFeatureKey]: AboutState;
|
||||||
[flowConfigurationFeatureKey]: FlowConfigurationState;
|
[flowConfigurationFeatureKey]: FlowConfigurationState;
|
||||||
|
[loginConfigurationFeatureKey]: LoginConfigurationState;
|
||||||
[statusHistoryFeatureKey]: StatusHistoryState;
|
[statusHistoryFeatureKey]: StatusHistoryState;
|
||||||
[controllerServiceStateFeatureKey]: ControllerServiceState;
|
[controllerServiceStateFeatureKey]: ControllerServiceState;
|
||||||
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
|
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
|
||||||
|
@ -62,6 +65,7 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||||
[extensionTypesFeatureKey]: extensionTypesReducer,
|
[extensionTypesFeatureKey]: extensionTypesReducer,
|
||||||
[aboutFeatureKey]: aboutReducer,
|
[aboutFeatureKey]: aboutReducer,
|
||||||
[flowConfigurationFeatureKey]: flowConfigurationReducer,
|
[flowConfigurationFeatureKey]: flowConfigurationReducer,
|
||||||
|
[loginConfigurationFeatureKey]: loginConfigurationReducer,
|
||||||
[statusHistoryFeatureKey]: statusHistoryReducer,
|
[statusHistoryFeatureKey]: statusHistoryReducer,
|
||||||
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
|
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
|
||||||
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
|
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
|
||||||
|
|
|
@ -15,19 +15,20 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
export const loginConfigurationFeatureKey = 'loginConfiguration';
|
||||||
|
|
||||||
import { LoadingInterceptor } from './loading.interceptor';
|
export interface LoadLoginConfigurationResponse {
|
||||||
|
loginConfiguration: LoginConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
describe('LoadingInterceptor', () => {
|
export interface LoginConfiguration {
|
||||||
let service: LoadingInterceptor;
|
loginSupported: boolean;
|
||||||
|
externalLoginRequired: boolean;
|
||||||
|
loginUri: string;
|
||||||
|
logoutUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
export interface LoginConfigurationState {
|
||||||
TestBed.configureTestingModule({});
|
loginConfiguration: LoginConfiguration | null;
|
||||||
service = TestBed.inject(LoadingInterceptor);
|
status: 'pending' | 'loading' | 'success';
|
||||||
});
|
}
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -15,19 +15,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { createAction, props } from '@ngrx/store';
|
||||||
|
import { LoadLoginConfigurationResponse } from './index';
|
||||||
|
|
||||||
import { AuthStorage } from './auth-storage.service';
|
export const loadLoginConfiguration = createAction('[Login Configuration] Load Login Configuration');
|
||||||
|
|
||||||
describe('AuthStorage', () => {
|
export const loadLoginConfigurationSuccess = createAction(
|
||||||
let service: AuthStorage;
|
'[Login Configuration] Load Login Configuration Success',
|
||||||
|
props<{ response: LoadLoginConfigurationResponse }>()
|
||||||
beforeEach(() => {
|
);
|
||||||
TestBed.configureTestingModule({});
|
|
||||||
service = TestBed.inject(AuthStorage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||||
|
import * as LoginConfigurationActions from './login-configuration.actions';
|
||||||
|
import { catchError, from, map, of, switchMap } from 'rxjs';
|
||||||
|
import * as ErrorActions from '../error/error.actions';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { ErrorHelper } from '../../service/error-helper.service';
|
||||||
|
import { AuthService } from '../../service/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoginConfigurationEffects {
|
||||||
|
constructor(
|
||||||
|
private actions$: Actions,
|
||||||
|
private authService: AuthService,
|
||||||
|
private errorHelper: ErrorHelper
|
||||||
|
) {}
|
||||||
|
|
||||||
|
loadLoginConfiguration$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(LoginConfigurationActions.loadLoginConfiguration),
|
||||||
|
switchMap(() => {
|
||||||
|
return from(
|
||||||
|
this.authService.getLoginConfiguration().pipe(
|
||||||
|
map((response) =>
|
||||||
|
LoginConfigurationActions.loadLoginConfigurationSuccess({
|
||||||
|
response
|
||||||
|
})
|
||||||
|
),
|
||||||
|
catchError((errorResponse: HttpErrorResponse) =>
|
||||||
|
of(
|
||||||
|
ErrorActions.snackBarError({
|
||||||
|
error: this.errorHelper.getErrorString(
|
||||||
|
errorResponse,
|
||||||
|
'Failed to load Login Configuration.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
|
@ -15,27 +15,24 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { createReducer, on } from '@ngrx/store';
|
||||||
|
import { LoginConfigurationState } from './index';
|
||||||
|
import { loadLoginConfiguration, loadLoginConfigurationSuccess } from './login-configuration.actions';
|
||||||
|
|
||||||
import { AuthInterceptor } from './auth.interceptor';
|
export const initialState: LoginConfigurationState = {
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
loginConfiguration: null,
|
||||||
import { initialState } from '../../state/error/error.reducer';
|
status: 'pending'
|
||||||
|
};
|
||||||
|
|
||||||
describe('AuthInterceptor', () => {
|
export const loginConfigurationReducer = createReducer(
|
||||||
let service: AuthInterceptor;
|
initialState,
|
||||||
|
on(loadLoginConfiguration, (state) => ({
|
||||||
beforeEach(() => {
|
...state,
|
||||||
TestBed.configureTestingModule({
|
status: 'loading' as const
|
||||||
providers: [
|
})),
|
||||||
provideMockStore({
|
on(loadLoginConfigurationSuccess, (state, { response }) => ({
|
||||||
initialState
|
...state,
|
||||||
})
|
loginConfiguration: response.loginConfiguration,
|
||||||
]
|
status: 'success' as const
|
||||||
});
|
}))
|
||||||
service = TestBed.inject(AuthInterceptor);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -15,23 +15,23 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||||
|
import { LoginConfiguration, LoginConfigurationState, loginConfigurationFeatureKey } from './index';
|
||||||
|
|
||||||
import { PollingInterceptor } from './polling.interceptor';
|
export const selectLoginConfigurationState =
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
createFeatureSelector<LoginConfigurationState>(loginConfigurationFeatureKey);
|
||||||
import { initialState } from '../../state/current-user/current-user.reducer';
|
|
||||||
|
|
||||||
describe('PollingInterceptor', () => {
|
export const selectLoginConfiguration = createSelector(
|
||||||
let service: PollingInterceptor;
|
selectLoginConfigurationState,
|
||||||
|
(state: LoginConfigurationState) => state.loginConfiguration
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
export const selectLoginUri = createSelector(
|
||||||
TestBed.configureTestingModule({
|
selectLoginConfiguration,
|
||||||
providers: [provideMockStore({ initialState })]
|
(loginConfiguration: LoginConfiguration | null) => loginConfiguration?.loginUri
|
||||||
});
|
);
|
||||||
service = TestBed.inject(PollingInterceptor);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
export const selectLogoutUri = createSelector(
|
||||||
expect(service).toBeTruthy();
|
selectLoginConfiguration,
|
||||||
});
|
(loginConfiguration: LoginConfiguration | null) => loginConfiguration?.logoutUri
|
||||||
});
|
);
|
|
@ -29,6 +29,8 @@ import { selectClusterSummary } from '../../../state/cluster-summary/cluster-sum
|
||||||
import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
|
import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
|
||||||
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
||||||
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
|
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
|
||||||
|
import { selectLoginConfiguration } from '../../../state/login-configuration/login-configuration.selectors';
|
||||||
|
import * as fromLoginConfiguration from '../../../state/login-configuration/login-configuration.reducer';
|
||||||
|
|
||||||
describe('AdvancedUi', () => {
|
describe('AdvancedUi', () => {
|
||||||
let component: AdvancedUi;
|
let component: AdvancedUi;
|
||||||
|
@ -59,6 +61,10 @@ describe('AdvancedUi', () => {
|
||||||
{
|
{
|
||||||
selector: selectFlowConfiguration,
|
selector: selectFlowConfiguration,
|
||||||
value: fromFlowConfiguration.initialState.flowConfiguration
|
value: fromFlowConfiguration.initialState.flowConfiguration
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: selectLoginConfiguration,
|
||||||
|
value: fromLoginConfiguration.initialState.loginConfiguration
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { ExtensionCreation } from './extension-creation.component';
|
import { ExtensionCreation } from './extension-creation.component';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
|
|
||||||
describe('ExtensionCreation', () => {
|
describe('ExtensionCreation', () => {
|
||||||
let component: ExtensionCreation;
|
let component: ExtensionCreation;
|
||||||
|
@ -26,7 +27,13 @@ describe('ExtensionCreation', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [ExtensionCreation, NoopAnimationsModule]
|
imports: [ExtensionCreation, NoopAnimationsModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(ExtensionCreation);
|
fixture = TestBed.createComponent(ExtensionCreation);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -32,11 +32,13 @@
|
||||||
@if (currentUser(); as user) {
|
@if (currentUser(); as user) {
|
||||||
<div class="flex justify-between items-center gap-x-1">
|
<div class="flex justify-between items-center gap-x-1">
|
||||||
<div class="flex flex-col justify-between items-end gap-y-1">
|
<div class="flex flex-col justify-between items-end gap-y-1">
|
||||||
<div class="current-user">{{ user.identity }}</div>
|
@if (!user.anonymous) {
|
||||||
|
<div class="current-user">{{ user.identity }}</div>
|
||||||
|
}
|
||||||
@if (allowLogin(user)) {
|
@if (allowLogin(user)) {
|
||||||
<a href="#">log in</a>
|
<a href="#">log in</a>
|
||||||
}
|
}
|
||||||
@if (hasToken()) {
|
@if (user.logoutSupported) {
|
||||||
<a (click)="logout()">log out</a>
|
<a (click)="logout()">log out</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,6 +28,8 @@ import { selectClusterSummary } from '../../../state/cluster-summary/cluster-sum
|
||||||
import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
|
import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
|
||||||
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
||||||
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
|
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
|
||||||
|
import { selectLoginConfiguration } from '../../../state/login-configuration/login-configuration.selectors';
|
||||||
|
import * as fromLoginConfiguration from '../../../state/login-configuration/login-configuration.reducer';
|
||||||
|
|
||||||
describe('Navigation', () => {
|
describe('Navigation', () => {
|
||||||
let component: Navigation;
|
let component: Navigation;
|
||||||
|
@ -51,6 +53,10 @@ describe('Navigation', () => {
|
||||||
{
|
{
|
||||||
selector: selectFlowConfiguration,
|
selector: selectFlowConfiguration,
|
||||||
value: fromFlowConfiguration.initialState.flowConfiguration
|
value: fromFlowConfiguration.initialState.flowConfiguration
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: selectLoginConfiguration,
|
||||||
|
value: fromLoginConfiguration.initialState.loginConfiguration
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,8 +23,6 @@ import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { getNodeStatusHistoryAndOpenDialog } from '../../../state/status-history/status-history.actions';
|
import { getNodeStatusHistoryAndOpenDialog } from '../../../state/status-history/status-history.actions';
|
||||||
import { getSystemDiagnosticsAndOpenDialog } from '../../../state/system-diagnostics/system-diagnostics.actions';
|
import { getSystemDiagnosticsAndOpenDialog } from '../../../state/system-diagnostics/system-diagnostics.actions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AuthStorage } from '../../../service/auth-storage.service';
|
|
||||||
import { AuthService } from '../../../service/auth.service';
|
|
||||||
import { CurrentUser } from '../../../state/current-user';
|
import { CurrentUser } from '../../../state/current-user';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
|
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
|
||||||
|
@ -35,7 +33,11 @@ import { Storage } from '../../../service/storage.service';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { DARK_THEME, LIGHT_THEME, OS_SETTING, ThemingService } from '../../../service/theming.service';
|
import { DARK_THEME, LIGHT_THEME, OS_SETTING, ThemingService } from '../../../service/theming.service';
|
||||||
import { loadFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.actions';
|
import { loadFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.actions';
|
||||||
import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
|
import {
|
||||||
|
logout,
|
||||||
|
startCurrentUserPolling,
|
||||||
|
stopCurrentUserPolling
|
||||||
|
} from '../../../state/current-user/current-user.actions';
|
||||||
import { loadAbout, openAboutDialog } from '../../../state/about/about.actions';
|
import { loadAbout, openAboutDialog } from '../../../state/about/about.actions';
|
||||||
import {
|
import {
|
||||||
loadClusterSummary,
|
loadClusterSummary,
|
||||||
|
@ -43,6 +45,7 @@ import {
|
||||||
stopClusterSummaryPolling
|
stopClusterSummaryPolling
|
||||||
} from '../../../state/cluster-summary/cluster-summary.actions';
|
} from '../../../state/cluster-summary/cluster-summary.actions';
|
||||||
import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
|
import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
|
||||||
|
import { selectLoginConfiguration } from '../../../state/login-configuration/login-configuration.selectors';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'navigation',
|
selector: 'navigation',
|
||||||
|
@ -69,12 +72,11 @@ export class Navigation implements OnInit, OnDestroy {
|
||||||
OS_SETTING: string = OS_SETTING;
|
OS_SETTING: string = OS_SETTING;
|
||||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||||
flowConfiguration = this.store.selectSignal(selectFlowConfiguration);
|
flowConfiguration = this.store.selectSignal(selectFlowConfiguration);
|
||||||
|
loginConfiguration = this.store.selectSignal(selectLoginConfiguration);
|
||||||
clusterSummary = this.store.selectSignal(selectClusterSummary);
|
clusterSummary = this.store.selectSignal(selectClusterSummary);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<NiFiState>,
|
private store: Store<NiFiState>,
|
||||||
private authStorage: AuthStorage,
|
|
||||||
private authService: AuthService,
|
|
||||||
private storage: Storage,
|
private storage: Storage,
|
||||||
private themingService: ThemingService
|
private themingService: ThemingService
|
||||||
) {
|
) {
|
||||||
|
@ -104,15 +106,16 @@ export class Navigation implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
allowLogin(user: CurrentUser): boolean {
|
allowLogin(user: CurrentUser): boolean {
|
||||||
return user.anonymous && location.protocol === 'https:';
|
const loginConfig = this.loginConfiguration();
|
||||||
}
|
if (loginConfig) {
|
||||||
|
return user.anonymous && loginConfig.loginSupported;
|
||||||
hasToken(): boolean {
|
} else {
|
||||||
return this.authStorage.hasToken();
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.authService.logout();
|
this.store.dispatch(logout());
|
||||||
}
|
}
|
||||||
|
|
||||||
viewNodeStatusHistory(): void {
|
viewNodeStatusHistory(): void {
|
||||||
|
|
|
@ -19,10 +19,10 @@
|
||||||
<div class="flex justify-between items-center gap-x-3">
|
<div class="flex justify-between items-center gap-x-3">
|
||||||
<h3 class="primary-color whitespace-nowrap overflow-hidden text-ellipsis" [title]="title">{{ title }}</h3>
|
<h3 class="primary-color whitespace-nowrap overflow-hidden text-ellipsis" [title]="title">{{ title }}</h3>
|
||||||
<div class="flex gap-x-3">
|
<div class="flex gap-x-3">
|
||||||
@if (hasToken()) {
|
@if (logoutSupported()) {
|
||||||
<a (click)="logout()" class="whitespace-nowrap">log out</a>
|
<a (click)="logout()" class="whitespace-nowrap">log out</a>
|
||||||
}
|
}
|
||||||
<a [routerLink]="['/']">home</a>
|
<a (click)="resetRoutedToFullScreenError()" [routerLink]="['/']">home</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
|
|
@ -21,6 +21,9 @@ import { PageContent } from './page-content.component';
|
||||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { currentUserFeatureKey } from '../../../state/current-user';
|
||||||
|
import { initialState } from '../../../state/current-user/current-user.reducer';
|
||||||
|
|
||||||
describe('PageContent', () => {
|
describe('PageContent', () => {
|
||||||
let component: PageContent;
|
let component: PageContent;
|
||||||
|
@ -28,7 +31,14 @@ describe('PageContent', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [PageContent, HttpClientTestingModule, RouterModule, RouterTestingModule]
|
imports: [PageContent, HttpClientTestingModule, RouterModule, RouterTestingModule],
|
||||||
|
providers: [
|
||||||
|
provideMockStore({
|
||||||
|
initialState: {
|
||||||
|
[currentUserFeatureKey]: initialState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(PageContent);
|
fixture = TestBed.createComponent(PageContent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -16,9 +16,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { AuthStorage } from '../../../service/auth-storage.service';
|
|
||||||
import { AuthService } from '../../../service/auth.service';
|
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { selectLogoutSupported } from '../../../state/current-user/current-user.selectors';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NiFiState } from '../../../state';
|
||||||
|
import { setRoutedToFullScreenError } from '../../../state/error/error.actions';
|
||||||
|
import { logout } from '../../../state/current-user/current-user.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-content',
|
selector: 'page-content',
|
||||||
|
@ -30,16 +33,15 @@ import { RouterLink } from '@angular/router';
|
||||||
export class PageContent {
|
export class PageContent {
|
||||||
@Input() title = '';
|
@Input() title = '';
|
||||||
|
|
||||||
constructor(
|
logoutSupported = this.store.selectSignal(selectLogoutSupported);
|
||||||
private authStorage: AuthStorage,
|
|
||||||
private authService: AuthService
|
constructor(private store: Store<NiFiState>) {}
|
||||||
) {}
|
|
||||||
|
resetRoutedToFullScreenError(): void {
|
||||||
|
this.store.dispatch(setRoutedToFullScreenError({ routedToFullScreenError: false }));
|
||||||
|
}
|
||||||
|
|
||||||
logout(): void {
|
logout(): void {
|
||||||
this.authService.logout();
|
this.store.dispatch(logout());
|
||||||
}
|
|
||||||
|
|
||||||
hasToken(): boolean {
|
|
||||||
return this.authStorage.hasToken();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { StatusHistory } from './status-history.component';
|
||||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { initialState } from '../../../state/extension-types/extension-types.reducer';
|
import { initialState } from '../../../state/extension-types/extension-types.reducer';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
|
||||||
describe('StatusHistory', () => {
|
describe('StatusHistory', () => {
|
||||||
let component: StatusHistory;
|
let component: StatusHistory;
|
||||||
|
@ -30,7 +30,14 @@ describe('StatusHistory', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [HttpClientTestingModule],
|
imports: [HttpClientTestingModule],
|
||||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: {} }, provideMockStore({ initialState })]
|
providers: [
|
||||||
|
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(StatusHistory);
|
fixture = TestBed.createComponent(StatusHistory);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { SystemDiagnosticsDialog } from './system-diagnostics-dialog.component';
|
import { SystemDiagnosticsDialog } from './system-diagnostics-dialog.component';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { initialSystemDiagnosticsState } from '../../../state/system-diagnostics/system-diagnostics.reducer';
|
import { initialSystemDiagnosticsState } from '../../../state/system-diagnostics/system-diagnostics.reducer';
|
||||||
|
import { MatDialogRef } from '@angular/material/dialog';
|
||||||
|
|
||||||
describe('SystemDiagnosticsDialog', () => {
|
describe('SystemDiagnosticsDialog', () => {
|
||||||
let component: SystemDiagnosticsDialog;
|
let component: SystemDiagnosticsDialog;
|
||||||
|
@ -28,7 +29,13 @@ describe('SystemDiagnosticsDialog', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [SystemDiagnosticsDialog],
|
imports: [SystemDiagnosticsDialog],
|
||||||
providers: [provideMockStore({ initialState: initialSystemDiagnosticsState })]
|
providers: [
|
||||||
|
provideMockStore({ initialState: initialSystemDiagnosticsState }),
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: null
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
fixture = TestBed.createComponent(SystemDiagnosticsDialog);
|
fixture = TestBed.createComponent(SystemDiagnosticsDialog);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
|
|
@ -19,47 +19,6 @@
|
||||||
version="6.0">
|
version="6.0">
|
||||||
<display-name>nifi-web-frontend</display-name>
|
<display-name>nifi-web-frontend</display-name>
|
||||||
|
|
||||||
<!-- servlet to login page -->
|
|
||||||
<!-- <servlet>-->
|
|
||||||
<!-- <servlet-name>Login</servlet-name>-->
|
|
||||||
<!-- <jsp-file>/WEB-INF/pages/login.jsp</jsp-file>-->
|
|
||||||
<!-- </servlet>-->
|
|
||||||
<!-- <servlet-mapping>-->
|
|
||||||
<!-- <servlet-name>Login</servlet-name>-->
|
|
||||||
<!-- <url-pattern>/login</url-pattern>-->
|
|
||||||
<!-- </servlet-mapping>-->
|
|
||||||
|
|
||||||
<!-- servlet to logout page -->
|
|
||||||
<!-- <servlet>-->
|
|
||||||
<!-- <servlet-name>Logout</servlet-name>-->
|
|
||||||
<!-- <jsp-file>/WEB-INF/pages/logout.jsp</jsp-file>-->
|
|
||||||
<!-- </servlet>-->
|
|
||||||
<!-- <servlet-mapping>-->
|
|
||||||
<!-- <servlet-name>Logout</servlet-name>-->
|
|
||||||
<!-- <url-pattern>/logout-complete</url-pattern>-->
|
|
||||||
<!-- </servlet-mapping>-->
|
|
||||||
|
|
||||||
<!-- login filter -->
|
|
||||||
<filter>
|
|
||||||
<filter-name>LoginFilter</filter-name>
|
|
||||||
<filter-class>org.apache.nifi.web.filter.LoginFilter</filter-class>
|
|
||||||
</filter>
|
|
||||||
<filter-mapping>
|
|
||||||
<filter-name>LoginFilter</filter-name>
|
|
||||||
<url-pattern>/login</url-pattern>
|
|
||||||
</filter-mapping>
|
|
||||||
|
|
||||||
<!-- logout filter -->
|
|
||||||
<filter>
|
|
||||||
<filter-name>LogoutFilter</filter-name>
|
|
||||||
<filter-class>org.apache.nifi.web.filter.LogoutFilter</filter-class>
|
|
||||||
</filter>
|
|
||||||
<filter-mapping>
|
|
||||||
<filter-name>LogoutFilter</filter-name>
|
|
||||||
<url-pattern>/logout</url-pattern>
|
|
||||||
</filter-mapping>
|
|
||||||
|
|
||||||
<!-- catch all filter -->
|
|
||||||
<filter>
|
<filter>
|
||||||
<filter-name>SanitizeContextPathFilter</filter-name>
|
<filter-name>SanitizeContextPathFilter</filter-name>
|
||||||
<filter-class>org.apache.nifi.web.filter.SanitizeContextPathFilter</filter-class>
|
<filter-class>org.apache.nifi.web.filter.SanitizeContextPathFilter</filter-class>
|
||||||
|
|
Loading…
Reference in New Issue