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();
     }
 }

Reply via email to