This is an automated email from the ASF dual-hosted git repository.
kdoran pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new edf5372af6 NIFI-14447 Add NiFi Registry login/logout and auth guards
edf5372af6 is described below
commit edf5372af624e12fc5e8c3f7a00dcbb483eb5817
Author: Scott Aslan <[email protected]>
AuthorDate: Thu Oct 2 11:02:00 2025 -0400
NIFI-14447 Add NiFi Registry login/logout and auth guards
This closes #10370.
Signed-off-by: Kevin Doran <[email protected]>
---
.../src/main/frontend/apps/nifi-registry/README.md | 334 +++++++++++++++++++++
.../nifi-registry/src/app/app-routing.module.ts | 58 +++-
.../apps/nifi-registry/src/app/app.module.ts | 6 +-
.../buckets/feature/buckets-routing.module.ts | 5 +-
.../buckets/feature/buckets.component.spec.ts | 10 +-
.../feature/_login.component-theme.scss} | 29 +-
.../feature/login-routing.module.ts} | 19 +-
.../app/pages/login/feature/login.component.html | 36 +++
.../feature/login.component.scss} | 25 +-
.../src/app/pages/login/feature/login.component.ts | 42 +++
.../src/app/pages/login/feature/login.module.ts | 42 +++
.../state/access/access.actions.ts} | 30 +-
.../app/pages/login/state/access/access.effects.ts | 82 +++++
.../app/pages/login/state/access/access.reducer.ts | 48 +++
.../state/access/access.selectors.ts} | 35 +--
.../state/access/index.ts} | 31 +-
.../state/index.ts} | 37 +--
.../login/ui/login-form/login-form.component.html | 50 +++
.../ui/login-form/login-form.component.scss} | 27 +-
.../ui/login-form/login-form.component.spec.ts | 102 +++++++
.../login/ui/login-form/login-form.component.ts | 88 ++++++
.../resources/feature/resources-routing.module.ts | 5 +-
.../resources/feature/resources.component.spec.ts | 18 +-
.../droplet-versions-dialog.component.spec.ts | 2 +-
.../export-droplet-version-dialog.component.html | 2 +-
...export-droplet-version-dialog.component.spec.ts | 2 +-
.../guard/admin-tenants.guard.ts} | 31 +-
.../guard/admin-workflow.guard.ts} | 31 +-
.../src/app/service/guard/base-guard.utils.ts | 123 ++++++++
.../guard/index.ts} | 27 +-
.../src/app/service/guard/login.guard.ts | 85 ++++++
.../guard/resources.guard.ts} | 29 +-
.../interceptors/registry-auth.interceptor.ts | 41 +++
.../src/app/service/registry-api.service.ts | 64 ++++
.../src/app/service/registry-auth.service.ts | 129 ++++++++
.../app/state/current-user/current-user.actions.ts | 40 +++
.../current-user/current-user.effects.spec.ts | 148 +++++++++
.../app/state/current-user/current-user.effects.ts | 140 +++++++++
.../app/state/current-user/current-user.reducer.ts | 75 +++++
.../state/current-user/current-user.selectors.ts | 44 +++
.../src/app/state/current-user/index.ts | 54 ++++
.../apps/nifi-registry/src/app/state/index.ts | 7 +-
.../src/app/ui/header/header.component.html | 14 +-
.../src/app/ui/header/header.component.spec.ts | 27 +-
.../src/app/ui/header/header.component.ts | 21 ++
.../frontend/apps/nifi-registry/src/styles.scss | 3 +
46 files changed, 2021 insertions(+), 277 deletions(-)
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/README.md
b/nifi-frontend/src/main/frontend/apps/nifi-registry/README.md
new file mode 100644
index 0000000000..fc3091a28b
--- /dev/null
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/README.md
@@ -0,0 +1,334 @@
+<!--
+ 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.
+-->
+
+# NiFi Registry LDAP setup
+
+This document contains instructions for testing the NiFi Registry LDAP
Identity Provider and LDAP User Group Provider.
+
+It utilizes a publicly accessible LDAP test server, made available by Forum
Systems. For more information on this server, see:
+
+http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/
+
+## Configuring NiFi Registry for HTTPS
+
+nifi-registry.properties is located in
nifi-registry/nifi-registry-assembly/target/nifi-registry-2.7.0-SNAPSHOT-bin/nifi-registry-2.7.0-SNAPSHOT/conf/.
You will need to update that file to configure the nifi registry to run
securely with LDAP.
+
+For running a NiFi Registry on localhost, you should already have test keys
and certs in your locally built clone of the nifi registry source code
repository:
+
+
/path/to/nifi/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys
+ +-- registry-ks.jks
+ +-- ca-ts.jks
+
+You need to copy those files into
nifi-registry/nifi-registry-assembly/target/nifi-registry-2.7.0-SNAPSHOT-bin/nifi-registry-2.7.0-SNAPSHOT/conf/.
+
+The NiFi Registry Server will need to know about the key store, which has its
key/cert pair, and the trust store, which has the certificate for the root CA
that generated the key/cert pair. To configure this, set the following
properties in your nifi-registry.properties file:
+
+ nifi.registry.web.https.host=localhost
+ nifi.registry.web.https.port=18443
+
+ nifi.registry.security.keystore=./conf/registry-ks.jks
+ nifi.registry.security.keystoreType=JKS
+ nifi.registry.security.keystorePasswd=password
+ nifi.registry.security.keyPasswd=password
+ nifi.registry.security.truststore=./conf/ca-ts.jks
+ nifi.registry.security.truststoreType=JKS
+ nifi.registry.security.truststorePasswd=password
+ nifi.registry.security.needClientAuth=false
+
nifi.registry.security.authorizers.configuration.file=./conf/authorizers.xml
+ nifi.registry.security.authorizer=managed-authorizer
+
nifi.registry.security.identity.providers.configuration.file=./conf/identity-providers.xml
+ nifi.registry.security.identity.provider=ldap-identity-provider
+
+This will make the NiFi Registry available at https://localhost:18443.
+
+Now stop any running nifi registy you have running locally. In
nifi-registry/nifi-registry-assembly/target/nifi-registry-2.7.0-SNAPSHOT-bin/nifi-registry-2.7.0-SNAPSHOT/conf/
update the authorizers.xml and identity-providers.xml with the following:
+
+### (authorizers.xml)
+
+```
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<authorizers>
+
+ <!--
+ The LdapUserGroupProvider will retrieve users and groups from an LDAP
server. The users and groups
+ are not configurable.
+
+ 'Authentication Strategy' - How the connection to the LDAP server is
authenticated. Possible
+ values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+
+ 'Manager DN' - The DN of the manager that is used to bind to the LDAP
server to search for users.
+ 'Manager Password' - The password of the manager that is used to bind
to the LDAP server to
+ search for users.
+
+ 'TLS - Keystore' - Path to the Keystore that is used when connecting
to LDAP using LDAPS or START_TLS.
+ 'TLS - Keystore Password' - Password for the Keystore that is used
when connecting to LDAP
+ using LDAPS or START_TLS.
+ 'TLS - Keystore Type' - Type of the Keystore that is used when
connecting to LDAP using
+ LDAPS or START_TLS (i.e. JKS or PKCS12).
+ 'TLS - Truststore' - Path to the Truststore that is used when
connecting to LDAP using LDAPS or START_TLS.
+ 'TLS - Truststore Password' - Password for the Truststore that is used
when connecting to
+ LDAP using LDAPS or START_TLS.
+ 'TLS - Truststore Type' - Type of the Truststore that is used when
connecting to LDAP using
+ LDAPS or START_TLS (i.e. JKS or PKCS12).
+ 'TLS - Client Auth' - Client authentication policy when connecting to
LDAP using LDAPS or START_TLS.
+ Possible values are REQUIRED, WANT, NONE.
+ 'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS
or START_TLS. (i.e. TLS,
+ TLSv1.1, TLSv1.2, etc).
+ 'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut
down gracefully
+ before the target context is closed. Defaults to false.
+
+ 'Referral Strategy' - Strategy for handling referrals. Possible values
are FOLLOW, IGNORE, THROW.
+ 'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+ 'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+
+ 'Url' - Space-separated list of URLs of the LDAP servers (i.e.
ldap://<hostname>:<port>).
+ 'Page Size' - Sets the page size when retrieving users and groups. If
not specified, no paging is performed.
+ 'Sync Interval' - Duration of time between syncing users and groups.
(i.e. 30 mins).
+
+ 'User Search Base' - Base DN for searching for users (i.e.
ou=users,o=nifi). Required to search users.
+ 'User Object Class' - Object class for identifying users (i.e.
person). Required if searching users.
+ 'User Search Scope' - Search scope for searching users (ONE_LEVEL,
OBJECT, or SUBTREE). Required if searching users.
+ 'User Search Filter' - Filter for searching for users against the
'User Search Base' (i.e. (memberof=cn=team1,ou=groups,o=nifi) ). Optional.
+ 'User Identity Attribute' - Attribute to use to extract user identity
(i.e. cn). Optional. If not set, the entire DN is used.
+ 'User Group Name Attribute' - Attribute to use to define group
membership (i.e. memberof). Optional. If not set
+ group membership will not be calculated through the users. Will
rely on group membership being defined
+ through 'Group Member Attribute' if set.
+
+ 'Group Search Base' - Base DN for searching for groups (i.e.
ou=groups,o=nifi). Required to search groups.
+ 'Group Object Class' - Object class for identifying groups (i.e.
groupOfNames). Required if searching groups.
+ 'Group Search Scope' - Search scope for searching groups (ONE_LEVEL,
OBJECT, or SUBTREE). Required if searching groups.
+ 'Group Search Filter' - Filter for searching for groups against the
'Group Search Base'. Optional.
+ 'Group Name Attribute' - Attribute to use to extract group name (i.e.
cn). Optional. If not set, the entire DN is used.
+ 'Group Member Attribute' - Attribute to use to define group membership
(i.e. member). Optional. If not set
+ group membership will not be calculated through the groups. Will
rely on group member being defined
+ through 'User Group Name Attribute' if set.
+
+ NOTE: Any identity mapping rules specified in nifi-registry.properties
will also be applied to the user identities.
+ Group names are not mapped.
+ -->
+ <userGroupProvider>
+ <identifier>ldap-user-group-provider</identifier>
+
<class>org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider</class>
+ <property name="Authentication Strategy">SIMPLE</property>
+
+ <property name="Manager
DN">cn=read-only-admin,dc=example,dc=com</property>
+ <property name="Manager Password">password</property>
+
+ <!--
+ <property name="TLS - Keystore"></property>
+ <property name="TLS - Keystore Password"></property>
+ <property name="TLS - Keystore Type"></property>
+ <property name="TLS - Truststore"></property>
+ <property name="TLS - Truststore Password"></property>
+ <property name="TLS - Truststore Type"></property>
+ <property name="TLS - Client Auth"></property>
+ <property name="TLS - Protocol"></property>
+ <property name="TLS - Shutdown Gracefully"></property>
+ -->
+
+ <property name="Referral Strategy">FOLLOW</property>
+ <property name="Connect Timeout">10 secs</property>
+ <property name="Read Timeout">10 secs</property>
+
+ <property name="Url">ldap://ldap.forumsys.com:389</property>
+ <!--<property name="Page Size"></property>-->
+ <property name="Sync Interval">30 mins</property>
+
+ <property name="User Search Base">dc=example,dc=com</property>
+ <property name="User Object Class">person</property>
+ <property name="User Search Scope">ONE_LEVEL</property>
+ <property name="User Search Filter">(uid=*)</property>
+ <property name="User Identity Attribute">uid</property>
+ <!--<property name="User Group Name Attribute">ou</property>-->
+
+ <property name="Group Search Base">dc=example,dc=com</property>
+ <property name="Group Object Class">groupOfUniqueNames</property>
+ <property name="Group Search Scope">ONE_LEVEL</property>
+ <property name="Group Search Filter">(ou=*)</property>
+ <property name="Group Name Attribute">ou</property>
+ <property name="Group Member Attribute">uniqueMember</property>
+ </userGroupProvider>
+
+ <!--
+ The FileAccessPolicyProvider will provide support for managing access
policies which is backed by a file
+ on the local file system.
+
+ - User Group Provider - The identifier for an User Group Provider
defined above that will be used to access
+ users and groups for use in the managed access policies.
+
+ - Authorizations File - The file where the FileAccessPolicyProvider
will store policies.
+
+ - Initial Admin Identity - The identity of an initial admin user that
will be granted access to the UI and
+ given the ability to create additional users, groups, and
policies. The value of this property could be
+ a DN when using certificates or LDAP. This property will only be
used when there
+ are no other policies defined.
+
+ NOTE: Any identity mapping rules specified in
nifi-registry.properties will also be applied to the initial admin identity,
+ so the value should be the unmapped identity. This identity must
be found in the configured User Group Provider.
+
+ - NiFi Identity [unique key] - The identity of a NiFi node that will
have access to this NiFi Registry and will be able
+ to act as a proxy on behalf of a NiFi Registry end user. A
property should be created for the identity of every NiFi
+ node that needs to access this NiFi Registry. The name of each
property must be unique, for example for three
+ NiFi clients:
+ "NiFi Identity A", "NiFi Identity B", "NiFi Identity C" or "NiFi
Identity 1", "NiFi Identity 2", "NiFi Identity 3"
+
+ NOTE: Any identity mapping rules specified in
nifi-registry.properties will also be applied to the nifi identities,
+ so the values should be the unmapped identities (i.e. full DN from
a certificate). This identity must be found
+ in the configured User Group Provider.
+ -->
+ <accessPolicyProvider>
+ <identifier>file-access-policy-provider</identifier>
+
<class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+ <property name="User Group
Provider">ldap-user-group-provider</property>
+ <property name="Authorizations
File">./conf/authorizations.xml</property>
+ <property name="Initial Admin Identity">nobel</property>
+
+ <!--<property name="NiFi Identity 1"></property>-->
+ </accessPolicyProvider>
+
+ <!--
+ The StandardManagedAuthorizer. This authorizer implementation must be
configured with the
+ Access Policy Provider which it will use to access and manage users,
groups, and policies.
+ These users, groups, and policies will be used to make all access
decisions during authorization
+ requests.
+
+ - Access Policy Provider - The identifier for an Access Policy
Provider defined above.
+ -->
+ <authorizer>
+ <identifier>managed-authorizer</identifier>
+
<class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+ <property name="Access Policy
Provider">file-access-policy-provider</property>
+ </authorizer>
+
+</authorizers>
+```
+
+### (identity-providers.xml)
+
+```
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<identityProviders>
+ <!--
+ Identity Provider for users logging in with username/password against
an LDAP server.
+
+ 'Authentication Strategy' - How the connection to the LDAP server is
authenticated. Possible
+ values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+
+ 'Manager DN' - The DN of the manager that is used to bind to the LDAP
server to search for users.
+ 'Manager Password' - The password of the manager that is used to bind
to the LDAP server to
+ search for users.
+
+ 'TLS - Keystore' - Path to the Keystore that is used when connecting
to LDAP using LDAPS or START_TLS.
+ 'TLS - Keystore Password' - Password for the Keystore that is used
when connecting to LDAP
+ using LDAPS or START_TLS.
+ 'TLS - Keystore Type' - Type of the Keystore that is used when
connecting to LDAP using
+ LDAPS or START_TLS (i.e. JKS or PKCS12).
+ 'TLS - Truststore' - Path to the Truststore that is used when
connecting to LDAP using LDAPS or START_TLS.
+ 'TLS - Truststore Password' - Password for the Truststore that is used
when connecting to
+ LDAP using LDAPS or START_TLS.
+ 'TLS - Truststore Type' - Type of the Truststore that is used when
connecting to LDAP using
+ LDAPS or START_TLS (i.e. JKS or PKCS12).
+ 'TLS - Client Auth' - Client authentication policy when connecting to
LDAP using LDAPS or START_TLS.
+ Possible values are REQUIRED, WANT, NONE.
+ 'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS
or START_TLS. (i.e. TLS,
+ TLSv1.1, TLSv1.2, etc).
+ 'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut
down gracefully
+ before the target context is closed. Defaults to false.
+
+ 'Referral Strategy' - Strategy for handling referrals. Possible values
are FOLLOW, IGNORE, THROW.
+ 'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+ 'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+
+ 'Url' - Space-separated list of URLs of the LDAP servers (i.e.
ldap://<hostname>:<port>).
+ 'User Search Base' - Base DN for searching for users (i.e.
CN=Users,DC=example,DC=com).
+ 'User Search Filter' - Filter for searching for users against the
'User Search Base'.
+ (i.e. sAMAccountName={0}). The user specified name is inserted
into '{0}'.
+
+ 'Identity Strategy' - Strategy to identify users. Possible values are
USE_DN and USE_USERNAME.
+ The default functionality if this property is missing is USE_DN in
order to retain
+ backward compatibility. USE_DN will use the full DN of the user
entry if possible.
+ USE_USERNAME will use the username the user logged in with.
+ 'Authentication Expiration' - The duration of how long the user
authentication is valid
+ for. If the user never logs out, they will be required to log back
in following
+ this duration.
+ -->
+ <provider>
+ <identifier>ldap-identity-provider</identifier>
+
<class>org.apache.nifi.registry.security.ldap.LdapIdentityProvider</class>
+ <property name="Authentication Strategy">SIMPLE</property>
+
+ <property name="Manager
DN">cn=read-only-admin,dc=example,dc=com</property>
+ <property name="Manager Password">password</property>
+
+ <property name="Referral Strategy">FOLLOW</property>
+ <property name="Connect Timeout">10 secs</property>
+ <property name="Read Timeout">10 secs</property>
+
+ <property name="Url">ldap://ldap.forumsys.com:389</property>
+ <property name="User Search Base">dc=example,dc=com</property>
+ <property name="User Search Filter">(uid={0})</property>
+
+ <!--<property name="Identity Strategy">USE_DN</property>-->
+ <property name="Identity Strategy">USE_USERNAME</property>
+ <property name="Authentication Expiration">12 hours</property>
+ </provider>
+
+</identityProviders>
+```
+
+Now delete the users.xml file if you have onw. It will get recreated when you
restart the nifi registry backend.
+
+Next, update your
nifi-frontend/src/main/frontend/apps/nifi-registry/proxy.config.mjs so that
when you start your local frontend dev server it will proxy through to the
secured nifi registry backend:
+
+```
+const target = {
+ target: 'https://localhost:18443',
+ secure: false,
+ logLevel: 'debug',
+ changeOrigin: true,
+ headers: {
+ 'X-ProxyScheme': 'http',
+ 'X-ProxyPort': 4204
+ },
+ configure: (proxy, _options) => {
+ proxy.on('error', (err, _req, _res) => {
+ console.log('proxy error', err);
+ });
+ proxy.on('proxyReq', (proxyReq, req, _res) => {
+ console.log('Sending Request to the Target:', req.method, req.url);
+ });
+ proxy.on('proxyRes', (proxyRes, req, _res) => {
+ console.log('Received Response from the Target:',
proxyRes.statusCode, req.url);
+ });
+ },
+ bypass: function (req) {
+ if (req.url.startsWith('/nifi-registry/')) {
+ return req.url;
+ }
+ }
+};
+
+export default {
+ '/**': target
+};
+
+```
+
+Now start the nifi registry backend again. You should be able to log in with
one of the test users listed on the forumsys.com page, such as:
+
+ User: nobel
+ Password: password
+
+Done!!! Good job!
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
index 64924f5986..dce8d715c6 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app-routing.module.ts
@@ -18,6 +18,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RouteNotFound } from
'./pages/route-not-found/feature/route-not-found.component';
+import {
+ resourcesGuard,
+ resourcesActivateGuard,
+ adminTenantsGuard,
+ adminTenantsActivateGuard,
+ adminWorkflowGuard,
+ adminWorkflowActivateGuard,
+ loginGuard
+} from './service/guard';
const routes: Routes = [
{
@@ -25,30 +34,59 @@ const routes: Routes = [
redirectTo: 'explorer',
pathMatch: 'full'
},
+ {
+ path: 'nifi-registry',
+ redirectTo: 'explorer',
+ pathMatch: 'full'
+ },
+ {
+ path: 'login',
+ canActivate: [loginGuard],
+ loadChildren: () =>
import('./pages/login/feature/login.module').then((m) => m.LoginModule)
+ },
{
path: 'explorer',
- loadChildren: () =>
import('./pages/resources/feature/resources.module').then((m) =>
m.ResourcesModule)
+ loadChildren: () =>
import('./pages/resources/feature/resources.module').then((m) =>
m.ResourcesModule),
+ canMatch: [resourcesGuard],
+ canActivate: [resourcesActivateGuard]
},
{
path: 'buckets',
- loadChildren: () =>
import('./pages/buckets/feature/buckets.module').then((m) => m.BucketsModule)
+ loadChildren: () =>
import('./pages/buckets/feature/buckets.module').then((m) => m.BucketsModule),
+ canMatch: [adminWorkflowGuard],
+ canActivate: [adminWorkflowActivateGuard]
},
{
path: 'administration',
children: [
+ {
+ path: '',
+ redirectTo: 'workflow',
+ pathMatch: 'full'
+ },
{
path: 'workflow',
- children: [
- {
- path: '',
- redirectTo: '/buckets',
- pathMatch: 'full'
- }
- ]
+ canMatch: [adminWorkflowGuard],
+ canActivate: [adminWorkflowActivateGuard],
+ loadChildren: () =>
import('./pages/buckets/feature/buckets.module').then((m) => m.BucketsModule)
+ },
+ {
+ path: 'users',
+ canMatch: [adminTenantsGuard],
+ canActivate: [adminTenantsActivateGuard],
+ loadChildren: () =>
import('./pages/buckets/feature/buckets.module').then((m) => m.BucketsModule)
}
]
},
- // TODO: Users/groups
+ {
+ path: 'explorer/grid-list',
+ redirectTo: 'explorer',
+ pathMatch: 'full'
+ },
+ {
+ path: 'explorer/grid-list/**',
+ redirectTo: 'explorer'
+ },
{
path: '**',
component: RouteNotFound
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.module.ts
index 2826024471..293abc6f8f 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.module.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/app.module.ts
@@ -27,6 +27,7 @@ import {
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { provideHttpClient, withInterceptors, withXsrfConfiguration } from
'@angular/common/http';
+import { registryAuthInterceptor } from
'./service/interceptors/registry-auth.interceptor';
import { NavigationActionTiming, RouterState, StoreRouterConnectingModule }
from '@ngrx/router-store';
import { EffectsModule } from '@ngrx/effects';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
@@ -34,6 +35,7 @@ import { rootReducers } from './state';
import { ErrorEffects } from './state/error/error.effects';
import { AboutEffects } from './state/about/about.effects';
import { environment } from '../environments/environment';
+import { CurrentUserEffects } from './state/current-user/current-user.effects';
const entry = localStorage.getItem('disable-animations');
let disableAnimations = '';
@@ -65,7 +67,7 @@ try {
routerState: RouterState.Minimal,
navigationActionTiming: NavigationActionTiming.PostActivation
}),
- EffectsModule.forRoot(ErrorEffects, AboutEffects),
+ EffectsModule.forRoot(ErrorEffects, CurrentUserEffects, AboutEffects),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production,
@@ -78,7 +80,7 @@ try {
disableAnimations === 'true' ? provideNoopAnimations() :
provideAnimations(),
{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance:
'outline' } },
provideHttpClient(
- withInterceptors([]),
+ withInterceptors([registryAuthInterceptor]),
withXsrfConfiguration({
cookieName: '__Secure-Request-Token',
headerName: 'Request-Token'
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
index c1152fc9ea..10010a8461 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
@@ -18,15 +18,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BucketsComponent } from './buckets.component';
+import { adminWorkflowActivateGuard } from
'../../../service/guard/admin-workflow.guard';
const routes: Routes = [
{
path: '',
component: BucketsComponent,
+ canActivate: [adminWorkflowActivateGuard],
children: [
{
path: ':id',
- component: BucketsComponent
+ component: BucketsComponent,
+ canActivate: [adminWorkflowActivateGuard]
}
]
}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.spec.ts
index e0cf59de5d..57bc0ab289 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets.component.spec.ts
@@ -25,6 +25,11 @@ import { ContextErrorBanner } from
'../../../ui/common/context-error-banner/cont
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { Router } from '@angular/router';
+import { HeaderComponent } from '../../../ui/header/header.component';
+import { currentUserFeatureKey } from '../../../state/current-user';
+import { initialState as currentUserInitialState } from
'../../../state/current-user/current-user.reducer';
+import { aboutFeatureKey } from '../../../state/about';
+import { initialState as aboutInitialState } from
'../../../state/about/about.reducer';
describe('BucketsComponent', () => {
let component: BucketsComponent;
@@ -38,7 +43,8 @@ describe('BucketsComponent', () => {
BucketTableFilterComponent,
ContextErrorBanner,
MatButtonModule,
- MatIconModule
+ MatIconModule,
+ HeaderComponent
],
providers: [
provideMockStore({
@@ -49,6 +55,8 @@ describe('BucketsComponent', () => {
status: 'pending'
}
},
+ [currentUserFeatureKey]: currentUserInitialState,
+ [aboutFeatureKey]: aboutInitialState,
error: {
bannerErrors: {}
}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/_login.component-theme.scss
similarity index 58%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/_login.component-theme.scss
index c1152fc9ea..4b6e7900e6 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/_login.component-theme.scss
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,12 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+@use 'sass:map';
+@use '@angular/material' as mat;
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
+@mixin generate-theme() {
+ .login-background {
+ background: var(--mat-sys-background)
url(../../../../../../../libs/shared/src/assets/icons/bg-error.png) left
+ top no-repeat;
}
-];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login-routing.module.ts
similarity index 71%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login-routing.module.ts
index c1152fc9ea..15a37b28ff 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login-routing.module.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -17,23 +17,12 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+import { LoginComponent } from './login.component';
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
+const routes: Routes = [{ path: '', component: LoginComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
-export class BucketsRoutingModule {}
+export class LoginRoutingModule {}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.html
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.html
new file mode 100644
index 0000000000..e351496e37
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.html
@@ -0,0 +1,36 @@
+<!--
+ ~ 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.
+ -->
+
+@if (loading) {
+ <div class="login-splash h-full">
+ <div class="splash-img h-full flex items-center justify-center">
+ <mat-spinner></mat-spinner>
+ </div>
+ </div>
+} @else {
+ <div class="login-background pt-24 pl-24 h-full">
+ @if (currentUserState$ | async; as userState) {
+ @if (userState.status === 'success' &&
!userState.currentUser.anonymous) {
+ <page-content [title]="'Success'">
+ <div class="text-base">Already logged in. Click home to
return to the resources explorer.</div>
+ </page-content>
+ } @else {
+ <nifi-registry-login-form></nifi-registry-login-form>
+ }
+ }
+ </div>
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.scss
similarity index 57%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.scss
index c1152fc9ea..2944f98194 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.scss
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -14,26 +14,3 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
-
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.ts
new file mode 100644
index 0000000000..c6cee35f3b
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.component.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { Component, inject } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { NiFiRegistryState } from '../../../state';
+import { selectCurrentUserState } from
'../../../state/current-user/current-user.selectors';
+import { Observable } from 'rxjs';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+
+@Component({
+ selector: 'nifi-registry-login',
+ templateUrl: './login.component.html',
+ styleUrls: ['./login.component.scss'],
+ standalone: false
+})
+export class LoginComponent {
+ private store = inject<Store<NiFiRegistryState>>(Store);
+
+ loading = true;
+ currentUserState$: Observable<any> =
this.store.select(selectCurrentUserState);
+
+ constructor() {
+ this.currentUserState$.pipe(takeUntilDestroyed()).subscribe((state) =>
{
+ this.loading = state.status === 'loading';
+ });
+ }
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.module.ts
new file mode 100644
index 0000000000..0f55511eb0
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/feature/login.module.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { LoginComponent } from './login.component';
+import { LoginRoutingModule } from './login-routing.module';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { LoginFormComponent } from '../ui/login-form/login-form.component';
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+import { loginFeatureKey, reducers } from '../state';
+import { AccessEffects } from '../state/access/access.effects';
+import { PageContent } from
'../../../ui/common/page-content/page-content.component';
+
+@NgModule({
+ declarations: [LoginComponent],
+ imports: [
+ CommonModule,
+ LoginRoutingModule,
+ MatProgressSpinnerModule,
+ LoginFormComponent,
+ StoreModule.forFeature(loginFeatureKey, reducers),
+ EffectsModule.forFeature(AccessEffects),
+ PageContent
+ ]
+})
+export class LoginModule {}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.actions.ts
similarity index 57%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.actions.ts
index c1152fc9ea..f6451e8979 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.actions.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,13 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+import { createAction, props } from '@ngrx/store';
+import { LoginRequest } from './index';
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
+export const login = createAction('[Access] Login', props<{ request:
LoginRequest }>());
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+export const loginSuccess = createAction('[Access] Login Success');
+
+export const loginFailure = createAction('[Access] Login Failure', props<{
loginFailure: string }>());
+
+export const resetLoginFailure = createAction('[Access] Reset Login Failure');
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.effects.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.effects.ts
new file mode 100644
index 0000000000..eb5e507ece
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.effects.ts
@@ -0,0 +1,82 @@
+/*
+ * 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, inject } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import * as AccessActions from './access.actions';
+import { catchError, map, of, switchMap, tap } from 'rxjs';
+import { RegistryApiService } from '../../../../service/registry-api.service';
+import { RegistryAuthService } from
'../../../../service/registry-auth.service';
+import { Router } from '@angular/router';
+import { HttpErrorResponse } from '@angular/common/http';
+import * as ErrorActions from '../../../../state/error/error.actions';
+import { Store } from '@ngrx/store';
+import { NiFiRegistryState } from '../../../../state';
+import { loadCurrentUser } from
'../../../../state/current-user/current-user.actions';
+
+@Injectable()
+export class AccessEffects {
+ private actions$ = inject(Actions);
+ private registryApi = inject(RegistryApiService);
+ private authService = inject(RegistryAuthService);
+ private router = inject(Router);
+ private store = inject<Store<NiFiRegistryState>>(Store);
+
+ login$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessActions.login),
+ switchMap(({ request }) =>
+ this.registryApi.login(request.username,
request.password).pipe(
+ map((jwt) => {
+ this.authService.storeToken(jwt);
+ return AccessActions.loginSuccess();
+ }),
+ catchError((error: HttpErrorResponse) =>
+ of(
+ AccessActions.loginFailure({
+ loginFailure:
+ error?.error?.message || error?.message ||
'Unable to login. Please try again.'
+ })
+ )
+ )
+ )
+ )
+ )
+ );
+
+ loginSuccess$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(AccessActions.loginSuccess),
+ tap(() => {
+ const redirect = this.authService.consumeRedirectUrl() ||
'/explorer';
+ this.router.navigateByUrl(redirect);
+ this.store.dispatch(loadCurrentUser());
+ })
+ ),
+ { dispatch: false }
+ );
+
+ loginFailure$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AccessActions.loginFailure),
+ map(({ loginFailure }) =>
+ ErrorActions.snackBarError({ error: loginFailure || 'Unable to
login. Please try again.' })
+ )
+ )
+ );
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.reducer.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.reducer.ts
new file mode 100644
index 0000000000..1629de1e79
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.reducer.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { createReducer, on } from '@ngrx/store';
+import { AccessState } from './index';
+import { login, loginFailure, loginSuccess, resetLoginFailure } from
'./access.actions';
+
+export const initialState: AccessState = {
+ failure: null,
+ pending: false
+};
+
+export const accessReducer = createReducer(
+ initialState,
+ on(login, (state) => ({
+ ...state,
+ failure: null,
+ pending: true
+ })),
+ on(loginSuccess, (state) => ({
+ ...state,
+ failure: null,
+ pending: false
+ })),
+ on(loginFailure, (state, { loginFailure }) => ({
+ ...state,
+ failure: loginFailure,
+ pending: false
+ })),
+ on(resetLoginFailure, (state) => ({
+ ...state,
+ failure: null
+ }))
+);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.selectors.ts
similarity index 56%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.selectors.ts
index c1152fc9ea..10dfb5436a 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/access.selectors.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,18 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+import { createSelector } from '@ngrx/store';
+import { accessFeatureKey, AccessState } from './index';
+import { selectLoginState } from '../index';
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
+export const selectAccessState = createSelector(selectLoginState, (state) =>
state?.[accessFeatureKey]);
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+export const selectLoginFailure = createSelector(
+ selectAccessState,
+ (state: AccessState | undefined) => state?.failure ?? null
+);
+
+export const selectLoginPending = createSelector(
+ selectAccessState,
+ (state: AccessState | undefined) => state?.pending ?? false
+);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/index.ts
similarity index 57%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/index.ts
index c1152fc9ea..8d6fa4079d 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/access/index.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,14 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+export interface LoginRequest {
+ username: string;
+ password: string;
+}
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
+export interface AccessState {
+ failure: string | null;
+ pending: boolean;
+}
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+export const accessFeatureKey = 'access';
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/index.ts
similarity index 55%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/index.ts
index c1152fc9ea..7f8a03981b 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/state/index.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,20 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
+import { accessFeatureKey, AccessState } from './access';
+import { accessReducer } from './access/access.reducer';
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
+export const loginFeatureKey = 'login';
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+export interface LoginState {
+ [accessFeatureKey]: AccessState;
+}
+
+export function reducers(state: LoginState | undefined, action: Action) {
+ return combineReducers({
+ [accessFeatureKey]: accessReducer
+ })(state, action);
+}
+
+export const selectLoginState =
createFeatureSelector<LoginState>(loginFeatureKey);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.html
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.html
new file mode 100644
index 0000000000..2d34e7977c
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.html
@@ -0,0 +1,50 @@
+<!--
+ ~ 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.
+ -->
+
+<div class="login-form w-96 flex flex-col gap-y-5">
+ <div class="flex justify-between items-start">
+ <h3 class="primary-color">Log In</h3>
+ <div class="flex gap-x-2">
+ @if (logoutSupported()) {
+ <a (click)="logout()">log out</a>
+ }
+ <a [routerLink]="['/']">home</a>
+ </div>
+ </div>
+ <div>
+ <form [formGroup]="loginForm" (ngSubmit)="login()" class="my-2">
+ <mat-form-field>
+ <mat-label>Username</mat-label>
+ <input matInput formControlName="username" type="text"
autofocus />
+ </mat-form-field>
+
+ <mat-form-field>
+ <mat-label>Password</mat-label>
+ <input matInput formControlName="password" type="password" />
+ </mat-form-field>
+
+ <div class="flex justify-end">
+ <button
+ mat-button
+ type="submit"
+ [disabled]="!loginForm.dirty || loginForm.invalid ||
loginForm.pending || loginPending()">
+ Log in
+ </button>
+ </div>
+ </form>
+ </div>
+</div>
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.scss
similarity index 57%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.scss
index c1152fc9ea..07e19b8920 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.scss
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,8 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
-
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
+.login-form {
+ .mat-mdc-form-field {
+ width: 100%;
}
-];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.spec.ts
new file mode 100644
index 0000000000..d837ed4ad6
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.spec.ts
@@ -0,0 +1,102 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+import { LoginFormComponent } from './login-form.component';
+import { provideMockStore, MockStore } from '@ngrx/store/testing';
+import { NiFiRegistryState } from '../../../../state';
+import { login, resetLoginFailure } from '../../state/access/access.actions';
+import { logout } from '../../../../state/current-user/current-user.actions';
+import { provideHttpClientTesting } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { RouterTestingModule } from '@angular/router/testing';
+import { selectLoginFailure, selectLoginPending } from
'../../state/access/access.selectors';
+import { selectLogoutSupported } from
'../../../../state/current-user/current-user.selectors';
+
+describe('LoginFormComponent', () => {
+ let component: LoginFormComponent;
+ let fixture: ComponentFixture<LoginFormComponent>;
+ let store: MockStore<NiFiRegistryState>;
+ let dispatchSpy: jest.SpyInstance;
+ let loginFailureSelector: any;
+ let loginPendingSelector: any;
+ let logoutSupportedSelector: any;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ LoginFormComponent,
+ ReactiveFormsModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatButtonModule,
+ RouterTestingModule
+ ],
+ providers: [provideMockStore(), provideHttpClientTesting()]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(LoginFormComponent);
+ component = fixture.componentInstance;
+ store = TestBed.inject(MockStore);
+ loginFailureSelector = store.overrideSelector(selectLoginFailure,
null);
+ loginPendingSelector = store.overrideSelector(selectLoginPending,
false);
+ logoutSupportedSelector =
store.overrideSelector(selectLogoutSupported, false);
+ dispatchSpy = jest.spyOn(store, 'dispatch');
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should dispatch login action when form is valid and submitted', () => {
+ dispatchSpy.mockClear();
+ component.loginForm.setValue({ username: 'user', password: 'pass' });
+ component.login();
+ expect(store.dispatch).toHaveBeenCalledWith(
+ login({
+ request: {
+ username: 'user',
+ password: 'pass'
+ }
+ })
+ );
+ });
+
+ it('should not dispatch login when form is invalid', () => {
+ dispatchSpy.mockClear();
+ component.loginForm.setValue({ username: '', password: '' });
+ component.login();
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+
+ it('should dispatch logout when logout is called', () => {
+ dispatchSpy.mockClear();
+ component.logout();
+ expect(store.dispatch).toHaveBeenCalledWith(logout());
+ });
+
+ it('should dispatch resetLoginFailure when login failure occurs', () => {
+ dispatchSpy.mockClear();
+ loginFailureSelector.setResult('Invalid credentials');
+ store.refreshState();
+ expect(store.dispatch).toHaveBeenCalledWith(resetLoginFailure());
+ });
+});
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.ts
new file mode 100644
index 0000000000..5f33def082
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/login/ui/login-form/login-form.component.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 { Component, inject } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators
} from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { login, resetLoginFailure } from '../../state/access/access.actions';
+import { selectLoginFailure, selectLoginPending } from
'../../state/access/access.selectors';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { RouterLink } from '@angular/router';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { selectCurrentUser, selectLogoutSupported } from
'../../../../state/current-user/current-user.selectors';
+import { NiFiRegistryState } from '../../../../state';
+import { logout } from '../../../../state/current-user/current-user.actions';
+
+@Component({
+ selector: 'nifi-registry-login-form',
+ standalone: true,
+ templateUrl: './login-form.component.html',
+ styleUrls: ['./login-form.component.scss'],
+ imports: [ReactiveFormsModule, RouterLink, MatFormFieldModule,
MatInputModule, MatButtonModule]
+})
+export class LoginFormComponent {
+ private store = inject<Store<NiFiRegistryState>>(Store);
+ private formBuilder = inject(FormBuilder);
+
+ loginFailure = this.store.selectSignal(selectLoginFailure);
+ loginPending = this.store.selectSignal(selectLoginPending);
+ currentUser = this.store.selectSignal(selectCurrentUser);
+ logoutSupported = this.store.selectSignal(selectLogoutSupported);
+
+ loginForm: FormGroup;
+
+ constructor() {
+ this.loginForm = this.formBuilder.group({
+ username: new FormControl('', Validators.required),
+ password: new FormControl('', Validators.required)
+ });
+
+ this.store
+ .select(selectLoginFailure)
+ .pipe(takeUntilDestroyed())
+ .subscribe((failure) => {
+ if (failure) {
+ this.loginForm.get('password')?.setValue('');
+ this.store.dispatch(resetLoginFailure());
+ }
+ });
+ }
+
+ login(): void {
+ if (this.loginForm.invalid) {
+ return;
+ }
+
+ const username = this.loginForm.get('username')?.value;
+ const password = this.loginForm.get('password')?.value;
+
+ this.store.dispatch(
+ login({
+ request: {
+ username,
+ password
+ }
+ })
+ );
+ }
+
+ logout(): void {
+ this.store.dispatch(logout());
+ }
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources-routing.module.ts
index bc01e8e2b1..646110b37a 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources-routing.module.ts
@@ -18,15 +18,18 @@
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { ResourcesComponent } from './resources.component';
+import { resourcesActivateGuard } from
'../../../service/guard/resources.guard';
const routes: Routes = [
{
path: '',
component: ResourcesComponent,
+ canActivate: [resourcesActivateGuard],
children: [
{
path: ':id',
- component: ResourcesComponent
+ component: ResourcesComponent,
+ canActivate: [resourcesActivateGuard]
}
]
},
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.spec.ts
index 7574886f9c..4981a590b5 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/resources.component.spec.ts
@@ -19,8 +19,8 @@ import { ComponentFixture, TestBed } from
'@angular/core/testing';
import { RouterModule } from '@angular/router';
import { provideMockStore } from '@ngrx/store/testing';
import { ResourcesComponent } from './resources.component';
-import { initialState } from '../../../state/droplets/droplets.reducer';
-import { initialState as initialBucketState } from
'../../../state/buckets/buckets.reducer';
+import { initialState as dropletsInitialState } from
'../../../state/droplets/droplets.reducer';
+import { initialState as bucketsInitialState } from
'../../../state/buckets/buckets.reducer';
import { resourcesFeatureKey } from '../../../state';
import { dropletsFeatureKey } from '../../../state/droplets';
import { bucketsFeatureKey } from '../../../state/buckets';
@@ -29,6 +29,11 @@ import { DropletTableFilterComponent } from
'./ui/droplet-table-filter/droplet-t
import { ContextErrorBanner } from
'../../../ui/common/context-error-banner/context-error-banner.component';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
+import { HeaderComponent } from '../../../ui/header/header.component';
+import { currentUserFeatureKey } from '../../../state/current-user';
+import { initialState as currentUserInitialState } from
'../../../state/current-user/current-user.reducer';
+import { aboutFeatureKey } from '../../../state/about';
+import { initialState as aboutInitialState } from
'../../../state/about/about.reducer';
describe('Resources', () => {
let component: ResourcesComponent;
@@ -43,15 +48,18 @@ describe('Resources', () => {
DropletTableFilterComponent,
ContextErrorBanner,
MatButtonModule,
- MatIconModule
+ MatIconModule,
+ HeaderComponent
],
providers: [
provideMockStore({
initialState: {
[resourcesFeatureKey]: {
- [dropletsFeatureKey]: initialState,
- [bucketsFeatureKey]: initialBucketState
+ [dropletsFeatureKey]: dropletsInitialState,
+ [bucketsFeatureKey]: bucketsInitialState
},
+ [currentUserFeatureKey]: currentUserInitialState,
+ [aboutFeatureKey]: aboutInitialState,
error: {
bannerErrors: {}
}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.spec.ts
index d4f9e56a59..ee1f7bd828 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/droplet-versions-dialog/droplet-versions-dialog.component.spec.ts
@@ -39,7 +39,7 @@ describe('DropletVersionsDialogComponent', () => {
link: { href: 'testHref', params: { rel: 'testRel' } },
modifiedTimestamp: 123456789,
name: 'testName',
- permissions: { canRead: true, canWrite: true },
+ permissions: { canRead: true, canWrite: true, canDelete: true },
revision: { version: 1 },
type: 'FLOW',
versionCount: 1
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.html
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.html
index ff75765f36..cc2bdd9a98 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.html
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.html
@@ -25,7 +25,7 @@ limitations under the License.
<div class="w-full bucket-dropdown-field">
<mat-form-field class="flex w-full">
<mat-select [(value)]="selectedVersion">
- @for (snapshotMeta of
[].constructor(droplet.versionCount); track snapshotMeta; let i = $index) {
+ @for (snapshotMeta of
[].constructor(droplet.versionCount); track i; let i = $index) {
<mat-option [value]="i + 1">
@if (snapshotMeta === i) {
<span>Latest (Version {{ i + 1 }})</span>
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.spec.ts
index dbfe86c79e..52afdc9e8a 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/resources/feature/ui/export-droplet-version-dialog/export-droplet-version-dialog.component.spec.ts
@@ -38,7 +38,7 @@ describe('ExportDropletVersionDialogComponent', () => {
link: { href: 'testHref', params: { rel: 'testRel' } },
modifiedTimestamp: 123456789,
name: 'testName',
- permissions: { canRead: true, canWrite: true },
+ permissions: { canRead: true, canWrite: true, canDelete: true },
revision: { version: 1 },
type: 'FLOW',
versionCount: 2
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/admin-tenants.guard.ts
similarity index 54%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/admin-tenants.guard.ts
index c1152fc9ea..925499aa8d 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/admin-tenants.guard.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,14 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+import { CanActivateFn, CanMatchFn } from '@angular/router';
+import { buildCanActivateGuard, buildCanMatchGuard } from './base-guard.utils';
+import { CurrentUser } from '../../state/current-user';
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
+const canReadTenants = (currentUser: CurrentUser): boolean => {
+ const permissions = currentUser.resourcePermissions;
+ return permissions.tenants.canRead ||
permissions.anyTopLevelResource.canRead;
+};
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+export const adminTenantsGuard: CanMatchFn =
buildCanMatchGuard(canReadTenants);
+export const adminTenantsActivateGuard: CanActivateFn =
buildCanActivateGuard(canReadTenants);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/admin-workflow.guard.ts
similarity index 54%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/admin-workflow.guard.ts
index c1152fc9ea..bdaa1f01a0 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/admin-workflow.guard.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,14 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+import { CanActivateFn, CanMatchFn } from '@angular/router';
+import { buildCanActivateGuard, buildCanMatchGuard } from './base-guard.utils';
+import { CurrentUser } from '../../state/current-user';
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
+const canReadWorkflow = (currentUser: CurrentUser): boolean => {
+ const permissions = currentUser.resourcePermissions;
+ return permissions.buckets.canRead ||
permissions.anyTopLevelResource.canRead;
+};
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+export const adminWorkflowGuard: CanMatchFn =
buildCanMatchGuard(canReadWorkflow);
+export const adminWorkflowActivateGuard: CanActivateFn =
buildCanActivateGuard(canReadWorkflow);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/base-guard.utils.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/base-guard.utils.ts
new file mode 100644
index 0000000000..4cbbe29d8e
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/base-guard.utils.ts
@@ -0,0 +1,123 @@
+/*
+ * 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 { inject } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivateFn, CanMatchFn, Router,
RouterStateSnapshot, UrlSegment, UrlTree } from '@angular/router';
+import { Store } from '@ngrx/store';
+import { NiFiRegistryState } from '../../state';
+import { RegistryAuthService } from '../registry-auth.service';
+import { loadCurrentUser } from
'../../state/current-user/current-user.actions';
+import { selectCurrentUserState } from
'../../state/current-user/current-user.selectors';
+import * as ErrorActions from '../../state/error/error.actions';
+import { CurrentUser, CurrentUserState } from '../../state/current-user';
+import { Observable, filter, map, of, switchMap, take } from 'rxjs';
+
+interface AuthorizationCheck {
+ (currentUser: CurrentUser): boolean;
+}
+
+const fallbackUrl = '/explorer';
+
+export const computeRequestedUrl = (segments: UrlSegment[]): string => {
+ const url = `/${segments
+ .map((segment) => segment.path)
+ .filter((part) => !!part)
+ .join('/')}`;
+ return url.length > 1 ? url : '/explorer';
+};
+
+export const computeRequestedUrlFromState = (state: RouterStateSnapshot):
string => state?.url ?? '/explorer';
+
+export const showAccessDenied = (store: Store, router: Router): UrlTree => {
+ store.dispatch(ErrorActions.snackBarError({ error: 'Access denied. Please
contact your system administrator.' }));
+ return router.parseUrl(fallbackUrl);
+};
+
+const redirectToLogin = (store: Store, router: Router, authService:
RegistryAuthService, requestedUrl: string): UrlTree => {
+ authService.setRedirectUrl(requestedUrl || fallbackUrl);
+ return router.parseUrl('/login');
+};
+
+const evaluateAuthorization = (
+ store: Store<NiFiRegistryState>,
+ router: Router,
+ authService: RegistryAuthService,
+ state: CurrentUserState,
+ authorizationCheck: AuthorizationCheck,
+ requestedUrl: string
+): boolean | UrlTree => {
+ const authorized = authorizationCheck(state.currentUser);
+ if (authorized) {
+ return true;
+ }
+
+ if (state.currentUser.anonymous) {
+ return redirectToLogin(store, router, authService, requestedUrl);
+ }
+
+ return showAccessDenied(store, router);
+};
+
+const authorize = (
+ requestedUrl: string,
+ authorizationCheck: AuthorizationCheck
+): Observable<boolean | UrlTree> => {
+ const store = inject(Store<NiFiRegistryState>);
+ const router = inject(Router);
+ const authService = inject(RegistryAuthService);
+
+ const state$ = store.select(selectCurrentUserState);
+
+ return state$.pipe(
+ take(1),
+ switchMap((state) => {
+ if (state.status === 'success') {
+ return of(evaluateAuthorization(store, router, authService,
state, authorizationCheck, requestedUrl));
+ }
+
+ if (state.status === 'error') {
+ return of(redirectToLogin(store, router, authService,
requestedUrl));
+ }
+
+ authService.setRedirectUrl(requestedUrl);
+ if (state.status === 'pending') {
+ store.dispatch(loadCurrentUser());
+ }
+
+ return state$.pipe(
+ filter((nextState) => nextState.status === 'success' ||
nextState.status === 'error'),
+ take(1),
+ map((nextState) =>
+ nextState.status === 'success'
+ ? evaluateAuthorization(store, router, authService,
nextState, authorizationCheck, requestedUrl)
+ : redirectToLogin(store, router, authService,
requestedUrl)
+ )
+ );
+ })
+ );
+};
+
+export const buildCanMatchGuard = (authorizationCheck: AuthorizationCheck):
CanMatchFn => {
+ return (_route, segments) => authorize(computeRequestedUrl(segments),
authorizationCheck);
+};
+
+export const buildCanActivateGuard = (authorizationCheck: AuthorizationCheck):
CanActivateFn => {
+ return (_route: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
+ authorize(computeRequestedUrlFromState(state), authorizationCheck);
+};
+
+export const buildGuard = (authorizationCheck: AuthorizationCheck): CanMatchFn
=> buildCanMatchGuard(authorizationCheck);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/index.ts
similarity index 57%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/index.ts
index c1152fc9ea..90ec14ca07 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/index.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,8 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+export * from './resources.guard';
+export * from './admin-workflow.guard';
+export * from './admin-tenants.guard';
+export * from './login.guard';
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/login.guard.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/login.guard.ts
new file mode 100644
index 0000000000..07a488576b
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/login.guard.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { CanActivateFn, Router, RouterStateSnapshot, UrlTree } from
'@angular/router';
+import { inject } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { NiFiRegistryState } from '../../state';
+import { selectCurrentUserState } from
'../../state/current-user/current-user.selectors';
+import { RegistryAuthService } from '../registry-auth.service';
+import { loadCurrentUser } from
'../../state/current-user/current-user.actions';
+import { filter, map, switchMap, take } from 'rxjs/operators';
+import { Observable, of } from 'rxjs';
+import { CurrentUserState } from '../../state/current-user';
+
+const handleLoginState = (
+ router: Router,
+ authService: RegistryAuthService,
+ state: CurrentUserState,
+ requestedUrl: string
+): boolean | UrlTree => {
+ const currentUser = state.currentUser;
+
+ if (!currentUser.anonymous && currentUser.canActivateResourcesAuthGuard) {
+ const redirectUrl = authService.consumeRedirectUrl() ?? requestedUrl
?? '/explorer';
+ router.navigateByUrl(redirectUrl);
+ return false;
+ }
+
+ if (currentUser.anonymous && !currentUser.loginSupported &&
!currentUser.oidcLoginSupported) {
+ router.navigateByUrl('/explorer');
+ return false;
+ }
+
+ return true;
+};
+
+export const loginGuard: CanActivateFn = (_route, state: RouterStateSnapshot):
Observable<boolean | UrlTree> => {
+ const store = inject(Store<NiFiRegistryState>);
+ const router = inject(Router);
+ const authService = inject(RegistryAuthService);
+
+ const requestedUrl = state?.url ?? '/explorer';
+
+ return store.select(selectCurrentUserState).pipe(
+ take(1),
+ switchMap((currentState) => {
+ if (currentState.status === 'success') {
+ return of(handleLoginState(router, authService, currentState,
requestedUrl));
+ }
+
+ if (currentState.status === 'error') {
+ return of(true);
+ }
+
+ if (currentState.status === 'pending') {
+ store.dispatch(loadCurrentUser());
+ }
+
+ return store.select(selectCurrentUserState).pipe(
+ filter((nextState) => nextState.status === 'success' ||
nextState.status === 'error'),
+ take(1),
+ map((nextState) =>
+ nextState.status === 'success'
+ ? handleLoginState(router, authService, nextState,
requestedUrl)
+ : true
+ )
+ );
+ })
+ );
+};
+
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/resources.guard.ts
similarity index 56%
copy from
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
copy to
nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/resources.guard.ts
index c1152fc9ea..b9c1107887 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/buckets/feature/buckets-routing.module.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/guard/resources.guard.ts
@@ -6,7 +6,7 @@
* (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
+ * 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,
@@ -15,25 +15,12 @@
* limitations under the License.
*/
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { BucketsComponent } from './buckets.component';
+import { CanActivateFn, CanMatchFn } from '@angular/router';
+import { buildCanActivateGuard, buildCanMatchGuard } from './base-guard.utils';
+import { CurrentUser } from '../../state/current-user';
-const routes: Routes = [
- {
- path: '',
- component: BucketsComponent,
- children: [
- {
- path: ':id',
- component: BucketsComponent
- }
- ]
- }
-];
+const canAccessResources = (currentUser: CurrentUser): boolean =>
+ currentUser.canActivateResourcesAuthGuard === true ||
currentUser.anonymous === true;
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule]
-})
-export class BucketsRoutingModule {}
+export const resourcesGuard: CanMatchFn =
buildCanMatchGuard(canAccessResources);
+export const resourcesActivateGuard: CanActivateFn =
buildCanActivateGuard(canAccessResources);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/interceptors/registry-auth.interceptor.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/interceptors/registry-auth.interceptor.ts
new file mode 100644
index 0000000000..1ecd98db67
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/interceptors/registry-auth.interceptor.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from
'@angular/common/http';
+import { inject } from '@angular/core';
+import { RegistryAuthService } from '../registry-auth.service';
+
+export const registryAuthInterceptor: HttpInterceptorFn = (request:
HttpRequest<unknown>, next: HttpHandlerFn) => {
+ const authService = inject(RegistryAuthService);
+
+ const token = authService.getStoredToken();
+ const requestToken = authService.getRequestTokenFromCookies();
+
+ if (token || requestToken) {
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+ if (requestToken) {
+ headers['Request-Token'] = requestToken;
+ }
+
+ request = request.clone({ setHeaders: headers });
+ }
+
+ return next(request);
+};
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/registry-api.service.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/registry-api.service.ts
new file mode 100644
index 0000000000..f891137caa
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/registry-api.service.ts
@@ -0,0 +1,64 @@
+/*
+ * 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, inject } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { CurrentUser } from '../state/current-user';
+
+@Injectable({ providedIn: 'root' })
+export class RegistryApiService {
+ private httpClient = inject(HttpClient);
+
+ private static readonly API_BASE = '../nifi-registry-api';
+
+ getCurrentUser(): Observable<CurrentUser> {
+ return
this.httpClient.get<CurrentUser>(`${RegistryApiService.API_BASE}/access`);
+ }
+
+ ticketExchangeKerberos(): Observable<string> {
+ return
this.httpClient.post(`${RegistryApiService.API_BASE}/access/token/kerberos`,
null, {
+ responseType: 'text'
+ });
+ }
+
+ ticketExchangeOidc(): Observable<string> {
+ return
this.httpClient.post(`${RegistryApiService.API_BASE}/access/oidc/exchange`,
null, {
+ responseType: 'text',
+ withCredentials: true
+ });
+ }
+
+ login(username: string, password: string): Observable<string> {
+ const headers = new HttpHeaders({
+ Authorization: `Basic ${btoa(`${username}:${password}`)}`
+ });
+
+ return
this.httpClient.post(`${RegistryApiService.API_BASE}/access/token/login`, null,
{
+ headers,
+ withCredentials: true,
+ responseType: 'text'
+ });
+ }
+
+ logout(): Observable<any> {
+ return
this.httpClient.delete(`${RegistryApiService.API_BASE}/access/logout`, {
+ withCredentials: true,
+ responseType: 'text'
+ });
+ }
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/registry-auth.service.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/registry-auth.service.ts
new file mode 100644
index 0000000000..e30f0a32e3
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/service/registry-auth.service.ts
@@ -0,0 +1,129 @@
+/*
+ * 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, inject } from '@angular/core';
+import { Storage } from '@nifi/shared';
+import { Observable, of } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+import { RegistryApiService } from './registry-api.service';
+
+interface JwtPayload {
+ exp?: number;
+}
+
+@Injectable({ providedIn: 'root' })
+export class RegistryAuthService {
+ private storage = inject(Storage);
+ private registryApi = inject(RegistryApiService);
+
+ private static readonly API_BASE = '../nifi-registry-api';
+ private static readonly JWT_STORAGE_KEY = 'jwt';
+ private static readonly REDIRECT_STORAGE_KEY =
'nifi-registry-redirect-url';
+ private static readonly REQUEST_TOKEN_PATTERN = /Request-Token=([^;]+)/;
+
+ ticketExchange(): Observable<string> {
+ const storedToken = this.getStoredToken();
+ if (storedToken) {
+ return of(storedToken);
+ }
+
+ return this.registryApi.ticketExchangeKerberos().pipe(
+ map((jwt) => this.handleJwt(jwt)),
+ catchError(() =>
+ this.registryApi.ticketExchangeOidc().pipe(
+ map((jwt) => this.handleJwt(jwt)),
+ catchError(() => of(''))
+ )
+ )
+ );
+ }
+
+ login(username: string, password: string): Observable<string> {
+ return this.registryApi.login(username, password).pipe(map((jwt) =>
this.handleJwt(jwt)));
+ }
+
+ getStoredToken(): string | null {
+ return
this.storage.getItem<string>(RegistryAuthService.JWT_STORAGE_KEY);
+ }
+
+ clearToken(): void {
+ this.storage.removeItem(RegistryAuthService.JWT_STORAGE_KEY);
+ }
+
+ setRedirectUrl(url: string): void {
+ try {
+
window.sessionStorage.setItem(RegistryAuthService.REDIRECT_STORAGE_KEY, url);
+ } catch (error) {
+ /* empty */
+ }
+ }
+
+ consumeRedirectUrl(): string | null {
+ try {
+ const url =
window.sessionStorage.getItem(RegistryAuthService.REDIRECT_STORAGE_KEY);
+
window.sessionStorage.removeItem(RegistryAuthService.REDIRECT_STORAGE_KEY);
+ return url;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ storeToken(jwt: string): void {
+ this.handleJwt(jwt);
+ }
+
+ logout(): Observable<any> {
+ return this.registryApi.logout();
+ }
+
+ getRequestTokenFromCookies(): string | null {
+ try {
+ const matcher =
RegistryAuthService.REQUEST_TOKEN_PATTERN.exec(document.cookie);
+ if (matcher && matcher[1]) {
+ return matcher[1];
+ }
+ } catch (error) {
+ /* empty */
+ }
+ return null;
+ }
+
+ private handleJwt(jwt: string): string {
+ if (!jwt) {
+ return jwt;
+ }
+
+ const payload = this.parseJwt(jwt);
+ const expiration = payload?.exp ? payload.exp * 1000 : undefined;
+ this.storage.setItem(RegistryAuthService.JWT_STORAGE_KEY, jwt,
expiration);
+ return jwt;
+ }
+
+ private parseJwt(token: string): JwtPayload | null {
+ try {
+ const parts = token.split('.');
+ if (parts.length !== 3) {
+ return null;
+ }
+ const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');
+ const payload = window.atob(normalized);
+ return JSON.parse(payload);
+ } catch (error) {
+ return null;
+ }
+ }
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.actions.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.actions.ts
new file mode 100644
index 0000000000..7879b4b876
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.actions.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { HttpErrorResponse } from '@angular/common/http';
+import { createAction, props } from '@ngrx/store';
+import { LoadCurrentUserResponse } from './index';
+
+export const loadCurrentUser = createAction('[Current User] Load Current
User');
+
+export const loadCurrentUserSuccess = createAction(
+ '[Current User] Load Current User Success',
+ props<{ response: LoadCurrentUserResponse }>()
+);
+
+export const loadCurrentUserFailure = createAction(
+ '[Current User] Load Current User Failure',
+ props<{ error: HttpErrorResponse }>()
+);
+
+export const resetCurrentUser = createAction('[Current User] Reset Current
User');
+
+export const logout = createAction('[Current User] Log Out');
+
+export const navigateToLogout = createAction('[Current User] Navigate To Log
Out');
+
+export const logoutFailure = createAction('[Current User] Log Out Failure',
props<{ error: HttpErrorResponse }>());
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.effects.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.effects.spec.ts
new file mode 100644
index 0000000000..317adf1392
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.effects.spec.ts
@@ -0,0 +1,148 @@
+/*
+ * 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 { TestBed } from '@angular/core/testing';
+import { provideMockActions } from '@ngrx/effects/testing';
+import { Observable, ReplaySubject, of, throwError } from 'rxjs';
+import { take, toArray } from 'rxjs/operators';
+import { CurrentUserEffects } from './current-user.effects';
+import { RegistryApiService } from '../../service/registry-api.service';
+import { ErrorHelper } from '../../service/error-helper.service';
+import { RegistryAuthService } from '../../service/registry-auth.service';
+import * as CurrentUserActions from './current-user.actions';
+import { HttpErrorResponse } from '@angular/common/http';
+import { Store } from '@ngrx/store';
+import { selectLoginFailure } from
'../../pages/login/state/access/access.selectors';
+import { resetLoginFailure } from
'../../pages/login/state/access/access.actions';
+import * as ErrorActions from '../error/error.actions';
+
+describe('CurrentUserEffects', () => {
+ let actions$: Observable<unknown>;
+ let effects: CurrentUserEffects;
+ let authService: jest.Mocked<RegistryAuthService>;
+ let errorHelper: jest.Mocked<ErrorHelper>;
+ let store: Store<any>;
+ let dispatchSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ const registryApiServiceMock = {
+ getCurrentUser: jest.fn()
+ } as unknown as RegistryApiService;
+
+ authService = {
+ ticketExchange: jest.fn(),
+ clearToken: jest.fn(),
+ getStoredToken: jest.fn(),
+ logout: jest.fn()
+ } as unknown as jest.Mocked<RegistryAuthService>;
+
+ errorHelper = {
+ getErrorString: jest.fn().mockReturnValue('error')
+ } as unknown as jest.Mocked<ErrorHelper>;
+
+ store = {
+ dispatch: jest.fn(),
+ select: jest.fn().mockReturnValue(of(null))
+ } as unknown as Store<any>;
+ dispatchSpy = jest.spyOn(store as any, 'dispatch');
+
+ TestBed.configureTestingModule({
+ providers: [
+ CurrentUserEffects,
+ provideMockActions(() => actions$),
+ { provide: RegistryApiService, useValue:
registryApiServiceMock },
+ { provide: RegistryAuthService, useValue: authService },
+ { provide: ErrorHelper, useValue: errorHelper },
+ { provide: Store, useValue: store }
+ ]
+ });
+
+ effects = TestBed.inject(CurrentUserEffects);
+ });
+
+ describe('logout$', () => {
+ it('should emit navigateToLogout when logout succeeds', (done) => {
+ authService.logout.mockReturnValue(of(undefined));
+ actions$ = of(CurrentUserActions.logout());
+ effects.logout$.subscribe((result) => {
+ expect(result).toEqual(CurrentUserActions.navigateToLogout());
+ expect(authService.logout).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('should emit logoutFailure when logout fails', (done) => {
+ const error = new HttpErrorResponse({ status: 500, statusText:
'Server Error' });
+ authService.logout.mockReturnValue(throwError(() => error));
+ const subject = new ReplaySubject(1);
+ subject.next(CurrentUserActions.logout());
+ actions$ = subject.asObservable();
+
+ effects.logout$.subscribe((result) => {
+ expect(result).toEqual(CurrentUserActions.logoutFailure({
error }));
+ expect(authService.logout).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+
+ describe('navigateToLogout$', () => {
+ const originalLocation = window.location;
+
+ beforeEach(() => {
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: {
+ origin: 'https://localhost',
+ href: 'https://localhost/old'
+ }
+ });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: originalLocation
+ });
+ });
+
+ it('should clear token and redirect to backend logout endpoint',
(done) => {
+ const subject = new ReplaySubject(1);
+ subject.next(CurrentUserActions.navigateToLogout());
+ actions$ = subject.asObservable();
+
+ effects.navigateToLogout$.subscribe(() => {
+ expect(authService.clearToken).toHaveBeenCalled();
+
expect(window.location.href).toEqual('https://localhost/nifi-registry/logout');
+ done();
+ });
+ });
+ });
+
+ it('should emit snack bar and reset actions when logout fails', (done) => {
+ const error = new HttpErrorResponse({ status: 401, statusText:
'Unauthorized' });
+ const subject = new ReplaySubject(1);
+ subject.next(CurrentUserActions.logoutFailure({ error }));
+ actions$ = subject.asObservable();
+
+ effects.logoutFailure$.pipe(take(2), toArray()).subscribe((actions) =>
{
+ expect(actions[0]).toEqual(ErrorActions.snackBarError({ error:
'error' }));
+ expect(actions[1]).toEqual(resetLoginFailure());
+ done();
+ });
+ });
+});
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.effects.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.effects.ts
new file mode 100644
index 0000000000..0d3af2d8ae
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.effects.ts
@@ -0,0 +1,140 @@
+/*
+ * 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, inject } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import { Action } from '@ngrx/store';
+import { HttpErrorResponse } from '@angular/common/http';
+import * as CurrentUserActions from './current-user.actions';
+import * as ErrorActions from '../error/error.actions';
+import { RegistryApiService } from '../../service/registry-api.service';
+import { ErrorHelper } from '../../service/error-helper.service';
+import { catchError, map, switchMap, tap } from 'rxjs/operators';
+import { Observable, of, from } from 'rxjs';
+import { RegistryAuthService } from '../../service/registry-auth.service';
+import { CurrentUser } from './index';
+import { resetLoginFailure } from
'../../pages/login/state/access/access.actions';
+
+@Injectable()
+export class CurrentUserEffects {
+ private actions$ = inject(Actions);
+ private registryApi = inject(RegistryApiService);
+ private errorHelper = inject(ErrorHelper);
+ private authService = inject(RegistryAuthService);
+
+ loadCurrentUser$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(CurrentUserActions.loadCurrentUser),
+ switchMap(() =>
+ this.registryApi.getCurrentUser().pipe(
+ map((currentUser) =>
+ CurrentUserActions.loadCurrentUserSuccess({
+ response: { currentUser:
this.normalizeCurrentUser(currentUser) }
+ })
+ ),
+ catchError((error: HttpErrorResponse) =>
this.handleLoadError(error))
+ )
+ )
+ )
+ );
+
+ private handleLoadError(error: HttpErrorResponse): Observable<Action> {
+ if (error.status === 401) {
+ return this.authService.ticketExchange().pipe(
+ switchMap((jwt) => {
+ if (!jwt) {
+ return this.handleFailure(error);
+ }
+ return this.registryApi.getCurrentUser().pipe(
+ map((currentUser) =>
+ CurrentUserActions.loadCurrentUserSuccess({
+ response: { currentUser:
this.normalizeCurrentUser(currentUser) }
+ })
+ ),
+ catchError((retryError: HttpErrorResponse) =>
this.handleFailure(retryError))
+ );
+ }),
+ catchError((exchangeError: HttpErrorResponse) =>
this.handleFailure(exchangeError))
+ );
+ }
+
+ return this.handleFailure(error);
+ }
+
+ private handleFailure(error: HttpErrorResponse): Observable<Action> {
+ if (error.status === 401) {
+ this.authService.clearToken();
+ }
+
+ const message = this.errorHelper.getErrorString(error, 'Unable to load
current user');
+ return of(CurrentUserActions.loadCurrentUserFailure({ error }),
ErrorActions.snackBarError({ error: message }));
+ }
+
+ logout$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(CurrentUserActions.logout),
+ switchMap(() =>
+ from(this.authService.logout()).pipe(
+ map(() => CurrentUserActions.navigateToLogout()),
+ catchError((error: HttpErrorResponse) =>
of(CurrentUserActions.logoutFailure({ error })))
+ )
+ )
+ )
+ );
+
+ logoutFailure$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(CurrentUserActions.logoutFailure),
+ switchMap(({ error }) =>
+ of(
+ ErrorActions.snackBarError({
+ error: this.errorHelper.getErrorString(error, 'Unable
to logout. Please try again.')
+ }),
+ resetLoginFailure()
+ )
+ )
+ )
+ );
+
+ navigateToLogout$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(CurrentUserActions.navigateToLogout),
+ tap(() => {
+ this.authService.clearToken();
+ window.location.href =
`${window.location.origin}/nifi-registry/logout`;
+ })
+ ),
+ { dispatch: false }
+ );
+
+ private normalizeCurrentUser(currentUser: CurrentUser): CurrentUser {
+ const hasToken = !!this.authService.getStoredToken();
+
+ return {
+ ...currentUser,
+ canLogout: !currentUser.anonymous && hasToken,
+ canActivateResourcesAuthGuard:
+ !currentUser.anonymous ||
+ currentUser.resourcePermissions.anyTopLevelResource.canRead ||
+ currentUser.resourcePermissions.buckets.canRead ||
+ currentUser.resourcePermissions.tenants.canRead ||
+ currentUser.resourcePermissions.policies.canRead ||
+ currentUser.resourcePermissions.proxy.canRead
+ };
+ }
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.reducer.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.reducer.ts
new file mode 100644
index 0000000000..7cd2f81bf4
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.reducer.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { createReducer, on } from '@ngrx/store';
+import { CurrentUserState } from './index';
+import { Permissions } from '../index';
+import {
+ loadCurrentUser,
+ loadCurrentUserFailure,
+ loadCurrentUserSuccess,
+ resetCurrentUser
+} from './current-user.actions';
+
+export const NO_PERMISSIONS: Permissions = {
+ canRead: false,
+ canWrite: false,
+ canDelete: false
+};
+
+export const initialState: CurrentUserState = {
+ currentUser: {
+ identity: '',
+ anonymous: true,
+ loginSupported: false,
+ oidcLoginSupported: false,
+ canLogout: false,
+ canActivateResourcesAuthGuard: false,
+ resourcePermissions: {
+ anyTopLevelResource: NO_PERMISSIONS,
+ buckets: NO_PERMISSIONS,
+ tenants: NO_PERMISSIONS,
+ policies: NO_PERMISSIONS,
+ proxy: NO_PERMISSIONS
+ }
+ },
+ status: 'pending',
+ error: null
+};
+
+export const currentUserReducer = createReducer(
+ initialState,
+ on(loadCurrentUser, (state) => ({
+ ...state,
+ status: 'loading' as const,
+ error: null
+ })),
+ on(loadCurrentUserSuccess, (state, { response }) => ({
+ ...state,
+ currentUser: response.currentUser,
+ status: 'success' as const,
+ error: null
+ })),
+ on(loadCurrentUserFailure, (state, { error }) => ({
+ ...state,
+ status: 'error' as const,
+ error
+ })),
+ on(resetCurrentUser, () => ({
+ ...initialState
+ }))
+);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.selectors.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.selectors.ts
new file mode 100644
index 0000000000..1ee1b3cac8
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/current-user.selectors.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { createFeatureSelector, createSelector } from '@ngrx/store';
+import { CurrentUserState, currentUserFeatureKey } from './index';
+
+export const selectCurrentUserState =
createFeatureSelector<CurrentUserState>(currentUserFeatureKey);
+
+export const selectCurrentUser = createSelector(selectCurrentUserState,
(state) => state.currentUser);
+
+export const selectCurrentUserStatus = createSelector(selectCurrentUserState,
(state) => state.status);
+
+export const selectCurrentUserError = createSelector(selectCurrentUserState,
(state) => state.error);
+
+export const selectAnyTopLevelRead = createSelector(
+ selectCurrentUser,
+ (currentUser) =>
currentUser.resourcePermissions.anyTopLevelResource.canRead
+);
+
+export const selectBucketsRead = createSelector(
+ selectCurrentUser,
+ (currentUser) => currentUser.resourcePermissions.buckets.canRead
+);
+
+export const selectTenantsRead = createSelector(
+ selectCurrentUser,
+ (currentUser) => currentUser.resourcePermissions.tenants.canRead
+);
+
+export const selectLogoutSupported = createSelector(selectCurrentUser,
(currentUser) => currentUser.canLogout);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/index.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/index.ts
new file mode 100644
index 0000000000..4a94cf7b8f
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/current-user/index.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { HttpErrorResponse } from '@angular/common/http';
+
+export const currentUserFeatureKey = 'currentUser';
+
+export interface LoadCurrentUserResponse {
+ currentUser: CurrentUser;
+}
+
+export interface ResourcePermissionDetails {
+ canRead: boolean;
+ canWrite: boolean;
+ canDelete: boolean;
+}
+
+export interface ResourcePermissions {
+ anyTopLevelResource: ResourcePermissionDetails;
+ buckets: ResourcePermissionDetails;
+ tenants: ResourcePermissionDetails;
+ policies: ResourcePermissionDetails;
+ proxy: ResourcePermissionDetails;
+}
+
+export interface CurrentUser {
+ identity: string;
+ anonymous: boolean;
+ loginSupported: boolean;
+ oidcLoginSupported: boolean;
+ canLogout?: boolean;
+ canActivateResourcesAuthGuard?: boolean;
+ resourcePermissions: ResourcePermissions;
+}
+
+export interface CurrentUserState {
+ currentUser: CurrentUser;
+ status: 'pending' | 'loading' | 'success' | 'error';
+ error: HttpErrorResponse | null;
+}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/index.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/index.ts
index 652c11d3cc..347581c1da 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/index.ts
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/index.ts
@@ -25,6 +25,8 @@ import { errorReducer } from './error/error.reducer';
import { errorFeatureKey, ErrorState } from './error';
import { aboutFeatureKey, AboutState } from './about';
import { aboutReducer } from './about/about.reducer';
+import { currentUserFeatureKey, CurrentUserState } from './current-user';
+import { currentUserReducer } from './current-user/current-user.reducer';
export const resourcesFeatureKey = 'resources';
@@ -44,6 +46,7 @@ export interface Revision {
export interface Permissions {
canRead: boolean;
canWrite: boolean;
+ canDelete: boolean;
}
export interface ResourcesState {
@@ -62,10 +65,12 @@ export interface NiFiRegistryState {
[DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
[errorFeatureKey]: ErrorState;
[aboutFeatureKey]: AboutState;
+ [currentUserFeatureKey]: CurrentUserState;
}
export const rootReducers: ActionReducerMap<NiFiRegistryState> = {
[DEFAULT_ROUTER_FEATURENAME]: routerReducer,
[errorFeatureKey]: errorReducer,
- [aboutFeatureKey]: aboutReducer
+ [aboutFeatureKey]: aboutReducer,
+ [currentUserFeatureKey]: currentUserReducer
};
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.html
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.html
index c990052ac0..94e203da4e 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.html
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.html
@@ -33,7 +33,19 @@
</div>
<div class="flex justify-between items-center gap-x-1">
<div class="flex flex-col justify-between items-end gap-y-1">
- <!-- TODO: current user/anonymous user
-->
+ @if (currentUser(); as user) {
+ @if (user.anonymous) {
+ @if (allowLogin(user)) {
+ <div class="current-user">{{ user.identity
}}</div>
+ <a (click)="login()">log in</a>
+ }
+ } @else {
+ <div class="current-user">{{ user.identity }}</div>
+ @if (logoutSupported()) {
+ <a (click)="logout()">log out</a>
+ }
+ }
+ }
</div>
<button mat-button [matMenuTriggerFor]="globalMenu"
class="global-menu-icon global-menu">
<i class="fa fa-navicon text-[32px]"></i>
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.spec.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.spec.ts
index bdfbbcba3a..2a55cd20b5 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.spec.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.spec.ts
@@ -20,6 +20,13 @@ import { HeaderComponent } from './header.component';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { NiFiRegistryState } from '../../state';
import { openAboutDialog } from '../../state/about/about.actions';
+import { selectAbout } from '../../state/about/about.selectors';
+import { aboutFeatureKey } from '../../state/about';
+import { initialState as aboutInitialState } from
'../../state/about/about.reducer';
+import { logout } from '../../state/current-user/current-user.actions';
+import { selectCurrentUser, selectLogoutSupported } from
'../../state/current-user/current-user.selectors';
+import { currentUserFeatureKey } from '../../state/current-user';
+import { initialState as currentUserInitialState } from
'../../state/current-user/current-user.reducer';
import { MatDialog } from '@angular/material/dialog';
import { of } from 'rxjs';
@@ -37,7 +44,15 @@ describe('HeaderComponent', () => {
TestBed.configureTestingModule({
imports: [HeaderComponent],
providers: [
- provideMockStore(),
+ provideMockStore({
+ initialState: {
+ [currentUserFeatureKey]: currentUserInitialState,
+ [aboutFeatureKey]: aboutInitialState,
+ error: {
+ bannerErrors: {}
+ }
+ }
+ }),
{
provide: MatDialog,
useValue: matDialogMock
@@ -50,7 +65,12 @@ describe('HeaderComponent', () => {
store = TestBed.inject(MockStore);
dialogOpenSpy = matDialogMock.open;
+ store.overrideSelector(selectCurrentUser,
currentUserInitialState.currentUser);
+ store.overrideSelector(selectLogoutSupported,
currentUserInitialState.currentUser.canLogout);
+ store.overrideSelector(selectAbout, aboutInitialState.about);
+
jest.spyOn(store, 'dispatch');
+ store.refreshState();
fixture.detectChanges();
});
@@ -63,4 +83,9 @@ describe('HeaderComponent', () => {
expect(store.dispatch).toHaveBeenCalledWith(openAboutDialog());
expect(dialogOpenSpy).not.toHaveBeenCalled();
});
+
+ it('should dispatch logout when logout is called', () => {
+ component.logout();
+ expect(store.dispatch).toHaveBeenCalledWith(logout());
+ });
});
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.ts
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.ts
index 477b9e672d..1fc4e6a1d3 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.ts
@@ -23,6 +23,9 @@ import { MatMenu, MatMenuItem, MatMenuTrigger } from
'@angular/material/menu';
import { DARK_THEME, LIGHT_THEME, OS_SETTING, Storage, ThemingService } from
'@nifi/shared';
import { Store } from '@ngrx/store';
import { NiFiRegistryState } from '../../state';
+import { selectCurrentUser, selectLogoutSupported } from
'../../state/current-user/current-user.selectors';
+import { logout } from '../../state/current-user/current-user.actions';
+import { CurrentUser } from '../../state/current-user';
import { loadAbout, openAboutDialog } from '../../state/about/about.actions';
import { selectAbout } from '../../state/about/about.selectors';
@@ -47,6 +50,8 @@ export class HeaderComponent implements OnInit {
DARK_THEME: string = DARK_THEME;
OS_SETTING: string = OS_SETTING;
disableAnimations: string | null;
+ currentUser = this.store.selectSignal(selectCurrentUser);
+ logoutSupported = this.store.selectSignal(selectLogoutSupported);
about$ = this.store.select(selectAbout);
constructor() {
@@ -90,4 +95,20 @@ export class HeaderComponent implements OnInit {
viewAbout(): void {
this.store.dispatch(openAboutDialog());
}
+
+ allowLogin(user: CurrentUser | null): boolean {
+ if (!user) {
+ return false;
+ }
+
+ return user.anonymous && user.loginSupported;
+ }
+
+ login(): void {
+ this.router.navigateByUrl('/login');
+ }
+
+ logout(): void {
+ this.store.dispatch(logout());
+ }
}
diff --git a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/styles.scss
b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/styles.scss
index 49961a0512..e08d8703e8 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi-registry/src/styles.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi-registry/src/styles.scss
@@ -20,6 +20,7 @@
@use 'libs/shared/src/assets/styles/app' as app;
@use 'app/ui/header/header.component-theme' as header;
+@use 'app/pages/login/feature/login.component-theme' as login;
@use 'font-awesome';
@use 'libs/shared/src/assets/fonts/flowfont/flowfont.css';
@@ -38,10 +39,12 @@ html {
@include app.generate-material-theme();
@include listing-table.generate-theme();
@include header.generate-theme();
+ @include login.generate-theme();
.dark-theme {
@include app.generate-material-theme();
@include listing-table.generate-theme();
@include header.generate-theme();
+ @include login.generate-theme();
}
}