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 { authenticationGuard } from './service/guard/authentication.guard';
|
||||
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 = [
|
||||
{
|
||||
path: 'login',
|
||||
canMatch: [
|
||||
checkLoginConfiguration((loginConfiguration: LoginConfiguration) => loginConfiguration.loginSupported)
|
||||
],
|
||||
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 { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
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 { rootReducers } from './state';
|
||||
import { CurrentUserEffects } from './state/current-user/current-user.effects';
|
||||
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 { 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 { MatNativeDateModule } from '@angular/material/core';
|
||||
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 { DocumentationEffects } from './state/documentation/documentation.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({
|
||||
declarations: [AppComponent],
|
||||
|
@ -70,6 +71,7 @@ import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.e
|
|||
ExtensionTypesEffects,
|
||||
AboutEffects,
|
||||
FlowConfigurationEffects,
|
||||
LoginConfigurationEffects,
|
||||
StatusHistoryEffects,
|
||||
ControllerServiceStateEffects,
|
||||
SystemDiagnosticsEffects,
|
||||
|
@ -89,22 +91,8 @@ import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.e
|
|||
PipesModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
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' } }
|
||||
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } },
|
||||
provideHttpClient(withInterceptors([authInterceptor, loadingInterceptor, pollingInterceptor]))
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
|
|
@ -15,18 +15,23 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { selectFullScreenError } from '../../../state/error/error.selectors';
|
||||
import { NiFiState } from '../../../state';
|
||||
import { resetErrorState } from '../../../state/error/error.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'error',
|
||||
templateUrl: './error.component.html',
|
||||
styleUrls: ['./error.component.scss']
|
||||
})
|
||||
export class Error {
|
||||
export class Error implements OnDestroy {
|
||||
errorDetail$ = this.store.select(selectFullScreenError);
|
||||
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(resetErrorState());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,7 @@
|
|||
<div class="flex flex-col">
|
||||
<div class="flex gap-x-2 items-center">
|
||||
<div class="logo flex flex-col items-center" [style.background-color]="color">
|
||||
<i
|
||||
class="icon accent-color icon-processor p-2"
|
||||
[style.color]="contrastColor"></i>
|
||||
<i class="icon accent-color icon-processor p-2" [style.color]="contrastColor"></i>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<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 { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
|
||||
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', () => {
|
||||
let component: HeaderComponent;
|
||||
|
@ -120,6 +122,10 @@ describe('HeaderComponent', () => {
|
|||
{
|
||||
selector: selectFlowConfiguration,
|
||||
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 { 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 { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||
|
@ -79,6 +79,10 @@ describe('EditLabel', () => {
|
|||
useValue: {
|
||||
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 { CreateComponentRequest } from '../../../../../state/flow';
|
||||
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 { initialState } from '../../../../../state/flow/flow.reducer';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
@ -44,7 +44,14 @@ describe('CreatePort', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
|
||||
import { EditPort } from './edit-port.component';
|
||||
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 { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||
|
@ -105,6 +105,10 @@ describe('EditPort', () => {
|
|||
useValue: {
|
||||
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 { 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 { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||
|
@ -156,7 +156,14 @@ describe('CreateProcessGroup', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
|
||||
import { CreateProcessor } from './create-processor.component';
|
||||
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 { initialState } from '../../../../../../../state/extension-types/extension-types.reducer';
|
||||
import { ComponentType } from '../../../../../../../state/shared';
|
||||
|
@ -60,7 +60,14 @@ describe('CreateProcessor', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
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 { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../../state/flow/flow.reducer';
|
||||
|
@ -44,7 +44,14 @@ describe('CreateRemoteProcessGroup', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -15,32 +15,22 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<div class="login-background pt-24 pl-24 h-screen">
|
||||
@if (access$ | async; as access) {
|
||||
@if (access.status === 'pending' || access.status === 'loading') {
|
||||
<div class="w-96">
|
||||
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
|
||||
</div>
|
||||
} @else {
|
||||
@if (access.error) {
|
||||
<page-content [title]="access.error.title">
|
||||
<div class="text-sm">{{ access.error.message }}</div>
|
||||
@if (loading) {
|
||||
<div class="splash h-screen p-20">
|
||||
<div class="splash-img h-full flex items-center justify-center">
|
||||
<mat-spinner color="warn"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="login-background pt-24 pl-24 h-screen">
|
||||
@if (currentUserState$ | async; as userState) {
|
||||
@if (userState.status === 'success') {
|
||||
<page-content [title]="'Success'">
|
||||
<div class="text-sm">Already logged in. Click home to return to canvas.</div>
|
||||
</page-content>
|
||||
} @else {
|
||||
@if (access.accessStatus.status === 'ACTIVE') {
|
||||
<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>
|
||||
}
|
||||
}
|
||||
<login-form></login-form>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -15,23 +15,36 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { LoginState } from '../state';
|
||||
import { selectAccess } from '../state/access/access.selectors';
|
||||
import { loadAccess } from '../state/access/access.actions';
|
||||
import { selectCurrentUserState } from '../../../state/current-user/current-user.selectors';
|
||||
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({
|
||||
selector: 'login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.scss']
|
||||
})
|
||||
export class Login implements OnInit {
|
||||
access$ = this.store.select(selectAccess);
|
||||
export class Login {
|
||||
currentUserState$ = this.store.select(selectCurrentUserState).pipe(take(1));
|
||||
loginConfiguration = this.store.selectSignal(selectLoginConfiguration);
|
||||
|
||||
constructor(private store: Store<LoginState>) {}
|
||||
loading: boolean = true;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(loadAccess());
|
||||
constructor(private store: Store<LoginState>) {
|
||||
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 { PageContent } from '../../../ui/common/page-content/page-content.component';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
|
||||
@NgModule({
|
||||
declarations: [Login, LoginForm],
|
||||
|
@ -45,6 +46,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
|||
MatInputModule,
|
||||
MatButtonModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
MatProgressSpinnerModule,
|
||||
PageContent
|
||||
]
|
||||
})
|
||||
|
|
|
@ -16,21 +16,12 @@
|
|||
*/
|
||||
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { AccessApiError, LoadAccessResponse, 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 }>());
|
||||
import { LoginRequest } from './index';
|
||||
|
||||
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 { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
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 { AuthStorage } from '../../../../service/auth-storage.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
|
||||
import { MEDIUM_DIALOG } from '../../../../index';
|
||||
import { ErrorHelper } from '../../../../service/error-helper.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { resetLoginFailure } from './access.actions';
|
||||
|
||||
@Injectable()
|
||||
export class AccessEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private store: Store<NiFiState>,
|
||||
private authService: AuthService,
|
||||
private authStorage: AuthStorage,
|
||||
private router: Router,
|
||||
private dialog: MatDialog,
|
||||
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(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(AccessActions.login),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.authService.login(request.username, request.password)).pipe(
|
||||
map((jwt) => {
|
||||
const sessionExpiration: string | null = this.authService.getSessionExpiration(jwt);
|
||||
if (sessionExpiration) {
|
||||
this.authStorage.setToken(sessionExpiration);
|
||||
}
|
||||
return AccessActions.verifyAccess();
|
||||
}),
|
||||
map(() => AccessActions.loginSuccess()),
|
||||
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(
|
||||
ofType(AccessActions.loginFailure),
|
||||
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),
|
||||
ofType(AccessActions.loginSuccess),
|
||||
tap(() => {
|
||||
this.router.navigate(['/']);
|
||||
})
|
||||
),
|
||||
{ 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 { Access, AccessConfig, AccessStatus } from './index';
|
||||
import { accessApiError, loadAccess, loadAccessSuccess } from './access.actions';
|
||||
|
||||
export const INITIAL_STATUS: AccessStatus = {
|
||||
identity: '',
|
||||
status: 'UNKNOWN',
|
||||
message: ''
|
||||
};
|
||||
|
||||
export const INITIAL_CONFIG: AccessConfig = {
|
||||
supportsLogin: false
|
||||
};
|
||||
import { Access } from './index';
|
||||
import { loginFailure, resetLoginFailure } from './access.actions';
|
||||
|
||||
export const initialState: Access = {
|
||||
accessStatus: INITIAL_STATUS,
|
||||
accessConfig: INITIAL_CONFIG,
|
||||
error: null,
|
||||
status: 'pending'
|
||||
loginFailure: null
|
||||
};
|
||||
|
||||
export const accessReducer = createReducer(
|
||||
initialState,
|
||||
on(loadAccess, (state) => ({
|
||||
on(loginFailure, (state, { loginFailure }) => ({
|
||||
...state,
|
||||
status: 'loading' as const
|
||||
loginFailure
|
||||
})),
|
||||
on(loadAccessSuccess, (state, { response }) => ({
|
||||
on(resetLoginFailure, (state) => ({
|
||||
...state,
|
||||
accessStatus: response.accessStatus,
|
||||
accessConfig: response.accessConfig,
|
||||
error: null,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(accessApiError, (state, { error }) => ({
|
||||
...state,
|
||||
error,
|
||||
status: 'error' as const
|
||||
loginFailure: null
|
||||
}))
|
||||
);
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
import { createSelector } from '@ngrx/store';
|
||||
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 selectLoginFailure = createSelector(selectAccess, (access: Access) => access.loginFailure);
|
||||
|
|
|
@ -22,29 +22,6 @@ export interface LoginRequest {
|
|||
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 {
|
||||
accessStatus: AccessStatus;
|
||||
accessConfig: AccessConfig;
|
||||
error: AccessApiError | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
loginFailure: string | null;
|
||||
}
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
<div class="flex justify-between items-center">
|
||||
<h3 class="primary-color">Log In</h3>
|
||||
<div class="flex gap-x-2">
|
||||
@if (hasToken()) {
|
||||
@if (logoutSupported()) {
|
||||
<a (click)="logout()">log out</a>
|
||||
}
|
||||
<a [routerLink]="['/']">home</a>
|
||||
<a (click)="resetRoutedToFullScreenError()" [routerLink]="['/']">home</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -27,6 +27,7 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { currentUserFeatureKey } from '../../../../state/current-user';
|
||||
|
||||
describe('LoginForm', () => {
|
||||
let component: LoginForm;
|
||||
|
@ -45,7 +46,13 @@ describe('LoginForm', () => {
|
|||
ReactiveFormsModule,
|
||||
MatInputModule
|
||||
],
|
||||
providers: [provideMockStore({ initialState })]
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState: {
|
||||
[currentUserFeatureKey]: initialState
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(LoginForm);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -17,11 +17,14 @@
|
|||
|
||||
import { Component } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { AuthStorage } from '../../../../service/auth-storage.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { LoginState } from '../../state';
|
||||
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({
|
||||
selector: 'login-form',
|
||||
|
@ -29,27 +32,36 @@ import { AuthService } from '../../../../service/auth.service';
|
|||
styleUrls: ['./login-form.component.scss']
|
||||
})
|
||||
export class LoginForm {
|
||||
logoutSupported = this.store.selectSignal(selectLogoutSupported);
|
||||
|
||||
loginForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private store: Store<LoginState>,
|
||||
private authStorage: AuthStorage,
|
||||
private authService: AuthService
|
||||
private store: Store<NiFiState>
|
||||
) {
|
||||
// build the form
|
||||
this.loginForm = this.formBuilder.group({
|
||||
username: new FormControl('', Validators.required),
|
||||
password: new FormControl('', Validators.required)
|
||||
});
|
||||
}
|
||||
|
||||
hasToken(): boolean {
|
||||
return this.authStorage.hasToken();
|
||||
this.store
|
||||
.select(selectLoginFailure)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((loginFailure) => {
|
||||
if (loginFailure) {
|
||||
this.loginForm.get('password')?.setValue('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
this.store.dispatch(logout());
|
||||
}
|
||||
|
||||
resetRoutedToFullScreenError(): void {
|
||||
this.store.dispatch(setRoutedToFullScreenError({ routedToFullScreenError: false }));
|
||||
}
|
||||
|
||||
login() {
|
||||
|
|
|
@ -21,6 +21,9 @@ import { RouteNotFound } from './route-not-found.component';
|
|||
import { PageContent } from '../../../ui/common/page-content/page-content.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/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', () => {
|
||||
let component: RouteNotFound;
|
||||
|
@ -29,7 +32,14 @@ describe('RouteNotFound', () => {
|
|||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RouteNotFound],
|
||||
imports: [PageContent, HttpClientTestingModule, RouterTestingModule]
|
||||
imports: [PageContent, HttpClientTestingModule, RouterTestingModule],
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState: {
|
||||
[currentUserFeatureKey]: initialState
|
||||
}
|
||||
})
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RouteNotFound);
|
||||
|
|
|
@ -20,7 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
import { UserAccessPolicies } from './user-access-policies.component';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
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', () => {
|
||||
let component: UserAccessPolicies;
|
||||
|
@ -53,7 +53,13 @@ describe('UserAccessPolicies', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -167,7 +167,8 @@ describe('UserTable', () => {
|
|||
}
|
||||
}
|
||||
],
|
||||
canVersionFlows: false
|
||||
canVersionFlows: false,
|
||||
logoutSupported: true
|
||||
};
|
||||
|
||||
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 { Observable, take } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { AuthStorage } from './auth-storage.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
private static readonly API: string = '../nifi-api';
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private authStorage: AuthStorage
|
||||
) {}
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
public kerberos(): Observable<string> {
|
||||
return this.httpClient.post<string>(`${AuthService.API}/access/kerberos`, null);
|
||||
}
|
||||
|
||||
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 getLoginConfiguration(): Observable<any> {
|
||||
return this.httpClient.get(`${AuthService.API}/authentication/configuration`);
|
||||
}
|
||||
|
||||
public login(username: string, password: string): Observable<string> {
|
||||
|
@ -53,61 +37,7 @@ export class AuthService {
|
|||
});
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
this.httpClient
|
||||
.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();
|
||||
public logout(): Observable<any> {
|
||||
return this.httpClient.delete(`${AuthService.API}/access/logout`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,9 @@ import { Injectable } from '@angular/core';
|
|||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import * as ErrorActions from '../state/error/error.actions';
|
||||
import { Action } from '@ngrx/store';
|
||||
import { NiFiCommon } from './nifi-common.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ErrorHelper {
|
||||
constructor(private nifiCommon: NiFiCommon) {}
|
||||
|
||||
fullScreenError(errorResponse: HttpErrorResponse, skipReplaceUrl?: boolean): Action {
|
||||
let title: string;
|
||||
let message: string;
|
||||
|
@ -51,6 +48,8 @@ export class ErrorHelper {
|
|||
if (errorResponse.status === 0 || !errorResponse.error) {
|
||||
message =
|
||||
'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 {
|
||||
message = this.getErrorString(errorResponse);
|
||||
}
|
||||
|
|
|
@ -18,118 +18,88 @@
|
|||
import { CanMatchFn } from '@angular/router';
|
||||
import { inject } from '@angular/core';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { AuthStorage } from '../auth-storage.service';
|
||||
import { take } from 'rxjs';
|
||||
import { catchError, from, map, of, switchMap, take, tap } from 'rxjs';
|
||||
import { CurrentUserService } from '../current-user.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CurrentUserState } from '../../state/current-user';
|
||||
import { loadCurrentUserSuccess } from '../../state/current-user/current-user.actions';
|
||||
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 = () => {
|
||||
const authStorage: AuthStorage = inject(AuthStorage);
|
||||
const authService: AuthService = inject(AuthService);
|
||||
const userService: CurrentUserService = inject(CurrentUserService);
|
||||
const errorHelper: ErrorHelper = inject(ErrorHelper);
|
||||
const store: Store<CurrentUserState> = inject(Store<CurrentUserState>);
|
||||
|
||||
const handleAuthentication: Promise<boolean> = new Promise((resolve) => {
|
||||
if (authStorage.hasToken()) {
|
||||
resolve(true);
|
||||
} else {
|
||||
authService
|
||||
.kerberos()
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: (jwt: string) => {
|
||||
// Use Expiration from JWT for tracking authentication status
|
||||
const sessionExpiration: string | null = authService.getSessionExpiration(jwt);
|
||||
if (sessionExpiration) {
|
||||
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);
|
||||
const getAuthenticationConfig = store.select(selectLoginConfiguration).pipe(
|
||||
take(1),
|
||||
switchMap((loginConfiguration) => {
|
||||
if (loginConfiguration) {
|
||||
return of(loginConfiguration);
|
||||
} else {
|
||||
return from(authService.getLoginConfiguration()).pipe(
|
||||
tap((response) => {
|
||||
store.dispatch(
|
||||
loadLoginConfigurationSuccess({
|
||||
response: {
|
||||
loginConfiguration: response.authenticationConfiguration
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
handleAuthentication.finally(() => {
|
||||
store
|
||||
.select(selectCurrentUserState)
|
||||
.pipe(take(1))
|
||||
.subscribe((userState) => {
|
||||
return getAuthenticationConfig.pipe(
|
||||
switchMap((authConfigResponse) => {
|
||||
return store.select(selectCurrentUserState).pipe(
|
||||
take(1),
|
||||
switchMap((userState) => {
|
||||
if (userState.status == 'success') {
|
||||
resolve(true);
|
||||
return of(true);
|
||||
} else {
|
||||
userService
|
||||
.getUser()
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
// store the loaded user
|
||||
store.dispatch(
|
||||
loadCurrentUserSuccess({
|
||||
response: {
|
||||
user: response
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
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 from(userService.getUser()).pipe(
|
||||
tap((response) => {
|
||||
store.dispatch(
|
||||
loadCurrentUserSuccess({
|
||||
response: {
|
||||
user: response
|
||||
}
|
||||
})
|
||||
);
|
||||
}),
|
||||
map(() => true),
|
||||
catchError((errorResponse: HttpErrorResponse) => {
|
||||
if (errorResponse.status !== 401 || authConfigResponse.loginSupported) {
|
||||
store.dispatch(errorHelper.fullScreenError(errorResponse));
|
||||
}
|
||||
});
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { AuthStorage } from '../auth-storage.service';
|
||||
import { inject } from '@angular/core';
|
||||
import { HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
|
||||
import { catchError, map, take, combineLatest, tap } from 'rxjs';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../state';
|
||||
import { fullScreenError } from '../../state/error/error.actions';
|
||||
import { NiFiCommon } from '../nifi-common.service';
|
||||
import { fullScreenError, setRoutedToFullScreenError } from '../../state/error/error.actions';
|
||||
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({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
routedToFullScreenError = false;
|
||||
export const authInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||
const store: Store<NiFiState> = inject(Store<NiFiState>);
|
||||
|
||||
constructor(
|
||||
private authStorage: AuthStorage,
|
||||
private store: Store<NiFiState>,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(request).pipe(
|
||||
tap({
|
||||
error: (errorResponse) => {
|
||||
if (errorResponse instanceof HttpErrorResponse) {
|
||||
if (errorResponse.status === 401) {
|
||||
if (this.authStorage.hasToken()) {
|
||||
this.routedToFullScreenError = true;
|
||||
|
||||
this.authStorage.removeToken();
|
||||
|
||||
let message: string = errorResponse.error;
|
||||
if (this.nifiCommon.isBlank(message)) {
|
||||
message = 'Your session has expired. Please navigate home to log in again.';
|
||||
} else {
|
||||
message += '. Please navigate home to log in again.';
|
||||
}
|
||||
|
||||
this.store.dispatch(
|
||||
fullScreenError({
|
||||
errorDetail: {
|
||||
title: 'Unauthorized',
|
||||
message
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (!this.routedToFullScreenError) {
|
||||
// the user has never logged in, redirect them to do so
|
||||
window.location.href = './login';
|
||||
}
|
||||
return next(request).pipe(
|
||||
catchError((errorResponse) => {
|
||||
if (errorResponse instanceof HttpErrorResponse && errorResponse.status === 401) {
|
||||
return combineLatest([
|
||||
store.select(selectCurrentUserState).pipe(
|
||||
take(1),
|
||||
tap(() => store.dispatch(resetCurrentUser()))
|
||||
),
|
||||
store.select(selectLoginConfiguration).pipe(take(1)),
|
||||
store.select(selectRoutedToFullScreenError).pipe(
|
||||
take(1),
|
||||
tap(() => store.dispatch(setRoutedToFullScreenError({ routedToFullScreenError: true })))
|
||||
)
|
||||
]).pipe(
|
||||
map(([currentUserState, loginConfiguration, routedToFullScreenError]) => {
|
||||
if (
|
||||
currentUserState.status === 'pending' &&
|
||||
loginConfiguration?.loginSupported &&
|
||||
!routedToFullScreenError
|
||||
) {
|
||||
store.dispatch(navigateToLogIn());
|
||||
} else {
|
||||
store.dispatch(
|
||||
fullScreenError({
|
||||
errorDetail: {
|
||||
title: 'Unauthorized',
|
||||
message: 'Your session has expired. Please navigate home to log in again.'
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw errorResponse;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
throw errorResponse;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,20 +15,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { finalize, Observable } from 'rxjs';
|
||||
import { inject } from '@angular/core';
|
||||
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
|
||||
import { finalize } from 'rxjs';
|
||||
import { LoadingService } from '../loading.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LoadingInterceptor implements HttpInterceptor {
|
||||
constructor(private loadingService: LoadingService) {}
|
||||
export const loadingInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||
const loadingService: LoadingService = inject(LoadingService);
|
||||
loadingService.set(true, request.url);
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
this.loadingService.set(true, request.url);
|
||||
|
||||
return next.handle(request).pipe(finalize(() => this.loadingService.set(false, request.url)));
|
||||
}
|
||||
}
|
||||
return next(request).pipe(finalize(() => loadingService.set(false, request.url)));
|
||||
};
|
||||
|
|
|
@ -15,32 +15,27 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { inject } from '@angular/core';
|
||||
import { HttpErrorResponse, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
|
||||
import { tap } from 'rxjs';
|
||||
import { NiFiState } from '../../state';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { stopCurrentUserPolling } from '../../state/current-user/current-user.actions';
|
||||
import { stopProcessGroupPolling } from '../../pages/flow-designer/state/flow/flow.actions';
|
||||
import { stopClusterSummaryPolling } from '../../state/cluster-summary/cluster-summary.actions';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PollingInterceptor implements HttpInterceptor {
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
export const pollingInterceptor: HttpInterceptorFn = (request: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||
const store: Store<NiFiState> = inject(Store<NiFiState>);
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(request).pipe(
|
||||
tap({
|
||||
error: (error) => {
|
||||
if (error instanceof HttpErrorResponse && error.status === 0) {
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
this.store.dispatch(stopProcessGroupPolling());
|
||||
this.store.dispatch(stopClusterSummaryPolling());
|
||||
}
|
||||
return next(request).pipe(
|
||||
tap({
|
||||
error: (error) => {
|
||||
if (error instanceof HttpErrorResponse && error.status === 0) {
|
||||
store.dispatch(stopCurrentUserPolling());
|
||||
store.dispatch(stopProcessGroupPolling());
|
||||
store.dispatch(stopClusterSummaryPolling());
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -28,3 +28,11 @@ export const loadCurrentUserSuccess = createAction(
|
|||
export const startCurrentUserPolling = createAction('[Current User] Start 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 { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
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 { 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()
|
||||
export class CurrentUserEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private store: Store<NiFiState>,
|
||||
private router: Router,
|
||||
private userService: CurrentUserService,
|
||||
private authService: AuthService,
|
||||
private errorHelper: ErrorHelper
|
||||
) {}
|
||||
|
||||
|
@ -61,4 +71,43 @@ export class CurrentUserEffects {
|
|||
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 { CurrentUserState } from './index';
|
||||
import { Permissions } from '../shared';
|
||||
import { loadCurrentUser, loadCurrentUserSuccess } from './current-user.actions';
|
||||
import { loadCurrentUser, loadCurrentUserSuccess, resetCurrentUser } from './current-user.actions';
|
||||
|
||||
export const NO_PERMISSIONS: Permissions = {
|
||||
canRead: false,
|
||||
|
@ -30,6 +30,7 @@ export const initialState: CurrentUserState = {
|
|||
identity: '',
|
||||
anonymous: true,
|
||||
canVersionFlows: false,
|
||||
logoutSupported: false,
|
||||
controllerPermissions: NO_PERMISSIONS,
|
||||
countersPermissions: NO_PERMISSIONS,
|
||||
parameterContextPermissions: NO_PERMISSIONS,
|
||||
|
@ -53,5 +54,8 @@ export const currentUserReducer = createReducer(
|
|||
...state,
|
||||
user: response.user,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(resetCurrentUser, () => ({
|
||||
...initialState
|
||||
}))
|
||||
);
|
||||
|
|
|
@ -16,8 +16,13 @@
|
|||
*/
|
||||
|
||||
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 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;
|
||||
anonymous: boolean;
|
||||
canVersionFlows: boolean;
|
||||
logoutSupported: boolean;
|
||||
provenancePermissions: Permissions;
|
||||
countersPermissions: 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 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 { ErrorState } from './index';
|
||||
import { resetErrorState, fullScreenError, addBannerError, clearBannerErrors } from './error.actions';
|
||||
import {
|
||||
resetErrorState,
|
||||
fullScreenError,
|
||||
addBannerError,
|
||||
clearBannerErrors,
|
||||
setRoutedToFullScreenError
|
||||
} from './error.actions';
|
||||
import { produce } from 'immer';
|
||||
|
||||
export const initialState: ErrorState = {
|
||||
bannerErrors: null,
|
||||
fullScreenError: null
|
||||
fullScreenError: null,
|
||||
routedToFullScreenError: false
|
||||
};
|
||||
|
||||
export const errorReducer = createReducer(
|
||||
|
@ -44,6 +51,10 @@ export const errorReducer = createReducer(
|
|||
...state,
|
||||
bannerErrors: null
|
||||
})),
|
||||
on(setRoutedToFullScreenError, (state, { routedToFullScreenError }) => ({
|
||||
...state,
|
||||
routedToFullScreenError
|
||||
})),
|
||||
on(resetErrorState, () => ({
|
||||
...initialState
|
||||
}))
|
||||
|
|
|
@ -23,3 +23,8 @@ export const selectErrorState = createFeatureSelector<ErrorState>(errorFeatureKe
|
|||
export const selectFullScreenError = createSelector(selectErrorState, (state: ErrorState) => state.fullScreenError);
|
||||
|
||||
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 {
|
||||
bannerErrors: string[] | null;
|
||||
fullScreenError: ErrorDetail | null;
|
||||
routedToFullScreenError: boolean;
|
||||
}
|
||||
|
|
|
@ -39,6 +39,8 @@ import { documentationFeatureKey, DocumentationState } from './documentation';
|
|||
import { documentationReducer } from './documentation/documentation.reducer';
|
||||
import { clusterSummaryFeatureKey, ClusterSummaryState } from './cluster-summary';
|
||||
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 {
|
||||
[DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
|
||||
|
@ -47,6 +49,7 @@ export interface NiFiState {
|
|||
[extensionTypesFeatureKey]: ExtensionTypesState;
|
||||
[aboutFeatureKey]: AboutState;
|
||||
[flowConfigurationFeatureKey]: FlowConfigurationState;
|
||||
[loginConfigurationFeatureKey]: LoginConfigurationState;
|
||||
[statusHistoryFeatureKey]: StatusHistoryState;
|
||||
[controllerServiceStateFeatureKey]: ControllerServiceState;
|
||||
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
|
||||
|
@ -62,6 +65,7 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
|
|||
[extensionTypesFeatureKey]: extensionTypesReducer,
|
||||
[aboutFeatureKey]: aboutReducer,
|
||||
[flowConfigurationFeatureKey]: flowConfigurationReducer,
|
||||
[loginConfigurationFeatureKey]: loginConfigurationReducer,
|
||||
[statusHistoryFeatureKey]: statusHistoryReducer,
|
||||
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
|
||||
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
|
||||
|
|
|
@ -15,19 +15,20 @@
|
|||
* 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', () => {
|
||||
let service: LoadingInterceptor;
|
||||
export interface LoginConfiguration {
|
||||
loginSupported: boolean;
|
||||
externalLoginRequired: boolean;
|
||||
loginUri: string;
|
||||
logoutUri: string;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(LoadingInterceptor);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
export interface LoginConfigurationState {
|
||||
loginConfiguration: LoginConfiguration | null;
|
||||
status: 'pending' | 'loading' | 'success';
|
||||
}
|
|
@ -15,19 +15,12 @@
|
|||
* 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', () => {
|
||||
let service: AuthStorage;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AuthStorage);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
export const loadLoginConfigurationSuccess = createAction(
|
||||
'[Login Configuration] Load Login Configuration Success',
|
||||
props<{ response: LoadLoginConfigurationResponse }>()
|
||||
);
|
|
@ -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.
|
||||
*/
|
||||
|
||||
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';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../state/error/error.reducer';
|
||||
export const initialState: LoginConfigurationState = {
|
||||
loginConfiguration: null,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
describe('AuthInterceptor', () => {
|
||||
let service: AuthInterceptor;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState
|
||||
})
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(AuthInterceptor);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
export const loginConfigurationReducer = createReducer(
|
||||
initialState,
|
||||
on(loadLoginConfiguration, (state) => ({
|
||||
...state,
|
||||
status: 'loading' as const
|
||||
})),
|
||||
on(loadLoginConfigurationSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
loginConfiguration: response.loginConfiguration,
|
||||
status: 'success' as const
|
||||
}))
|
||||
);
|
|
@ -15,23 +15,23 @@
|
|||
* 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';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../state/current-user/current-user.reducer';
|
||||
export const selectLoginConfigurationState =
|
||||
createFeatureSelector<LoginConfigurationState>(loginConfigurationFeatureKey);
|
||||
|
||||
describe('PollingInterceptor', () => {
|
||||
let service: PollingInterceptor;
|
||||
export const selectLoginConfiguration = createSelector(
|
||||
selectLoginConfigurationState,
|
||||
(state: LoginConfigurationState) => state.loginConfiguration
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideMockStore({ initialState })]
|
||||
});
|
||||
service = TestBed.inject(PollingInterceptor);
|
||||
});
|
||||
export const selectLoginUri = createSelector(
|
||||
selectLoginConfiguration,
|
||||
(loginConfiguration: LoginConfiguration | null) => loginConfiguration?.loginUri
|
||||
);
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
export const selectLogoutUri = createSelector(
|
||||
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 { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
||||
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', () => {
|
||||
let component: AdvancedUi;
|
||||
|
@ -59,6 +61,10 @@ describe('AdvancedUi', () => {
|
|||
{
|
||||
selector: selectFlowConfiguration,
|
||||
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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
describe('ExtensionCreation', () => {
|
||||
let component: ExtensionCreation;
|
||||
|
@ -26,7 +27,13 @@ describe('ExtensionCreation', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ExtensionCreation, NoopAnimationsModule]
|
||||
imports: [ExtensionCreation, NoopAnimationsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: null
|
||||
}
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(ExtensionCreation);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -32,11 +32,13 @@
|
|||
@if (currentUser(); as user) {
|
||||
<div class="flex justify-between items-center gap-x-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)) {
|
||||
<a href="#">log in</a>
|
||||
}
|
||||
@if (hasToken()) {
|
||||
@if (user.logoutSupported) {
|
||||
<a (click)="logout()">log out</a>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,8 @@ import { selectClusterSummary } from '../../../state/cluster-summary/cluster-sum
|
|||
import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
|
||||
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
||||
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', () => {
|
||||
let component: Navigation;
|
||||
|
@ -51,6 +53,10 @@ describe('Navigation', () => {
|
|||
{
|
||||
selector: selectFlowConfiguration,
|
||||
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 { getSystemDiagnosticsAndOpenDialog } from '../../../state/system-diagnostics/system-diagnostics.actions';
|
||||
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 { RouterLink } from '@angular/router';
|
||||
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 { DARK_THEME, LIGHT_THEME, OS_SETTING, ThemingService } from '../../../service/theming.service';
|
||||
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 {
|
||||
loadClusterSummary,
|
||||
|
@ -43,6 +45,7 @@ import {
|
|||
stopClusterSummaryPolling
|
||||
} from '../../../state/cluster-summary/cluster-summary.actions';
|
||||
import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
|
||||
import { selectLoginConfiguration } from '../../../state/login-configuration/login-configuration.selectors';
|
||||
|
||||
@Component({
|
||||
selector: 'navigation',
|
||||
|
@ -69,12 +72,11 @@ export class Navigation implements OnInit, OnDestroy {
|
|||
OS_SETTING: string = OS_SETTING;
|
||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||
flowConfiguration = this.store.selectSignal(selectFlowConfiguration);
|
||||
loginConfiguration = this.store.selectSignal(selectLoginConfiguration);
|
||||
clusterSummary = this.store.selectSignal(selectClusterSummary);
|
||||
|
||||
constructor(
|
||||
private store: Store<NiFiState>,
|
||||
private authStorage: AuthStorage,
|
||||
private authService: AuthService,
|
||||
private storage: Storage,
|
||||
private themingService: ThemingService
|
||||
) {
|
||||
|
@ -104,15 +106,16 @@ export class Navigation implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
allowLogin(user: CurrentUser): boolean {
|
||||
return user.anonymous && location.protocol === 'https:';
|
||||
}
|
||||
|
||||
hasToken(): boolean {
|
||||
return this.authStorage.hasToken();
|
||||
const loginConfig = this.loginConfiguration();
|
||||
if (loginConfig) {
|
||||
return user.anonymous && loginConfig.loginSupported;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
this.store.dispatch(logout());
|
||||
}
|
||||
|
||||
viewNodeStatusHistory(): void {
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
<div class="flex justify-between items-center gap-x-3">
|
||||
<h3 class="primary-color whitespace-nowrap overflow-hidden text-ellipsis" [title]="title">{{ title }}</h3>
|
||||
<div class="flex gap-x-3">
|
||||
@if (hasToken()) {
|
||||
@if (logoutSupported()) {
|
||||
<a (click)="logout()" class="whitespace-nowrap">log out</a>
|
||||
}
|
||||
<a [routerLink]="['/']">home</a>
|
||||
<a (click)="resetRoutedToFullScreenError()" [routerLink]="['/']">home</a>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
|
|
|
@ -21,6 +21,9 @@ import { PageContent } from './page-content.component';
|
|||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { RouterModule } from '@angular/router';
|
||||
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', () => {
|
||||
let component: PageContent;
|
||||
|
@ -28,7 +31,14 @@ describe('PageContent', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [PageContent, HttpClientTestingModule, RouterModule, RouterTestingModule]
|
||||
imports: [PageContent, HttpClientTestingModule, RouterModule, RouterTestingModule],
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState: {
|
||||
[currentUserFeatureKey]: initialState
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(PageContent);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -16,9 +16,12 @@
|
|||
*/
|
||||
|
||||
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 { 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({
|
||||
selector: 'page-content',
|
||||
|
@ -30,16 +33,15 @@ import { RouterLink } from '@angular/router';
|
|||
export class PageContent {
|
||||
@Input() title = '';
|
||||
|
||||
constructor(
|
||||
private authStorage: AuthStorage,
|
||||
private authService: AuthService
|
||||
) {}
|
||||
logoutSupported = this.store.selectSignal(selectLogoutSupported);
|
||||
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
resetRoutedToFullScreenError(): void {
|
||||
this.store.dispatch(setRoutedToFullScreenError({ routedToFullScreenError: false }));
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
hasToken(): boolean {
|
||||
return this.authStorage.hasToken();
|
||||
this.store.dispatch(logout());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { StatusHistory } from './status-history.component';
|
|||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
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', () => {
|
||||
let component: StatusHistory;
|
||||
|
@ -30,7 +30,14 @@ describe('StatusHistory', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
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);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -20,6 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
import { SystemDiagnosticsDialog } from './system-diagnostics-dialog.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialSystemDiagnosticsState } from '../../../state/system-diagnostics/system-diagnostics.reducer';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
describe('SystemDiagnosticsDialog', () => {
|
||||
let component: SystemDiagnosticsDialog;
|
||||
|
@ -28,7 +29,13 @@ describe('SystemDiagnosticsDialog', () => {
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SystemDiagnosticsDialog],
|
||||
providers: [provideMockStore({ initialState: initialSystemDiagnosticsState })]
|
||||
providers: [
|
||||
provideMockStore({ initialState: initialSystemDiagnosticsState }),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: null
|
||||
}
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(SystemDiagnosticsDialog);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -19,47 +19,6 @@
|
|||
version="6.0">
|
||||
<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-name>SanitizeContextPathFilter</filter-name>
|
||||
<filter-class>org.apache.nifi.web.filter.SanitizeContextPathFilter</filter-class>
|
||||
|
|
Loading…
Reference in New Issue