Skip to content

Commit c61a0c0

Browse files
labrenbesbernauermaltesanderNickLarsenNZ
authored
feat: OIDC support (#660)
* fix: Correctly encode user given content, such as passwords (#627) * fix: Correctly encode user given content, such as passwords * changelog * fix bcrypt problems * Update rust/operator-binary/src/security/authentication.rs Co-authored-by: Malte Sander <[email protected]> --------- Co-authored-by: Malte Sander <[email protected]> * add vscode debugging profile * wip: add integration test for oidc * wip: debug oidc & jwts * wip * wip: map over ContainerBuilders * fix oidc test * fix oidc test * fix clippy, update CRD and remove debug files * run cargo fmt * address clippy and yamllint feedback * remove unneccessary return * reenable all tests * add docs and fix oidc test * remove reporting task from oidc test * add debug logging * fix test logging * use nifi-latest in oidc test * add comment why nifi-latest is used * clean up code and add comment * address feedback from review * improve oidc integration test * fix oidc test for nifi 2.0.0-M4 * increase timeout on test job creation * fix docs on oidc * move config for debugger to operator-templating * add comment to test job assert --------- Co-authored-by: Sebastian Bernauer <[email protected]> Co-authored-by: Malte Sander <[email protected]> Co-authored-by: Nick Larsen <[email protected]>
1 parent a2a7648 commit c61a0c0

31 files changed

+984
-117
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git",
2828
strum = { version = "0.26", features = ["derive"] }
2929
tokio = { version = "1.39", features = ["full"] }
3030
tracing = "0.1"
31+
url = { version = "2.5.2" }
3132
xml-rs = "0.8"
3233

3334
# [patch."https://github.com/stackabletech/operator-rs.git"]

deploy/helm/nifi-operator/crds/crds.yaml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,27 @@ spec:
3333
items:
3434
properties:
3535
authenticationClass:
36-
description: Name of the [AuthenticationClass](https://docs.stackable.tech/home/nightly/concepts/authentication) used to authenticate users. Supported providers are `static` and `ldap`. For `static` the "admin" user needs to be present in the referenced secret, and only this user will be added to NiFi, other users are ignored.
36+
description: A name/key which references an authentication class. To get the concrete [`AuthenticationClass`], we must resolve it. This resolution can be achieved by using [`ClientAuthenticationDetails::resolve_class`].
3737
type: string
38+
oidc:
39+
description: |-
40+
This field contains authentication provider specific configuration.
41+
42+
Use [`ClientAuthenticationDetails::oidc_or_error`] to get the value or report an error to the user.
43+
nullable: true
44+
properties:
45+
clientCredentialsSecret:
46+
description: A reference to the OIDC client credentials secret. The secret contains the client id and secret.
47+
type: string
48+
extraScopes:
49+
default: []
50+
description: An optional list of extra scopes which get merged with the scopes defined in the [`AuthenticationClass`].
51+
items:
52+
type: string
53+
type: array
54+
required:
55+
- clientCredentialsSecret
56+
type: object
3857
required:
3958
- authenticationClass
4059
type: object

docs/modules/nifi/pages/usage_guide/security.adoc

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ spec:
2222
serverSecretClass: non-default-secret-class # <1>
2323
----
2424

25-
<1> The name of the `SecretClass` that will be used for certificates for the NiFi UI.
25+
<1> The name of the SecretClass that will be used for certificates for the NiFi UI.
2626

2727
== Authentication
2828

@@ -49,8 +49,8 @@ spec:
4949
name: nifi-admin-credentials # <2>
5050
----
5151

52-
<1> The name of the `AuthenticationClass` that will be referenced in the NiFi cluster.
53-
<2> The name of the `Secret` containing the admin credentials.
52+
<1> The name of the AuthenticationClass that will be referenced in the NiFi cluster.
53+
<2> The name of the Secret containing the admin credentials.
5454

5555
[source,yaml]
5656
----
@@ -63,9 +63,9 @@ stringData:
6363
bob: bob # <3>
6464
----
6565

66-
<1> The name of the `Secret` containing the admin user credentials.
67-
<2> The user and password combination of the admin user. The username *must* be "admin" and cannot be changed. The NiFi pods will not start if they cannot mount the "admin" entry from the secret. The password can be adapted.
68-
<3> The secret maybe used by other products of the Stackable Data Platform that allow more than one user. The Stackable Operator for Apache NiFi will ignore all users except for "admin".
66+
<1> The name of the Secret containing the admin user credentials.
67+
<2> The user and password combination of the admin user. The username *must* be "admin" and cannot be changed. The NiFi pods will not start if they cannot mount the "admin" entry from the Secret. The password can be adapted.
68+
<3> The Secret maybe used by other products of the Stackable Data Platform that allow more than one user. The Stackable Operator for Apache NiFi will ignore all users except for "admin".
6969

7070
[source,yaml]
7171
----
@@ -75,7 +75,7 @@ spec:
7575
- authenticationClass: simple-nifi-users # <1>
7676
----
7777

78-
<1> The reference to an `AuthenticationClass`. NiFi only supports one authentication mechanism at a time.
78+
<1> The reference to an AuthenticationClass. NiFi only supports one authentication mechanism at a time.
7979

8080
[#authentication-ldap]
8181
=== LDAP
@@ -100,13 +100,72 @@ spec:
100100

101101
You can follow the xref:tutorials:authentication_with_openldap.adoc[] tutorial to learn how to set up an AuthenticationClass for an LDAP server, as well as consulting the {crd-docs}/authentication.stackable.tech/authenticationclass/v1alpha1/[AuthenticationClass reference {external-link-icon}^].
102102

103+
[#authentication-oidc]
104+
=== OIDC
105+
106+
NiFi supports xref:concepts:authentication.adoc[authentication] of users against an OIDC provider.
107+
This requires setting up an AuthenticationClass for the OIDC provider and specifying a Secret containing the OIDC client id and client secret as part of the NiFi configuration.
108+
The AuthenticationClass and the OIDC client credentials Secret are then referenced in the NifiCluster resource:
109+
110+
[source,yaml]
111+
----
112+
apiVersion: nifi.stackable.tech/v1alpha1
113+
kind: NifiCluster
114+
metadata:
115+
name: test-nifi
116+
spec:
117+
clusterConfig:
118+
authentication:
119+
- authenticationClass: oidc # <1>
120+
oidc:
121+
clientCredentialsSecret: nifi-oidc-client # <2>
122+
----
123+
124+
<1> The reference to an AuthenticationClass called `oidc`
125+
<2> The reference to an existing Secret called `nifi-oidc-client`
126+
127+
[source,yaml]
128+
----
129+
apiVersion: authentication.stackable.tech/v1alpha1
130+
kind: AuthenticationClass
131+
metadata:
132+
name: oidc
133+
spec:
134+
provider:
135+
oidc:
136+
hostname: keycloak.example.com
137+
rootPath: /realms/test/ # <1>
138+
principalClaim: preferred_username
139+
scopes:
140+
- openid
141+
- email
142+
- profile
143+
port: 8080
144+
tls: null
145+
[...]
146+
----
147+
148+
<1> A trailing slash in `rootPath` is necessary.
149+
150+
[source,yaml]
151+
----
152+
apiVersion: v1
153+
kind: Secret
154+
metadata:
155+
name: nifi-oidc-client
156+
stringData:
157+
clientId: <client-id>
158+
clientSecret: <client-secret>
159+
----
160+
103161
[#authorization]
104162
== Authorization
105163

106164
NiFi supports {nifi-docs-authorization}[multiple authorization methods], the available authorization methods depend on the chosen authentication method.
107165

108166
Authorization is not fully implemented by the Stackable Operator for Apache NiFi.
109167

168+
[#authorization-single-user]
110169
=== Single user
111170

112171
With this authorization method, a single user has administrator capabilities.
@@ -118,6 +177,14 @@ The operator uses the {nifi-docs-fileusergroupprovider}[`FileUserGroupProvider`]
118177
This user is then able to create and modify groups and policies in the web interface.
119178
These changes local to the Pod running NiFi and are *not* persistent.
120179

180+
[#authorization-oidc]
181+
=== OIDC
182+
183+
With this authorization method, all authenticated users have administrator capabilities.
184+
185+
An admin user with an auto-generated password is created that can access the NiFi API.
186+
The password for this user is stored in a Kubernetes Secret called `<nifi-name>-oidc-admin-password`.
187+
121188
[#encrypting-sensitive-properties]
122189
== Encrypting sensitive properties on disk
123190

rust/crd/src/authentication.rs

Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
use serde::{Deserialize, Serialize};
1+
use std::future::Future;
2+
23
use snafu::{ResultExt, Snafu};
3-
use stackable_operator::commons::authentication::AuthenticationClassProvider;
4+
use stackable_operator::commons::authentication::{
5+
ldap, oidc, static_, AuthenticationClassProvider, ClientAuthenticationDetails,
6+
};
47
use stackable_operator::kube::ResourceExt;
58
use stackable_operator::{
6-
client::Client,
7-
commons::authentication::AuthenticationClass,
9+
client::Client, commons::authentication::AuthenticationClass,
810
kube::runtime::reflector::ObjectRef,
9-
schemars::{self, JsonSchema},
1011
};
1112

13+
use crate::NifiCluster;
14+
1215
#[derive(Snafu, Debug)]
1316
pub enum Error {
14-
#[snafu(display("Failed to retrieve AuthenticationClass {authentication_class}"))]
15-
AuthenticationClassRetrieval {
17+
#[snafu(display("failed to retrieve AuthenticationClass"))]
18+
AuthenticationClassRetrievalFailed {
1619
source: stackable_operator::client::Error,
17-
authentication_class: ObjectRef<AuthenticationClass>,
1820
},
1921

2022
#[snafu(display("The nifi-operator does not support running Nifi without any authentication. Please provide a AuthenticationClass to use."))]
@@ -33,67 +35,102 @@ pub enum Error {
3335
NoLdapTlsVerificationNotSupported {
3436
authentication_class: ObjectRef<AuthenticationClass>,
3537
},
38+
39+
#[snafu(display("invalid OIDC configuration"))]
40+
OidcConfigurationInvalid {
41+
source: stackable_operator::commons::authentication::Error,
42+
},
3643
}
3744

3845
type Result<T, E = Error> = std::result::Result<T, E>;
3946

40-
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
41-
#[serde(rename_all = "camelCase")]
42-
pub struct NifiAuthenticationClassRef {
43-
/// Name of the [AuthenticationClass](DOCS_BASE_URL_PLACEHOLDER/concepts/authentication) used to authenticate users.
44-
/// Supported providers are `static` and `ldap`.
45-
/// For `static` the "admin" user needs to be present in the referenced secret, and only this user will be added to NiFi, other users are ignored.
46-
pub authentication_class: String,
47+
#[derive(Clone)]
48+
pub enum AuthenticationClassResolved {
49+
Static {
50+
provider: static_::AuthenticationProvider,
51+
},
52+
Ldap {
53+
provider: ldap::AuthenticationProvider,
54+
},
55+
Oidc {
56+
provider: oidc::AuthenticationProvider,
57+
oidc: oidc::ClientAuthenticationOptions<()>,
58+
nifi: NifiCluster,
59+
},
4760
}
4861

49-
/// Retrieve all provided `AuthenticationClass` references.
50-
pub async fn resolve_authentication_classes(
51-
client: &Client,
52-
authentication_class_refs: &Vec<NifiAuthenticationClassRef>,
53-
) -> Result<Vec<AuthenticationClass>> {
54-
let mut resolved_auth_classes = vec![];
55-
56-
match authentication_class_refs.len() {
57-
0 => NoAuthenticationNotSupportedSnafu.fail()?,
58-
1 => {}
59-
_ => MultipleAuthenticationClassesNotSupportedSnafu.fail()?,
62+
impl AuthenticationClassResolved {
63+
pub async fn from(
64+
nifi: &NifiCluster,
65+
client: &Client,
66+
) -> Result<Vec<AuthenticationClassResolved>> {
67+
let resolve_auth_class = |auth_details: ClientAuthenticationDetails| async move {
68+
auth_details.resolve_class(client).await
69+
};
70+
AuthenticationClassResolved::resolve(nifi, resolve_auth_class).await
6071
}
6172

62-
for auth_class in authentication_class_refs {
63-
let resolved_auth_class =
64-
AuthenticationClass::resolve(client, &auth_class.authentication_class)
73+
/// Retrieve all provided `AuthenticationClass` references.
74+
pub async fn resolve<R>(
75+
nifi: &NifiCluster,
76+
resolve_auth_class: impl Fn(ClientAuthenticationDetails) -> R,
77+
) -> Result<Vec<AuthenticationClassResolved>>
78+
where
79+
R: Future<Output = Result<AuthenticationClass, stackable_operator::client::Error>>,
80+
{
81+
let mut resolved_auth_classes = vec![];
82+
let auth_details = &nifi.spec.cluster_config.authentication;
83+
84+
match auth_details.len() {
85+
0 => NoAuthenticationNotSupportedSnafu.fail()?,
86+
1 => {}
87+
_ => MultipleAuthenticationClassesNotSupportedSnafu.fail()?,
88+
}
89+
90+
for entry in auth_details {
91+
let auth_class = resolve_auth_class(entry.clone())
6592
.await
66-
.context(AuthenticationClassRetrievalSnafu {
67-
authentication_class: ObjectRef::<AuthenticationClass>::new(
68-
&auth_class.authentication_class,
69-
),
70-
})?;
71-
72-
let resolved_auth_class_name = resolved_auth_class.name_any();
73-
74-
match &resolved_auth_class.spec.provider {
75-
AuthenticationClassProvider::Static(_) => {}
76-
AuthenticationClassProvider::Ldap(ldap) => {
77-
if ldap.tls.uses_tls() && !ldap.tls.uses_tls_verification() {
78-
NoLdapTlsVerificationNotSupportedSnafu {
79-
authentication_class: ObjectRef::<AuthenticationClass>::new(
80-
&resolved_auth_class_name,
81-
),
93+
.context(AuthenticationClassRetrievalFailedSnafu)?;
94+
95+
let auth_class_name = auth_class.name_any();
96+
97+
match &auth_class.spec.provider {
98+
AuthenticationClassProvider::Static(provider) => {
99+
resolved_auth_classes.push(AuthenticationClassResolved::Static {
100+
provider: provider.to_owned(),
101+
})
102+
}
103+
AuthenticationClassProvider::Ldap(provider) => {
104+
if provider.tls.uses_tls() && !provider.tls.uses_tls_verification() {
105+
NoLdapTlsVerificationNotSupportedSnafu {
106+
authentication_class: ObjectRef::<AuthenticationClass>::new(
107+
&auth_class_name,
108+
),
109+
}
110+
.fail()?
82111
}
83-
.fail()?
112+
resolved_auth_classes.push(AuthenticationClassResolved::Ldap {
113+
provider: provider.to_owned(),
114+
})
84115
}
85-
}
86-
_ => AuthenticationClassProviderNotSupportedSnafu {
87-
authentication_class_provider: resolved_auth_class.spec.provider.to_string(),
88-
authentication_class: ObjectRef::<AuthenticationClass>::new(
89-
&resolved_auth_class_name,
90-
),
91-
}
92-
.fail()?,
93-
};
116+
AuthenticationClassProvider::Oidc(provider) => {
117+
resolved_auth_classes.push(Ok(AuthenticationClassResolved::Oidc {
118+
provider: provider.to_owned(),
119+
oidc: entry
120+
.oidc_or_error(&auth_class_name)
121+
.context(OidcConfigurationInvalidSnafu)?
122+
.clone(),
123+
nifi: nifi.clone(),
124+
})?)
125+
}
126+
_ => AuthenticationClassProviderNotSupportedSnafu {
127+
authentication_class_provider: auth_class.spec.provider.to_string(),
128+
authentication_class: ObjectRef::<AuthenticationClass>::new(&auth_class_name),
129+
}
130+
.fail()?,
131+
};
132+
}
94133

95-
resolved_auth_classes.push(resolved_auth_class);
134+
Ok(resolved_auth_classes)
96135
}
97-
98-
Ok(resolved_auth_classes)
99136
}

0 commit comments

Comments
 (0)