mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-24 04:57:00 +01:00
2390 1980 allow native applications (#2428)
This commit is contained in:
parent
84204ee7ce
commit
8dc884f38e
|
@ -16,18 +16,18 @@ retrieve ssh public keys, and then perform sudo authentication.
|
||||||
This has the obvious caveat that anyone can stand up a machine that trusts a Kanidm instance. This
|
This has the obvious caveat that anyone can stand up a machine that trusts a Kanidm instance. This
|
||||||
presents a double edged sword:
|
presents a double edged sword:
|
||||||
|
|
||||||
- By configuring a machine to authenticate via Kanidm, there is full trust
|
- By configuring a machine to authenticate via Kanidm, there is full trust in the authentication
|
||||||
in the authentication decisions Kanidm makes.
|
decisions Kanidm makes.
|
||||||
- Users of Kanidm may be tricked into accessing a machine that is not managed
|
- Users of Kanidm may be tricked into accessing a machine that is not managed by their IT or other
|
||||||
by their IT or other central authority.
|
central authority.
|
||||||
|
|
||||||
To prevent this, UNIX authentication should be configurable to prevent usage from unregistered machines.
|
To prevent this, UNIX authentication should be configurable to prevent usage from unregistered
|
||||||
This will require the machine to present machine authentication credentials simultaneously with the
|
machines. This will require the machine to present machine authentication credentials simultaneously
|
||||||
user's credentials.
|
with the user's credentials.
|
||||||
|
|
||||||
A potential change is removing the current unix password auth mechanism as a whole. Instead the
|
A potential change is removing the current unix password auth mechanism as a whole. Instead the
|
||||||
user's auth token would contain a TPM bound credential that only the domain joined machine's TPM could
|
user's auth token would contain a TPM bound credential that only the domain joined machine's TPM
|
||||||
access and use.
|
could access and use.
|
||||||
|
|
||||||
### Requesting Cryptographic Credentials
|
### Requesting Cryptographic Credentials
|
||||||
|
|
||||||
|
|
|
@ -200,6 +200,78 @@ kanidm system oauth2 reset-secrets
|
||||||
Each resource server has unique signing keys and access secrets, so this is limited to each resource
|
Each resource server has unique signing keys and access secrets, so this is limited to each resource
|
||||||
server.
|
server.
|
||||||
|
|
||||||
|
## Custom Claim Maps
|
||||||
|
|
||||||
|
Some OIDC clients may consume custom claims from an id token for access control or other policy
|
||||||
|
decisions. Each custom claim is a key:values set, where there can be many values associated to a
|
||||||
|
claim name. Different applications may expect these values to be formatted (joined) in different
|
||||||
|
ways.
|
||||||
|
|
||||||
|
Claim values are mapped based on membership to groups. When an account is a member of multiple
|
||||||
|
groups that would recieve the same claim, the values of these maps are merged.
|
||||||
|
|
||||||
|
To create or update a claim map on a client:
|
||||||
|
|
||||||
|
```
|
||||||
|
kanidm system oauth2 update-claim-map <name> <claim_name> <kanidm_group_name> [values]...
|
||||||
|
kanidm system oauth2 update-claim-map nextcloud account_role nextcloud_admins admin login ...
|
||||||
|
```
|
||||||
|
|
||||||
|
To change the join strategy for a claim name. Valid strategies are csv (comma separated value), ssv
|
||||||
|
(space separated value) and array (a native json array). The default strategy is array.
|
||||||
|
|
||||||
|
```
|
||||||
|
kanidm system oauth2 update-claim-map-join <name> <claim_name> [csv|ssv|array]
|
||||||
|
kanidm system oauth2 update-claim-map-join nextcloud account_role csv
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# Example claim formats
|
||||||
|
# csv
|
||||||
|
claim: "value_a,value_b"
|
||||||
|
|
||||||
|
# ssv
|
||||||
|
claim: "value_a value_b"
|
||||||
|
|
||||||
|
# array
|
||||||
|
claim: ["value_a", "value_b"]
|
||||||
|
```
|
||||||
|
|
||||||
|
To delete a group from a claim map
|
||||||
|
|
||||||
|
```
|
||||||
|
kanidm system oauth2 delete-claim-map <name> <claim_name> <kanidm_group_name>
|
||||||
|
kanidm system oauth2 delete-claim-map nextcloud account_role nextcloud_admins
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public Client Configuration
|
||||||
|
|
||||||
|
Some applications are unable to provide client authentication. A common example is single page web
|
||||||
|
applications that act as the OAuth2 client and its corresponding webserver that is the resource
|
||||||
|
server. In this case the SPA is unable to act as a confidential client since the basic secret would
|
||||||
|
need to be embedded in every client.
|
||||||
|
|
||||||
|
Another common example is native applications that use a redirect to localhost. These can't have a
|
||||||
|
client secret embedded, so must act as public clients.
|
||||||
|
|
||||||
|
Public clients for this reason require PKCE to bind a specific browser session to its OAuth2
|
||||||
|
exchange. PKCE can not be disabled for public clients for this reason.
|
||||||
|
|
||||||
|
To create an OAuth2 public resource server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kanidm system oauth2 create-public <name> <displayname> <origin>
|
||||||
|
kanidm system oauth2 create-public mywebapp "My Web App" https://webapp.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
To allow localhost redirection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kanidm system oauth2 enable-localhost-redirects <name>
|
||||||
|
kanidm system oauth2 disable-localhost-redirects <name>
|
||||||
|
kanidm system oauth2 enable-localhost-redirects mywebapp
|
||||||
|
```
|
||||||
|
|
||||||
## Extended Options for Legacy Clients
|
## Extended Options for Legacy Clients
|
||||||
|
|
||||||
Not all resource servers support modern standards like PKCE or ECDSA. In these situations it may be
|
Not all resource servers support modern standards like PKCE or ECDSA. In these situations it may be
|
||||||
|
@ -228,33 +300,6 @@ To enable legacy cryptograhy (RSA PKCS1-5 SHA256):
|
||||||
kanidm system oauth2 warning-enable-legacy-crypto <resource server name>
|
kanidm system oauth2 warning-enable-legacy-crypto <resource server name>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Public Client Configuration
|
|
||||||
|
|
||||||
Some applications are unable to provide client authentication. A common example is single page web
|
|
||||||
applications that act as the OAuth2 client and its corresponding webserver that is the resource
|
|
||||||
server. In this case the SPA is unable to act as a confidential client since the basic secret would
|
|
||||||
need to be embedded in every client.
|
|
||||||
|
|
||||||
Public clients for this reason require PKCE to bind a specific browser session to its OAuth2
|
|
||||||
exchange. PKCE can not be disabled for public clients for this reason.
|
|
||||||
|
|
||||||
<!-- deno-fmt-ignore-start -->
|
|
||||||
|
|
||||||
{{#template ../templates/kani-warning.md
|
|
||||||
imagepath=../images
|
|
||||||
title=WARNING
|
|
||||||
text=Public clients have many limitations compared to confidential clients. You should avoid them if possible.
|
|
||||||
}}
|
|
||||||
|
|
||||||
<!-- deno-fmt-ignore-end -->
|
|
||||||
|
|
||||||
To create an OAuth2 public resource server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kanidm system oauth2 create-public <name> <displayname> <origin>
|
|
||||||
kanidm system oauth2 create-public mywebapp "My Web App" https://webapp.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Integrations
|
## Example Integrations
|
||||||
|
|
||||||
### Apache mod\_auth\_openidc
|
### Apache mod\_auth\_openidc
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::{ClientError, KanidmClient};
|
use crate::{ClientError, KanidmClient};
|
||||||
use kanidm_proto::constants::{
|
use kanidm_proto::constants::{
|
||||||
ATTR_DISPLAYNAME, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE,
|
ATTR_DISPLAYNAME, ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE,
|
||||||
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, ATTR_OAUTH2_RS_NAME, ATTR_OAUTH2_RS_ORIGIN,
|
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT, ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
|
||||||
|
ATTR_OAUTH2_PREFER_SHORT_USERNAME, ATTR_OAUTH2_RS_NAME, ATTR_OAUTH2_RS_ORIGIN,
|
||||||
};
|
};
|
||||||
use kanidm_proto::internal::ImageValue;
|
use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
|
||||||
use kanidm_proto::v1::Entry;
|
use kanidm_proto::v1::Entry;
|
||||||
use reqwest::multipart;
|
use reqwest::multipart;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
@ -302,7 +303,7 @@ impl KanidmClient {
|
||||||
attrs: BTreeMap::new(),
|
attrs: BTreeMap::new(),
|
||||||
};
|
};
|
||||||
update_oauth2_rs.attrs.insert(
|
update_oauth2_rs.attrs.insert(
|
||||||
"oauth2_prefer_short_username".to_string(),
|
ATTR_OAUTH2_PREFER_SHORT_USERNAME.to_string(),
|
||||||
vec!["true".to_string()],
|
vec!["true".to_string()],
|
||||||
);
|
);
|
||||||
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
|
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
|
||||||
|
@ -314,10 +315,80 @@ impl KanidmClient {
|
||||||
attrs: BTreeMap::new(),
|
attrs: BTreeMap::new(),
|
||||||
};
|
};
|
||||||
update_oauth2_rs.attrs.insert(
|
update_oauth2_rs.attrs.insert(
|
||||||
"oauth2_prefer_short_username".to_string(),
|
ATTR_OAUTH2_PREFER_SHORT_USERNAME.to_string(),
|
||||||
vec!["false".to_string()],
|
vec!["false".to_string()],
|
||||||
);
|
);
|
||||||
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
|
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn idm_oauth2_rs_enable_public_localhost_redirect(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let mut update_oauth2_rs = Entry {
|
||||||
|
attrs: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
update_oauth2_rs.attrs.insert(
|
||||||
|
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT.to_string(),
|
||||||
|
vec!["true".to_string()],
|
||||||
|
);
|
||||||
|
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn idm_oauth2_rs_disable_public_localhost_redirect(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let mut update_oauth2_rs = Entry {
|
||||||
|
attrs: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
update_oauth2_rs.attrs.insert(
|
||||||
|
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT.to_string(),
|
||||||
|
vec!["false".to_string()],
|
||||||
|
);
|
||||||
|
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn idm_oauth2_rs_update_claim_map(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
claim_name: &str,
|
||||||
|
group_id: &str,
|
||||||
|
values: &[String],
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let values: Vec<String> = values.to_vec();
|
||||||
|
self.perform_post_request(
|
||||||
|
format!("/v1/oauth2/{}/_claimmap/{}/{}", id, claim_name, group_id).as_str(),
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn idm_oauth2_rs_update_claim_map_join(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
claim_name: &str,
|
||||||
|
join: Oauth2ClaimMapJoin,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
self.perform_post_request(
|
||||||
|
format!("/v1/oauth2/{}/_claimmap/{}", id, claim_name).as_str(),
|
||||||
|
join,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn idm_oauth2_rs_delete_claim_map(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
claim_name: &str,
|
||||||
|
group_id: &str,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
self.perform_delete_request(
|
||||||
|
format!("/v1/oauth2/{}/_claimmap/{}/{}", id, claim_name, group_id).as_str(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,10 +120,12 @@ pub const ATTR_NSUNIQUEID: &str = "nsuniqueid";
|
||||||
|
|
||||||
pub const ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE: &str =
|
pub const ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE: &str =
|
||||||
"oauth2_allow_insecure_client_disable_pkce";
|
"oauth2_allow_insecure_client_disable_pkce";
|
||||||
|
pub const ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT: &str = "oauth2_allow_localhost_redirect";
|
||||||
pub const ATTR_OAUTH2_CONSENT_SCOPE_MAP: &str = "oauth2_consent_scope_map";
|
pub const ATTR_OAUTH2_CONSENT_SCOPE_MAP: &str = "oauth2_consent_scope_map";
|
||||||
pub const ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE: &str = "oauth2_jwt_legacy_crypto_enable";
|
pub const ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE: &str = "oauth2_jwt_legacy_crypto_enable";
|
||||||
pub const ATTR_OAUTH2_PREFER_SHORT_USERNAME: &str = "oauth2_prefer_short_username";
|
pub const ATTR_OAUTH2_PREFER_SHORT_USERNAME: &str = "oauth2_prefer_short_username";
|
||||||
pub const ATTR_OAUTH2_RS_BASIC_SECRET: &str = "oauth2_rs_basic_secret";
|
pub const ATTR_OAUTH2_RS_BASIC_SECRET: &str = "oauth2_rs_basic_secret";
|
||||||
|
pub const ATTR_OAUTH2_RS_CLAIM_MAP: &str = "oauth2_rs_claim_map";
|
||||||
pub const ATTR_OAUTH2_RS_IMPLICIT_SCOPES: &str = "oauth2_rs_implicit_scopes";
|
pub const ATTR_OAUTH2_RS_IMPLICIT_SCOPES: &str = "oauth2_rs_implicit_scopes";
|
||||||
pub const ATTR_OAUTH2_RS_NAME: &str = "oauth2_rs_name";
|
pub const ATTR_OAUTH2_RS_NAME: &str = "oauth2_rs_name";
|
||||||
pub const ATTR_OAUTH2_RS_ORIGIN_LANDING: &str = "oauth2_rs_origin_landing";
|
pub const ATTR_OAUTH2_RS_ORIGIN_LANDING: &str = "oauth2_rs_origin_landing";
|
||||||
|
|
|
@ -151,25 +151,32 @@ impl FsType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for FsType {
|
impl TryFrom<&str> for FsType {
|
||||||
fn from(s: String) -> Self {
|
type Error = ();
|
||||||
s.as_str().into()
|
|
||||||
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
|
match s {
|
||||||
|
"zfs" => Ok(FsType::Zfs),
|
||||||
|
"generic" => Ok(FsType::Generic),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for FsType {
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||||
fn from(s: &str) -> Self {
|
pub enum Oauth2ClaimMapJoin {
|
||||||
match s {
|
#[serde(rename = "csv")]
|
||||||
"zfs" => FsType::Zfs,
|
Csv,
|
||||||
_ => FsType::Generic,
|
#[serde(rename = "ssv")]
|
||||||
}
|
Ssv,
|
||||||
}
|
#[serde(rename = "array")]
|
||||||
|
Array,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fstype_deser() {
|
fn test_fstype_deser() {
|
||||||
assert_eq!(FsType::from("zfs"), FsType::Zfs);
|
assert_eq!(FsType::try_from("zfs"), Ok(FsType::Zfs));
|
||||||
assert_eq!(FsType::from("generic"), FsType::Generic);
|
assert_eq!(FsType::try_from("generic"), Ok(FsType::Generic));
|
||||||
assert_eq!(FsType::from(" "), FsType::Generic);
|
assert_eq!(FsType::try_from(" "), Err(()));
|
||||||
assert_eq!(FsType::from("crab🦀"), FsType::Generic);
|
assert_eq!(FsType::try_from("crab🦀"), Err(()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{iter, sync::Arc};
|
use std::{iter, sync::Arc};
|
||||||
|
|
||||||
use kanidm_proto::internal::ImageValue;
|
use kanidm_proto::internal::ImageValue;
|
||||||
|
use kanidm_proto::internal::Oauth2ClaimMapJoin as ProtoOauth2ClaimMapJoin;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest, DeleteRequest,
|
AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest, DeleteRequest,
|
||||||
Entry as ProtoEntry, GroupUnixExtend, Modify as ProtoModify, ModifyList as ProtoModifyList,
|
Entry as ProtoEntry, GroupUnixExtend, Modify as ProtoModify, ModifyList as ProtoModifyList,
|
||||||
|
@ -30,7 +31,7 @@ use kanidmd_lib::{
|
||||||
idm::server::{IdmServer, IdmServerTransaction},
|
idm::server::{IdmServer, IdmServerTransaction},
|
||||||
idm::serviceaccount::{DestroyApiTokenEvent, GenerateApiTokenEvent},
|
idm::serviceaccount::{DestroyApiTokenEvent, GenerateApiTokenEvent},
|
||||||
modify::{Modify, ModifyInvalid, ModifyList},
|
modify::{Modify, ModifyInvalid, ModifyList},
|
||||||
value::{PartialValue, Value},
|
value::{OauthClaimMapJoin, PartialValue, Value},
|
||||||
};
|
};
|
||||||
|
|
||||||
use kanidmd_lib::prelude::*;
|
use kanidmd_lib::prelude::*;
|
||||||
|
@ -1362,6 +1363,183 @@ impl QueryServerWriteV1 {
|
||||||
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
level = "info",
|
||||||
|
skip_all,
|
||||||
|
fields(uuid = ?eventid)
|
||||||
|
)]
|
||||||
|
pub async fn handle_oauth2_claimmap_update(
|
||||||
|
&self,
|
||||||
|
client_auth_info: ClientAuthInfo,
|
||||||
|
claim_name: String,
|
||||||
|
group: String,
|
||||||
|
claims: Vec<String>,
|
||||||
|
filter: Filter<FilterInvalid>,
|
||||||
|
eventid: Uuid,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
// Because this is from internal, we can generate a real modlist, rather
|
||||||
|
// than relying on the proto ones.
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
let ident = idms_prox_write
|
||||||
|
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Invalid identity");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let group_uuid = idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.name_to_uuid(group.as_str())
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Error resolving group name to target");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ml = ModifyList::new_append(
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Value::new_oauthclaimmap(claim_name, group_uuid, claims.into_iter().collect())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
OperationError::InvalidAttribute("Invalid Oauth Claim Map syntax".to_string())
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mdf = match ModifyEvent::from_internal_parts(
|
||||||
|
ident,
|
||||||
|
&ml,
|
||||||
|
&filter,
|
||||||
|
&idms_prox_write.qs_write,
|
||||||
|
) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
admin_error!(err = ?e, "Failed to begin modify");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(?mdf, "Begin modify event");
|
||||||
|
|
||||||
|
idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.modify(&mdf)
|
||||||
|
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
level = "info",
|
||||||
|
skip_all,
|
||||||
|
fields(uuid = ?eventid)
|
||||||
|
)]
|
||||||
|
pub async fn handle_oauth2_claimmap_join_update(
|
||||||
|
&self,
|
||||||
|
client_auth_info: ClientAuthInfo,
|
||||||
|
claim_name: String,
|
||||||
|
join: ProtoOauth2ClaimMapJoin,
|
||||||
|
filter: Filter<FilterInvalid>,
|
||||||
|
eventid: Uuid,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
// Because this is from internal, we can generate a real modlist, rather
|
||||||
|
// than relying on the proto ones.
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
let ident = idms_prox_write
|
||||||
|
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Invalid identity");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let join = match join {
|
||||||
|
ProtoOauth2ClaimMapJoin::Csv => OauthClaimMapJoin::CommaSeparatedValue,
|
||||||
|
ProtoOauth2ClaimMapJoin::Ssv => OauthClaimMapJoin::SpaceSeparatedValue,
|
||||||
|
ProtoOauth2ClaimMapJoin::Array => OauthClaimMapJoin::JsonArray,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ml = ModifyList::new_append(
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Value::OauthClaimMap(claim_name, join),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mdf = match ModifyEvent::from_internal_parts(
|
||||||
|
ident,
|
||||||
|
&ml,
|
||||||
|
&filter,
|
||||||
|
&idms_prox_write.qs_write,
|
||||||
|
) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
admin_error!(err = ?e, "Failed to begin modify");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(?mdf, "Begin modify event");
|
||||||
|
|
||||||
|
idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.modify(&mdf)
|
||||||
|
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
level = "info",
|
||||||
|
skip_all,
|
||||||
|
fields(uuid = ?eventid)
|
||||||
|
)]
|
||||||
|
pub async fn handle_oauth2_claimmap_delete(
|
||||||
|
&self,
|
||||||
|
client_auth_info: ClientAuthInfo,
|
||||||
|
claim_name: String,
|
||||||
|
group: String,
|
||||||
|
filter: Filter<FilterInvalid>,
|
||||||
|
eventid: Uuid,
|
||||||
|
) -> Result<(), OperationError> {
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
let mut idms_prox_write = self.idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
let ident = idms_prox_write
|
||||||
|
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Invalid identity");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let group_uuid = idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.name_to_uuid(group.as_str())
|
||||||
|
.map_err(|e| {
|
||||||
|
admin_error!(err = ?e, "Error resolving group name to target");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ml = ModifyList::new_remove(
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
PartialValue::OauthClaim(claim_name, group_uuid),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mdf = match ModifyEvent::from_internal_parts(
|
||||||
|
ident,
|
||||||
|
&ml,
|
||||||
|
&filter,
|
||||||
|
&idms_prox_write.qs_write,
|
||||||
|
) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
admin_error!(err = ?e, "Failed to begin modify");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(?mdf, "Begin modify event");
|
||||||
|
|
||||||
|
idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.modify(&mdf)
|
||||||
|
.and_then(|_| idms_prox_write.commit().map(|_| ()))
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(
|
#[instrument(
|
||||||
level = "info",
|
level = "info",
|
||||||
skip_all,
|
skip_all,
|
||||||
|
|
|
@ -2856,6 +2856,15 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
|
||||||
post(super::v1_oauth2::oauth2_id_sup_scopemap_post)
|
post(super::v1_oauth2::oauth2_id_sup_scopemap_post)
|
||||||
.delete(super::v1_oauth2::oauth2_id_sup_scopemap_delete),
|
.delete(super::v1_oauth2::oauth2_id_sup_scopemap_delete),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/oauth2/:rs_name/_claimmap/:claim_name/:group",
|
||||||
|
post(super::v1_oauth2::oauth2_id_claimmap_post)
|
||||||
|
.delete(super::v1_oauth2::oauth2_id_claimmap_delete),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/oauth2/:rs_name/_claimmap/:claim_name",
|
||||||
|
post(super::v1_oauth2::oauth2_id_claimmap_join_post),
|
||||||
|
)
|
||||||
.route("/v1/raw/create", post(raw_create))
|
.route("/v1/raw/create", post(raw_create))
|
||||||
.route("/v1/raw/modify", post(raw_modify))
|
.route("/v1/raw/modify", post(raw_modify))
|
||||||
.route("/v1/raw/delete", post(raw_delete))
|
.route("/v1/raw/delete", post(raw_delete))
|
||||||
|
|
|
@ -8,7 +8,7 @@ use super::ServerState;
|
||||||
use crate::https::extractors::VerifiedClientInformation;
|
use crate::https::extractors::VerifiedClientInformation;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::{Extension, Json};
|
use axum::{Extension, Json};
|
||||||
use kanidm_proto::internal::{ImageType, ImageValue};
|
use kanidm_proto::internal::{ImageType, ImageValue, Oauth2ClaimMapJoin};
|
||||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||||
use kanidmd_lib::prelude::*;
|
use kanidmd_lib::prelude::*;
|
||||||
use kanidmd_lib::valueset::image::ImageValueThings;
|
use kanidmd_lib::valueset::image::ImageValueThings;
|
||||||
|
@ -221,6 +221,98 @@ pub(crate) async fn oauth2_id_scopemap_delete(
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = "/v1/oauth2/{rs_name}/_claimmap/{claim_name}/{group}",
|
||||||
|
request_body=Vec<String>,
|
||||||
|
responses(
|
||||||
|
DefaultApiResponse,
|
||||||
|
),
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/oauth2",
|
||||||
|
)]
|
||||||
|
/// Modify the claim map for a given OAuth2 Resource Server
|
||||||
|
pub(crate) async fn oauth2_id_claimmap_post(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
Path((rs_name, claim_name, group)): Path<(String, String, String)>,
|
||||||
|
Json(claims): Json<Vec<String>>,
|
||||||
|
) -> Result<Json<()>, WebError> {
|
||||||
|
let filter = oauth2_id(&rs_name);
|
||||||
|
state
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_oauth2_claimmap_update(
|
||||||
|
client_auth_info,
|
||||||
|
claim_name,
|
||||||
|
group,
|
||||||
|
claims,
|
||||||
|
filter,
|
||||||
|
kopid.eventid,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(Json::from)
|
||||||
|
.map_err(WebError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = "/v1/oauth2/{rs_name}/_claimmap/{claim_name}",
|
||||||
|
request_body=Oauth2ClaimMapJoin,
|
||||||
|
responses(
|
||||||
|
DefaultApiResponse,
|
||||||
|
),
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/oauth2",
|
||||||
|
)]
|
||||||
|
/// Modify the claim map join strategy for a given OAuth2 Resource Server
|
||||||
|
pub(crate) async fn oauth2_id_claimmap_join_post(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
Path((rs_name, claim_name)): Path<(String, String)>,
|
||||||
|
Json(join): Json<Oauth2ClaimMapJoin>,
|
||||||
|
) -> Result<Json<()>, WebError> {
|
||||||
|
let filter = oauth2_id(&rs_name);
|
||||||
|
state
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_oauth2_claimmap_join_update(
|
||||||
|
client_auth_info,
|
||||||
|
claim_name,
|
||||||
|
join,
|
||||||
|
filter,
|
||||||
|
kopid.eventid,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(Json::from)
|
||||||
|
.map_err(WebError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/v1/oauth2/{rs_name}/_claimmap/{claim_name}/{group}",
|
||||||
|
responses(
|
||||||
|
DefaultApiResponse,
|
||||||
|
),
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/oauth2",
|
||||||
|
)]
|
||||||
|
// Delete a claim map for a given OAuth2 Resource Server
|
||||||
|
pub(crate) async fn oauth2_id_claimmap_delete(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
Path((rs_name, claim_name, group)): Path<(String, String, String)>,
|
||||||
|
) -> Result<Json<()>, WebError> {
|
||||||
|
let filter = oauth2_id(&rs_name);
|
||||||
|
state
|
||||||
|
.qe_w_ref
|
||||||
|
.handle_oauth2_claimmap_delete(client_auth_info, claim_name, group, filter, kopid.eventid)
|
||||||
|
.await
|
||||||
|
.map(Json::from)
|
||||||
|
.map_err(WebError::from)
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/v1/oauth2/{rs_name}/_sup_scopemap/{group}",
|
path = "/v1/oauth2/{rs_name}/_sup_scopemap/{group}",
|
||||||
|
|
|
@ -5,6 +5,7 @@ use hashbrown::HashSet;
|
||||||
use kanidm_proto::internal::ImageType;
|
use kanidm_proto::internal::ImageType;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use webauthn_rs::prelude::{
|
use webauthn_rs::prelude::{
|
||||||
|
@ -398,6 +399,28 @@ pub struct DbValueAddressV1 {
|
||||||
pub country: String,
|
pub country: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum DbValueOauthClaimMapJoinV1 {
|
||||||
|
#[serde(rename = "c")]
|
||||||
|
CommaSeparatedValue,
|
||||||
|
#[serde(rename = "s")]
|
||||||
|
SpaceSeparatedValue,
|
||||||
|
#[serde(rename = "a")]
|
||||||
|
JsonArray,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub enum DbValueOauthClaimMap {
|
||||||
|
V1 {
|
||||||
|
#[serde(rename = "n")]
|
||||||
|
name: String,
|
||||||
|
#[serde(rename = "j")]
|
||||||
|
join: DbValueOauthClaimMapJoinV1,
|
||||||
|
#[serde(rename = "d")]
|
||||||
|
values: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct DbValueOauthScopeMapV1 {
|
pub struct DbValueOauthScopeMapV1 {
|
||||||
#[serde(rename = "u")]
|
#[serde(rename = "u")]
|
||||||
|
@ -672,6 +695,8 @@ pub enum DbValueSetV2 {
|
||||||
OauthScope(Vec<String>),
|
OauthScope(Vec<String>),
|
||||||
#[serde(rename = "OM")]
|
#[serde(rename = "OM")]
|
||||||
OauthScopeMap(Vec<DbValueOauthScopeMapV1>),
|
OauthScopeMap(Vec<DbValueOauthScopeMapV1>),
|
||||||
|
#[serde(rename = "OC")]
|
||||||
|
OauthClaimMap(Vec<DbValueOauthClaimMap>),
|
||||||
#[serde(rename = "E2")]
|
#[serde(rename = "E2")]
|
||||||
PrivateBinary(Vec<Vec<u8>>),
|
PrivateBinary(Vec<Vec<u8>>),
|
||||||
#[serde(rename = "PB")]
|
#[serde(rename = "PB")]
|
||||||
|
@ -736,6 +761,7 @@ impl DbValueSetV2 {
|
||||||
DbValueSetV2::PhoneNumber(_primary, set) => set.len(),
|
DbValueSetV2::PhoneNumber(_primary, set) => set.len(),
|
||||||
DbValueSetV2::Address(set) => set.len(),
|
DbValueSetV2::Address(set) => set.len(),
|
||||||
DbValueSetV2::Url(set) => set.len(),
|
DbValueSetV2::Url(set) => set.len(),
|
||||||
|
DbValueSetV2::OauthClaimMap(set) => set.len(),
|
||||||
DbValueSetV2::OauthScope(set) => set.len(),
|
DbValueSetV2::OauthScope(set) => set.len(),
|
||||||
DbValueSetV2::OauthScopeMap(set) => set.len(),
|
DbValueSetV2::OauthScopeMap(set) => set.len(),
|
||||||
DbValueSetV2::PrivateBinary(set) => set.len(),
|
DbValueSetV2::PrivateBinary(set) => set.len(),
|
||||||
|
|
|
@ -1886,7 +1886,7 @@ impl IdlSqlite {
|
||||||
OperationError::BackendEngine
|
OperationError::BackendEngine
|
||||||
})?;
|
})?;
|
||||||
// Get not pop here
|
// Get not pop here
|
||||||
let conn = guard.get(0).ok_or_else(|| {
|
let conn = guard.front().ok_or_else(|| {
|
||||||
error!("Unable to retrieve connection from pool");
|
error!("Unable to retrieve connection from pool");
|
||||||
OperationError::BackendEngine
|
OperationError::BackendEngine
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -622,6 +622,104 @@ lazy_static! {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref IDM_ACP_OAUTH2_MANAGE_DL4: BuiltinAcp = BuiltinAcp {
|
||||||
|
classes: vec![
|
||||||
|
EntryClass::Object,
|
||||||
|
EntryClass::AccessControlProfile,
|
||||||
|
EntryClass::AccessControlCreate,
|
||||||
|
EntryClass::AccessControlDelete,
|
||||||
|
EntryClass::AccessControlModify,
|
||||||
|
EntryClass::AccessControlSearch
|
||||||
|
],
|
||||||
|
name: "idm_acp_hp_oauth2_manage_priv",
|
||||||
|
uuid: UUID_IDM_ACP_OAUTH2_MANAGE_V1,
|
||||||
|
description: "Builtin IDM Control for managing oauth2 resource server integrations.",
|
||||||
|
receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_OAUTH2_ADMINS]),
|
||||||
|
target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
|
||||||
|
match_class_filter!(EntryClass::OAuth2ResourceServer),
|
||||||
|
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone(),
|
||||||
|
])),
|
||||||
|
search_attrs: vec![
|
||||||
|
Attribute::Class,
|
||||||
|
Attribute::Description,
|
||||||
|
Attribute::DisplayName,
|
||||||
|
Attribute::OAuth2RsName,
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Attribute::OAuth2RsOriginLanding,
|
||||||
|
Attribute::OAuth2RsScopeMap,
|
||||||
|
Attribute::OAuth2RsSupScopeMap,
|
||||||
|
Attribute::OAuth2RsBasicSecret,
|
||||||
|
Attribute::OAuth2RsTokenKey,
|
||||||
|
Attribute::Es256PrivateKeyDer,
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
Attribute::Rs256PrivateKeyDer,
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
modify_removed_attrs: vec![
|
||||||
|
Attribute::Description,
|
||||||
|
Attribute::DisplayName,
|
||||||
|
Attribute::OAuth2RsName,
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Attribute::OAuth2RsOriginLanding,
|
||||||
|
Attribute::OAuth2RsScopeMap,
|
||||||
|
Attribute::OAuth2RsSupScopeMap,
|
||||||
|
Attribute::OAuth2RsBasicSecret,
|
||||||
|
Attribute::OAuth2RsTokenKey,
|
||||||
|
Attribute::Es256PrivateKeyDer,
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
Attribute::Rs256PrivateKeyDer,
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
modify_present_attrs: vec![
|
||||||
|
Attribute::Description,
|
||||||
|
Attribute::DisplayName,
|
||||||
|
Attribute::OAuth2RsName,
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Attribute::OAuth2RsOriginLanding,
|
||||||
|
Attribute::OAuth2RsSupScopeMap,
|
||||||
|
Attribute::OAuth2RsScopeMap,
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
create_attrs: vec![
|
||||||
|
Attribute::Class,
|
||||||
|
Attribute::Description,
|
||||||
|
Attribute::DisplayName,
|
||||||
|
Attribute::OAuth2RsName,
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Attribute::OAuth2RsOriginLanding,
|
||||||
|
Attribute::OAuth2RsSupScopeMap,
|
||||||
|
Attribute::OAuth2RsScopeMap,
|
||||||
|
Attribute::OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
|
Attribute::OAuth2PreferShortUsername,
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Attribute::Image,
|
||||||
|
],
|
||||||
|
create_classes: vec![
|
||||||
|
EntryClass::Object,
|
||||||
|
EntryClass::OAuth2ResourceServer,
|
||||||
|
EntryClass::OAuth2ResourceServerBasic,
|
||||||
|
EntryClass::OAuth2ResourceServerPublic,
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref IDM_ACP_DOMAIN_ADMIN_V1: BuiltinAcp = BuiltinAcp {
|
pub static ref IDM_ACP_DOMAIN_ADMIN_V1: BuiltinAcp = BuiltinAcp {
|
||||||
classes: vec![
|
classes: vec![
|
||||||
|
|
|
@ -118,10 +118,12 @@ pub enum Attribute {
|
||||||
NsUniqueId,
|
NsUniqueId,
|
||||||
NsAccountLock,
|
NsAccountLock,
|
||||||
OAuth2AllowInsecureClientDisablePkce,
|
OAuth2AllowInsecureClientDisablePkce,
|
||||||
|
OAuth2AllowLocalhostRedirect,
|
||||||
OAuth2ConsentScopeMap,
|
OAuth2ConsentScopeMap,
|
||||||
OAuth2JwtLegacyCryptoEnable,
|
OAuth2JwtLegacyCryptoEnable,
|
||||||
OAuth2PreferShortUsername,
|
OAuth2PreferShortUsername,
|
||||||
OAuth2RsBasicSecret,
|
OAuth2RsBasicSecret,
|
||||||
|
OAuth2RsClaimMap,
|
||||||
OAuth2RsImplicitScopes,
|
OAuth2RsImplicitScopes,
|
||||||
OAuth2RsName,
|
OAuth2RsName,
|
||||||
OAuth2RsOrigin,
|
OAuth2RsOrigin,
|
||||||
|
@ -304,10 +306,12 @@ impl TryFrom<String> for Attribute {
|
||||||
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE => {
|
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE => {
|
||||||
Attribute::OAuth2AllowInsecureClientDisablePkce
|
Attribute::OAuth2AllowInsecureClientDisablePkce
|
||||||
}
|
}
|
||||||
|
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT => Attribute::OAuth2AllowLocalhostRedirect,
|
||||||
ATTR_OAUTH2_CONSENT_SCOPE_MAP => Attribute::OAuth2ConsentScopeMap,
|
ATTR_OAUTH2_CONSENT_SCOPE_MAP => Attribute::OAuth2ConsentScopeMap,
|
||||||
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE => Attribute::OAuth2JwtLegacyCryptoEnable,
|
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE => Attribute::OAuth2JwtLegacyCryptoEnable,
|
||||||
ATTR_OAUTH2_PREFER_SHORT_USERNAME => Attribute::OAuth2PreferShortUsername,
|
ATTR_OAUTH2_PREFER_SHORT_USERNAME => Attribute::OAuth2PreferShortUsername,
|
||||||
ATTR_OAUTH2_RS_BASIC_SECRET => Attribute::OAuth2RsBasicSecret,
|
ATTR_OAUTH2_RS_BASIC_SECRET => Attribute::OAuth2RsBasicSecret,
|
||||||
|
ATTR_OAUTH2_RS_CLAIM_MAP => Attribute::OAuth2RsClaimMap,
|
||||||
ATTR_OAUTH2_RS_IMPLICIT_SCOPES => Attribute::OAuth2RsImplicitScopes,
|
ATTR_OAUTH2_RS_IMPLICIT_SCOPES => Attribute::OAuth2RsImplicitScopes,
|
||||||
ATTR_OAUTH2_RS_NAME => Attribute::OAuth2RsName,
|
ATTR_OAUTH2_RS_NAME => Attribute::OAuth2RsName,
|
||||||
ATTR_OAUTH2_RS_ORIGIN => Attribute::OAuth2RsOrigin,
|
ATTR_OAUTH2_RS_ORIGIN => Attribute::OAuth2RsOrigin,
|
||||||
|
@ -465,10 +469,12 @@ impl From<Attribute> for &'static str {
|
||||||
Attribute::OAuth2AllowInsecureClientDisablePkce => {
|
Attribute::OAuth2AllowInsecureClientDisablePkce => {
|
||||||
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE
|
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE
|
||||||
}
|
}
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect => ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
|
||||||
Attribute::OAuth2ConsentScopeMap => ATTR_OAUTH2_CONSENT_SCOPE_MAP,
|
Attribute::OAuth2ConsentScopeMap => ATTR_OAUTH2_CONSENT_SCOPE_MAP,
|
||||||
Attribute::OAuth2JwtLegacyCryptoEnable => ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
|
Attribute::OAuth2JwtLegacyCryptoEnable => ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
|
||||||
Attribute::OAuth2PreferShortUsername => ATTR_OAUTH2_PREFER_SHORT_USERNAME,
|
Attribute::OAuth2PreferShortUsername => ATTR_OAUTH2_PREFER_SHORT_USERNAME,
|
||||||
Attribute::OAuth2RsBasicSecret => ATTR_OAUTH2_RS_BASIC_SECRET,
|
Attribute::OAuth2RsBasicSecret => ATTR_OAUTH2_RS_BASIC_SECRET,
|
||||||
|
Attribute::OAuth2RsClaimMap => ATTR_OAUTH2_RS_CLAIM_MAP,
|
||||||
Attribute::OAuth2RsImplicitScopes => ATTR_OAUTH2_RS_IMPLICIT_SCOPES,
|
Attribute::OAuth2RsImplicitScopes => ATTR_OAUTH2_RS_IMPLICIT_SCOPES,
|
||||||
Attribute::OAuth2RsName => ATTR_OAUTH2_RS_NAME,
|
Attribute::OAuth2RsName => ATTR_OAUTH2_RS_NAME,
|
||||||
Attribute::OAuth2RsOrigin => ATTR_OAUTH2_RS_ORIGIN,
|
Attribute::OAuth2RsOrigin => ATTR_OAUTH2_RS_ORIGIN,
|
||||||
|
|
|
@ -46,12 +46,13 @@ pub type DomainVersion = u32;
|
||||||
pub const DOMAIN_LEVEL_1: DomainVersion = 1;
|
pub const DOMAIN_LEVEL_1: DomainVersion = 1;
|
||||||
pub const DOMAIN_LEVEL_2: DomainVersion = 2;
|
pub const DOMAIN_LEVEL_2: DomainVersion = 2;
|
||||||
pub const DOMAIN_LEVEL_3: DomainVersion = 3;
|
pub const DOMAIN_LEVEL_3: DomainVersion = 3;
|
||||||
|
pub const DOMAIN_LEVEL_4: DomainVersion = 4;
|
||||||
// The minimum supported domain functional level
|
// The minimum supported domain functional level
|
||||||
pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_LEVEL_2;
|
pub const DOMAIN_MIN_LEVEL: DomainVersion = DOMAIN_LEVEL_2;
|
||||||
// The target supported domain functional level
|
// The target supported domain functional level
|
||||||
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_3;
|
pub const DOMAIN_TGT_LEVEL: DomainVersion = DOMAIN_LEVEL_4;
|
||||||
// The maximum supported domain functional level
|
// The maximum supported domain functional level
|
||||||
pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_3;
|
pub const DOMAIN_MAX_LEVEL: DomainVersion = DOMAIN_LEVEL_4;
|
||||||
|
|
||||||
// On test builds, define to 60 seconds
|
// On test builds, define to 60 seconds
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -322,12 +322,35 @@ pub static ref SCHEMA_ATTR_OAUTH2_RS_ORIGIN: SchemaAttribute = SchemaAttribute {
|
||||||
pub static ref SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING: SchemaAttribute = SchemaAttribute {
|
pub static ref SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING: SchemaAttribute = SchemaAttribute {
|
||||||
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING,
|
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN_LANDING,
|
||||||
name: Attribute::OAuth2RsOriginLanding.into(),
|
name: Attribute::OAuth2RsOriginLanding.into(),
|
||||||
description: "The landing page of an RS, that will automatically trigger the auth process.to_string().".to_string(),
|
description: "The landing page of an RS, that will automatically trigger the auth process".to_string(),
|
||||||
|
|
||||||
syntax: SyntaxType::Url,
|
syntax: SyntaxType::Url,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Introduced in DomainLevel4
|
||||||
|
pub static ref SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT_DL4: SchemaAttribute = SchemaAttribute {
|
||||||
|
uuid: UUID_SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
|
||||||
|
name: Attribute::OAuth2AllowLocalhostRedirect.into(),
|
||||||
|
description: "Allow public clients associated to this RS to redirect to localhost".to_string(),
|
||||||
|
|
||||||
|
syntax: SyntaxType::Boolean,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4: SchemaAttribute = SchemaAttribute {
|
||||||
|
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP,
|
||||||
|
name: Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
description:
|
||||||
|
"A set of custom claims mapped to group memberships of accounts.".to_string(),
|
||||||
|
|
||||||
|
index: vec![IndexType::Equality],
|
||||||
|
multivalue: true,
|
||||||
|
// CHANGE ME
|
||||||
|
syntax: SyntaxType::OauthClaimMap,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
pub static ref SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: SchemaAttribute = SchemaAttribute {
|
pub static ref SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP: SchemaAttribute = SchemaAttribute {
|
||||||
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP,
|
uuid: UUID_SCHEMA_ATTR_OAUTH2_RS_SCOPE_MAP,
|
||||||
name: Attribute::OAuth2RsScopeMap.into(),
|
name: Attribute::OAuth2RsScopeMap.into(),
|
||||||
|
@ -828,6 +851,32 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS: SchemaClass = SchemaClass {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub static ref SCHEMA_CLASS_OAUTH2_RS_DL4: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS,
|
||||||
|
name: EntryClass::OAuth2ResourceServer.into(),
|
||||||
|
description: "The class representing a configured Oauth2 Resource Server".to_string(),
|
||||||
|
|
||||||
|
systemmay: vec![
|
||||||
|
Attribute::Description.into(),
|
||||||
|
Attribute::OAuth2RsScopeMap.into(),
|
||||||
|
Attribute::OAuth2RsSupScopeMap.into(),
|
||||||
|
Attribute::Rs256PrivateKeyDer.into(),
|
||||||
|
Attribute::OAuth2JwtLegacyCryptoEnable.into(),
|
||||||
|
Attribute::OAuth2PreferShortUsername.into(),
|
||||||
|
Attribute::OAuth2RsOriginLanding.into(),
|
||||||
|
Attribute::Image.into(),
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
],
|
||||||
|
systemmust: vec![
|
||||||
|
Attribute::OAuth2RsName.into(),
|
||||||
|
Attribute::DisplayName.into(),
|
||||||
|
Attribute::OAuth2RsOrigin.into(),
|
||||||
|
Attribute::OAuth2RsTokenKey.into(),
|
||||||
|
Attribute::Es256PrivateKeyDer.into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
|
pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
|
||||||
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC,
|
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC,
|
||||||
name: EntryClass::OAuth2ResourceServerBasic.into(),
|
name: EntryClass::OAuth2ResourceServerBasic.into(),
|
||||||
|
@ -839,12 +888,22 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_BASIC: SchemaClass = SchemaClass {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
pub static ref SCHEMA_CLASS_OAUTH2_RS_PUBLIC: SchemaClass = SchemaClass {
|
pub static ref SCHEMA_CLASS_OAUTH2_RS_PUBLIC: SchemaClass = SchemaClass {
|
||||||
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC,
|
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC,
|
||||||
name: EntryClass::OAuth2ResourceServerPublic.into(),
|
name: EntryClass::OAuth2ResourceServerPublic.into(),
|
||||||
|
|
||||||
description: "The class representing a configured Oauth2 Resource Server with public clients and pkce verification".to_string(),
|
description: "The class representing a configured Oauth2 Resource Server with public clients and pkce verification".to_string(),
|
||||||
|
|
||||||
|
systemexcludes: vec![EntryClass::OAuth2ResourceServerBasic.into()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Introduced in DomainLevel4
|
||||||
|
pub static ref SCHEMA_CLASS_OAUTH2_RS_PUBLIC_DL4: SchemaClass = SchemaClass {
|
||||||
|
uuid: UUID_SCHEMA_CLASS_OAUTH2_RS_PUBLIC,
|
||||||
|
name: EntryClass::OAuth2ResourceServerPublic.into(),
|
||||||
|
description: "The class representing a configured Oauth2 Resource Server with public clients and pkce verification".to_string(),
|
||||||
|
|
||||||
|
systemmay: vec![Attribute::OAuth2AllowLocalhostRedirect.into()],
|
||||||
systemexcludes: vec![EntryClass::OAuth2ResourceServerBasic.into()],
|
systemexcludes: vec![EntryClass::OAuth2ResourceServerBasic.into()],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
|
@ -270,6 +270,10 @@ pub const UUID_SCHEMA_CLASS_ACCESS_CONTROL_TARGET_SCOPE: Uuid =
|
||||||
pub const UUID_SCHEMA_ATTR_ENTRY_MANAGED_BY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000156");
|
pub const UUID_SCHEMA_ATTR_ENTRY_MANAGED_BY: Uuid = uuid!("00000000-0000-0000-0000-ffff00000156");
|
||||||
pub const UUID_SCHEMA_ATTR_UNIX_PASSWORD_IMPORT: Uuid =
|
pub const UUID_SCHEMA_ATTR_UNIX_PASSWORD_IMPORT: Uuid =
|
||||||
uuid!("00000000-0000-0000-0000-ffff00000157");
|
uuid!("00000000-0000-0000-0000-ffff00000157");
|
||||||
|
pub const UUID_SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT: Uuid =
|
||||||
|
uuid!("00000000-0000-0000-0000-ffff00000158");
|
||||||
|
pub const UUID_SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP: Uuid =
|
||||||
|
uuid!("00000000-0000-0000-0000-ffff00000159");
|
||||||
|
|
||||||
// System and domain infos
|
// System and domain infos
|
||||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||||
|
|
|
@ -74,6 +74,10 @@ macro_rules! upg_from_account_e {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Group {
|
impl Group {
|
||||||
|
pub(crate) fn uuid(&self) -> Uuid {
|
||||||
|
self.uuid
|
||||||
|
}
|
||||||
|
|
||||||
pub fn try_from_account_entry_reduced<'a, TXN>(
|
pub fn try_from_account_entry_reduced<'a, TXN>(
|
||||||
value: &Entry<EntryReduced, EntryCommitted>,
|
value: &Entry<EntryReduced, EntryCommitted>,
|
||||||
qs: &mut TXN,
|
qs: &mut TXN,
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
//! integrations, which are then able to be used an accessed from the IDM layer
|
//! integrations, which are then able to be used an accessed from the IDM layer
|
||||||
//! for operations involving OAuth2 authentication processing.
|
//! for operations involving OAuth2 authentication processing.
|
||||||
|
|
||||||
|
use std::collections::btree_map::Entry as BTreeEntry;
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -40,7 +41,7 @@ use crate::idm::server::{
|
||||||
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
|
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::value::{Oauth2Session, SessionState, OAUTHSCOPE_RE};
|
use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
@ -202,7 +203,9 @@ enum OauthRSType {
|
||||||
enable_pkce: bool,
|
enable_pkce: bool,
|
||||||
},
|
},
|
||||||
// Public clients must have pkce.
|
// Public clients must have pkce.
|
||||||
Public,
|
Public {
|
||||||
|
allow_localhost_redirect: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for OauthRSType {
|
impl std::fmt::Debug for OauthRSType {
|
||||||
|
@ -212,7 +215,11 @@ impl std::fmt::Debug for OauthRSType {
|
||||||
OauthRSType::Basic { enable_pkce, .. } => {
|
OauthRSType::Basic { enable_pkce, .. } => {
|
||||||
ds.field("type", &"basic").field("pkce", enable_pkce)
|
ds.field("type", &"basic").field("pkce", enable_pkce)
|
||||||
}
|
}
|
||||||
OauthRSType::Public => ds.field("type", &"public"),
|
OauthRSType::Public {
|
||||||
|
allow_localhost_redirect,
|
||||||
|
} => ds
|
||||||
|
.field("type", &"public")
|
||||||
|
.field("allow_localhost_redirect", allow_localhost_redirect),
|
||||||
};
|
};
|
||||||
ds.finish()
|
ds.finish()
|
||||||
}
|
}
|
||||||
|
@ -224,6 +231,40 @@ enum Oauth2JwsSigner {
|
||||||
RS256 { signer: JwsRs256Signer },
|
RS256 { signer: JwsRs256Signer },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ClaimValue {
|
||||||
|
join: OauthClaimMapJoin,
|
||||||
|
values: BTreeSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClaimValue {
|
||||||
|
fn merge(&mut self, other: &Self) {
|
||||||
|
self.values.extend(other.values.iter().cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_json_value(&self) -> serde_json::Value {
|
||||||
|
let join_char = match self.join {
|
||||||
|
OauthClaimMapJoin::CommaSeparatedValue => ',',
|
||||||
|
OauthClaimMapJoin::SpaceSeparatedValue => ' ',
|
||||||
|
OauthClaimMapJoin::JsonArray => {
|
||||||
|
let arr: Vec<_> = self
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(serde_json::Value::String)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// This shortcuts out.
|
||||||
|
return serde_json::Value::Array(arr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let joined = str_concat!(&self.values, join_char);
|
||||||
|
|
||||||
|
serde_json::Value::String(joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Oauth2RS {
|
pub struct Oauth2RS {
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -231,13 +272,12 @@ pub struct Oauth2RS {
|
||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
origin: Origin,
|
origin: Origin,
|
||||||
origin_https: bool,
|
origin_https: bool,
|
||||||
|
claim_map: BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
|
||||||
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
sup_scope_maps: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
// Our internal exchange encryption material for this rs.
|
// Our internal exchange encryption material for this rs.
|
||||||
token_fernet: Fernet,
|
token_fernet: Fernet,
|
||||||
|
|
||||||
jws_signer: Oauth2JwsSigner,
|
jws_signer: Oauth2JwsSigner,
|
||||||
|
|
||||||
// For oidc we also need our issuer url.
|
// For oidc we also need our issuer url.
|
||||||
iss: Url,
|
iss: Url,
|
||||||
// For discovery we need to build and keep a number of values.
|
// For discovery we need to build and keep a number of values.
|
||||||
|
@ -262,6 +302,7 @@ impl std::fmt::Debug for Oauth2RS {
|
||||||
.field("origin", &self.origin)
|
.field("origin", &self.origin)
|
||||||
.field("scope_maps", &self.scope_maps)
|
.field("scope_maps", &self.scope_maps)
|
||||||
.field("sup_scope_maps", &self.sup_scope_maps)
|
.field("sup_scope_maps", &self.sup_scope_maps)
|
||||||
|
.field("claim_map", &self.claim_map)
|
||||||
.field("has_custom_image", &self.has_custom_image)
|
.field("has_custom_image", &self.has_custom_image)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
@ -352,7 +393,13 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
enable_pkce,
|
enable_pkce,
|
||||||
}
|
}
|
||||||
} else if ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerPublic.into()) {
|
} else if ent.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerPublic.into()) {
|
||||||
OauthRSType::Public
|
let allow_localhost_redirect = ent
|
||||||
|
.get_ava_single_bool(Attribute::OAuth2AllowLocalhostRedirect)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
OauthRSType::Public {
|
||||||
|
allow_localhost_redirect
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("Missing class determining OAuth2 rs type");
|
error!("Missing class determining OAuth2 rs type");
|
||||||
return Err(OperationError::InvalidEntryState);
|
return Err(OperationError::InvalidEntryState);
|
||||||
|
@ -400,6 +447,51 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let e_claim_maps = ent
|
||||||
|
.get_ava_set(Attribute::OAuth2RsClaimMap)
|
||||||
|
.and_then(|vs| vs.as_oauthclaim_map());
|
||||||
|
|
||||||
|
// ⚠️ Claim Maps as they are stored in the DB are optimised
|
||||||
|
// for referential integrity and user interaction. However we
|
||||||
|
// need to "invert" these for fast lookups during actual
|
||||||
|
// operation of the oauth2 client.
|
||||||
|
let claim_map = if let Some(e_claim_maps) = e_claim_maps {
|
||||||
|
let mut claim_map = BTreeMap::default();
|
||||||
|
|
||||||
|
for (claim_name, claim_mapping) in e_claim_maps.iter() {
|
||||||
|
for (group_uuid, claim_values) in claim_mapping.values().iter() {
|
||||||
|
// We always insert/append here because the outer claim_name has
|
||||||
|
// to be unique.
|
||||||
|
match claim_map.entry(*group_uuid) {
|
||||||
|
BTreeEntry::Vacant(e) => {
|
||||||
|
e.insert(
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
claim_name.clone(), ClaimValue {
|
||||||
|
join: claim_mapping.join(),
|
||||||
|
values: claim_values.clone()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
BTreeEntry::Occupied(mut e) => {
|
||||||
|
e.get_mut().push((
|
||||||
|
claim_name.clone(), ClaimValue {
|
||||||
|
join: claim_mapping.join(),
|
||||||
|
values: claim_values.clone()
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
claim_map
|
||||||
|
} else {
|
||||||
|
BTreeMap::default()
|
||||||
|
};
|
||||||
|
|
||||||
trace!("{}", Attribute::OAuth2JwtLegacyCryptoEnable.as_ref());
|
trace!("{}", Attribute::OAuth2JwtLegacyCryptoEnable.as_ref());
|
||||||
let jws_signer = if ent.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false) {
|
let jws_signer = if ent.get_ava_single_bool(Attribute::OAuth2JwtLegacyCryptoEnable).unwrap_or(false) {
|
||||||
trace!("{}", Attribute::Rs256PrivateKeyDer);
|
trace!("{}", Attribute::Rs256PrivateKeyDer);
|
||||||
|
@ -473,6 +565,7 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
||||||
origin_https,
|
origin_https,
|
||||||
scope_maps,
|
scope_maps,
|
||||||
sup_scope_maps,
|
sup_scope_maps,
|
||||||
|
claim_map,
|
||||||
token_fernet,
|
token_fernet,
|
||||||
jws_signer,
|
jws_signer,
|
||||||
iss,
|
iss,
|
||||||
|
@ -528,7 +621,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Relies on the token to be valid.
|
// Relies on the token to be valid.
|
||||||
OauthRSType::Public => {}
|
OauthRSType::Public { .. } => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// We are authenticated! Yay! Now we can actually check things ...
|
// We are authenticated! Yay! Now we can actually check things ...
|
||||||
|
@ -654,7 +747,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Relies on the token to be valid - no further action needed.
|
// Relies on the token to be valid - no further action needed.
|
||||||
OauthRSType::Public => {}
|
OauthRSType::Public { .. } => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// We are authenticated! Yay! Now we can actually check things ...
|
// We are authenticated! Yay! Now we can actually check things ...
|
||||||
|
@ -807,7 +900,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
|
|
||||||
let require_pkce = match &o2rs.type_ {
|
let require_pkce = match &o2rs.type_ {
|
||||||
OauthRSType::Basic { enable_pkce, .. } => *enable_pkce,
|
OauthRSType::Basic { enable_pkce, .. } => *enable_pkce,
|
||||||
OauthRSType::Public => true,
|
OauthRSType::Public { .. } => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we have a verifier present, we MUST assert that a code challenge is present!
|
// If we have a verifier present, we MUST assert that a code challenge is present!
|
||||||
|
@ -1083,7 +1176,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let s_claims = s_claims_for_account(o2rs, &account, &scopes);
|
let s_claims = s_claims_for_account(o2rs, &account, &scopes);
|
||||||
let extra_claims = extra_claims_for_account(&account, &scopes);
|
let extra_claims = extra_claims_for_account(&account, &o2rs.claim_map, &scopes);
|
||||||
|
|
||||||
let oidc = OidcToken {
|
let oidc = OidcToken {
|
||||||
iss,
|
iss,
|
||||||
|
@ -1229,7 +1322,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Relies on the token to be valid.
|
// Relies on the token to be valid.
|
||||||
OauthRSType::Public => {}
|
OauthRSType::Public { .. } => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
o2rs.token_fernet
|
o2rs.token_fernet
|
||||||
|
@ -1289,8 +1382,24 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
Oauth2Error::InvalidClientId
|
Oauth2Error::InvalidClientId
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// redirect_uri must be part of the client_id origin.
|
let allow_localhost_redirect = match &o2rs.type_ {
|
||||||
if auth_req.redirect_uri.origin() != o2rs.origin {
|
OauthRSType::Basic { .. } => false,
|
||||||
|
OauthRSType::Public {
|
||||||
|
allow_localhost_redirect,
|
||||||
|
} => *allow_localhost_redirect,
|
||||||
|
};
|
||||||
|
|
||||||
|
let localhost_redirect = auth_req
|
||||||
|
.redirect_uri
|
||||||
|
.domain()
|
||||||
|
.map(|domain| domain == "localhost")
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// redirect_uri must be part of the client_id origin, unless the client is public and then it MAY
|
||||||
|
// be localhost.
|
||||||
|
if !(auth_req.redirect_uri.origin() == o2rs.origin
|
||||||
|
|| (allow_localhost_redirect && localhost_redirect))
|
||||||
|
{
|
||||||
admin_warn!(
|
admin_warn!(
|
||||||
origin = ?o2rs.origin,
|
origin = ?o2rs.origin,
|
||||||
"Invalid OAuth2 redirect_uri (must be related to origin {:?}) - got {:?}",
|
"Invalid OAuth2 redirect_uri (must be related to origin {:?}) - got {:?}",
|
||||||
|
@ -1300,7 +1409,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
return Err(Oauth2Error::InvalidOrigin);
|
return Err(Oauth2Error::InvalidOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if o2rs.origin_https && auth_req.redirect_uri.scheme() != "https" {
|
if !localhost_redirect && o2rs.origin_https && auth_req.redirect_uri.scheme() != "https" {
|
||||||
admin_warn!(
|
admin_warn!(
|
||||||
origin = ?o2rs.origin,
|
origin = ?o2rs.origin,
|
||||||
"Invalid OAuth2 redirect_uri (must be https for secure origin) - got {:?}", auth_req.redirect_uri.scheme()
|
"Invalid OAuth2 redirect_uri (must be https for secure origin) - got {:?}", auth_req.redirect_uri.scheme()
|
||||||
|
@ -1310,7 +1419,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
|
|
||||||
let require_pkce = match &o2rs.type_ {
|
let require_pkce = match &o2rs.type_ {
|
||||||
OauthRSType::Basic { enable_pkce, .. } => *enable_pkce,
|
OauthRSType::Basic { enable_pkce, .. } => *enable_pkce,
|
||||||
OauthRSType::Public => true,
|
OauthRSType::Public { .. } => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let code_challenge = if let Some(pkce_request) = &auth_req.pkce_request {
|
let code_challenge = if let Some(pkce_request) = &auth_req.pkce_request {
|
||||||
|
@ -1634,7 +1743,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Relies on the token to be valid.
|
// Relies on the token to be valid.
|
||||||
OauthRSType::Public => {}
|
OauthRSType::Public { .. } => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// We are authenticated! Yay! Now we can actually check things ...
|
// We are authenticated! Yay! Now we can actually check things ...
|
||||||
|
@ -1799,7 +1908,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||||
let iss = o2rs.iss.clone();
|
let iss = o2rs.iss.clone();
|
||||||
|
|
||||||
let s_claims = s_claims_for_account(o2rs, &account, &scopes);
|
let s_claims = s_claims_for_account(o2rs, &account, &scopes);
|
||||||
let extra_claims = extra_claims_for_account(&account, &scopes);
|
let extra_claims = extra_claims_for_account(&account, &o2rs.claim_map, &scopes);
|
||||||
let exp = expiry.unix_timestamp();
|
let exp = expiry.unix_timestamp();
|
||||||
|
|
||||||
// ==== good to generate response ====
|
// ==== good to generate response ====
|
||||||
|
@ -1993,17 +2102,53 @@ fn s_claims_for_account(
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extra_claims_for_account(
|
fn extra_claims_for_account(
|
||||||
account: &Account,
|
account: &Account,
|
||||||
|
|
||||||
|
claim_map: &BTreeMap<Uuid, Vec<(String, ClaimValue)>>,
|
||||||
|
|
||||||
scopes: &BTreeSet<String>,
|
scopes: &BTreeSet<String>,
|
||||||
) -> BTreeMap<String, serde_json::Value> {
|
) -> BTreeMap<String, serde_json::Value> {
|
||||||
let mut extra_claims = BTreeMap::new();
|
let mut extra_claims = BTreeMap::new();
|
||||||
|
|
||||||
|
let mut account_claims: BTreeMap<&str, ClaimValue> = BTreeMap::new();
|
||||||
|
|
||||||
|
// for each group
|
||||||
|
for group_uuid in account.groups.iter().map(|g| g.uuid()) {
|
||||||
|
// Does this group have any custom claims?
|
||||||
|
if let Some(claim) = claim_map.get(&group_uuid) {
|
||||||
|
// If so, iterate over the set of claims and values.
|
||||||
|
for (claim_name, claim_value) in claim.iter() {
|
||||||
|
// Does this claim name already exist in our in-progress map?
|
||||||
|
match account_claims.entry(claim_name.as_str()) {
|
||||||
|
BTreeEntry::Vacant(e) => {
|
||||||
|
e.insert(claim_value.clone());
|
||||||
|
}
|
||||||
|
BTreeEntry::Occupied(mut e) => {
|
||||||
|
let mut_claim_value = e.get_mut();
|
||||||
|
// Merge the extra details into this.
|
||||||
|
mut_claim_value.merge(claim_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, flatten all these into the final structure.
|
||||||
|
for (claim_name, claim_value) in account_claims {
|
||||||
|
extra_claims.insert(claim_name.to_string(), claim_value.to_json_value());
|
||||||
|
}
|
||||||
|
|
||||||
if scopes.contains(&"groups".to_string()) {
|
if scopes.contains(&"groups".to_string()) {
|
||||||
extra_claims.insert(
|
extra_claims.insert(
|
||||||
"groups".to_string(),
|
"groups".to_string(),
|
||||||
account.groups.iter().map(|x| x.to_proto().uuid).collect(),
|
account.groups.iter().map(|x| x.to_proto().uuid).collect(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trace!(?extra_claims);
|
||||||
|
|
||||||
extra_claims
|
extra_claims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2041,6 +2186,7 @@ mod tests {
|
||||||
use crate::idm::oauth2::{AuthoriseResponse, Oauth2Error};
|
use crate::idm::oauth2::{AuthoriseResponse, Oauth2Error};
|
||||||
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::value::OauthClaimMapJoin;
|
||||||
use crate::value::SessionState;
|
use crate::value::SessionState;
|
||||||
|
|
||||||
use crate::credential::Credential;
|
use crate::credential::Credential;
|
||||||
|
@ -2397,8 +2543,6 @@ mod tests {
|
||||||
|
|
||||||
let idms_prox_read = idms.proxy_read().await;
|
let idms_prox_read = idms.proxy_read().await;
|
||||||
|
|
||||||
// Get an ident/uat for now.
|
|
||||||
|
|
||||||
// == Setup the authorisation request
|
// == Setup the authorisation request
|
||||||
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
|
@ -4298,7 +4442,6 @@ mod tests {
|
||||||
assert!(idms_prox_write.commit().is_ok());
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[idm_test]
|
|
||||||
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.8
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.8
|
||||||
//
|
//
|
||||||
// It was reported we were vulnerable to this attack, but that isn't the case. First
|
// It was reported we were vulnerable to this attack, but that isn't the case. First
|
||||||
|
@ -4318,6 +4461,7 @@ mod tests {
|
||||||
// exchange that code exchange with out the verifier, but I'm not sure what damage that would
|
// exchange that code exchange with out the verifier, but I'm not sure what damage that would
|
||||||
// lead to? Regardless, we test for and close off that possible hole in this test.
|
// lead to? Regardless, we test for and close off that possible hole in this test.
|
||||||
//
|
//
|
||||||
|
#[idm_test]
|
||||||
async fn test_idm_oauth2_1076_pkce_downgrade(
|
async fn test_idm_oauth2_1076_pkce_downgrade(
|
||||||
idms: &IdmServer,
|
idms: &IdmServer,
|
||||||
_idms_delayed: &mut IdmServerDelayed,
|
_idms_delayed: &mut IdmServerDelayed,
|
||||||
|
@ -4935,4 +5079,328 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert!(token_req.is_ok());
|
assert!(token_req.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[idm_test]
|
||||||
|
async fn test_idm_oauth2_custom_claims(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
|
||||||
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
let (secret, _uat, ident, oauth2_rs_uuid) =
|
||||||
|
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
|
||||||
|
|
||||||
|
// Setup custom claim maps here.
|
||||||
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
let modlist = ModifyList::new_list(vec![
|
||||||
|
// Member of a claim map.
|
||||||
|
Modify::Present(
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
Value::OauthClaimMap(
|
||||||
|
"custom_a".to_string(),
|
||||||
|
OauthClaimMapJoin::CommaSeparatedValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Modify::Present(
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
Value::OauthClaimValue(
|
||||||
|
"custom_a".to_string(),
|
||||||
|
UUID_SYSTEM_ADMINS,
|
||||||
|
btreeset!["value_a".to_string()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// If you are a member of two groups, the claim maps merge.
|
||||||
|
Modify::Present(
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
Value::OauthClaimValue(
|
||||||
|
"custom_a".to_string(),
|
||||||
|
UUID_IDM_ALL_ACCOUNTS,
|
||||||
|
btreeset!["value_b".to_string()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Map with a different seperator
|
||||||
|
Modify::Present(
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
Value::OauthClaimMap(
|
||||||
|
"custom_b".to_string(),
|
||||||
|
OauthClaimMapJoin::SpaceSeparatedValue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Modify::Present(
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
Value::OauthClaimValue(
|
||||||
|
"custom_b".to_string(),
|
||||||
|
UUID_SYSTEM_ADMINS,
|
||||||
|
btreeset!["value_a".to_string()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Modify::Present(
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
Value::OauthClaimValue(
|
||||||
|
"custom_b".to_string(),
|
||||||
|
UUID_IDM_ALL_ACCOUNTS,
|
||||||
|
btreeset!["value_b".to_string()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Not a member of the claim map.
|
||||||
|
Modify::Present(
|
||||||
|
Attribute::OAuth2RsClaimMap.into(),
|
||||||
|
Value::OauthClaimValue(
|
||||||
|
"custom_b".to_string(),
|
||||||
|
UUID_IDM_ADMINS,
|
||||||
|
btreeset!["value_c".to_string()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.internal_modify(
|
||||||
|
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(oauth2_rs_uuid))),
|
||||||
|
&modlist,
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
|
||||||
|
// Claim maps setup, lets go.
|
||||||
|
let client_authz =
|
||||||
|
Some(general_purpose::STANDARD.encode(format!("test_resource_server:{secret}")));
|
||||||
|
|
||||||
|
let idms_prox_read = idms.proxy_read().await;
|
||||||
|
|
||||||
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
|
let consent_request = good_authorisation_request!(
|
||||||
|
idms_prox_read,
|
||||||
|
&ident,
|
||||||
|
ct,
|
||||||
|
code_challenge,
|
||||||
|
OAUTH2_SCOPE_OPENID.to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let consent_token =
|
||||||
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request {
|
||||||
|
consent_token
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
// == Manually submit the consent token to the permit for the permit_success
|
||||||
|
drop(idms_prox_read);
|
||||||
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
let permit_success = idms_prox_write
|
||||||
|
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
|
||||||
|
.expect("Failed to perform OAuth2 permit");
|
||||||
|
|
||||||
|
// == Submit the token exchange code.
|
||||||
|
let token_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
|
||||||
|
code: permit_success.code,
|
||||||
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
|
// From the first step.
|
||||||
|
code_verifier,
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let token_response = idms_prox_write
|
||||||
|
.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct)
|
||||||
|
.expect("Failed to perform OAuth2 token exchange");
|
||||||
|
|
||||||
|
// 🎉 We got a token!
|
||||||
|
assert!(token_response.token_type == "Bearer");
|
||||||
|
|
||||||
|
let id_token = token_response.id_token.expect("No id_token in response!");
|
||||||
|
let access_token = token_response.access_token;
|
||||||
|
|
||||||
|
// Get the read txn for inspecting the tokens
|
||||||
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
|
||||||
|
let mut idms_prox_read = idms.proxy_read().await;
|
||||||
|
|
||||||
|
let mut jwkset = idms_prox_read
|
||||||
|
.oauth2_openid_publickey("test_resource_server")
|
||||||
|
.expect("Failed to get public key");
|
||||||
|
|
||||||
|
let public_jwk = jwkset.keys.pop().expect("no such jwk");
|
||||||
|
|
||||||
|
let jws_validator =
|
||||||
|
JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
|
||||||
|
|
||||||
|
let oidc_unverified =
|
||||||
|
OidcUnverified::from_str(&id_token).expect("Failed to parse id_token");
|
||||||
|
|
||||||
|
let iat = ct.as_secs() as i64;
|
||||||
|
|
||||||
|
let oidc = jws_validator
|
||||||
|
.verify(&oidc_unverified)
|
||||||
|
.unwrap()
|
||||||
|
.verify_exp(iat)
|
||||||
|
.expect("Failed to verify oidc");
|
||||||
|
|
||||||
|
// Are the id_token values what we expect?
|
||||||
|
assert!(
|
||||||
|
oidc.iss
|
||||||
|
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(oidc.sub == OidcSubject::U(UUID_ADMIN));
|
||||||
|
assert!(oidc.aud == "test_resource_server");
|
||||||
|
assert!(oidc.iat == iat);
|
||||||
|
assert!(oidc.nbf == Some(iat));
|
||||||
|
// Previously this was the auth session but it's now inline with the access token expiry.
|
||||||
|
assert!(oidc.exp == iat + (OAUTH2_ACCESS_TOKEN_EXPIRY as i64));
|
||||||
|
assert!(oidc.auth_time.is_none());
|
||||||
|
// Is nonce correctly passed through?
|
||||||
|
assert!(oidc.nonce == Some("abcdef".to_string()));
|
||||||
|
assert!(oidc.at_hash.is_none());
|
||||||
|
assert!(oidc.acr.is_none());
|
||||||
|
assert!(oidc.amr.is_none());
|
||||||
|
assert!(oidc.azp == Some("test_resource_server".to_string()));
|
||||||
|
assert!(oidc.jti.is_none());
|
||||||
|
assert!(oidc.s_claims.name == Some("System Administrator".to_string()));
|
||||||
|
assert!(oidc.s_claims.preferred_username == Some("admin@example.com".to_string()));
|
||||||
|
assert!(
|
||||||
|
oidc.s_claims.scopes == vec![OAUTH2_SCOPE_OPENID.to_string(), "supplement".to_string()]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
oidc.claims.get("custom_a").and_then(|v| v.as_str()),
|
||||||
|
Some("value_a,value_b")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
oidc.claims.get("custom_b").and_then(|v| v.as_str()),
|
||||||
|
Some("value_a value_b")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Does our access token work with the userinfo endpoint?
|
||||||
|
// Do the id_token details line up to the userinfo?
|
||||||
|
let userinfo = idms_prox_read
|
||||||
|
.oauth2_openid_userinfo("test_resource_server", &access_token, ct)
|
||||||
|
.expect("failed to get userinfo");
|
||||||
|
|
||||||
|
assert!(oidc.iss == userinfo.iss);
|
||||||
|
assert!(oidc.sub == userinfo.sub);
|
||||||
|
assert!(oidc.aud == userinfo.aud);
|
||||||
|
assert!(oidc.iat == userinfo.iat);
|
||||||
|
assert!(oidc.nbf == userinfo.nbf);
|
||||||
|
assert!(oidc.exp == userinfo.exp);
|
||||||
|
assert!(userinfo.auth_time.is_none());
|
||||||
|
assert!(userinfo.nonce == Some("abcdef".to_string()));
|
||||||
|
assert!(userinfo.at_hash.is_none());
|
||||||
|
assert!(userinfo.acr.is_none());
|
||||||
|
assert!(oidc.amr == userinfo.amr);
|
||||||
|
assert!(oidc.azp == userinfo.azp);
|
||||||
|
assert!(userinfo.jti.is_none());
|
||||||
|
assert!(oidc.s_claims == userinfo.s_claims);
|
||||||
|
assert_eq!(oidc.claims, userinfo.claims);
|
||||||
|
|
||||||
|
// Check the oauth2 introspect bits.
|
||||||
|
let intr_request = AccessTokenIntrospectRequest {
|
||||||
|
token: access_token,
|
||||||
|
token_type_hint: None,
|
||||||
|
};
|
||||||
|
let intr_response = idms_prox_read
|
||||||
|
.check_oauth2_token_introspect(client_authz.as_deref().unwrap(), &intr_request, ct)
|
||||||
|
.expect("Failed to inspect token");
|
||||||
|
|
||||||
|
eprintln!("👉 {intr_response:?}");
|
||||||
|
assert!(intr_response.active);
|
||||||
|
assert!(intr_response.scope.as_deref() == Some("openid supplement"));
|
||||||
|
assert!(intr_response.client_id.as_deref() == Some("test_resource_server"));
|
||||||
|
assert!(intr_response.username.as_deref() == Some("admin@example.com"));
|
||||||
|
assert!(intr_response.token_type.as_deref() == Some("access_token"));
|
||||||
|
assert!(intr_response.iat == Some(ct.as_secs() as i64));
|
||||||
|
assert!(intr_response.nbf == Some(ct.as_secs() as i64));
|
||||||
|
// Introspect doesn't have custom claims.
|
||||||
|
|
||||||
|
drop(idms_prox_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[idm_test]
|
||||||
|
async fn test_idm_oauth2_public_allow_localhost_redirect(
|
||||||
|
idms: &IdmServer,
|
||||||
|
_idms_delayed: &mut IdmServerDelayed,
|
||||||
|
) {
|
||||||
|
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||||
|
let (_uat, ident, oauth2_rs_uuid) = setup_oauth2_resource_server_public(idms, ct).await;
|
||||||
|
|
||||||
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
let modlist = ModifyList::new_list(vec![Modify::Present(
|
||||||
|
Attribute::OAuth2AllowLocalhostRedirect.into(),
|
||||||
|
Value::Bool(true),
|
||||||
|
)]);
|
||||||
|
|
||||||
|
assert!(idms_prox_write
|
||||||
|
.qs_write
|
||||||
|
.internal_modify(
|
||||||
|
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(oauth2_rs_uuid))),
|
||||||
|
&modlist,
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
|
||||||
|
let idms_prox_read = idms.proxy_read().await;
|
||||||
|
|
||||||
|
// == Setup the authorisation request
|
||||||
|
let (code_verifier, code_challenge) = create_code_verifier!("Whar Garble");
|
||||||
|
|
||||||
|
let auth_req = AuthorisationRequest {
|
||||||
|
response_type: "code".to_string(),
|
||||||
|
client_id: "test_resource_server".to_string(),
|
||||||
|
state: "123".to_string(),
|
||||||
|
pkce_request: Some(PkceRequest {
|
||||||
|
code_challenge: Base64UrlSafeData(code_challenge),
|
||||||
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
|
}),
|
||||||
|
redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(),
|
||||||
|
scope: OAUTH2_SCOPE_OPENID.to_string(),
|
||||||
|
nonce: Some("abcdef".to_string()),
|
||||||
|
oidc_ext: Default::default(),
|
||||||
|
unknown_keys: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let consent_request = idms_prox_read
|
||||||
|
.check_oauth2_authorisation(&ident, &auth_req, ct)
|
||||||
|
.expect("OAuth2 authorisation failed");
|
||||||
|
|
||||||
|
// Should be in the consent phase;
|
||||||
|
let consent_token =
|
||||||
|
if let AuthoriseResponse::ConsentRequested { consent_token, .. } = consent_request {
|
||||||
|
consent_token
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
// == Manually submit the consent token to the permit for the permit_success
|
||||||
|
drop(idms_prox_read);
|
||||||
|
let mut idms_prox_write = idms.proxy_write(ct).await;
|
||||||
|
|
||||||
|
let permit_success = idms_prox_write
|
||||||
|
.check_oauth2_authorise_permit(&ident, &consent_token, ct)
|
||||||
|
.expect("Failed to perform OAuth2 permit");
|
||||||
|
|
||||||
|
// Check we are reflecting the CSRF properly.
|
||||||
|
assert!(permit_success.state == "123");
|
||||||
|
|
||||||
|
// == Submit the token exchange code.
|
||||||
|
let token_req = AccessTokenRequest {
|
||||||
|
grant_type: GrantTypeReq::AuthorizationCode {
|
||||||
|
code: permit_success.code,
|
||||||
|
redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(),
|
||||||
|
// From the first step.
|
||||||
|
code_verifier,
|
||||||
|
},
|
||||||
|
client_id: Some("test_resource_server".to_string()),
|
||||||
|
client_secret: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_response = idms_prox_write
|
||||||
|
.check_oauth2_token_exchange(None, &token_req, ct)
|
||||||
|
.expect("Failed to perform OAuth2 token exchange");
|
||||||
|
|
||||||
|
// 🎉 We got a token! In the future we can then check introspection from this point.
|
||||||
|
assert!(token_response.token_type == "Bearer");
|
||||||
|
|
||||||
|
assert!(idms_prox_write.commit().is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -652,3 +652,24 @@ macro_rules! limmediate_warning {
|
||||||
eprint!($($arg)*)
|
eprint!($($arg)*)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! str_concat {
|
||||||
|
($str_iter:expr, $join_char:expr) => {{
|
||||||
|
// Sub 1 here because we need N minus 1 join chars
|
||||||
|
let max_join_chars: usize = $str_iter.len() - 1;
|
||||||
|
let data_len: usize = $str_iter
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.len())
|
||||||
|
.fold(max_join_chars, |acc, x| acc + x);
|
||||||
|
|
||||||
|
let mut joined = String::with_capacity(data_len);
|
||||||
|
for (i, value) in $str_iter.iter().enumerate() {
|
||||||
|
joined.push_str(value);
|
||||||
|
if i < max_join_chars {
|
||||||
|
joined.push($join_char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joined
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ fn enforce_unique<VALID, STATE>(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// A conflict was found!
|
// A conflict was found!
|
||||||
if let Some(conflict_cand_zero) = conflict_cand.get(0) {
|
if let Some(conflict_cand_zero) = conflict_cand.first() {
|
||||||
if cand_query.len() >= 2 {
|
if cand_query.len() >= 2 {
|
||||||
// Continue to split to isolate.
|
// Continue to split to isolate.
|
||||||
let mid = cand_query.len() / 2;
|
let mid = cand_query.len() / 2;
|
||||||
|
|
|
@ -454,7 +454,7 @@ mod tests {
|
||||||
|
|
||||||
use crate::event::CreateEvent;
|
use crate::event::CreateEvent;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::value::{Oauth2Session, Session, SessionState};
|
use crate::value::{Oauth2Session, OauthClaimMapJoin, Session, SessionState};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::credential::Credential;
|
use crate::credential::Credential;
|
||||||
|
@ -1253,4 +1253,79 @@ mod tests {
|
||||||
|
|
||||||
assert!(server_txn.commit().is_ok());
|
assert!(server_txn.commit().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_remove_reference_oauth2_claim_map() {
|
||||||
|
let ea: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
|
(Attribute::Class, EntryClass::Object.to_value()),
|
||||||
|
(
|
||||||
|
Attribute::Class,
|
||||||
|
EntryClass::OAuth2ResourceServer.to_value()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Attribute::Class,
|
||||||
|
EntryClass::OAuth2ResourceServerPublic.to_value()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Attribute::OAuth2RsName,
|
||||||
|
Value::new_iname("test_resource_server")
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Attribute::DisplayName,
|
||||||
|
Value::new_utf8s("test_resource_server")
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Attribute::OAuth2RsOrigin,
|
||||||
|
Value::new_url_s("https://demo.example.com").unwrap()
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Value::OauthClaimMap(
|
||||||
|
"custom_a".to_string(),
|
||||||
|
OauthClaimMapJoin::CommaSeparatedValue,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Attribute::OAuth2RsClaimMap,
|
||||||
|
Value::OauthClaimValue(
|
||||||
|
"custom_a".to_string(),
|
||||||
|
Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap(),
|
||||||
|
btreeset!["value_a".to_string()],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let eb: Entry<EntryInit, EntryNew> = entry_init!(
|
||||||
|
(Attribute::Class, EntryClass::Group.to_value()),
|
||||||
|
(Attribute::Name, Value::new_iname("testgroup")),
|
||||||
|
(
|
||||||
|
Attribute::Uuid,
|
||||||
|
Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
|
||||||
|
),
|
||||||
|
(Attribute::Description, Value::new_utf8s("testgroup"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let preload = vec![ea, eb];
|
||||||
|
|
||||||
|
run_delete_test!(
|
||||||
|
Ok(()),
|
||||||
|
preload,
|
||||||
|
filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
|
||||||
|
None,
|
||||||
|
|qs: &mut QueryServerWriteTransaction| {
|
||||||
|
let cands = qs
|
||||||
|
.internal_search(filter!(f_eq(
|
||||||
|
Attribute::OAuth2RsName,
|
||||||
|
PartialValue::new_iname("test_resource_server")
|
||||||
|
)))
|
||||||
|
.expect("Internal search failure");
|
||||||
|
let ue = cands.first().expect("No entry");
|
||||||
|
|
||||||
|
assert!(ue
|
||||||
|
.get_ava_set(Attribute::OAuth2RsClaimMap)
|
||||||
|
.and_then(|vs| vs.as_oauthclaim_map())
|
||||||
|
.is_none())
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use super::cid::Cid;
|
||||||
use super::entry::EntryChangeState;
|
use super::entry::EntryChangeState;
|
||||||
use super::entry::State;
|
use super::entry::State;
|
||||||
use crate::be::dbvalue::DbValueImage;
|
use crate::be::dbvalue::DbValueImage;
|
||||||
|
use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
|
||||||
use crate::entry::Eattrs;
|
use crate::entry::Eattrs;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::{SchemaReadTransaction, SchemaTransaction};
|
use crate::schema::{SchemaReadTransaction, SchemaTransaction};
|
||||||
|
@ -253,6 +254,13 @@ pub struct ReplOauthScopeMapV1 {
|
||||||
pub data: BTreeSet<String>,
|
pub data: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ReplOauthClaimMapV1 {
|
||||||
|
pub name: String,
|
||||||
|
pub join: DbValueOauthClaimMapJoinV1,
|
||||||
|
pub values: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
pub struct ReplOauth2SessionV1 {
|
pub struct ReplOauth2SessionV1 {
|
||||||
pub refer: Uuid,
|
pub refer: Uuid,
|
||||||
|
@ -412,6 +420,9 @@ pub enum ReplAttrV1 {
|
||||||
OauthScopeMap {
|
OauthScopeMap {
|
||||||
set: Vec<ReplOauthScopeMapV1>,
|
set: Vec<ReplOauthScopeMapV1>,
|
||||||
},
|
},
|
||||||
|
OauthClaimMap {
|
||||||
|
set: Vec<ReplOauthClaimMapV1>,
|
||||||
|
},
|
||||||
Oauth2Session {
|
Oauth2Session {
|
||||||
set: Vec<ReplOauth2SessionV1>,
|
set: Vec<ReplOauth2SessionV1>,
|
||||||
},
|
},
|
||||||
|
|
|
@ -213,6 +213,12 @@ impl SchemaAttribute {
|
||||||
SyntaxType::Url => matches!(v, PartialValue::Url(_)),
|
SyntaxType::Url => matches!(v, PartialValue::Url(_)),
|
||||||
SyntaxType::OauthScope => matches!(v, PartialValue::OauthScope(_)),
|
SyntaxType::OauthScope => matches!(v, PartialValue::OauthScope(_)),
|
||||||
SyntaxType::OauthScopeMap => matches!(v, PartialValue::Refer(_)),
|
SyntaxType::OauthScopeMap => matches!(v, PartialValue::Refer(_)),
|
||||||
|
SyntaxType::OauthClaimMap => {
|
||||||
|
matches!(v, PartialValue::Iutf8(_))
|
||||||
|
|| matches!(v, PartialValue::Refer(_))
|
||||||
|
|| matches!(v, PartialValue::OauthClaimValue(_, _, _))
|
||||||
|
|| matches!(v, PartialValue::OauthClaim(_, _))
|
||||||
|
}
|
||||||
SyntaxType::PrivateBinary => matches!(v, PartialValue::PrivateBinary),
|
SyntaxType::PrivateBinary => matches!(v, PartialValue::PrivateBinary),
|
||||||
SyntaxType::IntentToken => matches!(v, PartialValue::IntentToken(_)),
|
SyntaxType::IntentToken => matches!(v, PartialValue::IntentToken(_)),
|
||||||
SyntaxType::Passkey => matches!(v, PartialValue::Passkey(_)),
|
SyntaxType::Passkey => matches!(v, PartialValue::Passkey(_)),
|
||||||
|
@ -270,6 +276,10 @@ impl SchemaAttribute {
|
||||||
SyntaxType::Url => matches!(v, Value::Url(_)),
|
SyntaxType::Url => matches!(v, Value::Url(_)),
|
||||||
SyntaxType::OauthScope => matches!(v, Value::OauthScope(_)),
|
SyntaxType::OauthScope => matches!(v, Value::OauthScope(_)),
|
||||||
SyntaxType::OauthScopeMap => matches!(v, Value::OauthScopeMap(_, _)),
|
SyntaxType::OauthScopeMap => matches!(v, Value::OauthScopeMap(_, _)),
|
||||||
|
SyntaxType::OauthClaimMap => {
|
||||||
|
matches!(v, Value::OauthClaimValue(_, _, _))
|
||||||
|
|| matches!(v, Value::OauthClaimMap(_, _))
|
||||||
|
}
|
||||||
SyntaxType::PrivateBinary => matches!(v, Value::PrivateBinary(_)),
|
SyntaxType::PrivateBinary => matches!(v, Value::PrivateBinary(_)),
|
||||||
SyntaxType::IntentToken => matches!(v, Value::IntentToken(_, _)),
|
SyntaxType::IntentToken => matches!(v, Value::IntentToken(_, _)),
|
||||||
SyntaxType::Passkey => matches!(v, Value::Passkey(_, _, _)),
|
SyntaxType::Passkey => matches!(v, Value::Passkey(_, _, _)),
|
||||||
|
@ -762,6 +772,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
||||||
// Update the unique and ref caches.
|
// Update the unique and ref caches.
|
||||||
if a.syntax == SyntaxType::ReferenceUuid ||
|
if a.syntax == SyntaxType::ReferenceUuid ||
|
||||||
a.syntax == SyntaxType::OauthScopeMap ||
|
a.syntax == SyntaxType::OauthScopeMap ||
|
||||||
|
a.syntax == SyntaxType::OauthClaimMap ||
|
||||||
// So that when an rs is removed we trigger removal of the sessions.
|
// So that when an rs is removed we trigger removal of the sessions.
|
||||||
a.syntax == SyntaxType::Oauth2Session
|
a.syntax == SyntaxType::Oauth2Session
|
||||||
// May not need to be a ref type since it doesn't have external links/impact?
|
// May not need to be a ref type since it doesn't have external links/impact?
|
||||||
|
|
|
@ -706,6 +706,39 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "info", skip_all)]
|
||||||
|
/// Migrations for Oauth to support multiple origins, and custom claims.
|
||||||
|
pub fn migrate_domain_3_to_4(&mut self) -> Result<(), OperationError> {
|
||||||
|
let idm_schema_attrs = [
|
||||||
|
SCHEMA_ATTR_OAUTH2_RS_CLAIM_MAP_DL4.clone().into(),
|
||||||
|
SCHEMA_ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT_DL4
|
||||||
|
.clone()
|
||||||
|
.into(),
|
||||||
|
];
|
||||||
|
|
||||||
|
idm_schema_attrs
|
||||||
|
.into_iter()
|
||||||
|
.try_for_each(|entry| self.internal_migrate_or_create(entry))
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(?err, "migrate_domain_3_to_4 -> Error");
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let idm_schema_classes = [
|
||||||
|
SCHEMA_CLASS_OAUTH2_RS_DL4.clone().into(),
|
||||||
|
SCHEMA_CLASS_OAUTH2_RS_PUBLIC_DL4.clone().into(),
|
||||||
|
IDM_ACP_OAUTH2_MANAGE_DL4.clone().into(),
|
||||||
|
];
|
||||||
|
|
||||||
|
idm_schema_classes
|
||||||
|
.into_iter()
|
||||||
|
.try_for_each(|entry| self.internal_migrate_or_create(entry))
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(?err, "migrate_domain_3_to_4 -> Error");
|
||||||
|
err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "info", skip_all)]
|
#[instrument(level = "info", skip_all)]
|
||||||
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
|
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
|
||||||
admin_debug!("initialise_schema_core -> start ...");
|
admin_debug!("initialise_schema_core -> start ...");
|
||||||
|
|
|
@ -583,6 +583,7 @@ pub trait QueryServerTransaction<'a> {
|
||||||
SyntaxType::WebauthnAttestationCaList => Value::new_webauthn_attestation_ca_list(value)
|
SyntaxType::WebauthnAttestationCaList => Value::new_webauthn_attestation_ca_list(value)
|
||||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Webauthn Attestation CA List".to_string())),
|
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Webauthn Attestation CA List".to_string())),
|
||||||
SyntaxType::OauthScopeMap => Err(OperationError::InvalidAttribute("Oauth Scope Maps can not be supplied through modification - please use the IDM api".to_string())),
|
SyntaxType::OauthScopeMap => Err(OperationError::InvalidAttribute("Oauth Scope Maps can not be supplied through modification - please use the IDM api".to_string())),
|
||||||
|
SyntaxType::OauthClaimMap => Err(OperationError::InvalidAttribute("Oauth Claim Maps can not be supplied through modification - please use the IDM api".to_string())),
|
||||||
SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute("Private Binary Values can not be supplied through modification".to_string())),
|
SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute("Private Binary Values can not be supplied through modification".to_string())),
|
||||||
SyntaxType::IntentToken => Err(OperationError::InvalidAttribute("Intent Token Values can not be supplied through modification".to_string())),
|
SyntaxType::IntentToken => Err(OperationError::InvalidAttribute("Intent Token Values can not be supplied through modification".to_string())),
|
||||||
SyntaxType::Passkey => Err(OperationError::InvalidAttribute("Passkey Values can not be supplied through modification".to_string())),
|
SyntaxType::Passkey => Err(OperationError::InvalidAttribute("Passkey Values can not be supplied through modification".to_string())),
|
||||||
|
@ -657,6 +658,11 @@ pub trait QueryServerTransaction<'a> {
|
||||||
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
|
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
|
||||||
Ok(PartialValue::Refer(un))
|
Ok(PartialValue::Refer(un))
|
||||||
}
|
}
|
||||||
|
SyntaxType::OauthClaimMap => self
|
||||||
|
.name_to_uuid(value)
|
||||||
|
.map(PartialValue::Refer)
|
||||||
|
.or_else(|_| Ok(PartialValue::new_iutf8(value))),
|
||||||
|
|
||||||
SyntaxType::JsonFilter => {
|
SyntaxType::JsonFilter => {
|
||||||
PartialValue::new_json_filter_s(value).ok_or_else(|| {
|
PartialValue::new_json_filter_s(value).ok_or_else(|| {
|
||||||
OperationError::InvalidAttribute("Invalid Filter syntax".to_string())
|
OperationError::InvalidAttribute("Invalid Filter syntax".to_string())
|
||||||
|
@ -1622,6 +1628,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
self.migrate_domain_2_to_3()?;
|
self.migrate_domain_2_to_3()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if previous_version <= DOMAIN_LEVEL_3 && domain_info_version >= DOMAIN_LEVEL_4 {
|
||||||
|
self.migrate_domain_3_to_4()?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ use webauthn_rs::prelude::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::be::dbentry::DbIdentSpn;
|
use crate::be::dbentry::DbIdentSpn;
|
||||||
|
use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
|
||||||
use crate::credential::{totp::Totp, Credential};
|
use crate::credential::{totp::Totp, Credential};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::repl::cid::Cid;
|
use crate::repl::cid::Cid;
|
||||||
|
@ -264,6 +265,7 @@ pub enum SyntaxType {
|
||||||
Image = 34,
|
Image = 34,
|
||||||
CredentialType = 35,
|
CredentialType = 35,
|
||||||
WebauthnAttestationCaList = 36,
|
WebauthnAttestationCaList = 36,
|
||||||
|
OauthClaimMap = 37,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for SyntaxType {
|
impl TryFrom<&str> for SyntaxType {
|
||||||
|
@ -309,6 +311,7 @@ impl TryFrom<&str> for SyntaxType {
|
||||||
"EC_KEY_PRIVATE" => Ok(SyntaxType::EcKeyPrivate),
|
"EC_KEY_PRIVATE" => Ok(SyntaxType::EcKeyPrivate),
|
||||||
"CREDENTIAL_TYPE" => Ok(SyntaxType::CredentialType),
|
"CREDENTIAL_TYPE" => Ok(SyntaxType::CredentialType),
|
||||||
"WEBAUTHN_ATTESTATION_CA_LIST" => Ok(SyntaxType::WebauthnAttestationCaList),
|
"WEBAUTHN_ATTESTATION_CA_LIST" => Ok(SyntaxType::WebauthnAttestationCaList),
|
||||||
|
"OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -354,6 +357,7 @@ impl fmt::Display for SyntaxType {
|
||||||
SyntaxType::Image => "IMAGE",
|
SyntaxType::Image => "IMAGE",
|
||||||
SyntaxType::CredentialType => "CREDENTIAL_TYPE",
|
SyntaxType::CredentialType => "CREDENTIAL_TYPE",
|
||||||
SyntaxType::WebauthnAttestationCaList => "WEBAUTHN_ATTESTATION_CA_LIST",
|
SyntaxType::WebauthnAttestationCaList => "WEBAUTHN_ATTESTATION_CA_LIST",
|
||||||
|
SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -471,6 +475,9 @@ pub enum PartialValue {
|
||||||
/// We compare on the value hash
|
/// We compare on the value hash
|
||||||
Image(String),
|
Image(String),
|
||||||
CredentialType(CredentialType),
|
CredentialType(CredentialType),
|
||||||
|
|
||||||
|
OauthClaim(String, Uuid),
|
||||||
|
OauthClaimValue(String, Uuid, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SyntaxType> for PartialValue {
|
impl From<SyntaxType> for PartialValue {
|
||||||
|
@ -833,8 +840,6 @@ impl PartialValue {
|
||||||
PartialValue::SecretValue | PartialValue::PrivateBinary => "_".to_string(),
|
PartialValue::SecretValue | PartialValue::PrivateBinary => "_".to_string(),
|
||||||
PartialValue::Spn(name, realm) => format!("{name}@{realm}"),
|
PartialValue::Spn(name, realm) => format!("{name}@{realm}"),
|
||||||
PartialValue::Uint32(u) => u.to_string(),
|
PartialValue::Uint32(u) => u.to_string(),
|
||||||
// This will never work, we don't allow equality searching on Cid's
|
|
||||||
PartialValue::Cid(_) => "_".to_string(),
|
|
||||||
PartialValue::DateTime(odt) => {
|
PartialValue::DateTime(odt) => {
|
||||||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
|
@ -849,6 +854,11 @@ impl PartialValue {
|
||||||
PartialValue::UiHint(u) => (*u as u16).to_string(),
|
PartialValue::UiHint(u) => (*u as u16).to_string(),
|
||||||
PartialValue::Image(imagehash) => imagehash.to_owned(),
|
PartialValue::Image(imagehash) => imagehash.to_owned(),
|
||||||
PartialValue::CredentialType(ct) => ct.to_string(),
|
PartialValue::CredentialType(ct) => ct.to_string(),
|
||||||
|
// This will never work, we don't allow equality searching on Cid's
|
||||||
|
PartialValue::Cid(_) => "_".to_string(),
|
||||||
|
// We don't allow searching on claim/uuid pairs.
|
||||||
|
PartialValue::OauthClaim(_, _) => "_".to_string(),
|
||||||
|
PartialValue::OauthClaimValue(_, _, _) => "_".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -982,6 +992,42 @@ impl fmt::Debug for Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub enum OauthClaimMapJoin {
|
||||||
|
CommaSeparatedValue,
|
||||||
|
SpaceSeparatedValue,
|
||||||
|
#[default]
|
||||||
|
JsonArray,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DbValueOauthClaimMapJoinV1> for OauthClaimMapJoin {
|
||||||
|
fn from(value: DbValueOauthClaimMapJoinV1) -> OauthClaimMapJoin {
|
||||||
|
match value {
|
||||||
|
DbValueOauthClaimMapJoinV1::CommaSeparatedValue => {
|
||||||
|
OauthClaimMapJoin::CommaSeparatedValue
|
||||||
|
}
|
||||||
|
DbValueOauthClaimMapJoinV1::SpaceSeparatedValue => {
|
||||||
|
OauthClaimMapJoin::SpaceSeparatedValue
|
||||||
|
}
|
||||||
|
DbValueOauthClaimMapJoinV1::JsonArray => OauthClaimMapJoin::JsonArray,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OauthClaimMapJoin> for DbValueOauthClaimMapJoinV1 {
|
||||||
|
fn from(value: OauthClaimMapJoin) -> DbValueOauthClaimMapJoinV1 {
|
||||||
|
match value {
|
||||||
|
OauthClaimMapJoin::CommaSeparatedValue => {
|
||||||
|
DbValueOauthClaimMapJoinV1::CommaSeparatedValue
|
||||||
|
}
|
||||||
|
OauthClaimMapJoin::SpaceSeparatedValue => {
|
||||||
|
DbValueOauthClaimMapJoinV1::SpaceSeparatedValue
|
||||||
|
}
|
||||||
|
OauthClaimMapJoin::JsonArray => DbValueOauthClaimMapJoinV1::JsonArray,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Oauth2Session {
|
pub struct Oauth2Session {
|
||||||
pub parent: Uuid,
|
pub parent: Uuid,
|
||||||
|
@ -1046,6 +1092,9 @@ pub enum Value {
|
||||||
Image(ImageValue),
|
Image(ImageValue),
|
||||||
CredentialType(CredentialType),
|
CredentialType(CredentialType),
|
||||||
WebauthnAttestationCaList(AttestationCaList),
|
WebauthnAttestationCaList(AttestationCaList),
|
||||||
|
|
||||||
|
OauthClaimValue(String, Uuid, BTreeSet<String>),
|
||||||
|
OauthClaimMap(String, OauthClaimMapJoin),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Value {
|
impl PartialEq for Value {
|
||||||
|
@ -1512,6 +1561,14 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_oauthclaimmap(n: String, u: Uuid, c: BTreeSet<String>) -> Option<Self> {
|
||||||
|
if OAUTHSCOPE_RE.is_match(&n) && c.iter().all(|s| OAUTHSCOPE_RE.is_match(s)) {
|
||||||
|
Some(Value::OauthClaimValue(n, u, c))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_oauthscopemap(&self) -> bool {
|
pub fn is_oauthscopemap(&self) -> bool {
|
||||||
matches!(&self, Value::OauthScopeMap(_, _))
|
matches!(&self, Value::OauthScopeMap(_, _))
|
||||||
}
|
}
|
||||||
|
@ -1841,6 +1898,11 @@ impl Value {
|
||||||
Value::OauthScope(s) => OAUTHSCOPE_RE.is_match(s),
|
Value::OauthScope(s) => OAUTHSCOPE_RE.is_match(s),
|
||||||
Value::OauthScopeMap(_, m) => m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)),
|
Value::OauthScopeMap(_, m) => m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)),
|
||||||
|
|
||||||
|
Value::OauthClaimMap(name, _) => OAUTHSCOPE_RE.is_match(name),
|
||||||
|
Value::OauthClaimValue(name, _, value) => {
|
||||||
|
OAUTHSCOPE_RE.is_match(name) && value.iter().all(|s| OAUTHSCOPE_RE.is_match(s))
|
||||||
|
}
|
||||||
|
|
||||||
Value::PhoneNumber(_, _) => true,
|
Value::PhoneNumber(_, _) => true,
|
||||||
Value::Address(_) => true,
|
Value::Address(_) => true,
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,9 @@ pub use self::iutf8::ValueSetIutf8;
|
||||||
pub use self::json::ValueSetJsonFilter;
|
pub use self::json::ValueSetJsonFilter;
|
||||||
pub use self::jws::{ValueSetJwsKeyEs256, ValueSetJwsKeyRs256};
|
pub use self::jws::{ValueSetJwsKeyEs256, ValueSetJwsKeyRs256};
|
||||||
pub use self::nsuniqueid::ValueSetNsUniqueId;
|
pub use self::nsuniqueid::ValueSetNsUniqueId;
|
||||||
pub use self::oauth::{ValueSetOauthScope, ValueSetOauthScopeMap};
|
pub use self::oauth::{
|
||||||
|
OauthClaimMapping, ValueSetOauthClaimMap, ValueSetOauthScope, ValueSetOauthScopeMap,
|
||||||
|
};
|
||||||
pub use self::restricted::ValueSetRestricted;
|
pub use self::restricted::ValueSetRestricted;
|
||||||
pub use self::secret::ValueSetSecret;
|
pub use self::secret::ValueSetSecret;
|
||||||
pub use self::session::{ValueSetApiToken, ValueSetOauth2Session, ValueSetSession};
|
pub use self::session::{ValueSetApiToken, ValueSetOauth2Session, ValueSetSession};
|
||||||
|
@ -365,6 +367,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_oauthclaim_map(&self) -> Option<&BTreeMap<String, OauthClaimMapping>> {
|
||||||
|
debug_assert!(false);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn to_value_single(&self) -> Option<Value> {
|
fn to_value_single(&self) -> Option<Value> {
|
||||||
if self.len() != 1 {
|
if self.len() != 1 {
|
||||||
None
|
None
|
||||||
|
@ -676,6 +683,8 @@ pub fn from_result_value_iter(
|
||||||
| Value::Session(_, _)
|
| Value::Session(_, _)
|
||||||
| Value::ApiToken(_, _)
|
| Value::ApiToken(_, _)
|
||||||
| Value::Oauth2Session(_, _)
|
| Value::Oauth2Session(_, _)
|
||||||
|
| Value::OauthClaimMap(_, _)
|
||||||
|
| Value::OauthClaimValue(_, _, _)
|
||||||
| Value::JwsKeyEs256(_)
|
| Value::JwsKeyEs256(_)
|
||||||
| Value::JwsKeyRs256(_) => {
|
| Value::JwsKeyRs256(_) => {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
|
@ -740,6 +749,10 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
||||||
Value::WebauthnAttestationCaList(ca_list) => {
|
Value::WebauthnAttestationCaList(ca_list) => {
|
||||||
ValueSetWebauthnAttestationCaList::new(ca_list)
|
ValueSetWebauthnAttestationCaList::new(ca_list)
|
||||||
}
|
}
|
||||||
|
Value::OauthClaimMap(name, join) => ValueSetOauthClaimMap::new(name, join),
|
||||||
|
Value::OauthClaimValue(name, group, claims) => {
|
||||||
|
ValueSetOauthClaimMap::new_value(name, group, claims)
|
||||||
|
}
|
||||||
Value::PhoneNumber(_, _) => {
|
Value::PhoneNumber(_, _) => {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
return Err(OperationError::InvalidValueState);
|
return Err(OperationError::InvalidValueState);
|
||||||
|
@ -800,6 +813,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
||||||
DbValueSetV2::WebauthnAttestationCaList { ca_list } => {
|
DbValueSetV2::WebauthnAttestationCaList { ca_list } => {
|
||||||
ValueSetWebauthnAttestationCaList::from_dbvs2(ca_list)
|
ValueSetWebauthnAttestationCaList::from_dbvs2(ca_list)
|
||||||
}
|
}
|
||||||
|
DbValueSetV2::OauthClaimMap(set) => ValueSetOauthClaimMap::from_dbvs2(set),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -849,5 +863,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
|
||||||
ReplAttrV1::WebauthnAttestationCaList { ca_list } => {
|
ReplAttrV1::WebauthnAttestationCaList { ca_list } => {
|
||||||
ValueSetWebauthnAttestationCaList::from_repl_v1(ca_list)
|
ValueSetWebauthnAttestationCaList::from_repl_v1(ca_list)
|
||||||
}
|
}
|
||||||
|
ReplAttrV1::OauthClaimMap { set } => ValueSetOauthClaimMap::from_repl_v1(set),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::collections::btree_map::Entry as BTreeEntry;
|
use std::collections::btree_map::Entry as BTreeEntry;
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use crate::be::dbvalue::DbValueOauthScopeMapV1;
|
use crate::be::dbvalue::{DbValueOauthClaimMap, DbValueOauthScopeMapV1};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::repl::proto::{ReplAttrV1, ReplOauthScopeMapV1};
|
use crate::repl::proto::{ReplAttrV1, ReplOauthClaimMapV1, ReplOauthScopeMapV1};
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::value::OAUTHSCOPE_RE;
|
use crate::value::{OauthClaimMapJoin, OAUTHSCOPE_RE};
|
||||||
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
|
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -365,3 +365,368 @@ impl ValueSetT for ValueSetOauthScopeMap {
|
||||||
Some(Box::new(self.map.keys().copied()))
|
Some(Box::new(self.map.keys().copied()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct OauthClaimMapping {
|
||||||
|
join: OauthClaimMapJoin,
|
||||||
|
values: BTreeMap<Uuid, BTreeSet<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OauthClaimMapping {
|
||||||
|
pub(crate) fn join(&self) -> OauthClaimMapJoin {
|
||||||
|
self.join
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn values(&self) -> &BTreeMap<Uuid, BTreeSet<String>> {
|
||||||
|
&self.values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ValueSetOauthClaimMap {
|
||||||
|
// Claim Name
|
||||||
|
map: BTreeMap<String, OauthClaimMapping>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueSetOauthClaimMap {
|
||||||
|
pub(crate) fn new(claim: String, join: OauthClaimMapJoin) -> Box<Self> {
|
||||||
|
let mapping = OauthClaimMapping {
|
||||||
|
join,
|
||||||
|
values: BTreeMap::default(),
|
||||||
|
};
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert(claim, mapping);
|
||||||
|
Box::new(ValueSetOauthClaimMap { map })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_value(claim: String, group: Uuid, claims: BTreeSet<String>) -> Box<Self> {
|
||||||
|
let mut values = BTreeMap::default();
|
||||||
|
values.insert(group, claims);
|
||||||
|
|
||||||
|
let mapping = OauthClaimMapping {
|
||||||
|
join: OauthClaimMapJoin::default(),
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert(claim, mapping);
|
||||||
|
Box::new(ValueSetOauthClaimMap { map })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub(crate) fn push(&mut self, claim: String, mapping: OauthClaimMapping) -> bool {
|
||||||
|
self.map.insert(claim, mapping).is_none()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub(crate) fn from_dbvs2(data: Vec<DbValueOauthClaimMap>) -> Result<ValueSet, OperationError> {
|
||||||
|
let map = data
|
||||||
|
.into_iter()
|
||||||
|
.map(|db_claim_map| match db_claim_map {
|
||||||
|
DbValueOauthClaimMap::V1 { name, join, values } => (
|
||||||
|
name.clone(),
|
||||||
|
OauthClaimMapping {
|
||||||
|
join: join.into(),
|
||||||
|
values: values.clone(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(Box::new(ValueSetOauthClaimMap { map }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_repl_v1(data: &[ReplOauthClaimMapV1]) -> Result<ValueSet, OperationError> {
|
||||||
|
let map = data
|
||||||
|
.iter()
|
||||||
|
.map(|ReplOauthClaimMapV1 { name, join, values }| {
|
||||||
|
(
|
||||||
|
name.clone(),
|
||||||
|
OauthClaimMapping {
|
||||||
|
join: (*join).into(),
|
||||||
|
values: values.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(Box::new(ValueSetOauthClaimMap { map }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
|
||||||
|
// types, and tuples are always foreign.
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub(crate) fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = (String, OauthClaimMapping)>,
|
||||||
|
{
|
||||||
|
let map = iter.into_iter().collect();
|
||||||
|
Some(Box::new(ValueSetOauthClaimMap { map }))
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueSetT for ValueSetOauthClaimMap {
|
||||||
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
|
match value {
|
||||||
|
Value::OauthClaimValue(name, uuid, claims) => {
|
||||||
|
// Add a value to this group associated to this claim.
|
||||||
|
match self.map.entry(name) {
|
||||||
|
BTreeEntry::Vacant(e) => {
|
||||||
|
// New map/value. Use a default joiner.
|
||||||
|
let mut values = BTreeMap::default();
|
||||||
|
values.insert(uuid, claims);
|
||||||
|
|
||||||
|
let claim_map = OauthClaimMapping {
|
||||||
|
join: OauthClaimMapJoin::default(),
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
e.insert(claim_map);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
BTreeEntry::Occupied(mut e) => {
|
||||||
|
// Just add the uuid/value, this claim name already exists.
|
||||||
|
let mapping_mut = e.get_mut();
|
||||||
|
match mapping_mut.values.entry(uuid) {
|
||||||
|
BTreeEntry::Vacant(e) => {
|
||||||
|
e.insert(claims);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
BTreeEntry::Occupied(mut e) => {
|
||||||
|
e.insert(claims);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::OauthClaimMap(name, join) => {
|
||||||
|
match self.map.entry(name) {
|
||||||
|
BTreeEntry::Vacant(e) => {
|
||||||
|
// Create a new empty claim mapping.
|
||||||
|
let claim_map = OauthClaimMapping {
|
||||||
|
join,
|
||||||
|
values: BTreeMap::default(),
|
||||||
|
};
|
||||||
|
e.insert(claim_map);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
BTreeEntry::Occupied(mut e) => {
|
||||||
|
// Just update the join strategy.
|
||||||
|
e.get_mut().join = join;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(OperationError::InvalidValueState),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
|
||||||
|
let res = match pv {
|
||||||
|
// Remove this claim as a whole
|
||||||
|
PartialValue::Iutf8(s) => self.map.remove(s).is_some(),
|
||||||
|
// Remove all references to this group from this claim map.
|
||||||
|
PartialValue::Refer(u) => {
|
||||||
|
let mut contained = false;
|
||||||
|
for mapping_mut in self.map.values_mut() {
|
||||||
|
contained |= mapping_mut.values.remove(u).is_some();
|
||||||
|
}
|
||||||
|
contained
|
||||||
|
}
|
||||||
|
PartialValue::OauthClaim(s, u) => {
|
||||||
|
// Remove a uuid from this claim type.
|
||||||
|
if let Some(mapping_mut) = self.map.get_mut(s) {
|
||||||
|
mapping_mut.values.remove(u).is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PartialValue::OauthClaimValue(s, u, v) => {
|
||||||
|
// Remove a value from this uuid, associated to this claim name.
|
||||||
|
if let Some(mapping_mut) = self.map.get_mut(s) {
|
||||||
|
if let Some(claim_mut) = mapping_mut.values.get_mut(u) {
|
||||||
|
claim_mut.remove(v)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Trim anything that is now empty.
|
||||||
|
self.map
|
||||||
|
.values_mut()
|
||||||
|
.for_each(|mapping_mut| mapping_mut.values.retain(|_k, v| !v.is_empty()));
|
||||||
|
|
||||||
|
self.map.retain(|_k, v| !v.values.is_empty());
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains(&self, pv: &PartialValue) -> bool {
|
||||||
|
match pv {
|
||||||
|
PartialValue::Iutf8(s) => self.map.contains_key(s),
|
||||||
|
PartialValue::Refer(u) => {
|
||||||
|
let mut contained = false;
|
||||||
|
for mapping in self.map.values() {
|
||||||
|
contained |= mapping.values.contains_key(u);
|
||||||
|
}
|
||||||
|
contained
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn startswith(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endswith(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.map.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||||
|
self.map
|
||||||
|
.keys()
|
||||||
|
.cloned()
|
||||||
|
.chain(
|
||||||
|
self.map.values().flat_map(|mapping| {
|
||||||
|
mapping.values.keys().map(|u| u.as_hyphenated().to_string())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntax(&self) -> SyntaxType {
|
||||||
|
SyntaxType::OauthClaimMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
|
self.map.keys().all(|s| OAUTHSCOPE_RE.is_match(s))
|
||||||
|
&& self
|
||||||
|
.map
|
||||||
|
.values()
|
||||||
|
.flat_map(|mapping| {
|
||||||
|
mapping
|
||||||
|
.values
|
||||||
|
.values()
|
||||||
|
.flat_map(|claim_values| claim_values.iter())
|
||||||
|
})
|
||||||
|
.all(|s| OAUTHSCOPE_RE.is_match(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
Box::new(self.map.iter().flat_map(|(name, mapping)| {
|
||||||
|
mapping.values.iter().map(move |(group, claims)| {
|
||||||
|
let join_char = match mapping.join {
|
||||||
|
OauthClaimMapJoin::CommaSeparatedValue => ',',
|
||||||
|
OauthClaimMapJoin::SpaceSeparatedValue => ' ',
|
||||||
|
// Should this be something else?
|
||||||
|
OauthClaimMapJoin::JsonArray => ';',
|
||||||
|
};
|
||||||
|
|
||||||
|
let joined = str_concat!(claims, join_char);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{}: {} \"{:?}\"",
|
||||||
|
name,
|
||||||
|
uuid_to_proto_string(*group),
|
||||||
|
joined
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
DbValueSetV2::OauthClaimMap(
|
||||||
|
self.map
|
||||||
|
.iter()
|
||||||
|
.map(|(name, mapping)| DbValueOauthClaimMap::V1 {
|
||||||
|
name: name.clone(),
|
||||||
|
join: mapping.join.into(),
|
||||||
|
values: mapping.values.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_repl_v1(&self) -> ReplAttrV1 {
|
||||||
|
ReplAttrV1::OauthClaimMap {
|
||||||
|
set: self
|
||||||
|
.map
|
||||||
|
.iter()
|
||||||
|
.map(|(name, mapping)| ReplOauthClaimMapV1 {
|
||||||
|
name: name.clone(),
|
||||||
|
join: mapping.join.into(),
|
||||||
|
values: mapping.values.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||||
|
Box::new(self.map.keys().cloned().map(PartialValue::Iutf8))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||||
|
debug_assert!(false);
|
||||||
|
Box::new(
|
||||||
|
std::iter::empty(), /*
|
||||||
|
self.map
|
||||||
|
.iter()
|
||||||
|
.map(|(u, m)| Value::OauthScopeMap(*u, m.clone())),
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn equal(&self, other: &ValueSet) -> bool {
|
||||||
|
if let Some(other) = other.as_oauthclaim_map() {
|
||||||
|
&self.map == other
|
||||||
|
} else {
|
||||||
|
debug_assert!(false);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||||
|
if let Some(b) = other.as_oauthclaim_map() {
|
||||||
|
mergemaps!(self.map, b)
|
||||||
|
} else {
|
||||||
|
debug_assert!(false);
|
||||||
|
Err(OperationError::InvalidValueState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_oauthclaim_map(&self) -> Option<&BTreeMap<String, OauthClaimMapping>> {
|
||||||
|
Some(&self.map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
|
||||||
|
// This is what ties us as a type that can be refint checked.
|
||||||
|
Some(Box::new(
|
||||||
|
self.map
|
||||||
|
.values()
|
||||||
|
.flat_map(|mapping| mapping.values.keys())
|
||||||
|
.copied(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||||
use compact_jwt::{JwkKeySet, JwsEs256Verifier, JwsVerifier, OidcToken, OidcUnverified};
|
use compact_jwt::{JwkKeySet, JwsEs256Verifier, JwsVerifier, OidcToken, OidcUnverified};
|
||||||
use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
|
use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
|
||||||
use kanidm_proto::constants::*;
|
use kanidm_proto::constants::*;
|
||||||
|
use kanidm_proto::internal::Oauth2ClaimMapJoin;
|
||||||
use kanidm_proto::oauth2::{
|
use kanidm_proto::oauth2::{
|
||||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||||
AccessTokenResponse, AuthorisationResponse, GrantTypeReq, OidcDiscoveryResponse,
|
AccessTokenResponse, AuthorisationResponse, GrantTypeReq, OidcDiscoveryResponse,
|
||||||
|
@ -485,6 +486,27 @@ async fn test_oauth2_openid_public_flow(rsclient: KanidmClient) {
|
||||||
.await
|
.await
|
||||||
.expect("Failed to update oauth2 scopes");
|
.expect("Failed to update oauth2 scopes");
|
||||||
|
|
||||||
|
// Add a custom claim map.
|
||||||
|
rsclient
|
||||||
|
.idm_oauth2_rs_update_claim_map(
|
||||||
|
"test_integration",
|
||||||
|
"test_claim",
|
||||||
|
IDM_ALL_ACCOUNTS.name,
|
||||||
|
&["claim_a".to_string(), "claim_b".to_string()],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update oauth2 claims");
|
||||||
|
|
||||||
|
// Set an alternate join
|
||||||
|
rsclient
|
||||||
|
.idm_oauth2_rs_update_claim_map_join(
|
||||||
|
"test_integration",
|
||||||
|
"test_claim",
|
||||||
|
Oauth2ClaimMapJoin::Ssv,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update oauth2 claims");
|
||||||
|
|
||||||
// Get our admin's auth token for our new client.
|
// Get our admin's auth token for our new client.
|
||||||
// We have to re-auth to update the mail field.
|
// We have to re-auth to update the mail field.
|
||||||
let res = rsclient
|
let res = rsclient
|
||||||
|
@ -648,6 +670,12 @@ async fn test_oauth2_openid_public_flow(rsclient: KanidmClient) {
|
||||||
assert!(oidc.s_claims.email.as_deref() == Some("oauth_test@localhost"));
|
assert!(oidc.s_claims.email.as_deref() == Some("oauth_test@localhost"));
|
||||||
assert!(oidc.s_claims.email_verified == Some(true));
|
assert!(oidc.s_claims.email_verified == Some(true));
|
||||||
|
|
||||||
|
eprintln!("{:?}", oidc.claims);
|
||||||
|
assert_eq!(
|
||||||
|
oidc.claims.get("test_claim").and_then(|v| v.as_str()),
|
||||||
|
Some("claim_a claim_b")
|
||||||
|
);
|
||||||
|
|
||||||
// Check the preflight works.
|
// Check the preflight works.
|
||||||
let response = client
|
let response = client
|
||||||
.request(
|
.request(
|
||||||
|
|
|
@ -151,7 +151,6 @@ impl CommonOpt {
|
||||||
let mut token_refs: Vec<_> = tokens
|
let mut token_refs: Vec<_> = tokens
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|(t, _)| t.starts_with(&filter_username))
|
.filter(|(t, _)| t.starts_with(&filter_username))
|
||||||
.map(|(k, v)| (k, v))
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
match token_refs.len() {
|
match token_refs.len() {
|
||||||
|
|
|
@ -3,6 +3,9 @@ use std::process::exit;
|
||||||
use crate::common::OpType;
|
use crate::common::OpType;
|
||||||
use crate::{handle_client_error, Oauth2Opt, OutputMode};
|
use crate::{handle_client_error, Oauth2Opt, OutputMode};
|
||||||
|
|
||||||
|
use crate::Oauth2ClaimMapJoin;
|
||||||
|
use kanidm_proto::internal::Oauth2ClaimMapJoin as ProtoOauth2ClaimMapJoin;
|
||||||
|
|
||||||
impl Oauth2Opt {
|
impl Oauth2Opt {
|
||||||
pub fn debug(&self) -> bool {
|
pub fn debug(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
@ -25,9 +28,13 @@ impl Oauth2Opt {
|
||||||
Oauth2Opt::DisableLegacyCrypto(nopt) => nopt.copt.debug,
|
Oauth2Opt::DisableLegacyCrypto(nopt) => nopt.copt.debug,
|
||||||
Oauth2Opt::PreferShortUsername(nopt) => nopt.copt.debug,
|
Oauth2Opt::PreferShortUsername(nopt) => nopt.copt.debug,
|
||||||
Oauth2Opt::PreferSPNUsername(nopt) => nopt.copt.debug,
|
Oauth2Opt::PreferSPNUsername(nopt) => nopt.copt.debug,
|
||||||
Oauth2Opt::CreateBasic { copt, .. } | Oauth2Opt::CreatePublic { copt, .. } => {
|
Oauth2Opt::CreateBasic { copt, .. }
|
||||||
copt.debug
|
| Oauth2Opt::CreatePublic { copt, .. }
|
||||||
}
|
| Oauth2Opt::UpdateClaimMap { copt, .. }
|
||||||
|
| Oauth2Opt::UpdateClaimMapJoin { copt, .. }
|
||||||
|
| Oauth2Opt::DeleteClaimMap { copt, .. }
|
||||||
|
| Oauth2Opt::EnablePublicLocalhost { copt, .. }
|
||||||
|
| Oauth2Opt::DisablePublicLocalhost { copt, .. } => copt.debug,
|
||||||
Oauth2Opt::SetOrigin { nopt, .. } => nopt.copt.debug,
|
Oauth2Opt::SetOrigin { nopt, .. } => nopt.copt.debug,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -325,6 +332,90 @@ impl Oauth2Opt {
|
||||||
Err(e) => handle_client_error(e, nopt.copt.output_mode),
|
Err(e) => handle_client_error(e, nopt.copt.output_mode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Oauth2Opt::UpdateClaimMap {
|
||||||
|
copt,
|
||||||
|
name,
|
||||||
|
group,
|
||||||
|
claim_name,
|
||||||
|
values,
|
||||||
|
} => {
|
||||||
|
let client = copt.to_client(OpType::Write).await;
|
||||||
|
match client
|
||||||
|
.idm_oauth2_rs_update_claim_map(
|
||||||
|
name.as_str(),
|
||||||
|
claim_name.as_str(),
|
||||||
|
group.as_str(),
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oauth2Opt::UpdateClaimMapJoin {
|
||||||
|
copt,
|
||||||
|
name,
|
||||||
|
claim_name,
|
||||||
|
join,
|
||||||
|
} => {
|
||||||
|
let client = copt.to_client(OpType::Write).await;
|
||||||
|
|
||||||
|
let join = match join {
|
||||||
|
Oauth2ClaimMapJoin::Csv => ProtoOauth2ClaimMapJoin::Csv,
|
||||||
|
Oauth2ClaimMapJoin::Ssv => ProtoOauth2ClaimMapJoin::Ssv,
|
||||||
|
Oauth2ClaimMapJoin::Array => ProtoOauth2ClaimMapJoin::Array,
|
||||||
|
};
|
||||||
|
|
||||||
|
match client
|
||||||
|
.idm_oauth2_rs_update_claim_map_join(name.as_str(), claim_name.as_str(), join)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Oauth2Opt::DeleteClaimMap {
|
||||||
|
copt,
|
||||||
|
name,
|
||||||
|
claim_name,
|
||||||
|
group,
|
||||||
|
} => {
|
||||||
|
let client = copt.to_client(OpType::Write).await;
|
||||||
|
match client
|
||||||
|
.idm_oauth2_rs_delete_claim_map(
|
||||||
|
name.as_str(),
|
||||||
|
claim_name.as_str(),
|
||||||
|
group.as_str(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Oauth2Opt::EnablePublicLocalhost { copt, name } => {
|
||||||
|
let client = copt.to_client(OpType::Write).await;
|
||||||
|
match client
|
||||||
|
.idm_oauth2_rs_enable_public_localhost_redirect(name.as_str())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Oauth2Opt::DisablePublicLocalhost { copt, name } => {
|
||||||
|
let client = copt.to_client(OpType::Write).await;
|
||||||
|
match client
|
||||||
|
.idm_oauth2_rs_disable_public_localhost_redirect(name.as_str())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => println!("Success"),
|
||||||
|
Err(e) => handle_client_error(e, copt.output_mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -800,6 +800,38 @@ pub struct Oauth2DeleteScopeMapOpt {
|
||||||
group: String,
|
group: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub enum Oauth2ClaimMapJoin {
|
||||||
|
Csv,
|
||||||
|
Ssv,
|
||||||
|
Array,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Oauth2ClaimMapJoin {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Csv => "csv",
|
||||||
|
Self::Ssv => "ssv",
|
||||||
|
Self::Array => "array",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueEnum for Oauth2ClaimMapJoin {
|
||||||
|
fn value_variants<'a>() -> &'a [Self] {
|
||||||
|
&[Self::Csv, Self::Ssv, Self::Array]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_possible_value(&self) -> Option<PossibleValue> {
|
||||||
|
Some(match self {
|
||||||
|
Self::Csv => PossibleValue::new("csv"),
|
||||||
|
Self::Ssv => PossibleValue::new("ssv"),
|
||||||
|
Self::Array => PossibleValue::new("array"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum Oauth2Opt {
|
pub enum Oauth2Opt {
|
||||||
#[clap(name = "list")]
|
#[clap(name = "list")]
|
||||||
|
@ -853,6 +885,36 @@ pub enum Oauth2Opt {
|
||||||
/// Remove a mapping from groups to scopes
|
/// Remove a mapping from groups to scopes
|
||||||
DeleteSupScopeMap(Oauth2DeleteScopeMapOpt),
|
DeleteSupScopeMap(Oauth2DeleteScopeMapOpt),
|
||||||
|
|
||||||
|
#[clap(name = "update-claim-map", visible_aliases=&["create-claim-map"])]
|
||||||
|
/// Update or add a new mapping from a group to custom claims that it provides to members
|
||||||
|
UpdateClaimMap {
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
name: String,
|
||||||
|
claim_name: String,
|
||||||
|
group: String,
|
||||||
|
values: Vec<String>,
|
||||||
|
},
|
||||||
|
#[clap(name = "update-claim-map-join")]
|
||||||
|
UpdateClaimMapJoin {
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
name: String,
|
||||||
|
claim_name: String,
|
||||||
|
/// The join strategy. Valid values are csv (comma separated value), ssv (space
|
||||||
|
/// separated value) and array.
|
||||||
|
join: Oauth2ClaimMapJoin,
|
||||||
|
},
|
||||||
|
#[clap(name = "delete-claim-map")]
|
||||||
|
/// Remove a mapping from groups to a custom claim
|
||||||
|
DeleteClaimMap {
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
name: String,
|
||||||
|
claim_name: String,
|
||||||
|
group: String,
|
||||||
|
},
|
||||||
|
|
||||||
#[clap(name = "reset-secrets")]
|
#[clap(name = "reset-secrets")]
|
||||||
/// Reset the secrets associated to this resource server
|
/// Reset the secrets associated to this resource server
|
||||||
ResetSecrets(Named),
|
ResetSecrets(Named),
|
||||||
|
@ -900,12 +962,28 @@ pub enum Oauth2Opt {
|
||||||
#[clap(name = "disable-legacy-crypto")]
|
#[clap(name = "disable-legacy-crypto")]
|
||||||
DisableLegacyCrypto(Named),
|
DisableLegacyCrypto(Named),
|
||||||
#[clap(name = "prefer-short-username")]
|
#[clap(name = "prefer-short-username")]
|
||||||
|
|
||||||
|
#[clap(name = "enable-localhost-redirects")]
|
||||||
|
/// Allow public clients to redirect to localhost.
|
||||||
|
EnablePublicLocalhost {
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Disable public clients redirecting to localhost.
|
||||||
|
#[clap(name = "disable-localhost-redirects")]
|
||||||
|
DisablePublicLocalhost {
|
||||||
|
#[clap(flatten)]
|
||||||
|
copt: CommonOpt,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// Use the 'name' attribute instead of 'spn' for the preferred_username
|
/// Use the 'name' attribute instead of 'spn' for the preferred_username
|
||||||
PreferShortUsername(Named),
|
PreferShortUsername(Named),
|
||||||
#[clap(name = "prefer-spn-username")]
|
#[clap(name = "prefer-spn-username")]
|
||||||
/// Use the 'spn' attribute instead of 'name' for the preferred_username
|
/// Use the 'spn' attribute instead of 'name' for the preferred_username
|
||||||
PreferSPNUsername(Named),
|
PreferSPNUsername(Named),
|
||||||
/// Set the origin of a client
|
/// Set the origin of an oauth2 client
|
||||||
#[clap(name = "set-origin")]
|
#[clap(name = "set-origin")]
|
||||||
SetOrigin {
|
SetOrigin {
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
|
|
Loading…
Reference in a new issue